nexus-coding-agent 0.1.0__py3-none-any.whl

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 (131) hide show
  1. nexus/__init__.py +4 -0
  2. nexus/application/__init__.py +2 -0
  3. nexus/application/approval_service.py +130 -0
  4. nexus/application/diff_service.py +177 -0
  5. nexus/application/event_publisher.py +180 -0
  6. nexus/application/execution_context.py +29 -0
  7. nexus/application/execution_ledger.py +126 -0
  8. nexus/application/model_call_context.py +29 -0
  9. nexus/application/observed_model_gateway.py +194 -0
  10. nexus/application/plan_approval_service.py +250 -0
  11. nexus/application/planning.py +668 -0
  12. nexus/application/runtime.py +624 -0
  13. nexus/application/session_service.py +411 -0
  14. nexus/application/structured_output.py +96 -0
  15. nexus/application/telemetry.py +396 -0
  16. nexus/application/telemetry_subscriber.py +216 -0
  17. nexus/application/tool_runtime.py +496 -0
  18. nexus/application/tracing_dispatcher.py +283 -0
  19. nexus/application/validation.py +293 -0
  20. nexus/config/__init__.py +17 -0
  21. nexus/config/loader.py +253 -0
  22. nexus/config/models.py +310 -0
  23. nexus/context/__init__.py +6 -0
  24. nexus/context/budget.py +24 -0
  25. nexus/context/builder.py +92 -0
  26. nexus/context/chunking.py +65 -0
  27. nexus/context/explorer.py +246 -0
  28. nexus/context/integration.py +63 -0
  29. nexus/context/manager.py +328 -0
  30. nexus/context/retrieval.py +261 -0
  31. nexus/domain/__init__.py +2 -0
  32. nexus/domain/agent_decision.py +113 -0
  33. nexus/domain/agent_state.py +49 -0
  34. nexus/domain/approvals.py +64 -0
  35. nexus/domain/context.py +108 -0
  36. nexus/domain/exploration.py +86 -0
  37. nexus/domain/mcp.py +51 -0
  38. nexus/domain/model.py +173 -0
  39. nexus/domain/observability.py +326 -0
  40. nexus/domain/persistence.py +122 -0
  41. nexus/domain/planning.py +329 -0
  42. nexus/domain/ports/__init__.py +61 -0
  43. nexus/domain/ports/agent_decision.py +9 -0
  44. nexus/domain/ports/approval_unit_of_work.py +27 -0
  45. nexus/domain/ports/checkpoint_provider.py +11 -0
  46. nexus/domain/ports/context.py +71 -0
  47. nexus/domain/ports/events.py +21 -0
  48. nexus/domain/ports/graph_runtime.py +30 -0
  49. nexus/domain/ports/mcp.py +23 -0
  50. nexus/domain/ports/model_gateway.py +21 -0
  51. nexus/domain/ports/model_input_budget.py +15 -0
  52. nexus/domain/ports/planning.py +62 -0
  53. nexus/domain/ports/repositories.py +54 -0
  54. nexus/domain/ports/repository_context.py +18 -0
  55. nexus/domain/ports/session_unit_of_work.py +33 -0
  56. nexus/domain/ports/skills.py +41 -0
  57. nexus/domain/ports/tooling.py +39 -0
  58. nexus/domain/ports/tracer.py +13 -0
  59. nexus/domain/ports/validation.py +32 -0
  60. nexus/domain/runtime_events.py +445 -0
  61. nexus/domain/skills.py +129 -0
  62. nexus/domain/tooling.py +163 -0
  63. nexus/domain/validation.py +161 -0
  64. nexus/errors/__init__.py +82 -0
  65. nexus/evaluation/__init__.py +7 -0
  66. nexus/evaluation/assertions.py +4 -0
  67. nexus/evaluation/evaluator.py +263 -0
  68. nexus/evaluation/fixtures.py +36 -0
  69. nexus/evaluation/harness.py +606 -0
  70. nexus/evaluation/loader.py +415 -0
  71. nexus/evaluation/metrics.py +27 -0
  72. nexus/evaluation/models.py +362 -0
  73. nexus/evaluation/report.py +43 -0
  74. nexus/evaluation/runner.py +480 -0
  75. nexus/infrastructure/__init__.py +2 -0
  76. nexus/infrastructure/asyncio_compat.py +13 -0
  77. nexus/infrastructure/bootstrap/__init__.py +17 -0
  78. nexus/infrastructure/bootstrap/composition.py +663 -0
  79. nexus/infrastructure/checkpoint/__init__.py +5 -0
  80. nexus/infrastructure/checkpoint/postgres.py +62 -0
  81. nexus/infrastructure/database.py +39 -0
  82. nexus/infrastructure/embedding.py +61 -0
  83. nexus/infrastructure/graph/__init__.py +6 -0
  84. nexus/infrastructure/graph/day4_runtime.py +966 -0
  85. nexus/infrastructure/graph/langgraph_runtime.py +172 -0
  86. nexus/infrastructure/mcp/__init__.py +5 -0
  87. nexus/infrastructure/mcp/manager.py +380 -0
  88. nexus/infrastructure/model_gateway/__init__.py +6 -0
  89. nexus/infrastructure/model_gateway/openai_compatible.py +345 -0
  90. nexus/infrastructure/observability/__init__.py +7 -0
  91. nexus/infrastructure/observability/console.py +31 -0
  92. nexus/infrastructure/observability/langsmith.py +236 -0
  93. nexus/infrastructure/observability/structured_logger.py +70 -0
  94. nexus/infrastructure/persistence/__init__.py +13 -0
  95. nexus/infrastructure/persistence/approval_unit_of_work.py +74 -0
  96. nexus/infrastructure/persistence/models.py +146 -0
  97. nexus/infrastructure/persistence/repositories.py +326 -0
  98. nexus/infrastructure/persistence/unit_of_work.py +82 -0
  99. nexus/infrastructure/repository_files.py +117 -0
  100. nexus/infrastructure/sandbox/__init__.py +5 -0
  101. nexus/infrastructure/sandbox/local_process.py +353 -0
  102. nexus/infrastructure/semantic.py +276 -0
  103. nexus/infrastructure/semantic_models.py +56 -0
  104. nexus/interfaces/__init__.py +2 -0
  105. nexus/interfaces/cli/__init__.py +2 -0
  106. nexus/interfaces/cli/__main__.py +6 -0
  107. nexus/interfaces/cli/app.py +266 -0
  108. nexus/interfaces/cli/renderer.py +118 -0
  109. nexus/security/__init__.py +14 -0
  110. nexus/security/approval_policies.py +73 -0
  111. nexus/security/command_policy.py +117 -0
  112. nexus/security/executables.py +107 -0
  113. nexus/security/git_commands.py +65 -0
  114. nexus/security/workspace.py +93 -0
  115. nexus/skills/__init__.py +7 -0
  116. nexus/skills/builtin/__init__.py +1 -0
  117. nexus/skills/builtin/debug-python/SKILL.md +26 -0
  118. nexus/skills/builtin/review-repository/SKILL.md +26 -0
  119. nexus/skills/builtin/write-tests/SKILL.md +26 -0
  120. nexus/skills/loader.py +439 -0
  121. nexus/skills/registry.py +146 -0
  122. nexus/skills/selector.py +152 -0
  123. nexus/tools/__init__.py +6 -0
  124. nexus/tools/editing.py +312 -0
  125. nexus/tools/mcp.py +160 -0
  126. nexus/tools/native.py +654 -0
  127. nexus/tools/registry.py +27 -0
  128. nexus_coding_agent-0.1.0.dist-info/METADATA +142 -0
  129. nexus_coding_agent-0.1.0.dist-info/RECORD +131 -0
  130. nexus_coding_agent-0.1.0.dist-info/WHEEL +4 -0
  131. nexus_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,496 @@
