Skip to content

Commit e19db7d

Browse files
authored
Merge pull request #59 from dgenio/copilot/add-structured-logging-kernel
feat: Add structured logging at kernel decision points
2 parents 5fe6610 + b418232 commit e19db7d

6 files changed

Lines changed: 684 additions & 20 deletions

File tree

src/agent_kernel/firewall/transform.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import datetime
66
import json
7+
import logging
78
from typing import Any
89

910
from ..models import (
@@ -17,6 +18,8 @@
1718
from .redaction import redact
1819
from .summarize import summarize
1920

21+
logger = logging.getLogger(__name__)
22+
2023

2124
class Firewall:
2225
"""Transforms :class:`RawResult` objects into LLM-safe :class:`Frame` objects.
@@ -90,18 +93,49 @@ def transform(
9093
data, redact_warnings = redact(data, max_depth=self._budgets.max_depth)
9194
warnings.extend(redact_warnings)
9295

96+
logger.debug(
97+
"firewall_redaction",
98+
extra={
99+
"action_id": action_id,
100+
"capability_id": raw.capability_id,
101+
"principal_id": principal_id,
102+
"redaction_warnings": len(redact_warnings),
103+
"needs_redaction": needs_redaction,
104+
},
105+
)
106+
93107
# ── Raw mode (admin only) ──────────────────────────────────────────────
94108
if response_mode == "raw":
95109
if "admin" not in principal_roles:
96110
warnings.append("raw mode requires admin role; falling back to summary.")
97111
response_mode = "summary"
112+
logger.debug(
113+
"firewall_mode_fallback",
114+
extra={
115+
"action_id": action_id,
116+
"capability_id": raw.capability_id,
117+
"requested_mode": "raw",
118+
"effective_mode": "summary",
119+
"reason": "principal lacks admin role",
120+
},
121+
)
98122
else:
99123
raw_size = len(json.dumps(data, default=str))
100124
if raw_size > self._budgets.max_chars:
101125
warnings.append(
102126
f"raw output ({raw_size} chars) exceeds budget "
103127
f"({self._budgets.max_chars} chars); data returned untruncated."
104128
)
129+
logger.debug(
130+
"firewall_transform",
131+
extra={
132+
"action_id": action_id,
133+
"capability_id": raw.capability_id,
134+
"response_mode": "raw",
135+
"raw_size_chars": raw_size,
136+
"budget_chars": self._budgets.max_chars,
137+
},
138+
)
105139
return Frame(
106140
action_id=action_id,
107141
capability_id=raw.capability_id,
@@ -114,6 +148,14 @@ def transform(
114148

115149
# ── Handle only ───────────────────────────────────────────────────────
116150
if response_mode == "handle_only":
151+
logger.debug(
152+
"firewall_transform",
153+
extra={
154+
"action_id": action_id,
155+
"capability_id": raw.capability_id,
156+
"response_mode": "handle_only",
157+
},
158+
)
117159
return Frame(
118160
action_id=action_id,
119161
capability_id=raw.capability_id,
@@ -126,6 +168,16 @@ def transform(
126168
# ── Table mode ────────────────────────────────────────────────────────
127169
if response_mode == "table":
128170
table_preview = self._make_table(data, max_rows=max_rows)
171+
logger.debug(
172+
"firewall_transform",
173+
extra={
174+
"action_id": action_id,
175+
"capability_id": raw.capability_id,
176+
"response_mode": "table",
177+
"rows_returned": len(table_preview),
178+
"max_rows": max_rows,
179+
},
180+
)
129181
return Frame(
130182
action_id=action_id,
131183
capability_id=raw.capability_id,
@@ -140,6 +192,16 @@ def transform(
140192
facts = summarize(data, max_facts=20)
141193
# Enforce char budget across all facts
142194
facts = _cap_facts(facts, self._budgets.max_chars)
195+
logger.debug(
196+
"firewall_transform",
197+
extra={
198+
"action_id": action_id,
199+
"capability_id": raw.capability_id,
200+
"response_mode": "summary",
201+
"facts_count": len(facts),
202+
"budget_chars": self._budgets.max_chars,
203+
},
204+
)
143205
return Frame(
144206
action_id=action_id,
145207
capability_id=raw.capability_id,

src/agent_kernel/kernel.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import datetime
6+
import logging
67
import uuid
78
from typing import Any
89

@@ -26,6 +27,8 @@
2627
from .tokens import CapabilityToken, HMACTokenProvider, TokenProvider
2728
from .trace import TraceStore
2829

30+
logger = logging.getLogger(__name__)
31+
2932

3033
class Kernel:
3134
"""The central orchestrator for capability-based AI agent security.
@@ -91,7 +94,15 @@ def request_capabilities(
9194
Returns:
9295
An ordered list of :class:`CapabilityRequest` objects (best match first).
9396
"""
94-
return self._registry.search(goal)
97+
results = self._registry.search(goal)
98+
logger.debug(
99+
"request_capabilities",
100+
extra={
101+
"goal": goal,
102+
"matches": len(results),
103+
},
104+
)
105+
return results
95106

96107
def grant_capability(
97108
self,
@@ -125,6 +136,16 @@ def grant_capability(
125136
constraints=decision.constraints,
126137
audit_id=audit_id,
127138
)
139+
logger.info(
140+
"grant_capability",
141+
extra={
142+
"principal_id": principal.principal_id,
143+
"capability_id": capability.capability_id,
144+
"safety_class": capability.safety_class.value,
145+
"audit_id": audit_id,
146+
"token_id": token.token_id,
147+
},
148+
)
128149
return CapabilityGrant(
129150
request=request,
130151
principal=principal,
@@ -188,6 +209,16 @@ async def invoke(
188209
self._registry.get(token.capability_id) # validate capability exists
189210
plan: RoutePlan = self._router.route(token.capability_id)
190211

212+
_log_ctx = {
213+
"action_id": action_id,
214+
"principal_id": principal.principal_id,
215+
"capability_id": token.capability_id,
216+
}
217+
logger.info(
218+
"invoke_start",
219+
extra={**_log_ctx, "token_id": token.token_id, "response_mode": response_mode},
220+
)
221+
191222
# ── Execute with fallback ─────────────────────────────────────────────
192223
raw_result = None
193224
used_driver_id = ""
@@ -207,13 +238,19 @@ async def invoke(
207238
try:
208239
raw_result = await driver.execute(ctx)
209240
used_driver_id = driver_id
241+
logger.debug("driver_success", extra={**_log_ctx, "driver_id": driver_id})
210242
break
211243
except DriverError as exc:
244+
logger.warning(
245+
"driver_failure",
246+
extra={**_log_ctx, "driver_id": driver_id, "error": str(exc)},
247+
)
212248
last_error = exc
213249
continue
214250

215251
if raw_result is None:
216252
err_msg = str(last_error) if last_error else "No drivers available."
253+
logger.warning("invoke_failure", extra={**_log_ctx, "error": err_msg})
217254
trace = ActionTrace(
218255
action_id=action_id,
219256
capability_id=token.capability_id,
@@ -263,6 +300,10 @@ async def invoke(
263300
)
264301
self._trace_store.record(trace)
265302

303+
logger.info(
304+
"invoke_success",
305+
extra={**_log_ctx, "response_mode": frame.response_mode, "driver_id": used_driver_id},
306+
)
266307
return frame
267308

268309
def expand(self, handle: Handle, *, query: dict[str, Any]) -> Frame:
@@ -279,6 +320,13 @@ def expand(self, handle: Handle, *, query: dict[str, Any]) -> Frame:
279320
HandleNotFound: If the handle is unknown.
280321
HandleExpired: If the handle has expired.
281322
"""
323+
logger.info(
324+
"expand",
325+
extra={
326+
"handle_id": handle.handle_id,
327+
"capability_id": handle.capability_id,
328+
},
329+
)
282330
return self._handle_store.expand(handle, query=query)
283331

284332
def explain(self, action_id: str) -> ActionTrace:
@@ -293,4 +341,10 @@ def explain(self, action_id: str) -> ActionTrace:
293341
Raises:
294342
AgentKernelError: If no trace exists for that action ID.
295343
"""
344+
logger.info(
345+
"explain",
346+
extra={
347+
"action_id": action_id,
348+
},
349+
)
296350
return self._trace_store.get(action_id)

0 commit comments

Comments
 (0)