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,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
+ )
@@ -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
+ ]