1
+ """Policy-governed single-invocation Day 3 Tool Runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ from collections.abc import Awaitable, Callable
8
+ from dataclasses import replace
9
+
10
+ from nexus.application.approval_service import ApprovalService
11
+ from nexus.application.execution_ledger import ToolExecutionLedger
12
+ from nexus.application.plan_approval_service import PlanApprovalService
13
+ from nexus.domain.approvals import ApprovalRequest
14
+ from nexus.domain.planning import ApprovedPlanEvidence
15
+ from nexus.domain.ports.tooling import ApprovalPolicy, CommandPolicy, Tool
16
+ from nexus.domain.runtime_events import (
17
+ ApprovalActorCategory,
18
+ ApprovalRequested,
19
+ ApprovalResolved,
20
+ ApprovalSubject,
21
+ RuntimeEvent,
22
+ ToolFinished,
23
+ ToolStarted,
24
+ )
25
+ from nexus.domain.tooling import (
26
+ ApprovalDecision,
27
+ PolicyDecision,
28
+ RiskLevel,
29
+ ToolError,
30
+ ToolInvocation,
31
+ ToolResult,
32
+ risk_rank,
33
+ )
34
+ from nexus.errors import NexusError, ToolExecutionError
35
+ from nexus.tools.registry import ToolRegistry
36
+
37
+ RuntimeEventEmitter = Callable[[RuntimeEvent], Awaitable[None]]
38
+
39
+
40
+ class ToolRuntime:
41
+ def __init__(
42
+ self,
43
+ registry: ToolRegistry,
44
+ command_policy: CommandPolicy,
45
+ approval_policy: ApprovalPolicy,
46
+ approval_service: ApprovalService,
47
+ *,
48
+ emit: RuntimeEventEmitter | None = None,
49
+ plan_approval_service: PlanApprovalService | None = None,
50
+ normalize_argv: Callable[[list[str]], list[str]] | None = None,
51
+ ledger: ToolExecutionLedger | None = None,
52
+ unsupported_write_operations: frozenset[str] = frozenset(),
53
+ ) -> None:
54
+ self._registry = registry
55
+ self._command_policy = command_policy
56
+ self._approval_policy = approval_policy
57
+ self._approval_service = approval_service
58
+ self._plan_approval_service = plan_approval_service
59
+ self._normalize_argv = normalize_argv or (lambda argv: list(argv))
60
+ self._ledger = ledger
61
+ self._unsupported_write_operations = frozenset(unsupported_write_operations)
62
+ self._emit = emit or _ignore_event
63
+
64
+ async def execute(
65
+ self,
66
+ invocation: ToolInvocation,
67
+ *,
68
+ authorization: ApprovedPlanEvidence | None = None,
69
+ ) -> ToolResult:
70
+ if self._ledger is not None:
71
+ self._ledger.begin(invocation)
72
+ started = time.perf_counter()
73
+ try:
74
+ tool = self._registry.resolve(invocation.tool_name)
75
+ except ToolExecutionError as exc:
76
+ result = replace(
77
+ _failure(
78
+ invocation,
79
+ risk_level=RiskLevel.DANGEROUS,
80
+ policy_decision=PolicyDecision.DENIED,
81
+ approval_decision=None,
82
+ error=exc,
83
+ ),
84
+ duration_ms=_duration_ms(started),
85
+ )
86
+ await self._emit(
87
+ ToolStarted(
88
+ run_id=invocation.run_id,
89
+ session_id=invocation.session_id,
90
+ invocation_id=invocation.invocation_id,
91
+ tool_name=invocation.tool_name,
92
+ risk_level=result.risk_level,
93
+ )
94
+ )
95
+ await self._emit(_finished_event(invocation, result))
96
+ if self._ledger is not None:
97
+ self._ledger.record(invocation, result)
98
+ return result
99
+
100
+ proposed_risk = self._command_policy.classify(
101
+ operation=invocation.tool_name,
102
+ arguments=invocation.arguments,
103
+ )
104
+ await self._emit(
105
+ ToolStarted(
106
+ run_id=invocation.run_id,
107
+ session_id=invocation.session_id,
108
+ invocation_id=invocation.invocation_id,
109
+ tool_name=invocation.tool_name,
110
+ risk_level=proposed_risk,
111
+ )
112
+ )
113
+ if proposed_risk is RiskLevel.DANGEROUS:
114
+ result = await self._deny_dangerous(invocation)
115
+ elif (
116
+ proposed_risk is RiskLevel.WRITE
117
+ and invocation.tool_name in self._unsupported_write_operations
118
+ ):
119
+ result = await self._deny_unsupported_mcp_write(invocation)
120
+ elif authorization is not None and invocation.tool_name in {
121
+ "apply_patch",
122
+ "write_file",
123
+ "shell",
124
+ }:
125
+ result = await self._resolve_authorized_action(
126
+ tool,
127
+ invocation,
128
+ authorization,
129
+ proposed_risk,
130
+ )
131
+ elif proposed_risk is RiskLevel.WRITE:
132
+ result = await self._resolve_write(invocation)
133
+ else:
134
+ result = await self._execute_safe(tool, invocation, proposed_risk)
135
+ result = replace(result, duration_ms=_duration_ms(started))
136
+ await self._emit(_finished_event(invocation, result))
137
+ if self._ledger is not None:
138
+ self._ledger.record(invocation, result)
139
+ return result
140
+
141
+ async def _resolve_authorized_action(
142
+ self,
143
+ tool: Tool,
144
+ invocation: ToolInvocation,
145
+ authorization: ApprovedPlanEvidence,
146
+ proposed_risk: RiskLevel,
147
+ ) -> ToolResult:
148
+ try:
149
+ if self._plan_approval_service is None:
150
+ raise ToolExecutionError(
151
+ "Plan authorization validation is unavailable.",
152
+ )
153
+ await self._plan_approval_service.require_approved(authorization)
154
+ if (
155
+ authorization.run_id != invocation.run_id
156
+ or authorization.session_id != invocation.session_id
157
+ or not self._is_in_scope(invocation, authorization)
158
+ ):
159
+ raise ToolExecutionError(
160
+ "The Tool action is outside the approved Plan scope.",
161
+ code="PLAN_SCOPE_DENIED",
162
+ )
163
+ except NexusError as exc:
164
+ return _failure(
165
+ invocation,
166
+ risk_level=proposed_risk,
167
+ policy_decision=PolicyDecision.DENIED,
168
+ approval_decision=(
169
+ ApprovalDecision.APPROVED
170
+ if exc.code == "PLAN_SCOPE_DENIED"
171
+ else None
172
+ ),
173
+ error=exc,
174
+ )
175
+ result = await self._execute_safe(tool, invocation, proposed_risk)
176
+ if result.risk_level is RiskLevel.DANGEROUS:
177
+ return result
178
+ return replace(
179
+ result,
180
+ policy_decision=PolicyDecision.ALLOWED,
181
+ approval_decision=ApprovalDecision.APPROVED,
182
+ )
183
+
184
+ def _is_in_scope(
185
+ self,
186
+ invocation: ToolInvocation,
187
+ authorization: ApprovedPlanEvidence,
188
+ ) -> bool:
189
+ scope = authorization.authorization_scope
190
+ if invocation.tool_name in {"apply_patch", "write_file"}:
191
+ path = invocation.arguments.get("path")
192
+ return isinstance(path, str) and (
193
+ invocation.tool_name,
194
+ path,
195
+ ) in scope.allowed_write_actions
196
+ if invocation.tool_name == "shell":
197
+ argv = invocation.arguments.get("argv")
198
+ cwd = invocation.arguments.get("cwd", ".")
199
+ if not isinstance(argv, list) or not all(isinstance(item, str) for item in argv):
200
+ return False
201
+ if not isinstance(cwd, str):
202
+ return False
203
+ return (tuple(self._normalize_argv(argv)), cwd) in scope.allowed_commands
204
+ return False
205
+
206
+ async def _execute_safe(
207
+ self,
208
+ tool: Tool,
209
+ invocation: ToolInvocation,
210
+ proposed_risk: RiskLevel,
211
+ ) -> ToolResult:
212
+ try:
213
+ result = await tool.execute(invocation)
214
+ except asyncio.CancelledError:
215
+ raise
216
+ except Exception:
217
+ return _failure(
218
+ invocation,
219
+ risk_level=proposed_risk,
220
+ policy_decision=PolicyDecision.ALLOWED,
221
+ approval_decision=None,
222
+ error=ToolExecutionError("The Tool failed safely."),
223
+ )
224
+ if result.invocation_id != invocation.invocation_id or result.tool_name != tool.name:
225
+ return _failure(
226
+ invocation,
227
+ risk_level=RiskLevel.DANGEROUS,
228
+ policy_decision=PolicyDecision.DENIED,
229
+ approval_decision=None,
230
+ error=ToolExecutionError("The Tool returned mismatched identity."),
231
+ )
232
+ if risk_rank(result.risk_level) < risk_rank(proposed_risk):
233
+ return _failure(
234
+ invocation,
235
+ risk_level=RiskLevel.DANGEROUS,
236
+ policy_decision=PolicyDecision.DENIED,
237
+ approval_decision=None,
238
+ error=ToolExecutionError("The Tool attempted to downgrade security risk."),
239
+ )
240
+ if result.risk_level is RiskLevel.DANGEROUS:
241
+ try:
242
+ decision = await self._approval_service.record_denied(
243
+ invocation,
244
+ risk_level=RiskLevel.DANGEROUS,
245
+ summary=_safe_summary(invocation),
246
+ reason="Resource security validation escalated the operation risk.",
247
+ )
248
+ await self._emit(_resolved_event(invocation, decision))
249
+ except NexusError as exc:
250
+ return _failure(
251
+ invocation,
252
+ risk_level=RiskLevel.DANGEROUS,
253
+ policy_decision=PolicyDecision.DENIED,
254
+ approval_decision=None,
255
+ error=exc,
256
+ )
257
+ if result.success:
258
+ result = replace(
259
+ result,
260
+ success=False,
261
+ output=None,
262
+ error=ToolError(
263
+ "PERMISSION_DENIED",
264
+ "Resource security validation denied the operation.",
265
+ False,
266
+ ),
267
+ )
268
+ result = replace(
269
+ result,
270
+ policy_decision=PolicyDecision.DENIED,
271
+ approval_decision=ApprovalDecision.DENIED,
272
+ )
273
+ elif risk_rank(result.risk_level) > risk_rank(proposed_risk):
274
+ return _failure(
275
+ invocation,
276
+ risk_level=result.risk_level,
277
+ policy_decision=PolicyDecision.DENIED,
278
+ approval_decision=None,
279
+ error=ToolExecutionError(
280
+ "The escalated operation requires authorization before execution."
281
+ ),
282
+ )
283
+ return result
284
+
285
+ async def _resolve_write(self, invocation: ToolInvocation) -> ToolResult:
286
+ try:
287
+ pending = await self._approval_service.create_pending(
288
+ invocation,
289
+ risk_level=RiskLevel.WRITE,
290
+ summary=_safe_summary(invocation),
291
+ )
292
+ await self._emit(
293
+ ApprovalRequested(
294
+ run_id=invocation.run_id,
295
+ session_id=invocation.session_id,
296
+ approval_id=pending.approval_id,
297
+ invocation_id=invocation.invocation_id,
298
+ operation=invocation.tool_name,
299
+ risk_level=RiskLevel.WRITE,
300
+ resource_or_command_summary=pending.resource_or_command_summary,
301
+ )
302
+ )
303
+ decision = await self._approval_policy.request(pending)
304
+ _validate_approval_decision(pending, decision)
305
+ decision = await self._approval_service.persist_decision(decision)
306
+ await self._emit(_resolved_event(invocation, decision))
307
+ except asyncio.CancelledError:
308
+ raise
309
+ except NexusError as exc:
310
+ return _failure(
311
+ invocation,
312
+ risk_level=RiskLevel.WRITE,
313
+ policy_decision=PolicyDecision.DENIED,
314
+ approval_decision=None,
315
+ error=exc,
316
+ )
317
+ except Exception:
318
+ return _failure(
319
+ invocation,
320
+ risk_level=RiskLevel.WRITE,
321
+ policy_decision=PolicyDecision.DENIED,
322
+ approval_decision=None,
323
+ error=ToolExecutionError("The approval decision source failed safely."),
324
+ )
325
+ if decision.decision is ApprovalDecision.APPROVED:
326
+ error = ToolExecutionError(
327
+ "WRITE execution requires an approved Plan, which Day 3 does not provide.",
328
+ code="PLAN_REQUIRED",
329
+ )
330
+ else:
331
+ error = ToolExecutionError(
332
+ "The WRITE operation was denied.",
333
+ code="COMMAND_DENIED",
334
+ )
335
+ return _failure(
336
+ invocation,
337
+ risk_level=RiskLevel.WRITE,
338
+ policy_decision=PolicyDecision.DENIED,
339
+ approval_decision=decision.decision,
340
+ error=error,
341
+ )
342
+
343
+ async def _deny_dangerous(self, invocation: ToolInvocation) -> ToolResult:
344
+ try:
345
+ decision = await self._approval_service.record_denied(
346
+ invocation,
347
+ risk_level=RiskLevel.DANGEROUS,
348
+ summary=_safe_summary(invocation),
349
+ reason="The operation is hard-denied by the Day 3 security policy.",
350
+ )
351
+ await self._emit(_resolved_event(invocation, decision))
352
+ except NexusError as exc:
353
+ return _failure(
354
+ invocation,
355
+ risk_level=RiskLevel.DANGEROUS,
356
+ policy_decision=PolicyDecision.DENIED,
357
+ approval_decision=None,
358
+ error=exc,
359
+ )
360
+ return _failure(
361
+ invocation,
362
+ risk_level=RiskLevel.DANGEROUS,
363
+ policy_decision=PolicyDecision.DENIED,
364
+ approval_decision=decision.decision,
365
+ error=ToolExecutionError(
366
+ "The operation is hard-denied by the Day 3 security policy.",
367
+ code="PERMISSION_DENIED",
368
+ ),
369
+ )
370
+
371
+ async def _deny_unsupported_mcp_write(
372
+ self,
373
+ invocation: ToolInvocation,
374
+ ) -> ToolResult:
375
+ try:
376
+ decision = await self._approval_service.record_denied(
377
+ invocation,
378
+ risk_level=RiskLevel.WRITE,
379
+ summary=_safe_summary(invocation),
380
+ reason="Generic MCP WRITE authorization is unavailable in Day 6.",
381
+ )
382
+ await self._emit(_resolved_event(invocation, decision))
383
+ except NexusError as exc:
384
+ return _failure(
385
+ invocation,
386
+ risk_level=RiskLevel.WRITE,
387
+ policy_decision=PolicyDecision.DENIED,
388
+ approval_decision=None,
389
+ error=exc,
390
+ )
391
+ return _failure(
392
+ invocation,
393
+ risk_level=RiskLevel.WRITE,
394
+ policy_decision=PolicyDecision.DENIED,
395
+ approval_decision=decision.decision,
396
+ error=ToolExecutionError(
397
+ "Generic MCP WRITE authorization is unavailable in Day 6.",
398
+ code="MCP_WRITE_NOT_AUTHORIZED",
399
+ ),
400
+ )
401
+
402
+
403
+ async def _ignore_event(event: RuntimeEvent) -> None:
404
+ del event
405
+
406
+
407
+ def _finished_event(invocation: ToolInvocation, result: ToolResult) -> ToolFinished:
408
+ return ToolFinished(
409
+ run_id=invocation.run_id,
410
+ session_id=invocation.session_id,
411
+ invocation_id=invocation.invocation_id,
412
+ tool_name=invocation.tool_name,
413
+ success=result.success,
414
+ risk_level=result.risk_level,
415
+ policy_decision=result.policy_decision,
416
+ approval_decision=result.approval_decision,
417
+ duration_ms=result.duration_ms,
418
+ error_code=None if result.error is None else result.error.code,
419
+ )
420
+
421
+
422
+ def _resolved_event(
423
+ invocation: ToolInvocation, approval: ApprovalRequest
424
+ ) -> ApprovalResolved:
425
+ return ApprovalResolved(
426
+ run_id=invocation.run_id,
427
+ session_id=invocation.session_id,
428
+ approval_id=approval.approval_id,
429
+ subject=ApprovalSubject.TOOL,
430
+ invocation_id=invocation.invocation_id,
431
+ plan_id=None,
432
+ plan_version=None,
433
+ decision=approval.decision,
434
+ actor_category=(
435
+ ApprovalActorCategory.USER
436
+ if approval.actor == "user"
437
+ else ApprovalActorCategory.POLICY
438
+ ),
439
+ )
440
+
441
+
442
+ def _failure(
443
+ invocation: ToolInvocation,
444
+ *,
445
+ risk_level: RiskLevel,
446
+ policy_decision: PolicyDecision,
447
+ approval_decision: ApprovalDecision | None,
448
+ error: NexusError,
449
+ ) -> ToolResult:
450
+ return ToolResult(
451
+ invocation.invocation_id,
452
+ invocation.tool_name,
453
+ False,
454
+ None,
455
+ ToolError(error.code, str(error), error.retryable),
456
+ risk_level,
457
+ policy_decision,
458
+ approval_decision,
459
+ 0,
460
+ )
461
+
462
+
463
+ def _safe_summary(invocation: ToolInvocation) -> str:
464
+ if invocation.tool_name == "shell":
465
+ argv = invocation.arguments.get("argv")
466
+ if isinstance(argv, list) and argv:
467
+ executable = str(argv[0]).rsplit("/", 1)[-1].rsplit("\\", 1)[-1]
468
+ return f"shell:{executable} ({len(argv)} argv items)"[:512]
469
+ path = invocation.arguments.get("path")
470
+ if isinstance(path, str):
471
+ return f"{invocation.tool_name}:{path}"[:512]
472
+ return invocation.tool_name[:512]
473
+
474
+
475
+ def _validate_approval_decision(
476
+ pending: ApprovalRequest,
477
+ decision: ApprovalRequest,
478
+ ) -> None:
479
+ immutable_fields = (
480
+ "approval_id",
481
+ "run_id",
482
+ "session_id",
483
+ "operation",
484
+ "risk_level",
485
+ "resource_or_command_summary",
486
+ "created_at",
487
+ )
488
+ if any(getattr(pending, field) != getattr(decision, field) for field in immutable_fields):
489
+ raise ToolExecutionError(
490
+ "The approval policy changed immutable request fields.",
491
+ code="INVALID_APPROVAL_TRANSITION",
492
+ )
493
+
494
+
495
+ def _duration_ms(started: float) -> int:
496
+ return max(0, round((time.perf_counter() - started) * 1000))