cortexhub 0.1.3__tar.gz → 0.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {cortexhub-0.1.3 → cortexhub-0.1.5}/.gitignore +3 -0
  2. {cortexhub-0.1.3 → cortexhub-0.1.5}/PKG-INFO +13 -1
  3. {cortexhub-0.1.3 → cortexhub-0.1.5}/README.md +10 -0
  4. {cortexhub-0.1.3 → cortexhub-0.1.5}/pyproject.toml +3 -1
  5. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/adapters/claude_agents.py +245 -7
  6. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/adapters/crewai.py +275 -1
  7. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/adapters/langgraph.py +119 -0
  8. cortexhub-0.1.5/src/cortexhub/adapters/openai_agents.py +516 -0
  9. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/client.py +84 -1
  10. cortexhub-0.1.3/src/cortexhub/adapters/openai_agents.py +0 -192
  11. {cortexhub-0.1.3 → cortexhub-0.1.5}/LICENSE +0 -0
  12. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/__init__.py +0 -0
  13. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/adapters/__init__.py +0 -0
  14. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/adapters/base.py +0 -0
  15. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/audit/__init__.py +0 -0
  16. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/audit/events.py +0 -0
  17. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/auto_protect.py +0 -0
  18. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/backend/__init__.py +0 -0
  19. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/backend/client.py +0 -0
  20. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/config.py +0 -0
  21. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/context/__init__.py +0 -0
  22. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/context/enricher.py +0 -0
  23. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/errors.py +0 -0
  24. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/frameworks.py +0 -0
  25. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/guardrails/__init__.py +0 -0
  26. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/guardrails/injection.py +0 -0
  27. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/guardrails/pii.py +0 -0
  28. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/guardrails/secrets.py +0 -0
  29. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/interceptors/__init__.py +0 -0
  30. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/interceptors/llm.py +0 -0
  31. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/interceptors/mcp.py +0 -0
  32. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/pipeline.py +0 -0
  33. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/policy/__init__.py +0 -0
  34. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/policy/effects.py +0 -0
  35. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/policy/evaluator.py +0 -0
  36. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/policy/loader.py +0 -0
  37. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/policy/models.py +0 -0
  38. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/policy/sync.py +0 -0
  39. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/telemetry/__init__.py +0 -0
  40. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/telemetry/otel.py +0 -0
  41. {cortexhub-0.1.3 → cortexhub-0.1.5}/src/cortexhub/version.py +0 -0
@@ -99,3 +99,6 @@ pnpm-debug.log*
99
99
  Thumbs.db
100
100
  python/cortexhub_data
101
101
 
102
+ .planning/
103
+ .cursor/
104
+ .claude/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cortexhub
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: CortexHub Python SDK - Policy-as-Code for AI Agents
5
5
  Project-URL: Homepage, https://cortexhub.ai
6
6
  Project-URL: Documentation, https://docs.cortexhub.ai
@@ -39,12 +39,14 @@ Requires-Dist: crewai>=0.50.0; extra == 'all'
39
39
  Requires-Dist: langchain-core>=0.2.0; extra == 'all'
40
40
  Requires-Dist: langchain-openai>=0.1.0; extra == 'all'
41
41
  Requires-Dist: langgraph>=0.2.0; extra == 'all'
42
+ Requires-Dist: litellm>=1.81.5; extra == 'all'
42
43
  Requires-Dist: openai-agents>=0.0.3; extra == 'all'
43
44
  Provides-Extra: claude-agents
44
45
  Requires-Dist: anthropic>=0.40.0; extra == 'claude-agents'
45
46
  Requires-Dist: claude-agent-sdk>=0.0.1; extra == 'claude-agents'
46
47
  Provides-Extra: crewai
47
48
  Requires-Dist: crewai>=0.50.0; extra == 'crewai'
49
+ Requires-Dist: litellm>=1.81.5; extra == 'crewai'
48
50
  Provides-Extra: dev
49
51
  Requires-Dist: mypy>=1.10.0; extra == 'dev'
50
52
  Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
