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,283 @@
|
|
|
1
|
+
"""Failure-isolated per-execution FIFO lifecycle dispatcher for Tracer sinks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from nexus.application.execution_context import bind_execution_context
|
|
10
|
+
from nexus.domain.observability import (
|
|
11
|
+
RunExecutionContext,
|
|
12
|
+
TelemetryEvent,
|
|
13
|
+
TraceRunFinish,
|
|
14
|
+
TraceRunStart,
|
|
15
|
+
)
|
|
16
|
+
from nexus.domain.ports.tracer import Tracer
|
|
17
|
+
from nexus.domain.runtime_events import TraceFallback, TraceOperation, TraceSink
|
|
18
|
+
|
|
19
|
+
WarningEmitter = Callable[
|
|
20
|
+
[str, TraceSink, TraceOperation, TraceFallback], Awaitable[None]
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class TracerSink:
|
|
26
|
+
kind: TraceSink
|
|
27
|
+
tracer: Tracer
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class _Start:
|
|
32
|
+
value: TraceRunStart
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, slots=True)
|
|
36
|
+
class _Record:
|
|
37
|
+
value: TelemetryEvent
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class _Finish:
|
|
42
|
+
value: TraceRunFinish
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_Control = _Start | _Record | _Finish
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(slots=True)
|
|
49
|
+
class _SinkExecution:
|
|
50
|
+
sink: TracerSink
|
|
51
|
+
context: RunExecutionContext
|
|
52
|
+
queue: asyncio.Queue[_Control]
|
|
53
|
+
worker: asyncio.Task[None] | None = None
|
|
54
|
+
healthy: bool = True
|
|
55
|
+
warned: bool = False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SafeTracerDispatcher:
|
|
59
|
+
"""Give every enabled sink one FIFO worker and contain every adapter failure."""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
sinks: tuple[TracerSink, ...],
|
|
64
|
+
*,
|
|
65
|
+
warning_emitter: WarningEmitter,
|
|
66
|
+
queue_size: int = 256,
|
|
67
|
+
flush_timeout_seconds: float = 2.0,
|
|
68
|
+
) -> None:
|
|
69
|
+
if queue_size < 1 or flush_timeout_seconds <= 0:
|
|
70
|
+
raise ValueError("Tracer queue and flush timeout must be positive.")
|
|
71
|
+
if len({sink.kind for sink in sinks}) != len(sinks):
|
|
72
|
+
raise ValueError("Tracer sink kinds must be unique.")
|
|
73
|
+
self._sinks = sinks
|
|
74
|
+
self._warning_emitter = warning_emitter
|
|
75
|
+
self._queue_size = queue_size
|
|
76
|
+
self._flush_timeout_seconds = flush_timeout_seconds
|
|
77
|
+
self._executions: dict[str, dict[TraceSink, _SinkExecution]] = {}
|
|
78
|
+
self._cleanup_tasks: dict[str, asyncio.Task[None]] = {}
|
|
79
|
+
|
|
80
|
+
def start_execution(self, start: TraceRunStart) -> None:
|
|
81
|
+
execution_id = start.context.execution_id
|
|
82
|
+
if execution_id in self._executions:
|
|
83
|
+
raise RuntimeError("Tracer execution already exists.")
|
|
84
|
+
states: dict[TraceSink, _SinkExecution] = {}
|
|
85
|
+
self._executions[execution_id] = states
|
|
86
|
+
for sink in self._sinks:
|
|
87
|
+
queue: asyncio.Queue[_Control] = asyncio.Queue(self._queue_size)
|
|
88
|
+
state = _SinkExecution(sink=sink, context=start.context, queue=queue)
|
|
89
|
+
state.worker = asyncio.create_task(self._work(execution_id, state))
|
|
90
|
+
states[sink.kind] = state
|
|
91
|
+
queue.put_nowait(_Start(start))
|
|
92
|
+
|
|
93
|
+
def record(self, event: TelemetryEvent) -> None:
|
|
94
|
+
states = self._executions.get(event.execution_id)
|
|
95
|
+
if states is None:
|
|
96
|
+
return
|
|
97
|
+
excluded = _warning_sink(event)
|
|
98
|
+
for kind, state in states.items():
|
|
99
|
+
if not state.healthy or kind is excluded:
|
|
100
|
+
continue
|
|
101
|
+
self._offer(execution_id=event.execution_id, state=state, item=_Record(event))
|
|
102
|
+
|
|
103
|
+
def finish_execution(self, finish: TraceRunFinish) -> None:
|
|
104
|
+
execution_id = finish.context.execution_id
|
|
105
|
+
states = self._executions.get(execution_id)
|
|
106
|
+
if states is None:
|
|
107
|
+
return
|
|
108
|
+
for state in states.values():
|
|
109
|
+
if state.healthy:
|
|
110
|
+
self._offer(
|
|
111
|
+
execution_id=execution_id,
|
|
112
|
+
state=state,
|
|
113
|
+
item=_Finish(finish),
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
_discard_queued(state.queue)
|
|
117
|
+
state.queue.put_nowait(_Finish(finish))
|
|
118
|
+
self._schedule_cleanup(execution_id)
|
|
119
|
+
|
|
120
|
+
async def drain_execution(self, execution_id: str) -> None:
|
|
121
|
+
task = self._cleanup_tasks.get(execution_id)
|
|
122
|
+
if task is None:
|
|
123
|
+
if execution_id not in self._executions:
|
|
124
|
+
return
|
|
125
|
+
task = self._schedule_cleanup(execution_id)
|
|
126
|
+
await asyncio.shield(task)
|
|
127
|
+
|
|
128
|
+
async def _drain_execution(self, execution_id: str) -> None:
|
|
129
|
+
states = self._executions.get(execution_id)
|
|
130
|
+
if not states:
|
|
131
|
+
self._executions.pop(execution_id, None)
|
|
132
|
+
return
|
|
133
|
+
workers = tuple(
|
|
134
|
+
state.worker for state in states.values() if state.worker is not None
|
|
135
|
+
)
|
|
136
|
+
if not workers:
|
|
137
|
+
self._executions.pop(execution_id, None)
|
|
138
|
+
return
|
|
139
|
+
_, pending = await asyncio.wait(
|
|
140
|
+
workers, timeout=self._flush_timeout_seconds
|
|
141
|
+
)
|
|
142
|
+
if pending:
|
|
143
|
+
for state in states.values():
|
|
144
|
+
if state.worker is not None and state.worker in pending:
|
|
145
|
+
state.worker.cancel()
|
|
146
|
+
await self._warn(
|
|
147
|
+
execution_id,
|
|
148
|
+
state,
|
|
149
|
+
"TRACE_FLUSH_FAILED",
|
|
150
|
+
TraceOperation.FLUSH,
|
|
151
|
+
)
|
|
152
|
+
await asyncio.gather(*workers, return_exceptions=True)
|
|
153
|
+
self._executions.pop(execution_id, None)
|
|
154
|
+
|
|
155
|
+
async def close(self) -> None:
|
|
156
|
+
execution_ids = tuple(self._executions.keys() | self._cleanup_tasks.keys())
|
|
157
|
+
for execution_id in execution_ids:
|
|
158
|
+
await self.drain_execution(execution_id)
|
|
159
|
+
|
|
160
|
+
def _schedule_cleanup(self, execution_id: str) -> asyncio.Task[None]:
|
|
161
|
+
existing = self._cleanup_tasks.get(execution_id)
|
|
162
|
+
if existing is not None:
|
|
163
|
+
return existing
|
|
164
|
+
task = asyncio.create_task(self._run_cleanup(execution_id))
|
|
165
|
+
self._cleanup_tasks[execution_id] = task
|
|
166
|
+
return task
|
|
167
|
+
|
|
168
|
+
async def _run_cleanup(self, execution_id: str) -> None:
|
|
169
|
+
try:
|
|
170
|
+
await self._drain_execution(execution_id)
|
|
171
|
+
finally:
|
|
172
|
+
self._executions.pop(execution_id, None)
|
|
173
|
+
current = asyncio.current_task()
|
|
174
|
+
if self._cleanup_tasks.get(execution_id) is current:
|
|
175
|
+
self._cleanup_tasks.pop(execution_id, None)
|
|
176
|
+
|
|
177
|
+
def _offer(
|
|
178
|
+
self,
|
|
179
|
+
*,
|
|
180
|
+
execution_id: str,
|
|
181
|
+
state: _SinkExecution,
|
|
182
|
+
item: _Control,
|
|
183
|
+
) -> None:
|
|
184
|
+
try:
|
|
185
|
+
state.queue.put_nowait(item)
|
|
186
|
+
except asyncio.QueueFull:
|
|
187
|
+
state.healthy = False
|
|
188
|
+
asyncio.create_task(
|
|
189
|
+
self._warn(
|
|
190
|
+
execution_id,
|
|
191
|
+
state,
|
|
192
|
+
"TRACE_SINK_OVERFLOW",
|
|
193
|
+
TraceOperation.RECORD,
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
async def _work(self, execution_id: str, state: _SinkExecution) -> None:
|
|
198
|
+
while True:
|
|
199
|
+
item = await state.queue.get()
|
|
200
|
+
if not state.healthy:
|
|
201
|
+
if isinstance(item, _Finish):
|
|
202
|
+
return
|
|
203
|
+
continue
|
|
204
|
+
try:
|
|
205
|
+
if isinstance(item, _Start):
|
|
206
|
+
await state.sink.tracer.start_run(item.value)
|
|
207
|
+
elif isinstance(item, _Record):
|
|
208
|
+
await state.sink.tracer.record(item.value)
|
|
209
|
+
else:
|
|
210
|
+
await state.sink.tracer.finish(item.value)
|
|
211
|
+
return
|
|
212
|
+
except asyncio.CancelledError:
|
|
213
|
+
raise
|
|
214
|
+
except Exception:
|
|
215
|
+
state.healthy = False
|
|
216
|
+
operation = (
|
|
217
|
+
TraceOperation.START
|
|
218
|
+
if isinstance(item, _Start)
|
|
219
|
+
else TraceOperation.RECORD
|
|
220
|
+
if isinstance(item, _Record)
|
|
221
|
+
else TraceOperation.FINISH
|
|
222
|
+
)
|
|
223
|
+
await self._warn(
|
|
224
|
+
execution_id,
|
|
225
|
+
state,
|
|
226
|
+
"TRACE_SINK_FAILED",
|
|
227
|
+
operation,
|
|
228
|
+
)
|
|
229
|
+
if isinstance(item, _Finish):
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
async def _warn(
|
|
233
|
+
self,
|
|
234
|
+
execution_id: str,
|
|
235
|
+
state: _SinkExecution,
|
|
236
|
+
code: str,
|
|
237
|
+
operation: TraceOperation,
|
|
238
|
+
) -> None:
|
|
239
|
+
if state.warned:
|
|
240
|
+
return
|
|
241
|
+
state.warned = True
|
|
242
|
+
try:
|
|
243
|
+
with bind_execution_context(state.context):
|
|
244
|
+
await self._warning_emitter(
|
|
245
|
+
code,
|
|
246
|
+
state.sink.kind,
|
|
247
|
+
operation,
|
|
248
|
+
self._fallback(execution_id, state.sink.kind),
|
|
249
|
+
)
|
|
250
|
+
except Exception:
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
def _fallback(self, execution_id: str, failed: TraceSink) -> TraceFallback:
|
|
254
|
+
states = self._executions.get(execution_id, {})
|
|
255
|
+
if failed is not TraceSink.CONSOLE:
|
|
256
|
+
console = states.get(TraceSink.CONSOLE)
|
|
257
|
+
if console is not None and console.healthy:
|
|
258
|
+
return TraceFallback.CONSOLE
|
|
259
|
+
if failed is not TraceSink.LANGSMITH:
|
|
260
|
+
langsmith = states.get(TraceSink.LANGSMITH)
|
|
261
|
+
if langsmith is not None and langsmith.healthy:
|
|
262
|
+
return TraceFallback.LANGSMITH
|
|
263
|
+
return TraceFallback.NOOP
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _warning_sink(event: TelemetryEvent) -> TraceSink | None:
|
|
267
|
+
if event.event_type.value != "observability.warning":
|
|
268
|
+
return None
|
|
269
|
+
value = event.payload.get("sink")
|
|
270
|
+
if not isinstance(value, str):
|
|
271
|
+
return None
|
|
272
|
+
try:
|
|
273
|
+
return TraceSink(value)
|
|
274
|
+
except ValueError:
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _discard_queued(queue: asyncio.Queue[_Control]) -> None:
|
|
279
|
+
while True:
|
|
280
|
+
try:
|
|
281
|
+
queue.get_nowait()
|
|
282
|
+
except asyncio.QueueEmpty:
|
|
283
|
+
return
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Deterministic Day 4 validation selection and governed execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
from nexus.application.tool_runtime import ToolRuntime
|
|
10
|
+
from nexus.domain.exploration import ExplorationResult, WorkingContext
|
|
11
|
+
from nexus.domain.planning import ApprovedPlanEvidence, ChangedFile, Plan
|
|
12
|
+
from nexus.domain.runtime_events import (
|
|
13
|
+
RuntimeEvent,
|
|
14
|
+
ValidationFinished,
|
|
15
|
+
ValidationStarted,
|
|
16
|
+
)
|
|
17
|
+
from nexus.domain.tooling import ToolInvocation, ToolResult
|
|
18
|
+
from nexus.domain.validation import (
|
|
19
|
+
ValidationCheck,
|
|
20
|
+
ValidationCheckKind,
|
|
21
|
+
ValidationCheckResult,
|
|
22
|
+
ValidationConfidence,
|
|
23
|
+
ValidationPlan,
|
|
24
|
+
ValidationResult,
|
|
25
|
+
ValidationStatus,
|
|
26
|
+
aggregate_repairable,
|
|
27
|
+
)
|
|
28
|
+
from nexus.errors import ValidationError
|
|
29
|
+
|
|
30
|
+
RuntimeEventEmitter = Callable[[RuntimeEvent], Awaitable[None]]
|
|
31
|
+
|
|
32
|
+
_PREFIX_KINDS = {
|
|
33
|
+
"TEST": ValidationCheckKind.TEST,
|
|
34
|
+
"BUILD": ValidationCheckKind.BUILD,
|
|
35
|
+
"LINT": ValidationCheckKind.LINT,
|
|
36
|
+
"TYPE_CHECK": ValidationCheckKind.TYPE_CHECK,
|
|
37
|
+
"REPOSITORY_COMMAND": ValidationCheckKind.REPOSITORY_COMMAND,
|
|
38
|
+
"GENERATED_TARGETED_TEST": ValidationCheckKind.GENERATED_TARGETED_TEST,
|
|
39
|
+
"BASIC_EXECUTION": ValidationCheckKind.BASIC_EXECUTION,
|
|
40
|
+
}
|
|
41
|
+
_PRIORITY = {kind: index for index, kind in enumerate(_PREFIX_KINDS.values())}
|
|
42
|
+
_CONCLUSIVE_CODE_KINDS = {
|
|
43
|
+
ValidationCheckKind.TEST,
|
|
44
|
+
ValidationCheckKind.BUILD,
|
|
45
|
+
ValidationCheckKind.GENERATED_TARGETED_TEST,
|
|
46
|
+
ValidationCheckKind.BASIC_EXECUTION,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class DeterministicValidationPlanner:
|
|
51
|
+
async def plan(
|
|
52
|
+
self,
|
|
53
|
+
*,
|
|
54
|
+
task: str,
|
|
55
|
+
plan: Plan,
|
|
56
|
+
exploration: ExplorationResult,
|
|
57
|
+
context: WorkingContext,
|
|
58
|
+
changed_files: tuple[ChangedFile, ...],
|
|
59
|
+
) -> ValidationPlan:
|
|
60
|
+
del task, exploration, context
|
|
61
|
+
candidates: list[tuple[int, int, ValidationCheckKind, tuple[str, ...], str]] = []
|
|
62
|
+
for step in plan.steps:
|
|
63
|
+
if step.command_argv is None or step.command_cwd is None:
|
|
64
|
+
continue
|
|
65
|
+
kind = _kind_from_description(step.description)
|
|
66
|
+
candidates.append(
|
|
67
|
+
(_PRIORITY[kind], step.sequence, kind, step.command_argv, step.command_cwd)
|
|
68
|
+
)
|
|
69
|
+
candidates.sort(key=lambda item: (item[0], item[1]))
|
|
70
|
+
|
|
71
|
+
checks: list[ValidationCheck] = []
|
|
72
|
+
for _, _, kind, argv, cwd in candidates:
|
|
73
|
+
checks.append(
|
|
74
|
+
ValidationCheck(
|
|
75
|
+
str(uuid4()),
|
|
76
|
+
len(checks) + 1,
|
|
77
|
+
kind,
|
|
78
|
+
"shell",
|
|
79
|
+
{"argv": list(argv), "cwd": cwd},
|
|
80
|
+
"Explicit validation command in the approved Plan.",
|
|
81
|
+
True,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
read_only = (
|
|
85
|
+
not changed_files
|
|
86
|
+
and not plan.authorization_scope.allowed_write_actions
|
|
87
|
+
and not plan.authorization_scope.allowed_commands
|
|
88
|
+
and not candidates
|
|
89
|
+
)
|
|
90
|
+
if changed_files or read_only:
|
|
91
|
+
checks.append(
|
|
92
|
+
ValidationCheck(
|
|
93
|
+
str(uuid4()),
|
|
94
|
+
len(checks) + 1,
|
|
95
|
+
ValidationCheckKind.DIFF_INSPECTION,
|
|
96
|
+
"git_diff",
|
|
97
|
+
{"staged": False},
|
|
98
|
+
"Every changed-file run requires exact diff inspection.",
|
|
99
|
+
True,
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
return ValidationPlan(tuple(checks))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ToolValidationRunner:
|
|
106
|
+
def __init__(
|
|
107
|
+
self,
|
|
108
|
+
tool_runtime: ToolRuntime,
|
|
109
|
+
*,
|
|
110
|
+
emit: RuntimeEventEmitter | None = None,
|
|
111
|
+
) -> None:
|
|
112
|
+
self._tool_runtime = tool_runtime
|
|
113
|
+
self._emit = emit or _ignore_event
|
|
114
|
+
|
|
115
|
+
async def run(
|
|
116
|
+
self,
|
|
117
|
+
plan: ValidationPlan,
|
|
118
|
+
*,
|
|
119
|
+
run_id: str,
|
|
120
|
+
session_id: str,
|
|
121
|
+
authorization: ApprovedPlanEvidence,
|
|
122
|
+
repair_count: int,
|
|
123
|
+
changed: bool,
|
|
124
|
+
) -> ValidationResult:
|
|
125
|
+
_validate_plan(plan, authorization)
|
|
126
|
+
started = time.perf_counter()
|
|
127
|
+
await self._emit(
|
|
128
|
+
ValidationStarted(
|
|
129
|
+
run_id=run_id,
|
|
130
|
+
session_id=session_id,
|
|
131
|
+
check_ids=tuple(check.check_id for check in plan.checks),
|
|
132
|
+
check_kinds=tuple(check.kind for check in plan.checks),
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
results: list[ValidationCheckResult] = []
|
|
136
|
+
for check in plan.checks:
|
|
137
|
+
invocation = ToolInvocation(
|
|
138
|
+
check.check_id,
|
|
139
|
+
check.tool_name,
|
|
140
|
+
check.arguments,
|
|
141
|
+
run_id,
|
|
142
|
+
session_id,
|
|
143
|
+
)
|
|
144
|
+
result = await self._tool_runtime.execute(
|
|
145
|
+
invocation,
|
|
146
|
+
authorization=authorization if check.tool_name == "shell" else None,
|
|
147
|
+
)
|
|
148
|
+
results.append(_check_result(check, result))
|
|
149
|
+
|
|
150
|
+
status = _aggregate_status(
|
|
151
|
+
tuple(results),
|
|
152
|
+
changed=changed,
|
|
153
|
+
)
|
|
154
|
+
confidence = _confidence(status, tuple(results))
|
|
155
|
+
repairable = aggregate_repairable(status, tuple(results))
|
|
156
|
+
validation = ValidationResult(
|
|
157
|
+
plan.checks,
|
|
158
|
+
tuple(results),
|
|
159
|
+
status,
|
|
160
|
+
confidence,
|
|
161
|
+
repairable,
|
|
162
|
+
repair_count,
|
|
163
|
+
_summary(status, tuple(results)),
|
|
164
|
+
)
|
|
165
|
+
await self._emit(
|
|
166
|
+
ValidationFinished(
|
|
167
|
+
run_id=run_id,
|
|
168
|
+
session_id=session_id,
|
|
169
|
+
validation_status=status,
|
|
170
|
+
confidence=confidence,
|
|
171
|
+
executed_check_count=len(results),
|
|
172
|
+
repair_count=repair_count,
|
|
173
|
+
duration_ms=max(0, int((time.perf_counter() - started) * 1000)),
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
return validation
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _kind_from_description(description: str) -> ValidationCheckKind:
|
|
180
|
+
prefix, separator, _ = description.partition(":")
|
|
181
|
+
if separator:
|
|
182
|
+
kind = _PREFIX_KINDS.get(prefix.strip().upper())
|
|
183
|
+
if kind is not None:
|
|
184
|
+
return kind
|
|
185
|
+
return ValidationCheckKind.REPOSITORY_COMMAND
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _check_result(check: ValidationCheck, result: ToolResult) -> ValidationCheckResult:
|
|
189
|
+
truncated = bool(
|
|
190
|
+
result.output
|
|
191
|
+
and (
|
|
192
|
+
result.output.get("truncated") is True
|
|
193
|
+
or result.output.get("output_truncated") is True
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
if result.success and not truncated:
|
|
197
|
+
status = ValidationStatus.PASS
|
|
198
|
+
summary = f"{check.kind.value} completed successfully."
|
|
199
|
+
elif result.error is not None and result.error.code == "COMMAND_EXIT_NONZERO":
|
|
200
|
+
status = ValidationStatus.FAIL
|
|
201
|
+
summary = f"{check.kind.value} completed with a non-zero exit."
|
|
202
|
+
else:
|
|
203
|
+
status = ValidationStatus.UNKNOWN
|
|
204
|
+
code = "NO_EVIDENCE" if result.error is None else result.error.code
|
|
205
|
+
summary = f"{check.kind.value} is inconclusive ({code})."
|
|
206
|
+
return ValidationCheckResult(check, status, result, summary)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _aggregate_status(
|
|
210
|
+
results: tuple[ValidationCheckResult, ...],
|
|
211
|
+
*,
|
|
212
|
+
changed: bool,
|
|
213
|
+
) -> ValidationStatus:
|
|
214
|
+
required = tuple(result for result in results if result.check.required)
|
|
215
|
+
if any(result.status is ValidationStatus.FAIL for result in required):
|
|
216
|
+
return ValidationStatus.FAIL
|
|
217
|
+
if not required or any(result.status is ValidationStatus.UNKNOWN for result in required):
|
|
218
|
+
return ValidationStatus.UNKNOWN
|
|
219
|
+
has_diff = any(
|
|
220
|
+
result.check.kind is ValidationCheckKind.DIFF_INSPECTION
|
|
221
|
+
and result.status is ValidationStatus.PASS
|
|
222
|
+
for result in required
|
|
223
|
+
)
|
|
224
|
+
has_code_check = any(
|
|
225
|
+
result.check.kind in _CONCLUSIVE_CODE_KINDS
|
|
226
|
+
and result.status is ValidationStatus.PASS
|
|
227
|
+
for result in required
|
|
228
|
+
)
|
|
229
|
+
if changed and (not has_diff or not has_code_check):
|
|
230
|
+
return ValidationStatus.UNKNOWN
|
|
231
|
+
return ValidationStatus.PASS
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _confidence(
|
|
235
|
+
status: ValidationStatus,
|
|
236
|
+
results: tuple[ValidationCheckResult, ...],
|
|
237
|
+
) -> ValidationConfidence:
|
|
238
|
+
if status is ValidationStatus.UNKNOWN:
|
|
239
|
+
return ValidationConfidence.LOW
|
|
240
|
+
behavioral_count = sum(
|
|
241
|
+
result.check.kind in _CONCLUSIVE_CODE_KINDS
|
|
242
|
+
and result.status is ValidationStatus.PASS
|
|
243
|
+
for result in results
|
|
244
|
+
)
|
|
245
|
+
if status is ValidationStatus.PASS and behavioral_count > 1:
|
|
246
|
+
return ValidationConfidence.HIGH
|
|
247
|
+
return ValidationConfidence.MEDIUM
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _summary(
|
|
251
|
+
status: ValidationStatus,
|
|
252
|
+
results: tuple[ValidationCheckResult, ...],
|
|
253
|
+
) -> str:
|
|
254
|
+
counts = {
|
|
255
|
+
value: sum(result.status is value for result in results)
|
|
256
|
+
for value in ValidationStatus
|
|
257
|
+
}
|
|
258
|
+
return (
|
|
259
|
+
f"Validation {status.value}: {counts[ValidationStatus.PASS]} passed, "
|
|
260
|
+
f"{counts[ValidationStatus.FAIL]} failed, "
|
|
261
|
+
f"{counts[ValidationStatus.UNKNOWN]} unknown."
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
async def _ignore_event(event: RuntimeEvent) -> None:
|
|
266
|
+
del event
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _validate_plan(
|
|
270
|
+
plan: ValidationPlan,
|
|
271
|
+
authorization: ApprovedPlanEvidence,
|
|
272
|
+
) -> None:
|
|
273
|
+
for check in plan.checks:
|
|
274
|
+
if check.kind is ValidationCheckKind.DIFF_INSPECTION:
|
|
275
|
+
valid = check.tool_name == "git_diff" and check.arguments == {
|
|
276
|
+
"staged": False
|
|
277
|
+
}
|
|
278
|
+
else:
|
|
279
|
+
argv = check.arguments.get("argv")
|
|
280
|
+
cwd = check.arguments.get("cwd")
|
|
281
|
+
valid = (
|
|
282
|
+
check.tool_name == "shell"
|
|
283
|
+
and isinstance(argv, list)
|
|
284
|
+
and all(isinstance(item, str) for item in argv)
|
|
285
|
+
and isinstance(cwd, str)
|
|
286
|
+
and (tuple(argv), cwd)
|
|
287
|
+
in authorization.authorization_scope.allowed_commands
|
|
288
|
+
)
|
|
289
|
+
if not valid:
|
|
290
|
+
raise ValidationError(
|
|
291
|
+
"A selected check violates the approved validation contract.",
|
|
292
|
+
code="VALIDATION_PLAN_INVALID",
|
|
293
|
+
)
|
nexus/config/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Typed runtime configuration and source resolution."""
|
|
2
|
+
|
|
3
|
+
from nexus.config.loader import load_runtime_config
|
|
4
|
+
from nexus.config.models import (
|
|
5
|
+
MCPServerConfig,
|
|
6
|
+
MCPToolRiskConfig,
|
|
7
|
+
MCPTransport,
|
|
8
|
+
RuntimeConfig,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"MCPServerConfig",
|
|
13
|
+
"MCPToolRiskConfig",
|
|
14
|
+
"MCPTransport",
|
|
15
|
+
"RuntimeConfig",
|
|
16
|
+
"load_runtime_config",
|
|
17
|
+
]
|