millforge 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.
- millforge/__init__.py +1174 -0
- millforge/_forge/LICENSE +21 -0
- millforge/_forge/PROVENANCE.json +295 -0
- millforge/_forge/UPDATE_POLICY.md +24 -0
- millforge/_forge/__init__.py +14 -0
- millforge/_forge/adapter.py +2232 -0
- millforge/_forge/base_runner.py +121 -0
- millforge/_forge/clients/__init__.py +10 -0
- millforge/_forge/clients/base.py +200 -0
- millforge/_forge/context/__init__.py +23 -0
- millforge/_forge/context/manager.py +178 -0
- millforge/_forge/context/strategies.py +335 -0
- millforge/_forge/core/__init__.py +16 -0
- millforge/_forge/core/inference.py +433 -0
- millforge/_forge/core/messages.py +119 -0
- millforge/_forge/core/runner.py +479 -0
- millforge/_forge/core/steps.py +108 -0
- millforge/_forge/core/workflow.py +400 -0
- millforge/_forge/errors.py +222 -0
- millforge/_forge/guardrails/__init__.py +21 -0
- millforge/_forge/guardrails/error_tracker.py +71 -0
- millforge/_forge/guardrails/guardrails.py +194 -0
- millforge/_forge/guardrails/nudge.py +47 -0
- millforge/_forge/guardrails/response_validator.py +119 -0
- millforge/_forge/guardrails/step_enforcer.py +183 -0
- millforge/_forge/prompts/__init__.py +16 -0
- millforge/_forge/prompts/nudges.py +95 -0
- millforge/_forge/prompts/templates.py +285 -0
- millforge/_version.py +3 -0
- millforge/artifacts.py +570 -0
- millforge/base/__init__.py +97 -0
- millforge/base/composition.py +402 -0
- millforge/base/context.py +285 -0
- millforge/base/harness.py +138 -0
- millforge/base/identity.py +465 -0
- millforge/base/options.py +34 -0
- millforge/base/platform.py +17 -0
- millforge/base/prompt.py +317 -0
- millforge/base/runner.py +546 -0
- millforge/compiled_plan.py +970 -0
- millforge/compiler/__init__.py +231 -0
- millforge/compiler/artifact_validation.py +257 -0
- millforge/compiler/canonicalization.py +169 -0
- millforge/compiler/capabilities.py +66 -0
- millforge/compiler/catalogs.py +500 -0
- millforge/compiler/diagnostics.py +491 -0
- millforge/compiler/graph.py +678 -0
- millforge/compiler/lowering.py +198 -0
- millforge/compiler/output.py +692 -0
- millforge/compiler/parsing.py +1424 -0
- millforge/compiler/requests.py +1180 -0
- millforge/compiler/schema_validation.py +272 -0
- millforge/compiler/semantic.py +490 -0
- millforge/compiler/service.py +448 -0
- millforge/compiler/source.py +375 -0
- millforge/compiler/validators.py +184 -0
- millforge/connectors/__init__.py +95 -0
- millforge/connectors/admission.py +801 -0
- millforge/connectors/broker.py +202 -0
- millforge/connectors/contracts.py +1159 -0
- millforge/connectors/diagnostics.py +189 -0
- millforge/connectors/fake.py +66 -0
- millforge/connectors/runtime.py +236 -0
- millforge/contracts.py +2860 -0
- millforge/custom_tools/__init__.py +67 -0
- millforge/custom_tools/compiler.py +724 -0
- millforge/custom_tools/contracts.py +1093 -0
- millforge/custom_tools/diagnostics.py +205 -0
- millforge/eval_artifacts.py +952 -0
- millforge/eval_boundary.py +2435 -0
- millforge/eval_fixtures/__init__.py +1 -0
- millforge/eval_fixtures/default_pack/__init__.py +1 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.bug_diagnosis.traceback.v1.json +52 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.direct_edit.import_sort.v1.json +52 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.evidence_discipline.no_source_change.v1.json +51 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.false_closure.visible_green.v1.json +52 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.multi_file.api_contract.v1.json +54 -0
- millforge/eval_fixtures/default_pack/fixtures/fixture.08a.recovery.malformed_artifact.v1.json +54 -0
- millforge/eval_fixtures/default_pack/manifest.json +12 -0
- millforge/eval_modes.py +1282 -0
- millforge/eval_presets.py +1398 -0
- millforge/eval_reports.py +2517 -0
- millforge/eval_suite.py +2429 -0
- millforge/eval_trials.py +2632 -0
- millforge/eval_workflow.py +794 -0
- millforge/exceptions.py +122 -0
- millforge/model_backend.py +2098 -0
- millforge/protocols.py +340 -0
- millforge/py.typed +0 -0
- millforge/runtime.py +1791 -0
- millforge/testing/__init__.py +1089 -0
- millforge/tools/__init__.py +83 -0
- millforge/tools/builtin_runtime.py +1339 -0
- millforge/tools/builtins.py +773 -0
- millforge/tools/execution.py +1545 -0
- millforge/tools/path_policy.py +155 -0
- millforge/tools/pi_compat/PI_LICENSE +21 -0
- millforge/tools/pi_compat/PROVENANCE.json +55 -0
- millforge/tools/pi_compat/UPDATE_POLICY.md +36 -0
- millforge/tools/pi_compat/__init__.py +34 -0
- millforge/tools/pi_compat/contracts.py +49 -0
- millforge/tools/pi_compat/editing.py +390 -0
- millforge/tools/pi_compat/mutations.py +57 -0
- millforge/tools/pi_compat/operations.py +401 -0
- millforge/tools/pi_compat/paths.py +155 -0
- millforge/tools/pi_compat/process.py +1375 -0
- millforge/tools/pi_compat/search.py +738 -0
- millforge/tools/pi_compat/truncation.py +267 -0
- millforge/tools/pi_compat_catalog.py +396 -0
- millforge/tools/pi_compat_runtime.py +460 -0
- millforge/tools/registry.py +553 -0
- millforge/tools/results.py +533 -0
- millforge-0.1.0.dist-info/METADATA +844 -0
- millforge-0.1.0.dist-info/RECORD +116 -0
- millforge-0.1.0.dist-info/WHEEL +4 -0
- millforge-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
"""Compile request, invocation, and result contracts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import errno
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import stat
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from pathlib import Path, PurePosixPath
|
|
13
|
+
from types import MappingProxyType
|
|
14
|
+
from typing import Any
|
|
15
|
+
from typing import Literal, Protocol
|
|
16
|
+
|
|
17
|
+
from pydantic import (
|
|
18
|
+
AliasChoices,
|
|
19
|
+
BaseModel,
|
|
20
|
+
ConfigDict,
|
|
21
|
+
Field,
|
|
22
|
+
StrictStr,
|
|
23
|
+
ValidationError,
|
|
24
|
+
field_validator,
|
|
25
|
+
model_validator,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from millforge.compiler.diagnostics import (
|
|
29
|
+
bound_diagnostics,
|
|
30
|
+
CompilerDiagnostic,
|
|
31
|
+
CompilerPhase,
|
|
32
|
+
DiagnosticField,
|
|
33
|
+
DiagnosticSeverity,
|
|
34
|
+
SourceReference,
|
|
35
|
+
detect_secret_candidate,
|
|
36
|
+
)
|
|
37
|
+
from millforge.compiler.parsing import (
|
|
38
|
+
MAX_SOURCE_SIZE_BYTES,
|
|
39
|
+
HarnessSourceParser,
|
|
40
|
+
HarnessSourceParserProtocol,
|
|
41
|
+
ParsedHarnessSource,
|
|
42
|
+
SourceDocument,
|
|
43
|
+
)
|
|
44
|
+
from millforge.compiler.source import HarnessSource
|
|
45
|
+
from millforge.compiler.validators import (
|
|
46
|
+
validate_harness_id,
|
|
47
|
+
validate_request_id,
|
|
48
|
+
validate_sha256,
|
|
49
|
+
validate_stage_kind_id,
|
|
50
|
+
validate_terminal_result,
|
|
51
|
+
validate_unique,
|
|
52
|
+
validate_utf8_size,
|
|
53
|
+
)
|
|
54
|
+
from millforge.contracts import CapabilityEnvelope, RedactionPolicy
|
|
55
|
+
|
|
56
|
+
_INVALID_REQUEST_ID = "request.invalid"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _freeze_json_like(value: Any) -> Any:
|
|
60
|
+
"""Recursively freeze JSON-like nested containers."""
|
|
61
|
+
if isinstance(value, Mapping):
|
|
62
|
+
return MappingProxyType(
|
|
63
|
+
{key: _freeze_json_like(item) for key, item in value.items()}
|
|
64
|
+
)
|
|
65
|
+
if isinstance(value, list):
|
|
66
|
+
return tuple(_freeze_json_like(item) for item in value)
|
|
67
|
+
if isinstance(value, tuple):
|
|
68
|
+
return tuple(_freeze_json_like(item) for item in value)
|
|
69
|
+
return value
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class CompileStatus(str, Enum):
|
|
73
|
+
"""Closed compile-result status values."""
|
|
74
|
+
|
|
75
|
+
FAILED = "failed"
|
|
76
|
+
PREPARED = "prepared"
|
|
77
|
+
COMMITTED = "committed"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class PlanCommitCertainty(str, Enum):
|
|
81
|
+
"""Closed plan publication certainty values."""
|
|
82
|
+
|
|
83
|
+
ABSENT = "absent"
|
|
84
|
+
COMMITTED = "committed"
|
|
85
|
+
UNKNOWN = "unknown"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class DiagnosticReportState(str, Enum):
|
|
89
|
+
"""Closed diagnostic report persistence states."""
|
|
90
|
+
|
|
91
|
+
ABSENT = "absent"
|
|
92
|
+
PREPARED = "prepared"
|
|
93
|
+
COMMITTED = "committed"
|
|
94
|
+
UNKNOWN = "unknown"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class HarnessCompileRequest(BaseModel):
|
|
98
|
+
"""Serializable attribute-frozen public compile request."""
|
|
99
|
+
|
|
100
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
101
|
+
|
|
102
|
+
request_id: StrictStr
|
|
103
|
+
source_path: StrictStr
|
|
104
|
+
source_root: StrictStr
|
|
105
|
+
source_format: Literal["yaml", "json"]
|
|
106
|
+
output_dir: StrictStr
|
|
107
|
+
output_root: StrictStr
|
|
108
|
+
expected_harness_id: StrictStr
|
|
109
|
+
stage_kind_id: StrictStr
|
|
110
|
+
legal_terminal_results: tuple[StrictStr, ...] = Field(min_length=1, max_length=64)
|
|
111
|
+
capability_envelope: CapabilityEnvelope
|
|
112
|
+
|
|
113
|
+
@field_validator("request_id")
|
|
114
|
+
@classmethod
|
|
115
|
+
def _request_id_valid(cls, value: str) -> str:
|
|
116
|
+
return validate_request_id(value)
|
|
117
|
+
|
|
118
|
+
@field_validator("source_path", "output_dir")
|
|
119
|
+
@classmethod
|
|
120
|
+
def _relative_path_size_valid(cls, value: str) -> str:
|
|
121
|
+
if not value.strip():
|
|
122
|
+
raise ValueError("path must be nonblank")
|
|
123
|
+
return validate_utf8_size(value, "path", 1024)
|
|
124
|
+
|
|
125
|
+
@field_validator("source_root", "output_root")
|
|
126
|
+
@classmethod
|
|
127
|
+
def _absolute_root_size_valid(cls, value: str) -> str:
|
|
128
|
+
if not value.startswith("/"):
|
|
129
|
+
raise ValueError("root must be an absolute path string")
|
|
130
|
+
return validate_utf8_size(value, "root", 4096)
|
|
131
|
+
|
|
132
|
+
@field_validator("expected_harness_id")
|
|
133
|
+
@classmethod
|
|
134
|
+
def _expected_harness_id_valid(cls, value: str) -> str:
|
|
135
|
+
return validate_harness_id(value)
|
|
136
|
+
|
|
137
|
+
@field_validator("stage_kind_id")
|
|
138
|
+
@classmethod
|
|
139
|
+
def _stage_kind_id_valid(cls, value: str) -> str:
|
|
140
|
+
return validate_stage_kind_id(value)
|
|
141
|
+
|
|
142
|
+
@field_validator("legal_terminal_results")
|
|
143
|
+
@classmethod
|
|
144
|
+
def _legal_terminal_results_valid(cls, value: tuple[str, ...]) -> tuple[str, ...]:
|
|
145
|
+
for item in value:
|
|
146
|
+
validate_terminal_result(item)
|
|
147
|
+
return validate_unique(value, "legal_terminal_results")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class CompileInvocation(BaseModel):
|
|
151
|
+
"""Private deeply snapshotted compile invocation."""
|
|
152
|
+
|
|
153
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
154
|
+
|
|
155
|
+
request: HarnessCompileRequest
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def from_request(cls, request: HarnessCompileRequest) -> CompileInvocation:
|
|
159
|
+
"""Build a private deep snapshot of request-owned data."""
|
|
160
|
+
snapshot = request.model_copy(deep=True)
|
|
161
|
+
for grant in snapshot.capability_envelope.grants:
|
|
162
|
+
if grant.constraints is not None:
|
|
163
|
+
object.__setattr__(
|
|
164
|
+
grant, "constraints", _freeze_json_like(grant.constraints)
|
|
165
|
+
)
|
|
166
|
+
return cls(request=snapshot)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class HarnessCompileResult(BaseModel):
|
|
170
|
+
"""Immutable compile result boundary."""
|
|
171
|
+
|
|
172
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
173
|
+
|
|
174
|
+
request_id: StrictStr
|
|
175
|
+
status: CompileStatus
|
|
176
|
+
plan_commit_certainty: PlanCommitCertainty
|
|
177
|
+
diagnostic_report_state: DiagnosticReportState = DiagnosticReportState.ABSENT
|
|
178
|
+
failure_phase: CompilerPhase | None = None
|
|
179
|
+
source_document_sha256: StrictStr | None = None
|
|
180
|
+
source_sha256: StrictStr | None = Field(
|
|
181
|
+
default=None,
|
|
182
|
+
validation_alias=AliasChoices("source_sha256", "source_semantic_sha256"),
|
|
183
|
+
)
|
|
184
|
+
request_identity_sha256: StrictStr | None = None
|
|
185
|
+
harness_id: StrictStr | None = None
|
|
186
|
+
compiled_plan_path: StrictStr | None = None
|
|
187
|
+
compiled_sha256: StrictStr | None = None
|
|
188
|
+
diagnostics_path: StrictStr | None = None
|
|
189
|
+
diagnostics: tuple[CompilerDiagnostic, ...] = Field(default_factory=tuple)
|
|
190
|
+
|
|
191
|
+
@field_validator("request_id")
|
|
192
|
+
@classmethod
|
|
193
|
+
def _request_id_valid(cls, value: str) -> str:
|
|
194
|
+
return validate_request_id(value)
|
|
195
|
+
|
|
196
|
+
@field_validator(
|
|
197
|
+
"source_document_sha256",
|
|
198
|
+
"source_sha256",
|
|
199
|
+
"request_identity_sha256",
|
|
200
|
+
"compiled_sha256",
|
|
201
|
+
)
|
|
202
|
+
@classmethod
|
|
203
|
+
def _hash_valid(cls, value: str | None, info: object) -> str | None:
|
|
204
|
+
if value is None:
|
|
205
|
+
return None
|
|
206
|
+
field_name = getattr(info, "field_name", "sha256")
|
|
207
|
+
return validate_sha256(value, field_name)
|
|
208
|
+
|
|
209
|
+
@field_validator("harness_id")
|
|
210
|
+
@classmethod
|
|
211
|
+
def _harness_id_valid(cls, value: str | None) -> str | None:
|
|
212
|
+
return None if value is None else validate_harness_id(value)
|
|
213
|
+
|
|
214
|
+
@field_validator("compiled_plan_path", "diagnostics_path")
|
|
215
|
+
@classmethod
|
|
216
|
+
def _relative_output_path_valid(cls, value: str | None) -> str | None:
|
|
217
|
+
if value is None:
|
|
218
|
+
return None
|
|
219
|
+
if not value.strip() or _relative_posix_parts(value) is None:
|
|
220
|
+
raise ValueError("output path must be a relative POSIX path string")
|
|
221
|
+
return validate_utf8_size(value, "output path", 1024)
|
|
222
|
+
|
|
223
|
+
@field_validator("diagnostics")
|
|
224
|
+
@classmethod
|
|
225
|
+
def _diagnostics_sorted(
|
|
226
|
+
cls, value: tuple[CompilerDiagnostic, ...]
|
|
227
|
+
) -> tuple[CompilerDiagnostic, ...]:
|
|
228
|
+
return bound_diagnostics(value)
|
|
229
|
+
|
|
230
|
+
@model_validator(mode="after")
|
|
231
|
+
def _result_invariants(self) -> HarnessCompileResult:
|
|
232
|
+
if self.source_sha256 is not None and self.source_document_sha256 is None:
|
|
233
|
+
raise ValueError("semantic hashes require a source document hash")
|
|
234
|
+
if self.harness_id is not None and self.source_sha256 is None:
|
|
235
|
+
raise ValueError("harness identity requires a semantic hash")
|
|
236
|
+
if self.diagnostic_report_state == DiagnosticReportState.ABSENT:
|
|
237
|
+
if self.diagnostics_path is not None:
|
|
238
|
+
raise ValueError("absent diagnostics cannot carry a diagnostics path")
|
|
239
|
+
elif self.diagnostic_report_state in {
|
|
240
|
+
DiagnosticReportState.PREPARED,
|
|
241
|
+
DiagnosticReportState.COMMITTED,
|
|
242
|
+
}:
|
|
243
|
+
if self.diagnostics_path is None:
|
|
244
|
+
raise ValueError("persisted diagnostics require a diagnostics path")
|
|
245
|
+
if self.status == CompileStatus.FAILED:
|
|
246
|
+
if self.failure_phase is None:
|
|
247
|
+
raise ValueError("failed results require a failure phase")
|
|
248
|
+
if self.plan_commit_certainty == PlanCommitCertainty.COMMITTED:
|
|
249
|
+
raise ValueError("failed results cannot have committed certainty")
|
|
250
|
+
if self.failure_phase == CompilerPhase.REQUEST:
|
|
251
|
+
if (
|
|
252
|
+
self.source_document_sha256 is not None
|
|
253
|
+
or self.source_sha256 is not None
|
|
254
|
+
or self.harness_id is not None
|
|
255
|
+
):
|
|
256
|
+
raise ValueError("request failures cannot carry source evidence")
|
|
257
|
+
if self.failure_phase == CompilerPhase.PARSE:
|
|
258
|
+
if self.source_sha256 is not None or self.harness_id is not None:
|
|
259
|
+
raise ValueError("parse failures cannot carry semantic evidence")
|
|
260
|
+
if self.plan_commit_certainty == PlanCommitCertainty.ABSENT:
|
|
261
|
+
if (
|
|
262
|
+
self.compiled_plan_path is not None
|
|
263
|
+
or self.compiled_sha256 is not None
|
|
264
|
+
):
|
|
265
|
+
raise ValueError(
|
|
266
|
+
"failed/absent carries no plan path or compiled hash"
|
|
267
|
+
)
|
|
268
|
+
if self.plan_commit_certainty == PlanCommitCertainty.UNKNOWN:
|
|
269
|
+
if self.failure_phase != CompilerPhase.OUTPUT:
|
|
270
|
+
raise ValueError(
|
|
271
|
+
"unknown certainty is only valid for output failures"
|
|
272
|
+
)
|
|
273
|
+
if self.compiled_plan_path is None or self.compiled_sha256 is None:
|
|
274
|
+
raise ValueError(
|
|
275
|
+
"unknown certainty requires candidate plan path and hash"
|
|
276
|
+
)
|
|
277
|
+
if self.status == CompileStatus.PREPARED:
|
|
278
|
+
if self.failure_phase is not None:
|
|
279
|
+
raise ValueError("prepared results cannot carry a failure phase")
|
|
280
|
+
if (
|
|
281
|
+
self.source_document_sha256 is None
|
|
282
|
+
or self.source_sha256 is None
|
|
283
|
+
or self.harness_id is None
|
|
284
|
+
):
|
|
285
|
+
raise ValueError(
|
|
286
|
+
"prepared results require source hashes and harness identity"
|
|
287
|
+
)
|
|
288
|
+
if self.plan_commit_certainty == PlanCommitCertainty.UNKNOWN:
|
|
289
|
+
raise ValueError("prepared results cannot have unknown certainty")
|
|
290
|
+
if self.plan_commit_certainty == PlanCommitCertainty.COMMITTED:
|
|
291
|
+
if self.compiled_plan_path is None or self.compiled_sha256 is None:
|
|
292
|
+
raise ValueError(
|
|
293
|
+
"prepared/committed results require compiled plan path and hash"
|
|
294
|
+
)
|
|
295
|
+
if (
|
|
296
|
+
self.plan_commit_certainty == PlanCommitCertainty.ABSENT
|
|
297
|
+
and self.compiled_plan_path is not None
|
|
298
|
+
):
|
|
299
|
+
raise ValueError("prepared/absent carries no committed plan path")
|
|
300
|
+
if self.status == CompileStatus.COMMITTED:
|
|
301
|
+
if self.failure_phase is not None:
|
|
302
|
+
raise ValueError("committed results cannot carry a failure phase")
|
|
303
|
+
if self.plan_commit_certainty != PlanCommitCertainty.COMMITTED:
|
|
304
|
+
raise ValueError("committed results require committed certainty")
|
|
305
|
+
required = (
|
|
306
|
+
self.compiled_plan_path,
|
|
307
|
+
self.compiled_sha256,
|
|
308
|
+
self.source_sha256,
|
|
309
|
+
self.source_document_sha256,
|
|
310
|
+
self.harness_id,
|
|
311
|
+
)
|
|
312
|
+
if any(item is None for item in required):
|
|
313
|
+
raise ValueError(
|
|
314
|
+
"committed results require paths, hashes, and harness identity"
|
|
315
|
+
)
|
|
316
|
+
return self
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class HarnessRequestAdmissionResult(BaseModel):
|
|
320
|
+
"""Raw request admission result."""
|
|
321
|
+
|
|
322
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
323
|
+
|
|
324
|
+
request: HarnessCompileRequest | None = None
|
|
325
|
+
parsed_source: ParsedHarnessSource | None = None
|
|
326
|
+
result: HarnessCompileResult | None = None
|
|
327
|
+
|
|
328
|
+
@model_validator(mode="after")
|
|
329
|
+
def _exactly_one_outcome(self) -> HarnessRequestAdmissionResult:
|
|
330
|
+
if (self.request is None) == (self.result is None):
|
|
331
|
+
raise ValueError("exactly one of request or result must be provided")
|
|
332
|
+
if self.request is None and self.parsed_source is not None:
|
|
333
|
+
raise ValueError("failed admission cannot carry parsed source")
|
|
334
|
+
if self.request is not None:
|
|
335
|
+
if self.parsed_source is None or self.parsed_source.source is None:
|
|
336
|
+
raise ValueError("successful admission requires parsed source")
|
|
337
|
+
return self
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class HarnessCompileRequestAdmission(Protocol):
|
|
341
|
+
"""Protocol for raw mapping admission into compile requests."""
|
|
342
|
+
|
|
343
|
+
def admit(
|
|
344
|
+
self,
|
|
345
|
+
raw_request: Mapping[str, object],
|
|
346
|
+
) -> HarnessRequestAdmissionResult:
|
|
347
|
+
"""Admit a raw request or return a failed compile result."""
|
|
348
|
+
...
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class DefaultHarnessCompileRequestAdmission:
|
|
352
|
+
"""Filesystem-safe admission boundary for raw compile requests."""
|
|
353
|
+
|
|
354
|
+
def __init__(
|
|
355
|
+
self,
|
|
356
|
+
*,
|
|
357
|
+
parser: HarnessSourceParserProtocol | None = None,
|
|
358
|
+
) -> None:
|
|
359
|
+
self._parser = parser or HarnessSourceParser()
|
|
360
|
+
|
|
361
|
+
def admit(
|
|
362
|
+
self,
|
|
363
|
+
raw_request: Mapping[str, object],
|
|
364
|
+
) -> HarnessRequestAdmissionResult:
|
|
365
|
+
"""Admit a raw request after path and source checks, or fail deterministically."""
|
|
366
|
+
request_id = _request_id_from_raw(raw_request)
|
|
367
|
+
try:
|
|
368
|
+
request = HarnessCompileRequest.model_validate(raw_request)
|
|
369
|
+
except ValidationError as exc:
|
|
370
|
+
return _failed_admission(
|
|
371
|
+
request_id=request_id,
|
|
372
|
+
code="MF-S018",
|
|
373
|
+
message="Compile request schema validation failed.",
|
|
374
|
+
phase=CompilerPhase.REQUEST,
|
|
375
|
+
fields=(_diagnostic_field("field_path", _validation_pointer(exc)),),
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
root_admission = _admit_roots_and_output(request)
|
|
379
|
+
if root_admission is not None:
|
|
380
|
+
return root_admission
|
|
381
|
+
|
|
382
|
+
source_admission = _read_admitted_source(request)
|
|
383
|
+
if isinstance(source_admission, HarnessRequestAdmissionResult):
|
|
384
|
+
return source_admission
|
|
385
|
+
|
|
386
|
+
parsed = self._parser.parse(
|
|
387
|
+
SourceDocument(
|
|
388
|
+
logical_path=request.source_path,
|
|
389
|
+
format=request.source_format,
|
|
390
|
+
content=source_admission,
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
if parsed.diagnostics:
|
|
394
|
+
return HarnessRequestAdmissionResult(
|
|
395
|
+
result=HarnessCompileResult(
|
|
396
|
+
request_id=request.request_id,
|
|
397
|
+
status=CompileStatus.FAILED,
|
|
398
|
+
plan_commit_certainty=PlanCommitCertainty.ABSENT,
|
|
399
|
+
failure_phase=parsed.diagnostics[0].phase,
|
|
400
|
+
source_document_sha256=(
|
|
401
|
+
None
|
|
402
|
+
if parsed.source_document_sha256 == "0" * 64
|
|
403
|
+
else parsed.source_document_sha256
|
|
404
|
+
),
|
|
405
|
+
diagnostics=parsed.diagnostics,
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
source_diagnostics = _post_parse_source_diagnostics(
|
|
409
|
+
request=request,
|
|
410
|
+
parsed=parsed,
|
|
411
|
+
)
|
|
412
|
+
if source_diagnostics:
|
|
413
|
+
return _failed_source_admission(
|
|
414
|
+
request=request,
|
|
415
|
+
parsed=parsed,
|
|
416
|
+
diagnostics=source_diagnostics,
|
|
417
|
+
)
|
|
418
|
+
return HarnessRequestAdmissionResult(request=request, parsed_source=parsed)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _request_id_from_raw(raw_request: Mapping[str, object]) -> str:
|
|
422
|
+
value = raw_request.get("request_id")
|
|
423
|
+
if isinstance(value, str):
|
|
424
|
+
try:
|
|
425
|
+
return validate_request_id(value)
|
|
426
|
+
except ValueError:
|
|
427
|
+
pass
|
|
428
|
+
return _INVALID_REQUEST_ID
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _admit_roots_and_output(
|
|
432
|
+
request: HarnessCompileRequest,
|
|
433
|
+
) -> HarnessRequestAdmissionResult | None:
|
|
434
|
+
diagnostics: list[CompilerDiagnostic] = []
|
|
435
|
+
|
|
436
|
+
source_parts = _relative_posix_parts(request.source_path)
|
|
437
|
+
if source_parts is None:
|
|
438
|
+
diagnostics.append(
|
|
439
|
+
_request_diagnostic(
|
|
440
|
+
code="MF-S001",
|
|
441
|
+
message="Source path must stay within source_root.",
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
output_parts = _relative_posix_parts(request.output_dir)
|
|
446
|
+
if output_parts is None:
|
|
447
|
+
diagnostics.append(
|
|
448
|
+
_request_diagnostic(
|
|
449
|
+
code="MF-S002",
|
|
450
|
+
message="Output directory must stay within output_root.",
|
|
451
|
+
)
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
output_root = _resolve_existing_directory(
|
|
455
|
+
request.output_root,
|
|
456
|
+
request_id=request.request_id,
|
|
457
|
+
code="MF-S016",
|
|
458
|
+
label="output_root",
|
|
459
|
+
)
|
|
460
|
+
if isinstance(output_root, HarnessRequestAdmissionResult):
|
|
461
|
+
assert output_root.result is not None
|
|
462
|
+
diagnostics.extend(output_root.result.diagnostics)
|
|
463
|
+
output_root_path: Path | None = None
|
|
464
|
+
else:
|
|
465
|
+
output_root_path = output_root
|
|
466
|
+
|
|
467
|
+
source_root = _resolve_existing_directory(
|
|
468
|
+
request.source_root,
|
|
469
|
+
request_id=request.request_id,
|
|
470
|
+
code="MF-S013",
|
|
471
|
+
label="source_root",
|
|
472
|
+
)
|
|
473
|
+
if isinstance(source_root, HarnessRequestAdmissionResult):
|
|
474
|
+
assert source_root.result is not None
|
|
475
|
+
diagnostics.extend(source_root.result.diagnostics)
|
|
476
|
+
source_root_path: Path | None = None
|
|
477
|
+
else:
|
|
478
|
+
source_root_path = source_root
|
|
479
|
+
|
|
480
|
+
if output_root_path is not None and output_parts is not None:
|
|
481
|
+
output_diagnostic = _admit_output_directory(
|
|
482
|
+
request=request,
|
|
483
|
+
output_root=output_root_path,
|
|
484
|
+
output_parts=output_parts,
|
|
485
|
+
)
|
|
486
|
+
if output_diagnostic is not None:
|
|
487
|
+
diagnostics.append(output_diagnostic)
|
|
488
|
+
|
|
489
|
+
if source_root_path is not None and source_parts is not None:
|
|
490
|
+
source_diagnostic = _admit_source_path(
|
|
491
|
+
source_root=source_root_path,
|
|
492
|
+
source_parts=source_parts,
|
|
493
|
+
)
|
|
494
|
+
if source_diagnostic is not None:
|
|
495
|
+
diagnostics.append(source_diagnostic)
|
|
496
|
+
|
|
497
|
+
if diagnostics:
|
|
498
|
+
return _failed_request_admission_many(
|
|
499
|
+
request_id=request.request_id,
|
|
500
|
+
diagnostics=tuple(diagnostics),
|
|
501
|
+
)
|
|
502
|
+
return None
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _resolve_existing_directory(
|
|
506
|
+
value: str,
|
|
507
|
+
*,
|
|
508
|
+
request_id: str,
|
|
509
|
+
code: str,
|
|
510
|
+
label: str,
|
|
511
|
+
) -> Path | HarnessRequestAdmissionResult:
|
|
512
|
+
try:
|
|
513
|
+
path = Path(value)
|
|
514
|
+
path_stat = path.lstat()
|
|
515
|
+
except OSError as exc:
|
|
516
|
+
return _failed_admission(
|
|
517
|
+
request_id=request_id,
|
|
518
|
+
code=code,
|
|
519
|
+
message=f"{label} must be an existing directory.",
|
|
520
|
+
phase=CompilerPhase.REQUEST,
|
|
521
|
+
fields=(_diagnostic_field("errno", exc.errno or errno.ENOENT),),
|
|
522
|
+
)
|
|
523
|
+
if not stat.S_ISDIR(path_stat.st_mode):
|
|
524
|
+
return _failed_admission(
|
|
525
|
+
request_id=request_id,
|
|
526
|
+
code=code,
|
|
527
|
+
message=f"{label} must be a directory.",
|
|
528
|
+
phase=CompilerPhase.REQUEST,
|
|
529
|
+
)
|
|
530
|
+
try:
|
|
531
|
+
resolved = path.resolve(strict=True)
|
|
532
|
+
except OSError as exc:
|
|
533
|
+
return _failed_admission(
|
|
534
|
+
request_id=request_id,
|
|
535
|
+
code=code,
|
|
536
|
+
message=f"{label} must be an existing directory.",
|
|
537
|
+
phase=CompilerPhase.REQUEST,
|
|
538
|
+
fields=(_diagnostic_field("errno", exc.errno or errno.ENOENT),),
|
|
539
|
+
)
|
|
540
|
+
return resolved
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _read_admitted_source(
|
|
544
|
+
request: HarnessCompileRequest,
|
|
545
|
+
) -> bytes | HarnessRequestAdmissionResult:
|
|
546
|
+
root = Path(request.source_root).resolve(strict=True)
|
|
547
|
+
source_parts = _relative_posix_parts(request.source_path)
|
|
548
|
+
if source_parts is None:
|
|
549
|
+
return _failed_admission(
|
|
550
|
+
request_id=request.request_id,
|
|
551
|
+
code="MF-S001",
|
|
552
|
+
message="Source path must stay within source_root.",
|
|
553
|
+
phase=CompilerPhase.REQUEST,
|
|
554
|
+
)
|
|
555
|
+
candidate = root.joinpath(*source_parts)
|
|
556
|
+
try:
|
|
557
|
+
parent_resolved = candidate.parent.resolve(strict=True)
|
|
558
|
+
except OSError as exc:
|
|
559
|
+
return _failed_admission(
|
|
560
|
+
request_id=request.request_id,
|
|
561
|
+
code="MF-S013",
|
|
562
|
+
message="Source file does not exist.",
|
|
563
|
+
phase=CompilerPhase.REQUEST,
|
|
564
|
+
fields=(_diagnostic_field("errno", exc.errno or errno.ENOENT),),
|
|
565
|
+
)
|
|
566
|
+
if not _is_relative_to(parent_resolved, root):
|
|
567
|
+
return _failed_admission(
|
|
568
|
+
request_id=request.request_id,
|
|
569
|
+
code="MF-S001",
|
|
570
|
+
message="Source path must stay within source_root.",
|
|
571
|
+
phase=CompilerPhase.REQUEST,
|
|
572
|
+
)
|
|
573
|
+
resolved = parent_resolved.joinpath(candidate.name)
|
|
574
|
+
|
|
575
|
+
try:
|
|
576
|
+
before = resolved.lstat()
|
|
577
|
+
except FileNotFoundError as exc:
|
|
578
|
+
return _failed_admission(
|
|
579
|
+
request_id=request.request_id,
|
|
580
|
+
code="MF-S013",
|
|
581
|
+
message="Source file does not exist.",
|
|
582
|
+
phase=CompilerPhase.REQUEST,
|
|
583
|
+
fields=(_diagnostic_field("errno", exc.errno or errno.ENOENT),),
|
|
584
|
+
)
|
|
585
|
+
except OSError as exc:
|
|
586
|
+
return _failed_admission(
|
|
587
|
+
request_id=request.request_id,
|
|
588
|
+
code="MF-S013",
|
|
589
|
+
message="Source file could not be read.",
|
|
590
|
+
phase=CompilerPhase.REQUEST,
|
|
591
|
+
fields=(_diagnostic_field("errno", exc.errno or errno.EIO),),
|
|
592
|
+
)
|
|
593
|
+
if not stat.S_ISREG(before.st_mode):
|
|
594
|
+
if stat.S_ISLNK(before.st_mode):
|
|
595
|
+
try:
|
|
596
|
+
symlink_target = resolved.resolve(strict=True)
|
|
597
|
+
except OSError:
|
|
598
|
+
symlink_target = None
|
|
599
|
+
else:
|
|
600
|
+
if symlink_target is not None and not _is_relative_to(
|
|
601
|
+
symlink_target, root
|
|
602
|
+
):
|
|
603
|
+
return _failed_admission(
|
|
604
|
+
request_id=request.request_id,
|
|
605
|
+
code="MF-S001",
|
|
606
|
+
message="Source path must stay within source_root.",
|
|
607
|
+
phase=CompilerPhase.REQUEST,
|
|
608
|
+
)
|
|
609
|
+
return _failed_admission(
|
|
610
|
+
request_id=request.request_id,
|
|
611
|
+
code="MF-S014",
|
|
612
|
+
message="Source path must be a regular file.",
|
|
613
|
+
phase=CompilerPhase.REQUEST,
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
try:
|
|
617
|
+
with resolved.open("rb") as handle:
|
|
618
|
+
opened = os.fstat(handle.fileno())
|
|
619
|
+
if not _same_file(before, opened) or not stat.S_ISREG(opened.st_mode):
|
|
620
|
+
return _source_replaced(request.request_id)
|
|
621
|
+
content = handle.read(MAX_SOURCE_SIZE_BYTES + 1)
|
|
622
|
+
if len(content) > MAX_SOURCE_SIZE_BYTES:
|
|
623
|
+
return _failed_admission(
|
|
624
|
+
request_id=request.request_id,
|
|
625
|
+
code="MF-S003",
|
|
626
|
+
message="Source file exceeds the maximum size.",
|
|
627
|
+
phase=CompilerPhase.PARSE,
|
|
628
|
+
fields=(_diagnostic_field("source_size", len(content)),),
|
|
629
|
+
)
|
|
630
|
+
after = resolved.stat()
|
|
631
|
+
if not _same_file(opened, after):
|
|
632
|
+
return _source_replaced(request.request_id)
|
|
633
|
+
return content
|
|
634
|
+
except OSError as exc:
|
|
635
|
+
return _failed_admission(
|
|
636
|
+
request_id=request.request_id,
|
|
637
|
+
code="MF-S013",
|
|
638
|
+
message="Source file could not be read.",
|
|
639
|
+
phase=CompilerPhase.REQUEST,
|
|
640
|
+
fields=(_diagnostic_field("errno", exc.errno or errno.EIO),),
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _relative_posix_parts(value: str) -> tuple[str, ...] | None:
|
|
645
|
+
path = PurePosixPath(value)
|
|
646
|
+
if path.is_absolute() or value.startswith("/") or "\\" in value:
|
|
647
|
+
return None
|
|
648
|
+
parts = path.parts
|
|
649
|
+
if not parts or any(_is_forbidden_path_part(part) for part in parts):
|
|
650
|
+
return None
|
|
651
|
+
return parts
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _is_forbidden_path_part(part: str) -> bool:
|
|
655
|
+
reserved_names = {
|
|
656
|
+
"con",
|
|
657
|
+
"prn",
|
|
658
|
+
"aux",
|
|
659
|
+
"nul",
|
|
660
|
+
*(f"com{index}" for index in range(1, 10)),
|
|
661
|
+
*(f"lpt{index}" for index in range(1, 10)),
|
|
662
|
+
}
|
|
663
|
+
stem = part.split(".", 1)[0].casefold()
|
|
664
|
+
return (
|
|
665
|
+
part in {"", ".", ".."}
|
|
666
|
+
or part.endswith((" ", "."))
|
|
667
|
+
or ":" in part
|
|
668
|
+
or "\x00" in part
|
|
669
|
+
or stem in reserved_names
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _is_relative_to(path: Path, root: Path) -> bool:
|
|
674
|
+
try:
|
|
675
|
+
path.relative_to(root)
|
|
676
|
+
return True
|
|
677
|
+
except ValueError:
|
|
678
|
+
return False
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _same_file(left: os.stat_result, right: os.stat_result) -> bool:
|
|
682
|
+
return (
|
|
683
|
+
left.st_dev,
|
|
684
|
+
left.st_ino,
|
|
685
|
+
left.st_size,
|
|
686
|
+
left.st_mtime_ns,
|
|
687
|
+
) == (
|
|
688
|
+
right.st_dev,
|
|
689
|
+
right.st_ino,
|
|
690
|
+
right.st_size,
|
|
691
|
+
right.st_mtime_ns,
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _source_replaced(request_id: str) -> HarnessRequestAdmissionResult:
|
|
696
|
+
return _failed_admission(
|
|
697
|
+
request_id=request_id,
|
|
698
|
+
code="MF-S015",
|
|
699
|
+
message="Source file changed during admission.",
|
|
700
|
+
phase=CompilerPhase.REQUEST,
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def _failed_source_admission(
|
|
705
|
+
*,
|
|
706
|
+
request: HarnessCompileRequest,
|
|
707
|
+
parsed: ParsedHarnessSource,
|
|
708
|
+
diagnostics: tuple[CompilerDiagnostic, ...],
|
|
709
|
+
) -> HarnessRequestAdmissionResult:
|
|
710
|
+
source = parsed.source
|
|
711
|
+
source_sha256 = None
|
|
712
|
+
harness_id = None
|
|
713
|
+
if source is not None:
|
|
714
|
+
source_sha256 = _source_semantic_sha256(source)
|
|
715
|
+
harness_id = source.harness_id
|
|
716
|
+
return HarnessRequestAdmissionResult(
|
|
717
|
+
result=HarnessCompileResult(
|
|
718
|
+
request_id=request.request_id,
|
|
719
|
+
status=CompileStatus.FAILED,
|
|
720
|
+
plan_commit_certainty=PlanCommitCertainty.ABSENT,
|
|
721
|
+
failure_phase=CompilerPhase.SCHEMA,
|
|
722
|
+
source_document_sha256=parsed.source_document_sha256,
|
|
723
|
+
source_sha256=source_sha256,
|
|
724
|
+
harness_id=harness_id,
|
|
725
|
+
diagnostics=diagnostics,
|
|
726
|
+
)
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def _source_semantic_sha256(source: HarnessSource) -> str:
|
|
731
|
+
payload = json.dumps(
|
|
732
|
+
source.model_dump(mode="json"),
|
|
733
|
+
sort_keys=True,
|
|
734
|
+
separators=(",", ":"),
|
|
735
|
+
).encode("utf-8")
|
|
736
|
+
return hashlib.sha256(payload).hexdigest()
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def _post_parse_source_diagnostics(
|
|
740
|
+
*,
|
|
741
|
+
request: HarnessCompileRequest,
|
|
742
|
+
parsed: ParsedHarnessSource,
|
|
743
|
+
) -> tuple[CompilerDiagnostic, ...]:
|
|
744
|
+
source = parsed.source
|
|
745
|
+
if source is None:
|
|
746
|
+
return ()
|
|
747
|
+
diagnostics = [
|
|
748
|
+
*_cross_field_diagnostics(
|
|
749
|
+
request=request,
|
|
750
|
+
source=source,
|
|
751
|
+
logical_path=request.source_path,
|
|
752
|
+
location_index=parsed.location_index,
|
|
753
|
+
),
|
|
754
|
+
*_secret_scalar_diagnostics(
|
|
755
|
+
source=source,
|
|
756
|
+
logical_path=request.source_path,
|
|
757
|
+
location_index=parsed.location_index,
|
|
758
|
+
),
|
|
759
|
+
]
|
|
760
|
+
return bound_diagnostics(diagnostics)
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def _cross_field_diagnostics(
|
|
764
|
+
*,
|
|
765
|
+
request: HarnessCompileRequest,
|
|
766
|
+
source: HarnessSource,
|
|
767
|
+
logical_path: str,
|
|
768
|
+
location_index: tuple[SourceReference, ...],
|
|
769
|
+
) -> tuple[CompilerDiagnostic, ...]:
|
|
770
|
+
diagnostics: list[CompilerDiagnostic] = []
|
|
771
|
+
if source.harness_id != request.expected_harness_id:
|
|
772
|
+
diagnostics.append(
|
|
773
|
+
_source_schema_diagnostic(
|
|
774
|
+
code="MF-S027",
|
|
775
|
+
message="Source harness_id does not match expected_harness_id.",
|
|
776
|
+
logical_path=logical_path,
|
|
777
|
+
field_path="/harness_id",
|
|
778
|
+
location_index=location_index,
|
|
779
|
+
fields=(_diagnostic_field("field_path", "/harness_id"),),
|
|
780
|
+
)
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
if request.stage_kind_id not in source.stage_scope.stage_kind_ids:
|
|
784
|
+
diagnostics.append(
|
|
785
|
+
_source_schema_diagnostic(
|
|
786
|
+
code="MF-S028",
|
|
787
|
+
message="Requested stage_kind_id is outside source stage_scope.",
|
|
788
|
+
logical_path=logical_path,
|
|
789
|
+
field_path="/stage_scope/stage_kind_ids",
|
|
790
|
+
location_index=location_index,
|
|
791
|
+
fields=(
|
|
792
|
+
_diagnostic_field("field_path", "/stage_scope/stage_kind_ids"),
|
|
793
|
+
),
|
|
794
|
+
)
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
terminal_diagnostic = _terminal_result_diagnostic(
|
|
798
|
+
request=request,
|
|
799
|
+
source=source,
|
|
800
|
+
logical_path=logical_path,
|
|
801
|
+
location_index=location_index,
|
|
802
|
+
)
|
|
803
|
+
if terminal_diagnostic is not None:
|
|
804
|
+
diagnostics.append(terminal_diagnostic)
|
|
805
|
+
return tuple(diagnostics)
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def _terminal_result_diagnostic(
|
|
809
|
+
*,
|
|
810
|
+
request: HarnessCompileRequest,
|
|
811
|
+
source: HarnessSource,
|
|
812
|
+
logical_path: str,
|
|
813
|
+
location_index: tuple[SourceReference, ...],
|
|
814
|
+
) -> CompilerDiagnostic | None:
|
|
815
|
+
terminal_paths: dict[str, str] = {}
|
|
816
|
+
for index, node in enumerate(source.graph.nodes):
|
|
817
|
+
if node.terminal_result is None:
|
|
818
|
+
continue
|
|
819
|
+
field_path = f"/graph/nodes/{index}/terminal_result"
|
|
820
|
+
if node.terminal_result in terminal_paths:
|
|
821
|
+
return _source_schema_diagnostic(
|
|
822
|
+
code="MF-S029",
|
|
823
|
+
message="Legal terminal results must match source terminal declarations.",
|
|
824
|
+
logical_path=logical_path,
|
|
825
|
+
field_path=field_path,
|
|
826
|
+
location_index=location_index,
|
|
827
|
+
fields=(_diagnostic_field("field_path", field_path),),
|
|
828
|
+
)
|
|
829
|
+
terminal_paths[node.terminal_result] = field_path
|
|
830
|
+
source_terminals = set(terminal_paths)
|
|
831
|
+
requested_terminals = set(request.legal_terminal_results)
|
|
832
|
+
|
|
833
|
+
missing_source_terminal = next(
|
|
834
|
+
(
|
|
835
|
+
terminal
|
|
836
|
+
for terminal in request.legal_terminal_results
|
|
837
|
+
if terminal not in source_terminals
|
|
838
|
+
),
|
|
839
|
+
None,
|
|
840
|
+
)
|
|
841
|
+
undeclared_source_terminal = next(
|
|
842
|
+
(
|
|
843
|
+
node.terminal_result
|
|
844
|
+
for node in source.graph.nodes
|
|
845
|
+
if node.terminal_result is not None
|
|
846
|
+
and node.terminal_result not in requested_terminals
|
|
847
|
+
),
|
|
848
|
+
None,
|
|
849
|
+
)
|
|
850
|
+
unused_artifact_mapping = next(
|
|
851
|
+
(
|
|
852
|
+
item.terminal_result
|
|
853
|
+
for item in source.artifacts.required_by_terminal
|
|
854
|
+
if item.terminal_result not in source_terminals
|
|
855
|
+
),
|
|
856
|
+
None,
|
|
857
|
+
)
|
|
858
|
+
if (
|
|
859
|
+
missing_source_terminal is None
|
|
860
|
+
and undeclared_source_terminal is None
|
|
861
|
+
and unused_artifact_mapping is None
|
|
862
|
+
):
|
|
863
|
+
return None
|
|
864
|
+
|
|
865
|
+
if undeclared_source_terminal is not None:
|
|
866
|
+
field_path = terminal_paths[undeclared_source_terminal]
|
|
867
|
+
elif unused_artifact_mapping is not None:
|
|
868
|
+
field_path = next(
|
|
869
|
+
(
|
|
870
|
+
f"/artifacts/required_by_terminal/{index}/terminal_result"
|
|
871
|
+
for index, item in enumerate(source.artifacts.required_by_terminal)
|
|
872
|
+
if item.terminal_result == unused_artifact_mapping
|
|
873
|
+
),
|
|
874
|
+
"/artifacts/required_by_terminal",
|
|
875
|
+
)
|
|
876
|
+
else:
|
|
877
|
+
field_path = "/graph/nodes"
|
|
878
|
+
return _source_schema_diagnostic(
|
|
879
|
+
code="MF-S029",
|
|
880
|
+
message="Legal terminal results must match source terminal declarations.",
|
|
881
|
+
logical_path=logical_path,
|
|
882
|
+
field_path=field_path,
|
|
883
|
+
location_index=location_index,
|
|
884
|
+
fields=(_diagnostic_field("field_path", field_path),),
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def _secret_scalar_diagnostics(
|
|
889
|
+
*,
|
|
890
|
+
source: HarnessSource,
|
|
891
|
+
logical_path: str,
|
|
892
|
+
location_index: tuple[SourceReference, ...],
|
|
893
|
+
) -> tuple[CompilerDiagnostic, ...]:
|
|
894
|
+
policy = RedactionPolicy()
|
|
895
|
+
diagnostics: list[CompilerDiagnostic] = []
|
|
896
|
+
for field_path, field_name, value in _source_scalar_strings(
|
|
897
|
+
source.model_dump(mode="python")
|
|
898
|
+
):
|
|
899
|
+
if not detect_secret_candidate(
|
|
900
|
+
field_path=field_path,
|
|
901
|
+
field_name=field_name,
|
|
902
|
+
value=value,
|
|
903
|
+
policy=policy,
|
|
904
|
+
):
|
|
905
|
+
continue
|
|
906
|
+
diagnostics.append(
|
|
907
|
+
_source_schema_diagnostic(
|
|
908
|
+
code="MF-S026",
|
|
909
|
+
message="Source scalar resembles a secret and must be removed.",
|
|
910
|
+
logical_path=logical_path,
|
|
911
|
+
field_path=field_path,
|
|
912
|
+
location_index=location_index,
|
|
913
|
+
fields=(_diagnostic_field("field_path", field_path),),
|
|
914
|
+
)
|
|
915
|
+
)
|
|
916
|
+
return tuple(diagnostics)
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def _source_scalar_strings(
|
|
920
|
+
value: object,
|
|
921
|
+
*,
|
|
922
|
+
field_path: str = "/",
|
|
923
|
+
field_name: str = "",
|
|
924
|
+
) -> tuple[tuple[str, str, str], ...]:
|
|
925
|
+
if isinstance(value, str):
|
|
926
|
+
return ((field_path, field_name, value),)
|
|
927
|
+
if isinstance(value, Mapping):
|
|
928
|
+
result: list[tuple[str, str, str]] = []
|
|
929
|
+
for key, item in value.items():
|
|
930
|
+
if not isinstance(key, str):
|
|
931
|
+
continue
|
|
932
|
+
result.extend(
|
|
933
|
+
_source_scalar_strings(
|
|
934
|
+
item,
|
|
935
|
+
field_path=_join_pointer(field_path, key),
|
|
936
|
+
field_name=key,
|
|
937
|
+
)
|
|
938
|
+
)
|
|
939
|
+
return tuple(result)
|
|
940
|
+
if isinstance(value, tuple | list):
|
|
941
|
+
result = []
|
|
942
|
+
for index, item in enumerate(value):
|
|
943
|
+
result.extend(
|
|
944
|
+
_source_scalar_strings(
|
|
945
|
+
item,
|
|
946
|
+
field_path=_join_pointer(field_path, str(index)),
|
|
947
|
+
field_name=field_name,
|
|
948
|
+
)
|
|
949
|
+
)
|
|
950
|
+
return tuple(result)
|
|
951
|
+
return ()
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def _source_schema_diagnostic(
|
|
955
|
+
*,
|
|
956
|
+
code: str,
|
|
957
|
+
message: str,
|
|
958
|
+
logical_path: str,
|
|
959
|
+
field_path: str,
|
|
960
|
+
location_index: tuple[SourceReference, ...],
|
|
961
|
+
fields: tuple[DiagnosticField, ...] = (),
|
|
962
|
+
) -> CompilerDiagnostic:
|
|
963
|
+
return CompilerDiagnostic(
|
|
964
|
+
code=code,
|
|
965
|
+
severity=DiagnosticSeverity.ERROR,
|
|
966
|
+
phase=CompilerPhase.SCHEMA,
|
|
967
|
+
message=message,
|
|
968
|
+
source_reference=_nearest_source_reference(
|
|
969
|
+
logical_path=logical_path,
|
|
970
|
+
field_path=field_path,
|
|
971
|
+
location_index=location_index,
|
|
972
|
+
),
|
|
973
|
+
fields=fields,
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
def _nearest_source_reference(
|
|
978
|
+
*,
|
|
979
|
+
logical_path: str,
|
|
980
|
+
field_path: str,
|
|
981
|
+
location_index: tuple[SourceReference, ...],
|
|
982
|
+
) -> SourceReference:
|
|
983
|
+
by_path = {reference.field_path: reference for reference in location_index}
|
|
984
|
+
candidate = field_path
|
|
985
|
+
while candidate:
|
|
986
|
+
if candidate in by_path:
|
|
987
|
+
reference = by_path[candidate]
|
|
988
|
+
if reference.field_path == field_path:
|
|
989
|
+
return reference
|
|
990
|
+
return SourceReference(
|
|
991
|
+
logical_path=reference.logical_path,
|
|
992
|
+
field_path=field_path,
|
|
993
|
+
location=reference.location,
|
|
994
|
+
)
|
|
995
|
+
if candidate == "/":
|
|
996
|
+
break
|
|
997
|
+
candidate = candidate.rsplit("/", 1)[0] or "/"
|
|
998
|
+
return SourceReference(logical_path=logical_path, field_path=field_path)
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def _failed_admission(
|
|
1002
|
+
*,
|
|
1003
|
+
request_id: str,
|
|
1004
|
+
code: str,
|
|
1005
|
+
message: str,
|
|
1006
|
+
phase: CompilerPhase,
|
|
1007
|
+
fields: tuple[DiagnosticField, ...] = (),
|
|
1008
|
+
) -> HarnessRequestAdmissionResult:
|
|
1009
|
+
return _failed_request_admission_many(
|
|
1010
|
+
request_id=request_id,
|
|
1011
|
+
diagnostics=(
|
|
1012
|
+
CompilerDiagnostic(
|
|
1013
|
+
code=code,
|
|
1014
|
+
severity=DiagnosticSeverity.ERROR,
|
|
1015
|
+
phase=phase,
|
|
1016
|
+
message=message,
|
|
1017
|
+
fields=fields,
|
|
1018
|
+
),
|
|
1019
|
+
),
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
def _failed_request_admission_many(
|
|
1024
|
+
*,
|
|
1025
|
+
request_id: str,
|
|
1026
|
+
diagnostics: tuple[CompilerDiagnostic, ...],
|
|
1027
|
+
) -> HarnessRequestAdmissionResult:
|
|
1028
|
+
return HarnessRequestAdmissionResult(
|
|
1029
|
+
result=HarnessCompileResult(
|
|
1030
|
+
request_id=request_id,
|
|
1031
|
+
status=CompileStatus.FAILED,
|
|
1032
|
+
plan_commit_certainty=PlanCommitCertainty.ABSENT,
|
|
1033
|
+
failure_phase=diagnostics[0].phase,
|
|
1034
|
+
diagnostics=diagnostics,
|
|
1035
|
+
)
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
def _request_diagnostic(
|
|
1040
|
+
*,
|
|
1041
|
+
code: str,
|
|
1042
|
+
message: str,
|
|
1043
|
+
fields: tuple[DiagnosticField, ...] = (),
|
|
1044
|
+
) -> CompilerDiagnostic:
|
|
1045
|
+
return CompilerDiagnostic(
|
|
1046
|
+
code=code,
|
|
1047
|
+
severity=DiagnosticSeverity.ERROR,
|
|
1048
|
+
phase=CompilerPhase.REQUEST,
|
|
1049
|
+
message=message,
|
|
1050
|
+
fields=fields,
|
|
1051
|
+
)
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
def _admit_output_directory(
|
|
1055
|
+
*,
|
|
1056
|
+
request: HarnessCompileRequest,
|
|
1057
|
+
output_root: Path,
|
|
1058
|
+
output_parts: tuple[str, ...],
|
|
1059
|
+
) -> CompilerDiagnostic | None:
|
|
1060
|
+
candidate = output_root.joinpath(*output_parts)
|
|
1061
|
+
try:
|
|
1062
|
+
parent_resolved = candidate.parent.resolve(strict=True)
|
|
1063
|
+
except OSError as exc:
|
|
1064
|
+
return _request_diagnostic(
|
|
1065
|
+
code="MF-S017",
|
|
1066
|
+
message="Output directory must already exist.",
|
|
1067
|
+
fields=(_diagnostic_field("errno", exc.errno or errno.ENOENT),),
|
|
1068
|
+
)
|
|
1069
|
+
if not _is_relative_to(parent_resolved, output_root):
|
|
1070
|
+
return _request_diagnostic(
|
|
1071
|
+
code="MF-S002",
|
|
1072
|
+
message="Output directory must stay within output_root.",
|
|
1073
|
+
)
|
|
1074
|
+
resolved = parent_resolved.joinpath(candidate.name)
|
|
1075
|
+
try:
|
|
1076
|
+
before = resolved.lstat()
|
|
1077
|
+
except FileNotFoundError as exc:
|
|
1078
|
+
return _request_diagnostic(
|
|
1079
|
+
code="MF-S017",
|
|
1080
|
+
message="Output directory must already exist.",
|
|
1081
|
+
fields=(_diagnostic_field("errno", exc.errno or errno.ENOENT),),
|
|
1082
|
+
)
|
|
1083
|
+
except OSError as exc:
|
|
1084
|
+
return _request_diagnostic(
|
|
1085
|
+
code="MF-S017",
|
|
1086
|
+
message="Output directory could not be admitted.",
|
|
1087
|
+
fields=(_diagnostic_field("errno", exc.errno or errno.EIO),),
|
|
1088
|
+
)
|
|
1089
|
+
if stat.S_ISLNK(before.st_mode):
|
|
1090
|
+
try:
|
|
1091
|
+
symlink_target = resolved.resolve(strict=True)
|
|
1092
|
+
except OSError as exc:
|
|
1093
|
+
return _request_diagnostic(
|
|
1094
|
+
code="MF-S017",
|
|
1095
|
+
message="Output directory could not be admitted.",
|
|
1096
|
+
fields=(_diagnostic_field("errno", exc.errno or errno.EIO),),
|
|
1097
|
+
)
|
|
1098
|
+
if not _is_relative_to(symlink_target, output_root):
|
|
1099
|
+
return _request_diagnostic(
|
|
1100
|
+
code="MF-S002",
|
|
1101
|
+
message="Output directory must stay within output_root.",
|
|
1102
|
+
)
|
|
1103
|
+
return _request_diagnostic(
|
|
1104
|
+
code="MF-S017",
|
|
1105
|
+
message="Output directory must be a directory.",
|
|
1106
|
+
fields=(_diagnostic_field("output_dir", request.output_dir),),
|
|
1107
|
+
)
|
|
1108
|
+
if not stat.S_ISDIR(before.st_mode):
|
|
1109
|
+
return _request_diagnostic(
|
|
1110
|
+
code="MF-S017",
|
|
1111
|
+
message="Output directory must be a directory.",
|
|
1112
|
+
fields=(_diagnostic_field("output_dir", request.output_dir),),
|
|
1113
|
+
)
|
|
1114
|
+
try:
|
|
1115
|
+
resolved = resolved.resolve(strict=True)
|
|
1116
|
+
except OSError as exc:
|
|
1117
|
+
return _request_diagnostic(
|
|
1118
|
+
code="MF-S017",
|
|
1119
|
+
message="Output directory could not be admitted.",
|
|
1120
|
+
fields=(_diagnostic_field("errno", exc.errno or errno.EIO),),
|
|
1121
|
+
)
|
|
1122
|
+
if not _is_relative_to(resolved, output_root):
|
|
1123
|
+
return _request_diagnostic(
|
|
1124
|
+
code="MF-S002",
|
|
1125
|
+
message="Output directory must stay within output_root.",
|
|
1126
|
+
)
|
|
1127
|
+
return None
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
def _admit_source_path(
|
|
1131
|
+
*,
|
|
1132
|
+
source_root: Path,
|
|
1133
|
+
source_parts: tuple[str, ...],
|
|
1134
|
+
) -> CompilerDiagnostic | None:
|
|
1135
|
+
candidate = source_root.joinpath(*source_parts)
|
|
1136
|
+
try:
|
|
1137
|
+
parent_resolved = candidate.parent.resolve(strict=True)
|
|
1138
|
+
except OSError:
|
|
1139
|
+
return None
|
|
1140
|
+
if not _is_relative_to(parent_resolved, source_root):
|
|
1141
|
+
return _request_diagnostic(
|
|
1142
|
+
code="MF-S001",
|
|
1143
|
+
message="Source path must stay within source_root.",
|
|
1144
|
+
)
|
|
1145
|
+
resolved = parent_resolved.joinpath(candidate.name)
|
|
1146
|
+
try:
|
|
1147
|
+
before = resolved.lstat()
|
|
1148
|
+
except OSError:
|
|
1149
|
+
return None
|
|
1150
|
+
if stat.S_ISLNK(before.st_mode):
|
|
1151
|
+
try:
|
|
1152
|
+
symlink_target = resolved.resolve(strict=True)
|
|
1153
|
+
except OSError:
|
|
1154
|
+
return None
|
|
1155
|
+
if not _is_relative_to(symlink_target, source_root):
|
|
1156
|
+
return _request_diagnostic(
|
|
1157
|
+
code="MF-S001",
|
|
1158
|
+
message="Source path must stay within source_root.",
|
|
1159
|
+
)
|
|
1160
|
+
return None
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
def _diagnostic_field(key: str, value: str | int) -> DiagnosticField:
|
|
1164
|
+
return DiagnosticField(key=key, value=value)
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
def _join_pointer(parent: str, token: str) -> str:
|
|
1168
|
+
escaped = token.replace("~", "~0").replace("/", "~1")
|
|
1169
|
+
return f"/{escaped}" if parent == "/" else f"{parent}/{escaped}"
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
def _validation_pointer(exc: ValidationError) -> str:
|
|
1173
|
+
errors = exc.errors()
|
|
1174
|
+
if not errors:
|
|
1175
|
+
return "/"
|
|
1176
|
+
loc = errors[0]["loc"]
|
|
1177
|
+
if not isinstance(loc, tuple) or not loc:
|
|
1178
|
+
return "/"
|
|
1179
|
+
parts = [str(part).replace("~", "~0").replace("/", "~1") for part in loc]
|
|
1180
|
+
return "/" + "/".join(parts)
|