@@ -105,6 +107,16 @@ from langgraph.prebuilt import create_react_agent
105
107
  | OpenAI Agents | `Framework.OPENAI_AGENTS` | `pip install cortexhub[openai-agents]` |
106
108
  | Claude Agents | `Framework.CLAUDE_AGENTS` | `pip install cortexhub[claude-agents]` |
107
109
 
110
+ ## Tracing Coverage
111
+
112
+ All frameworks emit `run.started` and `run.completed`/`run.failed` for each run.
113
+ Tool spans (`tool.invoke`) and model spans (`llm.call`) vary by SDK:
114
+
115
+ - **LangGraph**: tool calls via `BaseTool.invoke`, LLM calls via `BaseChatModel.invoke/ainvoke`
116
+ - **CrewAI**: tool calls via `CrewStructuredTool.invoke`/`BaseTool.run`, LLM calls via LiteLLM and `BaseLLM.call/acall`
117
+ - **OpenAI Agents**: tool calls via `function_tool`, LLM calls via `OpenAIResponsesModel` and `OpenAIChatCompletionsModel`
118
+ - **Claude Agents**: tool calls via `@tool` and built-in tool hooks; LLM calls run inside the Claude Code CLI and are not intercepted by the Python SDK
119
+
108
120
  ## Configuration
109
121
 
110
122
  ```bash
@@ -44,6 +44,16 @@ from langgraph.prebuilt import create_react_agent
44
44
  | OpenAI Agents | `Framework.OPENAI_AGENTS` | `pip install cortexhub[openai-agents]` |
45
45
  | Claude Agents | `Framework.CLAUDE_AGENTS` | `pip install cortexhub[claude-agents]` |
46
46
 
47
+ ## Tracing Coverage
48
+
49
+ All frameworks emit `run.started` and `run.completed`/`run.failed` for each run.
50
+ Tool spans (`tool.invoke`) and model spans (`llm.call`) vary by SDK:
51
+
52
+ - **LangGraph**: tool calls via `BaseTool.invoke`, LLM calls via `BaseChatModel.invoke/ainvoke`
53
+ - **CrewAI**: tool calls via `CrewStructuredTool.invoke`/`BaseTool.run`, LLM calls via LiteLLM and `BaseLLM.call/acall`
54
+ - **OpenAI Agents**: tool calls via `function_tool`, LLM calls via `OpenAIResponsesModel` and `OpenAIChatCompletionsModel`
55
+ - **Claude Agents**: tool calls via `@tool` and built-in tool hooks; LLM calls run inside the Claude Code CLI and are not intercepted by the Python SDK
56
+
47
57
  ## Configuration
48
58
 
49
59
  ```bash
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cortexhub"
3
- version = "0.1.3"
3
+ version = "0.1.5"
4
4
  description = "CortexHub Python SDK - Policy-as-Code for AI Agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10,<3.14"
