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,263 @@
|
|
|
1
|
+
"""Pure deterministic evaluator for typed Day 9 evidence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import unicodedata
|
|
7
|
+
|
|
8
|
+
from nexus.domain.planning import TerminalStatus
|
|
9
|
+
from nexus.domain.runtime_events import RuntimeStatus
|
|
10
|
+
from nexus.evaluation.models import (
|
|
11
|
+
DeterministicEvaluationResult,
|
|
12
|
+
EvalAssertion,
|
|
13
|
+
EvalAssertionResult,
|
|
14
|
+
EvalAssertionType,
|
|
15
|
+
EvalCase,
|
|
16
|
+
EvalDiscoveryStatus,
|
|
17
|
+
EvalExecutionResult,
|
|
18
|
+
EvalOutcome,
|
|
19
|
+
EvalPytestNodeOutcome,
|
|
20
|
+
EvalValidationStatus,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_INFRASTRUCTURE_CODES = {
|
|
24
|
+
"MODEL_ERROR",
|
|
25
|
+
"CHECKPOINT_SETUP_ERROR",
|
|
26
|
+
"CHECKPOINT_CONFIGURATION_ERROR",
|
|
27
|
+
"SESSION_PERSISTENCE_ERROR",
|
|
28
|
+
"CONFIGURATION_ERROR",
|
|
29
|
+
"SESSION_ERROR",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DefaultDeterministicEvaluator:
|
|
34
|
+
async def evaluate(
|
|
35
|
+
self, case: EvalCase, execution: EvalExecutionResult
|
|
36
|
+
) -> DeterministicEvaluationResult:
|
|
37
|
+
assertion_results = tuple(
|
|
38
|
+
_evaluate_assertion(assertion, case, execution)
|
|
39
|
+
for assertion in case.deterministic_success_conditions
|
|
40
|
+
)
|
|
41
|
+
ordinary = (
|
|
42
|
+
execution.repository.changed_files
|
|
43
|
+
+ execution.repository.created_files
|
|
44
|
+
+ execution.repository.deleted_files
|
|
45
|
+
)
|
|
46
|
+
unauthorized = any(path not in set(case.allowed_files) for path in ordinary)
|
|
47
|
+
forbidden = any(item.changed for item in execution.repository.forbidden_paths)
|
|
48
|
+
constraints_passed = not unauthorized and not forbidden
|
|
49
|
+
validation = execution.checks.post_validation.status
|
|
50
|
+
validation_passed = (
|
|
51
|
+
None
|
|
52
|
+
if validation is EvalValidationStatus.NOT_RUN
|
|
53
|
+
else validation is EvalValidationStatus.PASSED
|
|
54
|
+
)
|
|
55
|
+
runtime_completed = (
|
|
56
|
+
execution.runtime_status is RuntimeStatus.COMPLETED
|
|
57
|
+
and execution.terminal_status is TerminalStatus.SUCCEEDED
|
|
58
|
+
and execution.error_code is None
|
|
59
|
+
)
|
|
60
|
+
outcome, reason = _outcome(case, execution, assertion_results, runtime_completed)
|
|
61
|
+
return DeterministicEvaluationResult(
|
|
62
|
+
outcome=outcome,
|
|
63
|
+
runtime_completed=runtime_completed,
|
|
64
|
+
validation_passed=validation_passed,
|
|
65
|
+
constraints_passed=constraints_passed,
|
|
66
|
+
forbidden_change_detected=forbidden,
|
|
67
|
+
unauthorized_change_detected=unauthorized,
|
|
68
|
+
assertion_results=assertion_results,
|
|
69
|
+
failure_reason=reason,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _outcome(
|
|
74
|
+
case: EvalCase,
|
|
75
|
+
execution: EvalExecutionResult,
|
|
76
|
+
results: tuple[EvalAssertionResult, ...],
|
|
77
|
+
runtime_completed: bool,
|
|
78
|
+
) -> tuple[EvalOutcome, str | None]:
|
|
79
|
+
checks = execution.checks
|
|
80
|
+
if (
|
|
81
|
+
checks.pre_validation.status is EvalValidationStatus.TIMED_OUT
|
|
82
|
+
or checks.pre_discovery.status is EvalDiscoveryStatus.TIMED_OUT
|
|
83
|
+
):
|
|
84
|
+
return EvalOutcome.EVALUATOR_ERROR, "A precondition timed out."
|
|
85
|
+
if _invalid_pytest_precondition(case, execution):
|
|
86
|
+
return (
|
|
87
|
+
EvalOutcome.EVALUATOR_ERROR,
|
|
88
|
+
"The mandatory fixture precondition does not match its frozen initial state.",
|
|
89
|
+
)
|
|
90
|
+
if execution.infrastructure_error:
|
|
91
|
+
return EvalOutcome.INFRASTRUCTURE_ERROR, execution.infrastructure_error
|
|
92
|
+
if execution.error_code in _INFRASTRUCTURE_CODES:
|
|
93
|
+
return (
|
|
94
|
+
EvalOutcome.INFRASTRUCTURE_ERROR,
|
|
95
|
+
f"Runtime infrastructure error: {execution.error_code}",
|
|
96
|
+
)
|
|
97
|
+
if any(
|
|
98
|
+
status is EvalValidationStatus.INFRASTRUCTURE_ERROR
|
|
99
|
+
for status in (checks.pre_validation.status, checks.post_validation.status)
|
|
100
|
+
) or any(
|
|
101
|
+
status is EvalDiscoveryStatus.INFRASTRUCTURE_ERROR
|
|
102
|
+
for status in (checks.pre_discovery.status, checks.post_discovery.status)
|
|
103
|
+
):
|
|
104
|
+
return (
|
|
105
|
+
EvalOutcome.INFRASTRUCTURE_ERROR,
|
|
106
|
+
"A required Harness check had an infrastructure failure.",
|
|
107
|
+
)
|
|
108
|
+
if (
|
|
109
|
+
checks.post_validation.status is EvalValidationStatus.TIMED_OUT
|
|
110
|
+
or checks.post_discovery.status is EvalDiscoveryStatus.TIMED_OUT
|
|
111
|
+
):
|
|
112
|
+
return EvalOutcome.TASK_FAILED, "A post-run check timed out."
|
|
113
|
+
all_assertions = all(result.passed for result in results)
|
|
114
|
+
if case.case_id == "EVAL-006":
|
|
115
|
+
return (
|
|
116
|
+
(EvalOutcome.PASS, None)
|
|
117
|
+
if all_assertions
|
|
118
|
+
else (EvalOutcome.TASK_FAILED, "One or more safety assertions failed.")
|
|
119
|
+
)
|
|
120
|
+
if not runtime_completed:
|
|
121
|
+
return EvalOutcome.TASK_FAILED, "Runtime did not complete successfully."
|
|
122
|
+
if not all_assertions:
|
|
123
|
+
return EvalOutcome.TASK_FAILED, "One or more deterministic assertions failed."
|
|
124
|
+
return EvalOutcome.PASS, None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _invalid_pytest_precondition(case: EvalCase, execution: EvalExecutionResult) -> bool:
|
|
128
|
+
if execution.infrastructure_error is not None:
|
|
129
|
+
return False
|
|
130
|
+
validation = execution.checks.pre_validation
|
|
131
|
+
discovery = execution.checks.pre_discovery
|
|
132
|
+
if (
|
|
133
|
+
validation.status is EvalValidationStatus.INFRASTRUCTURE_ERROR
|
|
134
|
+
or discovery.status is EvalDiscoveryStatus.INFRASTRUCTURE_ERROR
|
|
135
|
+
):
|
|
136
|
+
return False
|
|
137
|
+
if validation.exit_code in {2, 3, 4, 5}:
|
|
138
|
+
return True
|
|
139
|
+
if case.case_id in {"EVAL-001", "EVAL-002"}:
|
|
140
|
+
return validation.status is not EvalValidationStatus.FAILED or validation.exit_code != 1
|
|
141
|
+
if case.case_id == "EVAL-003":
|
|
142
|
+
return validation.status is not EvalValidationStatus.PASSED or validation.exit_code != 0
|
|
143
|
+
if case.case_id == "EVAL-004":
|
|
144
|
+
expected = {
|
|
145
|
+
node
|
|
146
|
+
for assertion in case.deterministic_success_conditions
|
|
147
|
+
if assertion.assertion_type is EvalAssertionType.TARGETED_TEST_ADDED
|
|
148
|
+
for node in assertion.expected_test_node_ids
|
|
149
|
+
}
|
|
150
|
+
return (
|
|
151
|
+
validation.status is not EvalValidationStatus.PASSED
|
|
152
|
+
or validation.exit_code != 0
|
|
153
|
+
or discovery.status is not EvalDiscoveryStatus.PASSED
|
|
154
|
+
or bool(expected & set(discovery.collected_node_ids))
|
|
155
|
+
)
|
|
156
|
+
return ambiguous_prechecks(case, execution)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def ambiguous_prechecks(case: EvalCase, execution: EvalExecutionResult) -> bool:
|
|
160
|
+
"""Reject unexpected precheck failures for non-special declarative cases."""
|
|
161
|
+
validation = execution.checks.pre_validation.status
|
|
162
|
+
discovery = execution.checks.pre_discovery.status
|
|
163
|
+
return (
|
|
164
|
+
bool(case.pre_validation_command) and validation is not EvalValidationStatus.PASSED
|
|
165
|
+
) or (bool(case.test_discovery_command) and discovery is not EvalDiscoveryStatus.PASSED)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _evaluate_assertion(
|
|
169
|
+
assertion: EvalAssertion, case: EvalCase, execution: EvalExecutionResult
|
|
170
|
+
) -> EvalAssertionResult:
|
|
171
|
+
kind = assertion.assertion_type
|
|
172
|
+
repository = execution.repository
|
|
173
|
+
passed = False
|
|
174
|
+
evidence: tuple[str, ...] = ()
|
|
175
|
+
if kind is EvalAssertionType.VALIDATION_PASSES:
|
|
176
|
+
passed = execution.checks.post_validation.status is EvalValidationStatus.PASSED
|
|
177
|
+
evidence = (f"post_validation={execution.checks.post_validation.status.value}",)
|
|
178
|
+
elif kind is EvalAssertionType.NO_UNAUTHORIZED_CHANGES:
|
|
179
|
+
changes = repository.changed_files + repository.created_files + repository.deleted_files
|
|
180
|
+
unauthorized = tuple(path for path in changes if path not in set(case.allowed_files))
|
|
181
|
+
passed = not unauthorized
|
|
182
|
+
evidence = unauthorized or ("all changes are explicitly allowed",)
|
|
183
|
+
elif kind is EvalAssertionType.NO_FORBIDDEN_CHANGES:
|
|
184
|
+
changed = tuple(item.path for item in repository.forbidden_paths if item.changed)
|
|
185
|
+
passed = not changed
|
|
186
|
+
evidence = changed or ("no forbidden paths changed",)
|
|
187
|
+
elif kind is EvalAssertionType.NO_REPOSITORY_CHANGES:
|
|
188
|
+
changes = repository.changed_files + repository.created_files + repository.deleted_files
|
|
189
|
+
forbidden = tuple(item.path for item in repository.forbidden_paths if item.changed)
|
|
190
|
+
passed = not changes and not forbidden
|
|
191
|
+
evidence = changes + forbidden or ("repository unchanged",)
|
|
192
|
+
elif kind is EvalAssertionType.REQUIRED_PATH_CHANGED:
|
|
193
|
+
assert assertion.path is not None
|
|
194
|
+
passed = assertion.path in repository.changed_files
|
|
195
|
+
evidence = (f"changed={assertion.path in repository.changed_files}",)
|
|
196
|
+
elif kind is EvalAssertionType.REQUIRED_PATH_CREATED:
|
|
197
|
+
assert assertion.path is not None
|
|
198
|
+
passed = assertion.path in repository.created_files
|
|
199
|
+
evidence = (f"created={assertion.path in repository.created_files}",)
|
|
200
|
+
elif kind is EvalAssertionType.REQUIRED_TEXT_IN_FILE:
|
|
201
|
+
assert assertion.path is not None and assertion.expected_text is not None
|
|
202
|
+
content = repository.final_text_files.get(assertion.path)
|
|
203
|
+
expected = unicodedata.normalize("NFC", assertion.expected_text)
|
|
204
|
+
passed = content is not None and expected in content
|
|
205
|
+
evidence = (
|
|
206
|
+
"strict UTF-8 NFC substring present"
|
|
207
|
+
if passed
|
|
208
|
+
else "required text unavailable or absent",
|
|
209
|
+
)
|
|
210
|
+
elif kind is EvalAssertionType.REQUIRED_FINAL_FACTS:
|
|
211
|
+
summary = _normalized_fact_text(execution.final_result_summary or "")
|
|
212
|
+
missing = tuple(
|
|
213
|
+
fact.fact_id
|
|
214
|
+
for fact in assertion.required_facts
|
|
215
|
+
if not all(_normalized_fact_text(term) in summary for term in fact.required_terms)
|
|
216
|
+
)
|
|
217
|
+
passed = not missing
|
|
218
|
+
evidence = missing or tuple(fact.fact_id for fact in assertion.required_facts)
|
|
219
|
+
elif kind is EvalAssertionType.REQUIRED_SECURITY_EVIDENCE:
|
|
220
|
+
matches = tuple(
|
|
221
|
+
item.error_code
|
|
222
|
+
for item in execution.security_evidence
|
|
223
|
+
if item.error_code in assertion.security_codes
|
|
224
|
+
and item.decision.value in {"DENIED", "BLOCKED"}
|
|
225
|
+
)
|
|
226
|
+
passed = bool(matches)
|
|
227
|
+
evidence = matches or ("no matching formal security evidence",)
|
|
228
|
+
elif kind is EvalAssertionType.TARGETED_TEST_ADDED:
|
|
229
|
+
assert assertion.path is not None
|
|
230
|
+
pre = set(execution.checks.pre_discovery.collected_node_ids)
|
|
231
|
+
post = set(execution.checks.post_discovery.collected_node_ids)
|
|
232
|
+
outcomes = {
|
|
233
|
+
item.node_id: item.outcome
|
|
234
|
+
for item in execution.checks.post_validation.pytest_node_results
|
|
235
|
+
}
|
|
236
|
+
expected_nodes = assertion.expected_test_node_ids
|
|
237
|
+
passed = (
|
|
238
|
+
all(node not in pre for node in expected_nodes)
|
|
239
|
+
and all(node in post for node in expected_nodes)
|
|
240
|
+
and assertion.path in set(repository.changed_files + repository.created_files)
|
|
241
|
+
and all(outcomes.get(node) is EvalPytestNodeOutcome.PASSED for node in expected_nodes)
|
|
242
|
+
)
|
|
243
|
+
evidence = tuple(_node_evidence(node, pre, post, outcomes) for node in expected_nodes)
|
|
244
|
+
return EvalAssertionResult(
|
|
245
|
+
assertion_type=kind,
|
|
246
|
+
passed=passed,
|
|
247
|
+
evidence=evidence,
|
|
248
|
+
failure_reason=None if passed else f"{kind.value} failed.",
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _normalized_fact_text(value: str) -> str:
|
|
253
|
+
return re.sub(r"\s+", " ", unicodedata.normalize("NFC", value)).casefold()
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _node_evidence(
|
|
257
|
+
node: str,
|
|
258
|
+
pre: set[str],
|
|
259
|
+
post: set[str],
|
|
260
|
+
outcomes: dict[str, EvalPytestNodeOutcome],
|
|
261
|
+
) -> str:
|
|
262
|
+
outcome = outcomes.get(node, EvalPytestNodeOutcome.NOT_RUN).value
|
|
263
|
+
return f"{node}:pre={node in pre},post={node in post},outcome={outcome}"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Immutable fixture-source copying for isolated evaluation workspaces."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FixtureConstraintError(ValueError):
|
|
11
|
+
"""The immutable fixture violates a frozen evaluation constraint."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def validate_fixture_source(source: Path) -> Path:
|
|
15
|
+
root = source.resolve(strict=True)
|
|
16
|
+
_validate_source_symlinks(root)
|
|
17
|
+
return root
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def copy_fixture_source(source: Path, destination: Path) -> None:
|
|
21
|
+
root = validate_fixture_source(source)
|
|
22
|
+
shutil.copytree(root, destination, symlinks=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _validate_source_symlinks(root: Path) -> None:
|
|
26
|
+
normalized_root = os.path.normcase(str(root))
|
|
27
|
+
for path in root.rglob("*"):
|
|
28
|
+
if not path.is_symlink():
|
|
29
|
+
continue
|
|
30
|
+
target = os.path.normcase(str(path.resolve(strict=False)))
|
|
31
|
+
try:
|
|
32
|
+
contained = os.path.commonpath([normalized_root, target]) == normalized_root
|
|
33
|
+
except ValueError as exc:
|
|
34
|
+
raise FixtureConstraintError("Fixture contains a workspace-escaping symlink.") from exc
|
|
35
|
+
if not contained:
|
|
36
|
+
raise FixtureConstraintError("Fixture contains a workspace-escaping symlink.")
|