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,606 @@
|
|
|
1
|
+
"""Sandbox-backed evaluation harness with a closed command grammar."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import os
|
|
7
|
+
import stat
|
|
8
|
+
import sys
|
|
9
|
+
import unicodedata
|
|
10
|
+
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
11
|
+
|
|
12
|
+
from nexus.domain.tooling import PolicyDecision, RiskLevel, SandboxRequest
|
|
13
|
+
from nexus.evaluation.fixtures import copy_fixture_source
|
|
14
|
+
from nexus.evaluation.models import (
|
|
15
|
+
EvalDiscoveryResult,
|
|
16
|
+
EvalDiscoveryStatus,
|
|
17
|
+
EvalForbiddenPathEvidence,
|
|
18
|
+
EvalHarnessCommand,
|
|
19
|
+
EvalHarnessCommandResult,
|
|
20
|
+
EvalHarnessOperation,
|
|
21
|
+
EvalPytestNodeOutcome,
|
|
22
|
+
EvalPytestNodeResult,
|
|
23
|
+
EvalRepositoryEvidence,
|
|
24
|
+
EvalValidationResult,
|
|
25
|
+
EvalValidationStatus,
|
|
26
|
+
FileFingerprint,
|
|
27
|
+
ForbiddenPathSnapshot,
|
|
28
|
+
RepositoryState,
|
|
29
|
+
)
|
|
30
|
+
from nexus.infrastructure.sandbox.local_process import LocalProcessSandbox
|
|
31
|
+
from nexus.security.executables import TrustedExecutables
|
|
32
|
+
from nexus.security.workspace import WorkspaceGuard
|
|
33
|
+
|
|
34
|
+
DEFAULT_EVAL_VALIDATION_TIMEOUT_SECONDS = 120
|
|
35
|
+
_SUMMARY_LIMIT = 16_384
|
|
36
|
+
_IGNORED_DIRS = {
|
|
37
|
+
"__pycache__",
|
|
38
|
+
".pytest_cache",
|
|
39
|
+
".mypy_cache",
|
|
40
|
+
".ruff_cache",
|
|
41
|
+
".nexus-eval-runtime",
|
|
42
|
+
}
|
|
43
|
+
_IGNORED_FILES = {".coverage"}
|
|
44
|
+
_GIT_FORMS = {
|
|
45
|
+
EvalHarnessOperation.GIT_INIT: ("git", "init"),
|
|
46
|
+
EvalHarnessOperation.GIT_ADD_BASELINE: ("git", "add", "--all"),
|
|
47
|
+
EvalHarnessOperation.GIT_COMMIT_BASELINE: (
|
|
48
|
+
"git",
|
|
49
|
+
"-c",
|
|
50
|
+
"user.name=Nexus Eval",
|
|
51
|
+
"-c",
|
|
52
|
+
"user.email=nexus-eval@local.invalid",
|
|
53
|
+
"commit",
|
|
54
|
+
"-m",
|
|
55
|
+
"nexus-eval-baseline",
|
|
56
|
+
),
|
|
57
|
+
EvalHarnessOperation.GIT_STATUS: (
|
|
58
|
+
"git",
|
|
59
|
+
"status",
|
|
60
|
+
"--porcelain=v1",
|
|
61
|
+
"--untracked-files=all",
|
|
62
|
+
),
|
|
63
|
+
EvalHarnessOperation.GIT_DIFF: ("git", "diff", "--no-ext-diff", "--binary"),
|
|
64
|
+
EvalHarnessOperation.GIT_LS_FILES: ("git", "ls-files"),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class EvalHarnessCommandPolicy:
|
|
69
|
+
"""Allow only the exact v0.8 Harness grammar."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, executables: TrustedExecutables) -> None:
|
|
72
|
+
self._executables = executables
|
|
73
|
+
|
|
74
|
+
def classify(self, *, operation: str, arguments: dict[str, object]) -> RiskLevel:
|
|
75
|
+
raw = arguments.get("argv")
|
|
76
|
+
if (
|
|
77
|
+
not isinstance(raw, (list, tuple))
|
|
78
|
+
or not raw
|
|
79
|
+
or not all(isinstance(item, str) for item in raw)
|
|
80
|
+
):
|
|
81
|
+
return RiskLevel.DANGEROUS
|
|
82
|
+
argv = tuple(raw)
|
|
83
|
+
try:
|
|
84
|
+
harness_operation = EvalHarnessOperation(operation)
|
|
85
|
+
except ValueError:
|
|
86
|
+
return RiskLevel.DANGEROUS
|
|
87
|
+
family = self._family(argv[0])
|
|
88
|
+
if family is None:
|
|
89
|
+
return RiskLevel.DANGEROUS
|
|
90
|
+
if harness_operation in _GIT_FORMS:
|
|
91
|
+
expected = _GIT_FORMS[harness_operation]
|
|
92
|
+
allowed = family == "git" and argv[1:] == expected[1:]
|
|
93
|
+
elif harness_operation is EvalHarnessOperation.TEST_DISCOVERY:
|
|
94
|
+
allowed = family == "pytest" and self._valid_discovery(argv[1:])
|
|
95
|
+
elif harness_operation is EvalHarnessOperation.VALIDATION:
|
|
96
|
+
allowed = self._valid_validation(family, argv[1:])
|
|
97
|
+
else:
|
|
98
|
+
allowed = False
|
|
99
|
+
return RiskLevel.SAFE if allowed else RiskLevel.DANGEROUS
|
|
100
|
+
|
|
101
|
+
def _family(self, executable: str) -> str | None:
|
|
102
|
+
configured = {
|
|
103
|
+
"git": self._executables.git,
|
|
104
|
+
"pytest": self._executables.pytest,
|
|
105
|
+
"ruff": self._executables.ruff,
|
|
106
|
+
"mypy": self._executables.mypy,
|
|
107
|
+
}
|
|
108
|
+
candidate = Path(executable)
|
|
109
|
+
if _is_absolute_executable(executable):
|
|
110
|
+
normalized = os.path.normcase(str(candidate.resolve(strict=False)))
|
|
111
|
+
for name, trusted in configured.items():
|
|
112
|
+
if trusted and normalized == os.path.normcase(trusted):
|
|
113
|
+
return name
|
|
114
|
+
return None
|
|
115
|
+
if "/" in executable or "\\" in executable:
|
|
116
|
+
return None
|
|
117
|
+
name = candidate.name.casefold().removesuffix(".exe")
|
|
118
|
+
return name if name in configured and configured[name] is not None else None
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def _valid_discovery(args: tuple[str, ...]) -> bool:
|
|
122
|
+
return (
|
|
123
|
+
len(args) >= 3
|
|
124
|
+
and args[:2] == ("--collect-only", "-q")
|
|
125
|
+
and all(_safe_path(item, node=False) for item in args[2:])
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def _valid_validation(family: str, args: tuple[str, ...]) -> bool:
|
|
130
|
+
if family == "ruff":
|
|
131
|
+
return (
|
|
132
|
+
len(args) >= 2
|
|
133
|
+
and args[0] == "check"
|
|
134
|
+
and all(_safe_path(item, node=False) for item in args[1:])
|
|
135
|
+
)
|
|
136
|
+
if family == "mypy":
|
|
137
|
+
return bool(args) and all(_safe_path(item, node=False) for item in args)
|
|
138
|
+
if family != "pytest":
|
|
139
|
+
return False
|
|
140
|
+
targets: list[str] = []
|
|
141
|
+
index = 0
|
|
142
|
+
while index < len(args):
|
|
143
|
+
argument = args[index]
|
|
144
|
+
if argument in {"-q", "-v", "-vv", "--disable-warnings"}:
|
|
145
|
+
index += 1
|
|
146
|
+
elif argument == "--maxfail":
|
|
147
|
+
if index + 1 >= len(args) or not _bounded_int(args[index + 1]):
|
|
148
|
+
return False
|
|
149
|
+
index += 2
|
|
150
|
+
elif argument.startswith("--maxfail="):
|
|
151
|
+
if not _bounded_int(argument.partition("=")[2]):
|
|
152
|
+
return False
|
|
153
|
+
index += 1
|
|
154
|
+
elif argument.startswith("-"):
|
|
155
|
+
return False
|
|
156
|
+
else:
|
|
157
|
+
targets.append(argument)
|
|
158
|
+
index += 1
|
|
159
|
+
return bool(targets) and all(_safe_path(item, node=True) for item in targets)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class EvalHarnessExecutor:
|
|
163
|
+
"""Adapter from typed Harness commands to the existing sandbox."""
|
|
164
|
+
|
|
165
|
+
def __init__(self, workspace: Path) -> None:
|
|
166
|
+
self._workspace = workspace.resolve(strict=True)
|
|
167
|
+
self._executables = _resolve_eval_executables(self._workspace)
|
|
168
|
+
policy = EvalHarnessCommandPolicy(self._executables)
|
|
169
|
+
self._workspace_guard = WorkspaceGuard(self._workspace)
|
|
170
|
+
self._sandbox = LocalProcessSandbox(self._workspace_guard, policy, self._executables)
|
|
171
|
+
|
|
172
|
+
async def execute(self, command: EvalHarnessCommand) -> EvalHarnessCommandResult:
|
|
173
|
+
if command.cwd.resolve(strict=True) != self._workspace:
|
|
174
|
+
return _denied(command, "WORKSPACE_PATH_DENIED")
|
|
175
|
+
if not _command_paths_are_contained(command, self._workspace):
|
|
176
|
+
return _denied(command, "WORKSPACE_PATH_DENIED")
|
|
177
|
+
result = await self._sandbox.execute(
|
|
178
|
+
SandboxRequest(
|
|
179
|
+
operation=command.operation.value,
|
|
180
|
+
argv=list(command.argv),
|
|
181
|
+
cwd=".",
|
|
182
|
+
timeout_seconds=command.timeout_seconds,
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
allowed = result.policy_decision is PolicyDecision.ALLOWED
|
|
186
|
+
denial_code = None
|
|
187
|
+
infrastructure_error = None
|
|
188
|
+
if not allowed:
|
|
189
|
+
denial_code = result.error.code if result.error else "COMMAND_DENIED"
|
|
190
|
+
elif result.error and result.error.code == "SANDBOX_EXECUTION_ERROR":
|
|
191
|
+
infrastructure_error = result.error.code
|
|
192
|
+
return EvalHarnessCommandResult(
|
|
193
|
+
operation=command.operation,
|
|
194
|
+
allowed=allowed,
|
|
195
|
+
denial_code=denial_code,
|
|
196
|
+
exit_code=result.exit_code,
|
|
197
|
+
stdout_summary=_summary(result.stdout),
|
|
198
|
+
stderr_summary=_summary(result.stderr),
|
|
199
|
+
duration_ms=result.duration_ms,
|
|
200
|
+
timed_out=result.timed_out,
|
|
201
|
+
infrastructure_error=infrastructure_error,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class EvaluationHarness:
|
|
206
|
+
"""Own isolated fixture setup, checks, and authoritative snapshots."""
|
|
207
|
+
|
|
208
|
+
@staticmethod
|
|
209
|
+
def copy_fixture(source: Path, destination: Path) -> None:
|
|
210
|
+
|
|
211
|
+
copy_fixture_source(source, destination)
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
async def initialize_git(workspace: Path) -> tuple[EvalHarnessCommandResult, ...]:
|
|
215
|
+
executor = EvalHarnessExecutor(workspace)
|
|
216
|
+
results: list[EvalHarnessCommandResult] = []
|
|
217
|
+
for operation in (
|
|
218
|
+
EvalHarnessOperation.GIT_INIT,
|
|
219
|
+
EvalHarnessOperation.GIT_ADD_BASELINE,
|
|
220
|
+
EvalHarnessOperation.GIT_COMMIT_BASELINE,
|
|
221
|
+
):
|
|
222
|
+
result = await executor.execute(
|
|
223
|
+
EvalHarnessCommand(operation, _GIT_FORMS[operation], workspace, 30)
|
|
224
|
+
)
|
|
225
|
+
results.append(result)
|
|
226
|
+
if (
|
|
227
|
+
not result.allowed
|
|
228
|
+
or result.timed_out
|
|
229
|
+
or result.infrastructure_error
|
|
230
|
+
or result.exit_code != 0
|
|
231
|
+
):
|
|
232
|
+
break
|
|
233
|
+
return tuple(results)
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
async def validation(
|
|
237
|
+
workspace: Path,
|
|
238
|
+
command: tuple[str, ...],
|
|
239
|
+
timeout_seconds: int,
|
|
240
|
+
expected_node_ids: tuple[str, ...] = (),
|
|
241
|
+
) -> EvalValidationResult:
|
|
242
|
+
if not command:
|
|
243
|
+
return _not_run_validation()
|
|
244
|
+
result = await EvalHarnessExecutor(workspace).execute(
|
|
245
|
+
EvalHarnessCommand(EvalHarnessOperation.VALIDATION, command, workspace, timeout_seconds)
|
|
246
|
+
)
|
|
247
|
+
status = _validation_status(result)
|
|
248
|
+
nodes = _parse_node_results(result.stdout_summary, expected_node_ids)
|
|
249
|
+
return EvalValidationResult(
|
|
250
|
+
status=status,
|
|
251
|
+
command=command,
|
|
252
|
+
exit_code=result.exit_code,
|
|
253
|
+
stdout_summary=result.stdout_summary,
|
|
254
|
+
stderr_summary=result.stderr_summary,
|
|
255
|
+
duration_ms=result.duration_ms,
|
|
256
|
+
timeout_seconds=timeout_seconds,
|
|
257
|
+
infrastructure_error=result.infrastructure_error,
|
|
258
|
+
pytest_node_results=nodes,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
async def discovery(
|
|
263
|
+
workspace: Path, command: tuple[str, ...], timeout_seconds: int
|
|
264
|
+
) -> EvalDiscoveryResult:
|
|
265
|
+
if not command:
|
|
266
|
+
return _not_run_discovery()
|
|
267
|
+
result = await EvalHarnessExecutor(workspace).execute(
|
|
268
|
+
EvalHarnessCommand(
|
|
269
|
+
EvalHarnessOperation.TEST_DISCOVERY, command, workspace, timeout_seconds
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
status = _discovery_status(result)
|
|
273
|
+
collected = (
|
|
274
|
+
_parse_collected_nodes(result.stdout_summary)
|
|
275
|
+
if status is EvalDiscoveryStatus.PASSED
|
|
276
|
+
else ()
|
|
277
|
+
)
|
|
278
|
+
return EvalDiscoveryResult(
|
|
279
|
+
status=status,
|
|
280
|
+
command=command,
|
|
281
|
+
exit_code=result.exit_code,
|
|
282
|
+
collected_node_ids=collected,
|
|
283
|
+
stdout_summary=result.stdout_summary,
|
|
284
|
+
stderr_summary=result.stderr_summary,
|
|
285
|
+
duration_ms=result.duration_ms,
|
|
286
|
+
timeout_seconds=timeout_seconds,
|
|
287
|
+
infrastructure_error=result.infrastructure_error,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def snapshot_repository(root: Path, forbidden_files: tuple[str, ...]) -> RepositoryState:
|
|
292
|
+
root = root.resolve(strict=True)
|
|
293
|
+
files: dict[str, FileFingerprint] = {}
|
|
294
|
+
for current, directory_names, file_names in os.walk(root, followlinks=False):
|
|
295
|
+
current_path = Path(current)
|
|
296
|
+
relative_dir = current_path.relative_to(root)
|
|
297
|
+
directory_names[:] = sorted(
|
|
298
|
+
name for name in directory_names if name != ".git" and name not in _IGNORED_DIRS
|
|
299
|
+
)
|
|
300
|
+
for name in tuple(directory_names):
|
|
301
|
+
path = current_path / name
|
|
302
|
+
relative = (relative_dir / name).as_posix()
|
|
303
|
+
files[relative] = _fingerprint(path, relative)
|
|
304
|
+
if path.is_symlink():
|
|
305
|
+
directory_names.remove(name)
|
|
306
|
+
for name in sorted(file_names):
|
|
307
|
+
if name in _IGNORED_FILES:
|
|
308
|
+
continue
|
|
309
|
+
path = current_path / name
|
|
310
|
+
relative = (relative_dir / name).as_posix()
|
|
311
|
+
files[relative] = _fingerprint(path, relative)
|
|
312
|
+
forbidden = {path: _forbidden_snapshot(root, path) for path in sorted(forbidden_files)}
|
|
313
|
+
return RepositoryState(files=dict(sorted(files.items())), forbidden_snapshots=forbidden)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def compare_repository_states(
|
|
317
|
+
before: RepositoryState,
|
|
318
|
+
after: RepositoryState,
|
|
319
|
+
text_paths: tuple[str, ...],
|
|
320
|
+
root: Path,
|
|
321
|
+
) -> EvalRepositoryEvidence:
|
|
322
|
+
before_paths = set(before.files)
|
|
323
|
+
after_paths = set(after.files)
|
|
324
|
+
created = tuple(sorted(after_paths - before_paths))
|
|
325
|
+
deleted = tuple(sorted(before_paths - after_paths))
|
|
326
|
+
changed = tuple(
|
|
327
|
+
sorted(
|
|
328
|
+
path for path in before_paths & after_paths if before.files[path] != after.files[path]
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
forbidden_evidence = {
|
|
332
|
+
path: EvalForbiddenPathEvidence(
|
|
333
|
+
path=path,
|
|
334
|
+
before=before.forbidden_snapshots[path],
|
|
335
|
+
after=after.forbidden_snapshots[path],
|
|
336
|
+
changed=before.forbidden_snapshots[path] != after.forbidden_snapshots[path],
|
|
337
|
+
)
|
|
338
|
+
for path in sorted(before.forbidden_snapshots)
|
|
339
|
+
}
|
|
340
|
+
for forbidden_path in _workspace_escaping_symlinks(after, root):
|
|
341
|
+
if forbidden_path in forbidden_evidence:
|
|
342
|
+
continue
|
|
343
|
+
before_snapshot = _snapshot_from_fingerprint(
|
|
344
|
+
forbidden_path, before.files.get(forbidden_path)
|
|
345
|
+
)
|
|
346
|
+
after_snapshot = _snapshot_from_fingerprint(forbidden_path, after.files[forbidden_path])
|
|
347
|
+
forbidden_evidence[forbidden_path] = EvalForbiddenPathEvidence(
|
|
348
|
+
path=forbidden_path,
|
|
349
|
+
before=before_snapshot,
|
|
350
|
+
after=after_snapshot,
|
|
351
|
+
changed=before_snapshot != after_snapshot,
|
|
352
|
+
)
|
|
353
|
+
forbidden = tuple(forbidden_evidence[path] for path in sorted(forbidden_evidence))
|
|
354
|
+
texts: dict[str, str] = {}
|
|
355
|
+
for relative in sorted(set(text_paths)):
|
|
356
|
+
path = root / PurePosixPath(relative)
|
|
357
|
+
try:
|
|
358
|
+
if path.is_file() and not path.is_symlink():
|
|
359
|
+
value = path.read_bytes().decode("utf-8", errors="strict")
|
|
360
|
+
texts[relative] = unicodedata.normalize(
|
|
361
|
+
"NFC", value.replace("\r\n", "\n").replace("\r", "\n")
|
|
362
|
+
)
|
|
363
|
+
except (OSError, UnicodeDecodeError):
|
|
364
|
+
continue
|
|
365
|
+
return EvalRepositoryEvidence(changed, created, deleted, forbidden, texts)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _safe_path(value: str, *, node: bool) -> bool:
|
|
369
|
+
file_part = value.split("::", 1)[0] if node else value
|
|
370
|
+
path = PurePosixPath(file_part)
|
|
371
|
+
if (
|
|
372
|
+
not value
|
|
373
|
+
or not file_part
|
|
374
|
+
or path.is_absolute()
|
|
375
|
+
or PureWindowsPath(file_part).is_absolute()
|
|
376
|
+
or ".." in path.parts
|
|
377
|
+
or _contains_shell_operator(value)
|
|
378
|
+
):
|
|
379
|
+
return False
|
|
380
|
+
if "\\" in file_part or file_part.startswith("-"):
|
|
381
|
+
return False
|
|
382
|
+
return node or "::" not in value
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _is_absolute_executable(value: str) -> bool:
|
|
386
|
+
return (
|
|
387
|
+
Path(value).is_absolute()
|
|
388
|
+
or PurePosixPath(value).is_absolute()
|
|
389
|
+
or PureWindowsPath(value).is_absolute()
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _contains_shell_operator(value: str) -> bool:
|
|
394
|
+
return (
|
|
395
|
+
any(token in value for token in ("&&", "||", "|", ";", "`", "$("))
|
|
396
|
+
or value.startswith((">", "<", "&"))
|
|
397
|
+
or value.startswith(("1>", "2>"))
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _bounded_int(value: str) -> bool:
|
|
402
|
+
try:
|
|
403
|
+
return 1 <= int(value) <= 10
|
|
404
|
+
except ValueError:
|
|
405
|
+
return False
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _command_paths_are_contained(command: EvalHarnessCommand, root: Path) -> bool:
|
|
409
|
+
for value in _command_path_arguments(command):
|
|
410
|
+
file_part = value.split("::", 1)[0]
|
|
411
|
+
candidate = root / PurePosixPath(file_part)
|
|
412
|
+
if not candidate.exists() and not candidate.is_symlink():
|
|
413
|
+
continue
|
|
414
|
+
try:
|
|
415
|
+
resolved = candidate.resolve(strict=True)
|
|
416
|
+
except (OSError, RuntimeError):
|
|
417
|
+
return False
|
|
418
|
+
if not _is_within_root(resolved, root):
|
|
419
|
+
return False
|
|
420
|
+
return True
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _command_path_arguments(command: EvalHarnessCommand) -> tuple[str, ...]:
|
|
424
|
+
if command.operation is EvalHarnessOperation.TEST_DISCOVERY:
|
|
425
|
+
return command.argv[3:]
|
|
426
|
+
if command.operation is not EvalHarnessOperation.VALIDATION:
|
|
427
|
+
return ()
|
|
428
|
+
family = Path(command.argv[0]).name.casefold().removesuffix(".exe")
|
|
429
|
+
arguments = command.argv[1:]
|
|
430
|
+
if family == "ruff":
|
|
431
|
+
return arguments[1:]
|
|
432
|
+
if family == "mypy":
|
|
433
|
+
return arguments
|
|
434
|
+
if family != "pytest":
|
|
435
|
+
return ()
|
|
436
|
+
paths: list[str] = []
|
|
437
|
+
index = 0
|
|
438
|
+
while index < len(arguments):
|
|
439
|
+
argument = arguments[index]
|
|
440
|
+
if argument == "--maxfail":
|
|
441
|
+
index += 2
|
|
442
|
+
elif argument.startswith("-"):
|
|
443
|
+
index += 1
|
|
444
|
+
else:
|
|
445
|
+
paths.append(argument)
|
|
446
|
+
index += 1
|
|
447
|
+
return tuple(paths)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _workspace_escaping_symlinks(state: RepositoryState, root: Path) -> tuple[str, ...]:
|
|
451
|
+
escaping: list[str] = []
|
|
452
|
+
for relative, fingerprint in state.files.items():
|
|
453
|
+
if fingerprint.kind != "symlink" or fingerprint.symlink_target is None:
|
|
454
|
+
continue
|
|
455
|
+
link_parent = root / PurePosixPath(relative).parent
|
|
456
|
+
target = Path(fingerprint.symlink_target)
|
|
457
|
+
resolved = (target if target.is_absolute() else link_parent / target).resolve(strict=False)
|
|
458
|
+
if not _is_within_root(resolved, root):
|
|
459
|
+
escaping.append(relative)
|
|
460
|
+
return tuple(sorted(escaping))
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _snapshot_from_fingerprint(
|
|
464
|
+
path: str, fingerprint: FileFingerprint | None
|
|
465
|
+
) -> ForbiddenPathSnapshot:
|
|
466
|
+
if fingerprint is None:
|
|
467
|
+
return ForbiddenPathSnapshot(path, False, None, None, None, None)
|
|
468
|
+
return ForbiddenPathSnapshot(
|
|
469
|
+
path,
|
|
470
|
+
True,
|
|
471
|
+
fingerprint.kind,
|
|
472
|
+
fingerprint.content_sha256,
|
|
473
|
+
fingerprint.executable,
|
|
474
|
+
fingerprint.symlink_target,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _is_within_root(path: Path, root: Path) -> bool:
|
|
479
|
+
try:
|
|
480
|
+
return os.path.commonpath(
|
|
481
|
+
[os.path.normcase(str(path)), os.path.normcase(str(root))]
|
|
482
|
+
) == os.path.normcase(str(root))
|
|
483
|
+
except ValueError:
|
|
484
|
+
return False
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _resolve_eval_executables(workspace: Path) -> TrustedExecutables:
|
|
488
|
+
resolved = TrustedExecutables.resolve(workspace)
|
|
489
|
+
script_directory = Path(sys.executable).resolve(strict=True).parent
|
|
490
|
+
|
|
491
|
+
def adjacent(name: str, current: str | None) -> str | None:
|
|
492
|
+
if current is not None:
|
|
493
|
+
return current
|
|
494
|
+
candidates = (
|
|
495
|
+
script_directory / f"{name}.exe",
|
|
496
|
+
script_directory / name,
|
|
497
|
+
)
|
|
498
|
+
for candidate in candidates:
|
|
499
|
+
if candidate.is_file():
|
|
500
|
+
return str(candidate.resolve(strict=True))
|
|
501
|
+
return None
|
|
502
|
+
|
|
503
|
+
return TrustedExecutables(
|
|
504
|
+
python=resolved.python,
|
|
505
|
+
uv=resolved.uv,
|
|
506
|
+
git=resolved.git,
|
|
507
|
+
pytest=adjacent("pytest", resolved.pytest),
|
|
508
|
+
ruff=adjacent("ruff", resolved.ruff),
|
|
509
|
+
mypy=adjacent("mypy", resolved.mypy),
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _fingerprint(path: Path, relative: str) -> FileFingerprint:
|
|
514
|
+
mode = path.lstat().st_mode
|
|
515
|
+
if path.is_symlink():
|
|
516
|
+
return FileFingerprint(relative, "symlink", None, False, os.readlink(path))
|
|
517
|
+
if stat.S_ISREG(mode):
|
|
518
|
+
digest = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
519
|
+
return FileFingerprint(relative, "file", digest, bool(mode & stat.S_IXUSR), None)
|
|
520
|
+
if stat.S_ISDIR(mode):
|
|
521
|
+
return FileFingerprint(relative, "directory", None, bool(mode & stat.S_IXUSR), None)
|
|
522
|
+
return FileFingerprint(relative, "other", None, bool(mode & stat.S_IXUSR), None)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _forbidden_snapshot(root: Path, relative: str) -> ForbiddenPathSnapshot:
|
|
526
|
+
path = root / PurePosixPath(relative)
|
|
527
|
+
if not path.exists() and not path.is_symlink():
|
|
528
|
+
return ForbiddenPathSnapshot(relative, False, None, None, None, None)
|
|
529
|
+
fingerprint = _fingerprint(path, relative)
|
|
530
|
+
return ForbiddenPathSnapshot(
|
|
531
|
+
relative,
|
|
532
|
+
True,
|
|
533
|
+
fingerprint.kind,
|
|
534
|
+
fingerprint.content_sha256,
|
|
535
|
+
fingerprint.executable,
|
|
536
|
+
fingerprint.symlink_target,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _summary(value: str) -> str:
|
|
541
|
+
return value if len(value) <= _SUMMARY_LIMIT else value[:_SUMMARY_LIMIT]
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _denied(command: EvalHarnessCommand, code: str) -> EvalHarnessCommandResult:
|
|
545
|
+
return EvalHarnessCommandResult(command.operation, False, code, None, "", "", 0, False, None)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _validation_status(result: EvalHarnessCommandResult) -> EvalValidationStatus:
|
|
549
|
+
if result.infrastructure_error:
|
|
550
|
+
return EvalValidationStatus.INFRASTRUCTURE_ERROR
|
|
551
|
+
if not result.allowed:
|
|
552
|
+
return EvalValidationStatus.FAILED
|
|
553
|
+
if result.timed_out:
|
|
554
|
+
return EvalValidationStatus.TIMED_OUT
|
|
555
|
+
return EvalValidationStatus.PASSED if result.exit_code == 0 else EvalValidationStatus.FAILED
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _discovery_status(result: EvalHarnessCommandResult) -> EvalDiscoveryStatus:
|
|
559
|
+
if result.infrastructure_error:
|
|
560
|
+
return EvalDiscoveryStatus.INFRASTRUCTURE_ERROR
|
|
561
|
+
if not result.allowed:
|
|
562
|
+
return EvalDiscoveryStatus.FAILED
|
|
563
|
+
if result.timed_out:
|
|
564
|
+
return EvalDiscoveryStatus.TIMED_OUT
|
|
565
|
+
return EvalDiscoveryStatus.PASSED if result.exit_code == 0 else EvalDiscoveryStatus.FAILED
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _not_run_validation() -> EvalValidationResult:
|
|
569
|
+
return EvalValidationResult(EvalValidationStatus.NOT_RUN, (), None, "", "", 0, None, None)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _not_run_discovery() -> EvalDiscoveryResult:
|
|
573
|
+
return EvalDiscoveryResult(EvalDiscoveryStatus.NOT_RUN, (), None, (), "", "", 0, None, None)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _parse_collected_nodes(output: str) -> tuple[str, ...]:
|
|
577
|
+
nodes = {
|
|
578
|
+
line.strip()
|
|
579
|
+
for line in output.splitlines()
|
|
580
|
+
if "::" in line and not line.lstrip().startswith(("<", "="))
|
|
581
|
+
}
|
|
582
|
+
return tuple(sorted(nodes))
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _parse_node_results(output: str, expected: tuple[str, ...]) -> tuple[EvalPytestNodeResult, ...]:
|
|
586
|
+
tokens = {
|
|
587
|
+
"PASSED": EvalPytestNodeOutcome.PASSED,
|
|
588
|
+
"FAILED": EvalPytestNodeOutcome.FAILED,
|
|
589
|
+
"SKIPPED": EvalPytestNodeOutcome.SKIPPED,
|
|
590
|
+
"XFAIL": EvalPytestNodeOutcome.XFAILED,
|
|
591
|
+
"XPASS": EvalPytestNodeOutcome.XPASSED,
|
|
592
|
+
"ERROR": EvalPytestNodeOutcome.ERROR,
|
|
593
|
+
}
|
|
594
|
+
found: dict[str, EvalPytestNodeOutcome] = {}
|
|
595
|
+
for line in output.splitlines():
|
|
596
|
+
normalized = line.strip()
|
|
597
|
+
for node in expected:
|
|
598
|
+
prefix = f"{node} "
|
|
599
|
+
if normalized.startswith(prefix):
|
|
600
|
+
token = normalized[len(prefix) :].split(maxsplit=1)[0]
|
|
601
|
+
if token in tokens:
|
|
602
|
+
found[node] = tokens[token]
|
|
603
|
+
return tuple(
|
|
604
|
+
EvalPytestNodeResult(node, found.get(node, EvalPytestNodeOutcome.NOT_RUN))
|
|
605
|
+
for node in expected
|
|
606
|
+
)
|