@@ -55,6 +55,7 @@ langgraph = [
55
55
 
56
56
  crewai = [
57
57
  "crewai>=0.50.0",
58
+ "litellm>=1.81.5",
58
59
  ]
59
60
 
60
61
  openai-agents = [
@@ -139,6 +140,7 @@ dev = [
139
140
  "langchain-openai>=0.1.0",
140
141
  "langgraph>=0.2.0",
141
142
  "crewai>=0.50.0",
143
+ "litellm>=1.81.5",
142
144
  "openai-agents>=0.0.3",
143
145
  "anthropic>=0.40.0",
144
146
  "claude-agent-sdk>=0.0.1",
@@ -19,11 +19,13 @@ Architectural rules:
19
19
  - No governance logic in adapter
20
20
  """
21
21
 
22
+ import json
22
23
  import os
23
24
  from functools import wraps
24
25
  from typing import TYPE_CHECKING, Any, Callable, Awaitable
25
26
 
26
27
  import structlog
28
+ from opentelemetry.trace import SpanKind, Status, StatusCode
27
29
 
28
30
  from cortexhub.adapters.base import ToolAdapter
29
31
  from cortexhub.pipeline import govern_execution
@@ -36,6 +38,10 @@ logger = structlog.get_logger(__name__)
36
38
  # Attribute names for storing originals
37
39
  _ORIGINAL_TOOL_ATTR = "__cortexhub_original_tool__"
38
40
  _PATCHED_ATTR = "__cortexhub_patched__"
41
+ _ORIGINAL_QUERY_ATTR = "__cortexhub_original_query__"
42
+ _ORIGINAL_RECEIVE_RESPONSE_ATTR = "__cortexhub_original_receive_response__"
43
+ _ORIGINAL_CLIENT_QUERY_ATTR = "__cortexhub_original_client_query__"
44
+ _PATCHED_RUN_ATTR = "__cortexhub_run_patched__"
39
45
 
40
46
 
41
47
  class ClaudeAgentsAdapter(ToolAdapter):
@@ -64,6 +70,11 @@ class ClaudeAgentsAdapter(ToolAdapter):
64
70
  def framework_name(self) -> str:
65
71
  return "claude_agents"
66
72
 
73
+ def __init__(self, cortex_hub: Any):
74
+ super().__init__(cortex_hub)
75
+ self._hook_spans: dict[str, Any] = {}
76
+ self._active_run_session_id: str | None = None
77
+
67
78
  def _get_framework_modules(self) -> list[str]:
68
79
  return ["claude_agent_sdk"]
69
80
 
@@ -145,6 +156,8 @@ class ClaudeAgentsAdapter(ToolAdapter):
145
156
  setattr(claude_agent_sdk, _PATCHED_ATTR, True)
146
157
 
147
158
  logger.info("Claude Agent SDK @tool decorator patched successfully")
159
+
160
+ self._patch_run_completion(cortex_hub)
148
161
 
149
162
  except ImportError:
150
163
  logger.debug("Claude Agent SDK not installed, skipping adapter")
@@ -161,6 +174,23 @@ class ClaudeAgentsAdapter(ToolAdapter):
161
174
  claude_agent_sdk.tool = original
162
175
  setattr(claude_agent_sdk, _PATCHED_ATTR, False)
163
176
  logger.info("Claude Agent SDK adapter unpatched")
177
+
178
+ try:
179
+ from claude_agent_sdk import ClaudeSDKClient
180
+
181
+ if hasattr(claude_agent_sdk, _ORIGINAL_QUERY_ATTR):
182
+ claude_agent_sdk.query = getattr(claude_agent_sdk, _ORIGINAL_QUERY_ATTR)
183
+ if hasattr(ClaudeSDKClient, _ORIGINAL_CLIENT_QUERY_ATTR):
184
+ ClaudeSDKClient.query = getattr(
185
+ ClaudeSDKClient, _ORIGINAL_CLIENT_QUERY_ATTR
186
+ )
187
+ if hasattr(ClaudeSDKClient, _ORIGINAL_RECEIVE_RESPONSE_ATTR):
188
+ ClaudeSDKClient.receive_response = getattr(
189
+ ClaudeSDKClient, _ORIGINAL_RECEIVE_RESPONSE_ATTR
190
+ )
191
+ setattr(claude_agent_sdk, _PATCHED_RUN_ATTR, False)
192
+ except ImportError:
193
+ pass
164
194
  except ImportError:
165
195
  pass
166
196
 
@@ -171,6 +201,94 @@ class ClaudeAgentsAdapter(ToolAdapter):
171
201
  def _discover_tools(self) -> list[dict[str, Any]]:
172
202
  """Discover tools from Claude Agent SDK (best-effort)."""
173
203
  return []
204
+
205
+ def _patch_run_completion(self, cortex_hub) -> None:
206
+ """Patch Claude Agent SDK runs to emit run completion."""
207
+ try:
208
+ import claude_agent_sdk
209
+ from claude_agent_sdk import ClaudeSDKClient, ResultMessage
210
+ adapter = self
211
+
212
+ if getattr(claude_agent_sdk, _PATCHED_RUN_ATTR, False):
213
+ return
214
+
215
+ if not hasattr(claude_agent_sdk, _ORIGINAL_QUERY_ATTR):
216
+ setattr(claude_agent_sdk, _ORIGINAL_QUERY_ATTR, claude_agent_sdk.query)
217
+ original_query = getattr(claude_agent_sdk, _ORIGINAL_QUERY_ATTR)
218
+
219
+ async def patched_query(*args, **kwargs):
220
+ status = None
221
+ failed = False
222
+ cortex_hub.start_run(framework="claude_agents")
223
+ adapter._active_run_session_id = cortex_hub.session_id
224
+ try:
225
+ async for message in original_query(*args, **kwargs):
226
+ if isinstance(message, ResultMessage):
227
+ status = "failed" if message.is_error else "completed"
228
+ yield message
229
+ except Exception:
230
+ failed = True
231
+ raise
232
+ finally:
233
+ if status is None and failed:
234
+ status = "failed"
235
+ if status:
236
+ cortex_hub.finish_run(framework="claude_agents", status=status)
237
+ adapter._active_run_session_id = None
238
+
239
+ claude_agent_sdk.query = patched_query
240
+
241
+ if not hasattr(ClaudeSDKClient, _ORIGINAL_RECEIVE_RESPONSE_ATTR):
242
+ setattr(
243
+ ClaudeSDKClient,
244
+ _ORIGINAL_RECEIVE_RESPONSE_ATTR,
245
+ ClaudeSDKClient.receive_response,
246
+ )
247
+ original_receive_response = getattr(ClaudeSDKClient, _ORIGINAL_RECEIVE_RESPONSE_ATTR)
248
+ if not hasattr(ClaudeSDKClient, _ORIGINAL_CLIENT_QUERY_ATTR):
249
+ setattr(
250
+ ClaudeSDKClient,
251
+ _ORIGINAL_CLIENT_QUERY_ATTR,
252
+ ClaudeSDKClient.query,
253
+ )
254
+ original_client_query = getattr(ClaudeSDKClient, _ORIGINAL_CLIENT_QUERY_ATTR)
255
+
256
+ async def patched_client_query(self, *args, **kwargs):
257
+ cortex_hub.start_run(framework="claude_agents")
258
+ adapter._active_run_session_id = cortex_hub.session_id
259
+ try:
260
+ return await original_client_query(self, *args, **kwargs)
261
+ except Exception:
262
+ cortex_hub.finish_run(framework="claude_agents", status="failed")
263
+ adapter._active_run_session_id = None
264
+ raise
265
+
266
+ async def patched_receive_response(self, *args, **kwargs):
267
+ status = None
268
+ failed = False
269
+ try:
270
+ async for message in original_receive_response(self, *args, **kwargs):
271
+ if isinstance(message, ResultMessage):
272
+ status = "failed" if message.is_error else "completed"
273
+ yield message
274
+ except Exception:
275
+ failed = True
276
+ raise
277
+ finally:
278
+ if status is None and failed:
279
+ status = "failed"
280
+ if status:
281
+ cortex_hub.finish_run(framework="claude_agents", status=status)
282
+ adapter._active_run_session_id = None
283
+
284
+ ClaudeSDKClient.query = patched_client_query
285
+ ClaudeSDKClient.receive_response = patched_receive_response
286
+ setattr(claude_agent_sdk, _PATCHED_RUN_ATTR, True)
287
+ logger.info("Claude Agent SDK run completion patched successfully")
288
+ except ImportError:
289
+ logger.debug("Claude Agent SDK run completion patch skipped")
290
+ except Exception as e:
291
+ logger.debug("Claude Agent SDK run completion patch failed", reason=str(e))
174
292
 
175
293
  def create_governance_hooks(self) -> dict[str, list]:
176
294
  """Create CortexHub governance hooks for Claude Agent SDK.
@@ -191,6 +309,65 @@ class ClaudeAgentsAdapter(ToolAdapter):
191
309
  )
192
310
  """
193
311
  cortex_hub = self.cortex_hub
312
+ span_store = self._hook_spans
313
+ adapter = self
314
+
315
+ def _current_session_id() -> str:
316
+ return adapter._active_run_session_id or cortex_hub.session_id
317
+
318
+ def _start_tool_span(
319
+ *,
320
+ tool_name: str,
321
+ tool_description: str,
322
+ policy_args: dict[str, Any],
323
+ raw_args: dict[str, Any],
324
+ tool_use_id: str | None,
325
+ ):
326
+ span = cortex_hub._tracer.start_span(
327
+ name="tool.invoke",
328
+ kind=SpanKind.INTERNAL,
329
+ )
330
+ span.set_attribute("cortexhub.session.id", _current_session_id())
331
+ span.set_attribute("cortexhub.agent.id", cortex_hub.agent_id)
332
+ span.set_attribute("cortexhub.tool.name", tool_name)
333
+ span.set_attribute("cortexhub.tool.framework", "claude_agents")
334
+ span.set_attribute("cortexhub.tool.description", tool_description)
335
+
336
+ if tool_use_id:
337
+ span.set_attribute("cortexhub.tool.use_id", tool_use_id)
338
+
339
+ if policy_args:
340
+ arg_names = list(policy_args.keys())
341
+ if arg_names:
342
+ span.set_attribute("cortexhub.tool.arg_names", arg_names)
343
+ arg_schema = cortex_hub._infer_arg_schema(policy_args)
344
+ if arg_schema:
345
+ span.set_attribute(
346
+ "cortexhub.tool.arg_schema",
347
+ json.dumps(arg_schema),
348
+ )
349
+
350
+ if not cortex_hub.privacy and raw_args:
351
+ span.set_attribute("cortexhub.raw.args", json.dumps(raw_args, default=str))
352
+
353
+ return span
354
+
355
+ def _finish_tool_span(
356
+ span,
357
+ *,
358
+ success: bool,
359
+ error_message: str | None = None,
360
+ result: Any | None = None,
361
+ ) -> None:
362
+ span.set_attribute("cortexhub.result.success", success)
363
+ if error_message:
364
+ span.set_attribute("cortexhub.error.message", error_message)
365
+ span.set_status(Status(StatusCode.ERROR, error_message))
366
+ else:
367
+ span.set_status(Status(StatusCode.OK))
368
+ if result is not None and not cortex_hub.privacy:
369
+ span.set_attribute("cortexhub.raw.result", json.dumps(result, default=str))
370
+ span.end()
194
371
 
195
372
  async def pre_tool_governance(
196
373
  input_data: dict[str, Any],
@@ -204,12 +381,18 @@ class ClaudeAgentsAdapter(ToolAdapter):
204
381
  """
205
382
  tool_name = input_data.get("tool_name", "unknown")
206
383
  tool_input = input_data.get("tool_input", {})
384
+ if not isinstance(tool_input, dict):
385
+ tool_input = {"_raw": tool_input}
386
+ policy_args = cortex_hub._sanitize_policy_args(tool_input)
207
387
 
208
- tool_metadata = {
209
- "name": tool_name,
210
- "description": f"Claude Agent SDK built-in tool: {tool_name}",
211
- "framework": "claude_agents",
212
- }
388
+ tool_description = f"Claude Agent SDK built-in tool: {tool_name}"
389
+ span = _start_tool_span(
390
+ tool_name=tool_name,
391
+ tool_description=tool_description,
392
+ policy_args=policy_args,
393
+ raw_args=tool_input,
394
+ tool_use_id=tool_use_id,
395
+ )
213
396
 
214
397
  # Build authorization request and evaluate
215
398
  from cortexhub.policy.models import (
@@ -222,7 +405,7 @@ class ClaudeAgentsAdapter(ToolAdapter):
222
405
  principal=Principal(type="Agent", id=cortex_hub.agent_id),
223
406
  action=Action(type="tool.invoke", name=tool_name),
224
407
  resource=PolicyResource(type="Tool", id=tool_name),
225
- args=tool_input,
408
+ args=policy_args,
226
409
  framework="claude_agents",
227
410
  )
228
411
 
@@ -230,8 +413,23 @@ class ClaudeAgentsAdapter(ToolAdapter):
230
413
  if cortex_hub.enforce and cortex_hub.evaluator:
231
414
  from cortexhub.policy.effects import Effect
232
415
  decision = cortex_hub.evaluator.evaluate(request)
416
+
417
+ span.add_event(
418
+ "policy.decision",
419
+ attributes={
420
+ "decision.effect": decision.effect.value,
421
+ "decision.policy_id": decision.policy_id or "",
422
+ "decision.reasoning": decision.reasoning,
423
+ "decision.policy_name": decision.policy_name or "",
424
+ },
425
+ )
233
426
 
234
427
  if decision.effect == Effect.DENY:
428
+ _finish_tool_span(
429
+ span,
430
+ success=False,
431
+ error_message=decision.reasoning,
432
+ )
235
433
  return {
236
434
  "hookSpecificOutput": {
237
435
  "hookEventName": "PreToolUse",
@@ -242,7 +440,7 @@ class ClaudeAgentsAdapter(ToolAdapter):
242
440
 
243
441
  if decision.effect == Effect.ESCALATE:
244
442
  try:
245
- context_hash = cortex_hub._compute_context_hash(tool_name, tool_input)
443
+ context_hash = cortex_hub._compute_context_hash(tool_name, policy_args)
246
444
  approval_response = cortex_hub.backend.create_approval(
247
445
  run_id=cortex_hub.session_id,
248
446
  trace_id=cortex_hub._get_current_trace_id(),
@@ -260,6 +458,11 @@ class ClaudeAgentsAdapter(ToolAdapter):
260
458
  approval_id = approval_response.get("approval_id", "unknown")
261
459
  except Exception as e:
262
460
  logger.error("Failed to create approval", error=str(e))
461
+ _finish_tool_span(
462
+ span,
463
+ success=False,
464
+ error_message=str(e),
465
+ )
263
466
  return {
264
467
  "hookSpecificOutput": {
265
468
  "hookEventName": "PreToolUse",
@@ -270,6 +473,20 @@ class ClaudeAgentsAdapter(ToolAdapter):
270
473
  }
271
474
  }
272
475
 
476
+ span.add_event(
477
+ "approval.created",
478
+ attributes={
479
+ "approval_id": approval_id,
480
+ "tool_name": tool_name,
481
+ "policy_id": decision.policy_id or "",
482
+ "expires_at": approval_response.get("expires_at", ""),
483
+ },
484
+ )
485
+ _finish_tool_span(
486
+ span,
487
+ success=False,
488
+ error_message="Approval required",
489
+ )
273
490
  return {
274
491
  "hookSpecificOutput": {
275
492
  "hookEventName": "PreToolUse",
@@ -281,6 +498,10 @@ class ClaudeAgentsAdapter(ToolAdapter):
281
498
  }
282
499
 
283
500
  # Allow execution
501
+ if tool_use_id:
502
+ span_store[tool_use_id] = span
503
+ else:
504
+ span.end()
284
505
  return {}
285
506
 
286
507
  async def post_tool_governance(
@@ -294,6 +515,21 @@ class ClaudeAgentsAdapter(ToolAdapter):
294
515
  """
295
516
  tool_name = input_data.get("tool_name", "unknown")
296
517
  tool_response = input_data.get("tool_response", {})
518
+ tool_input = input_data.get("tool_input", {})
519
+ if not isinstance(tool_input, dict):
520
+ tool_input = {"_raw": tool_input}
521
+ policy_args = cortex_hub._sanitize_policy_args(tool_input)
522
+
523
+ span = span_store.pop(tool_use_id, None) if tool_use_id else None
524
+ if span is None:
525
+ tool_description = f"Claude Agent SDK built-in tool: {tool_name}"
526
+ span = _start_tool_span(
527
+ tool_name=tool_name,
528
+ tool_description=tool_description,
529
+ policy_args=policy_args,
530
+ raw_args=tool_input,
531
+ tool_use_id=tool_use_id,
532
+ )
297
533
 
298
534
  # Log the tool execution
299
535
  logger.debug(
@@ -301,6 +537,8 @@ class ClaudeAgentsAdapter(ToolAdapter):
301
537
  tool=tool_name,
302
538
  framework="claude_agents",
303
539
  )
540
+
541
+ _finish_tool_span(span, success=True, result=tool_response)
304
542
 
305
543
  return {}
306
544