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.
- nexus/__init__.py +4 -0
- nexus/application/__init__.py +2 -0
- nexus/application/approval_service.py +130 -0
- nexus/application/diff_service.py +177 -0
- nexus/application/event_publisher.py +180 -0
- nexus/application/execution_context.py +29 -0
- nexus/application/execution_ledger.py +126 -0
- nexus/application/model_call_context.py +29 -0
- nexus/application/observed_model_gateway.py +194 -0
- nexus/application/plan_approval_service.py +250 -0
- nexus/application/planning.py +668 -0
- nexus/application/runtime.py +624 -0
- nexus/application/session_service.py +411 -0
- nexus/application/structured_output.py +96 -0
- nexus/application/telemetry.py +396 -0
- nexus/application/telemetry_subscriber.py +216 -0
- nexus/application/tool_runtime.py +496 -0
- nexus/application/tracing_dispatcher.py +283 -0
- nexus/application/validation.py +293 -0
- nexus/config/__init__.py +17 -0
- nexus/config/loader.py +253 -0
- nexus/config/models.py +310 -0
- nexus/context/__init__.py +6 -0
- nexus/context/budget.py +24 -0
- nexus/context/builder.py +92 -0
- nexus/context/chunking.py +65 -0
- nexus/context/explorer.py +246 -0
- nexus/context/integration.py +63 -0
- nexus/context/manager.py +328 -0
- nexus/context/retrieval.py +261 -0
- nexus/domain/__init__.py +2 -0
- nexus/domain/agent_decision.py +113 -0
- nexus/domain/agent_state.py +49 -0
- nexus/domain/approvals.py +64 -0
- nexus/domain/context.py +108 -0
- nexus/domain/exploration.py +86 -0
- nexus/domain/mcp.py +51 -0
- nexus/domain/model.py +173 -0
- nexus/domain/observability.py +326 -0
- nexus/domain/persistence.py +122 -0
- nexus/domain/planning.py +329 -0
- nexus/domain/ports/__init__.py +61 -0
- nexus/domain/ports/agent_decision.py +9 -0
- nexus/domain/ports/approval_unit_of_work.py +27 -0
- nexus/domain/ports/checkpoint_provider.py +11 -0
- nexus/domain/ports/context.py +71 -0
- nexus/domain/ports/events.py +21 -0
- nexus/domain/ports/graph_runtime.py +30 -0
- nexus/domain/ports/mcp.py +23 -0
- nexus/domain/ports/model_gateway.py +21 -0
- nexus/domain/ports/model_input_budget.py +15 -0
- nexus/domain/ports/planning.py +62 -0
- nexus/domain/ports/repositories.py +54 -0
- nexus/domain/ports/repository_context.py +18 -0
- nexus/domain/ports/session_unit_of_work.py +33 -0
- nexus/domain/ports/skills.py +41 -0
- nexus/domain/ports/tooling.py +39 -0
- nexus/domain/ports/tracer.py +13 -0
- nexus/domain/ports/validation.py +32 -0
- nexus/domain/runtime_events.py +445 -0
- nexus/domain/skills.py +129 -0
- nexus/domain/tooling.py +163 -0
- nexus/domain/validation.py +161 -0
- nexus/errors/__init__.py +82 -0
- nexus/evaluation/__init__.py +7 -0
- nexus/evaluation/assertions.py +4 -0
- nexus/evaluation/evaluator.py +263 -0
- nexus/evaluation/fixtures.py +36 -0
- nexus/evaluation/harness.py +606 -0
- nexus/evaluation/loader.py +415 -0
- nexus/evaluation/metrics.py +27 -0
- nexus/evaluation/models.py +362 -0
- nexus/evaluation/report.py +43 -0
- nexus/evaluation/runner.py +480 -0
- nexus/infrastructure/__init__.py +2 -0
- nexus/infrastructure/asyncio_compat.py +13 -0
- nexus/infrastructure/bootstrap/__init__.py +17 -0
- nexus/infrastructure/bootstrap/composition.py +663 -0
- nexus/infrastructure/checkpoint/__init__.py +5 -0
- nexus/infrastructure/checkpoint/postgres.py +62 -0
- nexus/infrastructure/database.py +39 -0
- nexus/infrastructure/embedding.py +61 -0
- nexus/infrastructure/graph/__init__.py +6 -0
- nexus/infrastructure/graph/day4_runtime.py +966 -0
- nexus/infrastructure/graph/langgraph_runtime.py +172 -0
- nexus/infrastructure/mcp/__init__.py +5 -0
- nexus/infrastructure/mcp/manager.py +380 -0
- nexus/infrastructure/model_gateway/__init__.py +6 -0
- nexus/infrastructure/model_gateway/openai_compatible.py +345 -0
- nexus/infrastructure/observability/__init__.py +7 -0
- nexus/infrastructure/observability/console.py +31 -0
- nexus/infrastructure/observability/langsmith.py +236 -0
- nexus/infrastructure/observability/structured_logger.py +70 -0
- nexus/infrastructure/persistence/__init__.py +13 -0
- nexus/infrastructure/persistence/approval_unit_of_work.py +74 -0
- nexus/infrastructure/persistence/models.py +146 -0
- nexus/infrastructure/persistence/repositories.py +326 -0
- nexus/infrastructure/persistence/unit_of_work.py +82 -0
- nexus/infrastructure/repository_files.py +117 -0
- nexus/infrastructure/sandbox/__init__.py +5 -0
- nexus/infrastructure/sandbox/local_process.py +353 -0
- nexus/infrastructure/semantic.py +276 -0
- nexus/infrastructure/semantic_models.py +56 -0
- nexus/interfaces/__init__.py +2 -0
- nexus/interfaces/cli/__init__.py +2 -0
- nexus/interfaces/cli/__main__.py +6 -0
- nexus/interfaces/cli/app.py +266 -0
- nexus/interfaces/cli/renderer.py +118 -0
- nexus/security/__init__.py +14 -0
- nexus/security/approval_policies.py +73 -0
- nexus/security/command_policy.py +117 -0
- nexus/security/executables.py +107 -0
- nexus/security/git_commands.py +65 -0
- nexus/security/workspace.py +93 -0
- nexus/skills/__init__.py +7 -0
- nexus/skills/builtin/__init__.py +1 -0
- nexus/skills/builtin/debug-python/SKILL.md +26 -0
- nexus/skills/builtin/review-repository/SKILL.md +26 -0
- nexus/skills/builtin/write-tests/SKILL.md +26 -0
- nexus/skills/loader.py +439 -0
- nexus/skills/registry.py +146 -0
- nexus/skills/selector.py +152 -0
- nexus/tools/__init__.py +6 -0
- nexus/tools/editing.py +312 -0
- nexus/tools/mcp.py +160 -0
- nexus/tools/native.py +654 -0
- nexus/tools/registry.py +27 -0
- nexus_coding_agent-0.1.0.dist-info/METADATA +142 -0
- nexus_coding_agent-0.1.0.dist-info/RECORD +131 -0
- nexus_coding_agent-0.1.0.dist-info/WHEEL +4 -0
- 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))
|