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,970 @@
|
|
|
1
|
+
"""02B compiled harness plan and runtime trace contracts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import math
|
|
8
|
+
import re
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
14
|
+
|
|
15
|
+
_SHA256_RE = re.compile(r"[0-9a-f]{64}")
|
|
16
|
+
JsonObject = dict[str, Any]
|
|
17
|
+
ConnectorApprovalDecision = Literal[
|
|
18
|
+
"approved",
|
|
19
|
+
"forbidden",
|
|
20
|
+
"pending",
|
|
21
|
+
"missing",
|
|
22
|
+
"wrong_stage",
|
|
23
|
+
"wrong_run",
|
|
24
|
+
"wrong_scope",
|
|
25
|
+
"expired_or_stale",
|
|
26
|
+
]
|
|
27
|
+
ConnectorApprovalPolicyValue = Literal[
|
|
28
|
+
"none",
|
|
29
|
+
"millrace_explicit",
|
|
30
|
+
"operator_out_of_band",
|
|
31
|
+
"forbidden",
|
|
32
|
+
]
|
|
33
|
+
ConnectorDriftDecision = Literal["passed", "failed", "not_reached"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SideEffectClass(str, Enum):
|
|
37
|
+
"""Closed side-effect classification for compiled nodes."""
|
|
38
|
+
|
|
39
|
+
READ_ONLY = "read_only"
|
|
40
|
+
ARTIFACT_WRITE = "artifact_write"
|
|
41
|
+
WORKSPACE_WRITE = "workspace_write"
|
|
42
|
+
PROCESS_EXECUTION = "process_execution"
|
|
43
|
+
NETWORK_READ = "network_read"
|
|
44
|
+
NETWORK_WRITE = "network_write"
|
|
45
|
+
TERMINAL = "terminal"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class IdempotencyClass(str, Enum):
|
|
49
|
+
"""Closed idempotency classification for compiled nodes."""
|
|
50
|
+
|
|
51
|
+
IDEMPOTENT = "idempotent"
|
|
52
|
+
IDEMPOTENT_WITH_KEY = "idempotent_with_key"
|
|
53
|
+
NON_IDEMPOTENT = "non_idempotent"
|
|
54
|
+
UNKNOWN = "unknown"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ToolTraceDecision(str, Enum):
|
|
58
|
+
"""Closed prerequisite and capability decision values for tool traces."""
|
|
59
|
+
|
|
60
|
+
ALLOWED = "allowed"
|
|
61
|
+
DENIED = "denied"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ToolExecutionStatus(str, Enum):
|
|
65
|
+
"""Closed tool execution status values used in tool traces."""
|
|
66
|
+
|
|
67
|
+
NOT_EXECUTED = "not_executed"
|
|
68
|
+
SUCCESS = "success"
|
|
69
|
+
SOFT_FAILURE = "soft_failure"
|
|
70
|
+
HARD_FAILURE = "hard_failure"
|
|
71
|
+
CANCELLED = "cancelled"
|
|
72
|
+
TIMED_OUT = "timed_out"
|
|
73
|
+
AMBIGUOUS = "ambiguous"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ToolTraceSideEffectClass(str, Enum):
|
|
77
|
+
"""Closed side-effect classification for persisted tool traces."""
|
|
78
|
+
|
|
79
|
+
READ_ONLY = "read_only"
|
|
80
|
+
ARTIFACT_WRITE = "artifact_write"
|
|
81
|
+
WORKSPACE_WRITE = "workspace_write"
|
|
82
|
+
PROCESS_EXECUTION = "process_execution"
|
|
83
|
+
NETWORK_READ = "network_read"
|
|
84
|
+
NETWORK_WRITE = "network_write"
|
|
85
|
+
TERMINAL = "terminal"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ToolTraceIdempotency(str, Enum):
|
|
89
|
+
"""Closed idempotency classification for persisted tool traces."""
|
|
90
|
+
|
|
91
|
+
IDEMPOTENT = "idempotent"
|
|
92
|
+
IDEMPOTENT_WITH_KEY = "idempotent_with_key"
|
|
93
|
+
NON_IDEMPOTENT = "non_idempotent"
|
|
94
|
+
UNKNOWN = "unknown"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class SideEffectCertainty(str, Enum):
|
|
98
|
+
"""Closed side-effect certainty values for persisted tool traces."""
|
|
99
|
+
|
|
100
|
+
NOT_ATTEMPTED = "not_attempted"
|
|
101
|
+
CONFIRMED_ABSENT = "confirmed_absent"
|
|
102
|
+
CONFIRMED_COMPLETE = "confirmed_complete"
|
|
103
|
+
ROLLED_BACK = "rolled_back"
|
|
104
|
+
COMPLETION_UNKNOWN = "completion_unknown"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class SessionEventType(str, Enum):
|
|
108
|
+
"""Closed runtime session event values from the 02B contract."""
|
|
109
|
+
|
|
110
|
+
RUNTIME_RECEIVED = "runtime_received"
|
|
111
|
+
RUNTIME_VERIFIED = "runtime_verified"
|
|
112
|
+
BACKEND_CONSTRUCTED = "backend_constructed"
|
|
113
|
+
BINDING_REJECTED = "binding_rejected"
|
|
114
|
+
COMPILED_HARNESS_INVALID = "compiled_harness_invalid"
|
|
115
|
+
BACKEND_FAILED = "backend_failed"
|
|
116
|
+
SESSION_STARTED = "session_started"
|
|
117
|
+
WORKFLOW_CONSTRUCTED = "workflow_constructed"
|
|
118
|
+
MODEL_REQUEST_STARTED = "model_request_started"
|
|
119
|
+
MODEL_REQUEST_COMPLETED = "model_request_completed"
|
|
120
|
+
MODEL_REQUEST_FAILED = "model_request_failed"
|
|
121
|
+
CORRECTION_ISSUED = "correction_issued"
|
|
122
|
+
PREMATURE_TERMINAL_REJECTED = "premature_terminal_rejected"
|
|
123
|
+
PREREQUISITE_REJECTED = "prerequisite_rejected"
|
|
124
|
+
TOOL_STARTED = "tool_started"
|
|
125
|
+
TOOL_COMPLETED = "tool_completed"
|
|
126
|
+
TOOL_FAILED = "tool_failed"
|
|
127
|
+
CONTEXT_COMPACTED = "context_compacted"
|
|
128
|
+
TERMINAL_INTENT_ACCEPTED = "terminal_intent_accepted"
|
|
129
|
+
TERMINAL_INTENT_REJECTED = "terminal_intent_rejected"
|
|
130
|
+
FINALIZATION_STARTED = "finalization_started"
|
|
131
|
+
FINALIZATION_COMPLETED = "finalization_completed"
|
|
132
|
+
FINALIZATION_FAILED = "finalization_failed"
|
|
133
|
+
BUDGET_EXHAUSTED = "budget_exhausted"
|
|
134
|
+
TIMED_OUT = "timed_out"
|
|
135
|
+
CANCELLED = "cancelled"
|
|
136
|
+
INTERNAL_FAILED = "internal_failed"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _nonblank(value: str, field_name: str) -> str:
|
|
140
|
+
if not value.strip():
|
|
141
|
+
raise ValueError(f"{field_name} must be a non-empty string")
|
|
142
|
+
return value
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _unique(values: tuple[str, ...], field_name: str) -> None:
|
|
146
|
+
if len(set(values)) != len(values):
|
|
147
|
+
raise ValueError(f"{field_name} values must be unique")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _validate_sha256(value: str, field_name: str) -> str:
|
|
151
|
+
if not _SHA256_RE.fullmatch(value):
|
|
152
|
+
raise ValueError(f"{field_name} must be exactly 64 lowercase hex characters")
|
|
153
|
+
return value
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class DiagnosticField(BaseModel):
|
|
157
|
+
"""Immutable bounded scalar diagnostic field."""
|
|
158
|
+
|
|
159
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
160
|
+
|
|
161
|
+
key: str = Field(description="Diagnostic field key")
|
|
162
|
+
value: str | int | float | bool | None = Field(
|
|
163
|
+
description="Bounded JSON scalar diagnostic field value"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
@field_validator("key")
|
|
167
|
+
@classmethod
|
|
168
|
+
def _key_nonblank(cls, value: str) -> str:
|
|
169
|
+
return _nonblank(value, "key")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class StageIdentity(BaseModel):
|
|
173
|
+
"""Immutable provider-local identity of a compiled harness stage.
|
|
174
|
+
|
|
175
|
+
On an execution request this identifies the stage that the selected
|
|
176
|
+
Millforge harness admits. It is not the caller's workflow plane, node,
|
|
177
|
+
route, dispatch identity, or authority.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
181
|
+
|
|
182
|
+
plane: Literal["execution", "planning", "learning"] = Field(
|
|
183
|
+
description="Provider-local plane the compiled harness stage belongs to"
|
|
184
|
+
)
|
|
185
|
+
node_id: str = Field(description="Provider-local compiled stage node identifier")
|
|
186
|
+
stage_kind_id: str = Field(
|
|
187
|
+
description="Provider-local compiled stage kind identifier"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
@field_validator("node_id", "stage_kind_id")
|
|
191
|
+
@classmethod
|
|
192
|
+
def _strings_nonblank(cls, value: str, info: Any) -> str:
|
|
193
|
+
return _nonblank(value, info.field_name)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class CompilerIdentity(BaseModel):
|
|
197
|
+
"""Identity of the compiler that produced the compiled plan."""
|
|
198
|
+
|
|
199
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
200
|
+
|
|
201
|
+
name: str
|
|
202
|
+
version: str
|
|
203
|
+
build_id: str
|
|
204
|
+
|
|
205
|
+
@field_validator("name", "version", "build_id")
|
|
206
|
+
@classmethod
|
|
207
|
+
def _strings_nonblank(cls, value: str, info: Any) -> str:
|
|
208
|
+
return _nonblank(value, info.field_name)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class CompiledModelProfile(BaseModel):
|
|
212
|
+
"""Logical model profile reference only."""
|
|
213
|
+
|
|
214
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
215
|
+
|
|
216
|
+
profile_id: str
|
|
217
|
+
|
|
218
|
+
@field_validator("profile_id")
|
|
219
|
+
@classmethod
|
|
220
|
+
def _profile_id_nonblank(cls, value: str) -> str:
|
|
221
|
+
return _nonblank(value, "profile_id")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class CompiledPromptPolicy(BaseModel):
|
|
225
|
+
"""Deterministic prompt policy included in the compiled plan."""
|
|
226
|
+
|
|
227
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
228
|
+
|
|
229
|
+
policy_id: str
|
|
230
|
+
system_instructions: str
|
|
231
|
+
include_request_context: bool
|
|
232
|
+
|
|
233
|
+
@field_validator("policy_id", "system_instructions")
|
|
234
|
+
@classmethod
|
|
235
|
+
def _strings_nonblank(cls, value: str, info: Any) -> str:
|
|
236
|
+
return _nonblank(value, info.field_name)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class CompiledBudgetPolicy(BaseModel):
|
|
240
|
+
"""Closed correction and enforcement budget policy."""
|
|
241
|
+
|
|
242
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
243
|
+
|
|
244
|
+
max_iterations: int = Field(gt=0)
|
|
245
|
+
max_validation_retries: int = Field(gt=0)
|
|
246
|
+
max_tool_errors: int = Field(gt=0)
|
|
247
|
+
max_prerequisite_violations: int = Field(gt=0)
|
|
248
|
+
max_premature_terminal_attempts: int = Field(gt=0)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class CompiledContextPolicy(BaseModel):
|
|
252
|
+
"""Context policy consumed by the deterministic runtime."""
|
|
253
|
+
|
|
254
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
255
|
+
|
|
256
|
+
strategy_id: Literal["forge.tiered.v1"]
|
|
257
|
+
budget_tokens: int = Field(gt=0)
|
|
258
|
+
keep_recent_iterations: int = Field(ge=0)
|
|
259
|
+
phase_thresholds: tuple[float, float, float]
|
|
260
|
+
|
|
261
|
+
@field_validator("phase_thresholds")
|
|
262
|
+
@classmethod
|
|
263
|
+
def _thresholds_valid(
|
|
264
|
+
cls, value: tuple[float, float, float]
|
|
265
|
+
) -> tuple[float, float, float]:
|
|
266
|
+
if any(not math.isfinite(item) for item in value):
|
|
267
|
+
raise ValueError("phase_thresholds must be finite")
|
|
268
|
+
if any(item <= 0 or item > 1 for item in value):
|
|
269
|
+
raise ValueError("phase_thresholds must be in (0, 1]")
|
|
270
|
+
if tuple(sorted(value)) != value:
|
|
271
|
+
raise ValueError("phase_thresholds must be non-decreasing")
|
|
272
|
+
return value
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class ToolBindingRef(BaseModel):
|
|
276
|
+
"""Resolved tool binding reference."""
|
|
277
|
+
|
|
278
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
279
|
+
|
|
280
|
+
tool_id: str
|
|
281
|
+
tool_version: int = Field(gt=0)
|
|
282
|
+
descriptor_sha256: str
|
|
283
|
+
implementation_id: str
|
|
284
|
+
|
|
285
|
+
@field_validator("tool_id", "implementation_id")
|
|
286
|
+
@classmethod
|
|
287
|
+
def _strings_nonblank(cls, value: str, info: Any) -> str:
|
|
288
|
+
return _nonblank(value, info.field_name)
|
|
289
|
+
|
|
290
|
+
@field_validator("descriptor_sha256")
|
|
291
|
+
@classmethod
|
|
292
|
+
def _descriptor_sha256_valid(cls, value: str) -> str:
|
|
293
|
+
return _validate_sha256(value, "descriptor_sha256")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class ArgumentMatch(BaseModel):
|
|
297
|
+
"""Mapping from prerequisite output argument to current input argument."""
|
|
298
|
+
|
|
299
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
300
|
+
|
|
301
|
+
prerequisite_argument: str
|
|
302
|
+
current_argument: str
|
|
303
|
+
|
|
304
|
+
@field_validator("prerequisite_argument", "current_argument")
|
|
305
|
+
@classmethod
|
|
306
|
+
def _strings_nonblank(cls, value: str, info: Any) -> str:
|
|
307
|
+
return _nonblank(value, info.field_name)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class CompiledPrerequisite(BaseModel):
|
|
311
|
+
"""Prerequisite node dependency for a compiled node."""
|
|
312
|
+
|
|
313
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
314
|
+
|
|
315
|
+
node_id: str
|
|
316
|
+
argument_matches: tuple[ArgumentMatch, ...] = Field(default_factory=tuple)
|
|
317
|
+
|
|
318
|
+
@field_validator("node_id")
|
|
319
|
+
@classmethod
|
|
320
|
+
def _node_id_nonblank(cls, value: str) -> str:
|
|
321
|
+
return _nonblank(value, "node_id")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class CompiledHarnessNode(BaseModel):
|
|
325
|
+
"""A single node in a compiled harness plan."""
|
|
326
|
+
|
|
327
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
328
|
+
|
|
329
|
+
node_id: str
|
|
330
|
+
model_tool_name: str
|
|
331
|
+
description: str
|
|
332
|
+
input_schema: JsonObject
|
|
333
|
+
binding: ToolBindingRef
|
|
334
|
+
prerequisites: tuple[CompiledPrerequisite, ...] = Field(default_factory=tuple)
|
|
335
|
+
required: bool
|
|
336
|
+
terminal_result: str | None = None
|
|
337
|
+
required_capabilities: tuple[str, ...] = Field(default_factory=tuple)
|
|
338
|
+
produced_artifact_ids: tuple[str, ...] = Field(default_factory=tuple)
|
|
339
|
+
side_effect_class: SideEffectClass
|
|
340
|
+
idempotency: IdempotencyClass
|
|
341
|
+
|
|
342
|
+
@field_validator("node_id", "model_tool_name", "description")
|
|
343
|
+
@classmethod
|
|
344
|
+
def _strings_nonblank(cls, value: str, info: Any) -> str:
|
|
345
|
+
return _nonblank(value, info.field_name)
|
|
346
|
+
|
|
347
|
+
@field_validator("terminal_result")
|
|
348
|
+
@classmethod
|
|
349
|
+
def _terminal_result_nonblank(cls, value: str | None) -> str | None:
|
|
350
|
+
return None if value is None else _nonblank(value, "terminal_result")
|
|
351
|
+
|
|
352
|
+
@field_validator("required_capabilities", "produced_artifact_ids")
|
|
353
|
+
@classmethod
|
|
354
|
+
def _tuple_strings_nonblank(
|
|
355
|
+
cls, value: tuple[str, ...], info: Any
|
|
356
|
+
) -> tuple[str, ...]:
|
|
357
|
+
for item in value:
|
|
358
|
+
_nonblank(item, info.field_name)
|
|
359
|
+
_unique(value, info.field_name)
|
|
360
|
+
return value
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class TerminalArtifactRequirement(BaseModel):
|
|
364
|
+
"""Artifacts required for a specific terminal result."""
|
|
365
|
+
|
|
366
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
367
|
+
|
|
368
|
+
terminal_result: str
|
|
369
|
+
artifact_ids: tuple[str, ...] = Field(min_length=1)
|
|
370
|
+
|
|
371
|
+
@field_validator("terminal_result")
|
|
372
|
+
@classmethod
|
|
373
|
+
def _terminal_result_nonblank(cls, value: str) -> str:
|
|
374
|
+
return _nonblank(value, "terminal_result")
|
|
375
|
+
|
|
376
|
+
@field_validator("artifact_ids")
|
|
377
|
+
@classmethod
|
|
378
|
+
def _artifact_ids_valid(cls, value: tuple[str, ...]) -> tuple[str, ...]:
|
|
379
|
+
for item in value:
|
|
380
|
+
_nonblank(item, "artifact_ids")
|
|
381
|
+
_unique(value, "artifact_ids")
|
|
382
|
+
return value
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class CompiledArtifactPolicy(BaseModel):
|
|
386
|
+
"""Declared and terminal-required artifacts for a compiled plan."""
|
|
387
|
+
|
|
388
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
389
|
+
|
|
390
|
+
declared_artifact_ids: tuple[str, ...] = Field(default_factory=tuple)
|
|
391
|
+
required_by_terminal: tuple[TerminalArtifactRequirement, ...] = Field(
|
|
392
|
+
default_factory=tuple
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
@field_validator("declared_artifact_ids")
|
|
396
|
+
@classmethod
|
|
397
|
+
def _declared_artifact_ids_valid(cls, value: tuple[str, ...]) -> tuple[str, ...]:
|
|
398
|
+
for item in value:
|
|
399
|
+
_nonblank(item, "declared_artifact_ids")
|
|
400
|
+
_unique(value, "declared_artifact_ids")
|
|
401
|
+
return value
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class CompiledHarnessPlan(BaseModel):
|
|
405
|
+
"""Immutable compiled harness plan consumed by the runtime."""
|
|
406
|
+
|
|
407
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
408
|
+
|
|
409
|
+
schema_version: Literal["1.0"]
|
|
410
|
+
kind: Literal["compiled_millforge_harness"]
|
|
411
|
+
harness_id: str
|
|
412
|
+
harness_version: int = Field(gt=0)
|
|
413
|
+
source_sha256: str
|
|
414
|
+
compiled_sha256: str
|
|
415
|
+
stage_kind_ids: tuple[str, ...]
|
|
416
|
+
model_profile: CompiledModelProfile
|
|
417
|
+
prompt_policy: CompiledPromptPolicy
|
|
418
|
+
budgets: CompiledBudgetPolicy
|
|
419
|
+
context_policy: CompiledContextPolicy
|
|
420
|
+
nodes: tuple[CompiledHarnessNode, ...] = Field(min_length=1)
|
|
421
|
+
required_capabilities: tuple[str, ...] = Field(default_factory=tuple)
|
|
422
|
+
terminal_result_map: dict[str, str]
|
|
423
|
+
artifact_policy: CompiledArtifactPolicy
|
|
424
|
+
compiler_identity: CompilerIdentity
|
|
425
|
+
|
|
426
|
+
@field_validator("harness_id")
|
|
427
|
+
@classmethod
|
|
428
|
+
def _harness_id_nonblank(cls, value: str) -> str:
|
|
429
|
+
return _nonblank(value, "harness_id")
|
|
430
|
+
|
|
431
|
+
@field_validator("source_sha256", "compiled_sha256")
|
|
432
|
+
@classmethod
|
|
433
|
+
def _hashes_valid(cls, value: str, info: Any) -> str:
|
|
434
|
+
return _validate_sha256(value, info.field_name)
|
|
435
|
+
|
|
436
|
+
@field_validator("stage_kind_ids", "required_capabilities")
|
|
437
|
+
@classmethod
|
|
438
|
+
def _tuple_strings_unique(
|
|
439
|
+
cls, value: tuple[str, ...], info: Any
|
|
440
|
+
) -> tuple[str, ...]:
|
|
441
|
+
for item in value:
|
|
442
|
+
_nonblank(item, info.field_name)
|
|
443
|
+
_unique(value, info.field_name)
|
|
444
|
+
return value
|
|
445
|
+
|
|
446
|
+
@field_validator("terminal_result_map")
|
|
447
|
+
@classmethod
|
|
448
|
+
def _terminal_result_map_nonblank(cls, value: dict[str, str]) -> dict[str, str]:
|
|
449
|
+
for key, item in value.items():
|
|
450
|
+
_nonblank(key, "terminal_result_map key")
|
|
451
|
+
_nonblank(item, "terminal_result_map value")
|
|
452
|
+
if len(set(value.values())) != len(value.values()):
|
|
453
|
+
raise ValueError("terminal_result_map values must be unique")
|
|
454
|
+
return value
|
|
455
|
+
|
|
456
|
+
@model_validator(mode="after")
|
|
457
|
+
def _check_plan_invariants(self) -> CompiledHarnessPlan:
|
|
458
|
+
violations = self.validate_plan_invariants()
|
|
459
|
+
if violations:
|
|
460
|
+
raise ValueError("; ".join(violations))
|
|
461
|
+
return self
|
|
462
|
+
|
|
463
|
+
def validate_plan_invariants(self) -> list[str]:
|
|
464
|
+
"""Return cross-field invariant violations."""
|
|
465
|
+
violations: list[str] = []
|
|
466
|
+
node_ids = [node.node_id for node in self.nodes]
|
|
467
|
+
model_tool_names = [node.model_tool_name for node in self.nodes]
|
|
468
|
+
binding_pairs = [
|
|
469
|
+
(node.binding.tool_id, node.binding.tool_version) for node in self.nodes
|
|
470
|
+
]
|
|
471
|
+
terminal_nodes: dict[str, CompiledHarnessNode] = {
|
|
472
|
+
node.node_id: node for node in self.nodes if node.terminal_result
|
|
473
|
+
}
|
|
474
|
+
terminal_results = {
|
|
475
|
+
node.terminal_result
|
|
476
|
+
for node in self.nodes
|
|
477
|
+
if node.terminal_result is not None
|
|
478
|
+
}
|
|
479
|
+
declared_artifacts = set(self.artifact_policy.declared_artifact_ids)
|
|
480
|
+
produced_artifacts = {
|
|
481
|
+
artifact_id
|
|
482
|
+
for node in self.nodes
|
|
483
|
+
for artifact_id in node.produced_artifact_ids
|
|
484
|
+
}
|
|
485
|
+
required_capabilities = set(self.required_capabilities)
|
|
486
|
+
|
|
487
|
+
if len(set(node_ids)) != len(node_ids):
|
|
488
|
+
violations.append("node_id values must be unique")
|
|
489
|
+
if len(set(model_tool_names)) != len(model_tool_names):
|
|
490
|
+
violations.append("model_tool_name values must be unique")
|
|
491
|
+
if len(set(binding_pairs)) != len(binding_pairs):
|
|
492
|
+
violations.append("binding tool_id/tool_version pairs must be unique")
|
|
493
|
+
if not terminal_nodes:
|
|
494
|
+
violations.append("Plan must contain at least one terminal node")
|
|
495
|
+
|
|
496
|
+
for node in self.nodes:
|
|
497
|
+
if node.terminal_result is not None and node.required:
|
|
498
|
+
violations.append(
|
|
499
|
+
f"Terminal node {node.node_id!r} must not be required"
|
|
500
|
+
)
|
|
501
|
+
for prereq in node.prerequisites:
|
|
502
|
+
if prereq.node_id not in node_ids:
|
|
503
|
+
violations.append(
|
|
504
|
+
f"Node {node.node_id!r} prerequisite references unknown "
|
|
505
|
+
f"node_id {prereq.node_id!r}"
|
|
506
|
+
)
|
|
507
|
+
if prereq.node_id == node.node_id:
|
|
508
|
+
violations.append(f"Node {node.node_id!r} cannot require itself")
|
|
509
|
+
for capability in node.required_capabilities:
|
|
510
|
+
if capability not in required_capabilities:
|
|
511
|
+
violations.append(
|
|
512
|
+
f"Node {node.node_id!r} requires undeclared capability "
|
|
513
|
+
f"{capability!r}"
|
|
514
|
+
)
|
|
515
|
+
for artifact_id in node.produced_artifact_ids:
|
|
516
|
+
if artifact_id not in declared_artifacts:
|
|
517
|
+
violations.append(
|
|
518
|
+
f"Node {node.node_id!r} produces undeclared artifact "
|
|
519
|
+
f"{artifact_id!r}"
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
if set(self.terminal_result_map) != set(terminal_nodes):
|
|
523
|
+
violations.append(
|
|
524
|
+
"terminal_result_map keys must exactly name terminal nodes"
|
|
525
|
+
)
|
|
526
|
+
for node_id, terminal_result in self.terminal_result_map.items():
|
|
527
|
+
terminal_node = terminal_nodes.get(node_id)
|
|
528
|
+
if (
|
|
529
|
+
terminal_node is not None
|
|
530
|
+
and terminal_node.terminal_result != terminal_result
|
|
531
|
+
):
|
|
532
|
+
violations.append(
|
|
533
|
+
f"terminal_result_map value for {node_id!r} must match node terminal_result"
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
for requirement in self.artifact_policy.required_by_terminal:
|
|
537
|
+
if requirement.terminal_result not in terminal_results:
|
|
538
|
+
violations.append(
|
|
539
|
+
f"Artifact requirement terminal_result "
|
|
540
|
+
f"{requirement.terminal_result!r} is not in terminal_result_map"
|
|
541
|
+
)
|
|
542
|
+
for artifact_id in requirement.artifact_ids:
|
|
543
|
+
if artifact_id not in declared_artifacts:
|
|
544
|
+
violations.append(
|
|
545
|
+
f"Required artifact {artifact_id!r} is not declared"
|
|
546
|
+
)
|
|
547
|
+
if artifact_id not in produced_artifacts:
|
|
548
|
+
violations.append(
|
|
549
|
+
f"Required artifact {artifact_id!r} has no producer"
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
return violations
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
class SessionEvent(BaseModel):
|
|
556
|
+
"""Ordered runtime session event record."""
|
|
557
|
+
|
|
558
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
559
|
+
|
|
560
|
+
schema_version: Literal["1.0"]
|
|
561
|
+
sequence: int = Field(gt=0)
|
|
562
|
+
occurred_at: str
|
|
563
|
+
monotonic_offset_ms: float = Field(ge=0)
|
|
564
|
+
event_type: SessionEventType
|
|
565
|
+
request_id: str
|
|
566
|
+
run_id: str
|
|
567
|
+
session_id: str
|
|
568
|
+
stage: StageIdentity
|
|
569
|
+
node_id: str | None = None
|
|
570
|
+
model_turn: int | None = Field(default=None, ge=0)
|
|
571
|
+
tool_call_id: str | None = None
|
|
572
|
+
code: str | None = None
|
|
573
|
+
fields: tuple[DiagnosticField, ...] = Field(default_factory=tuple)
|
|
574
|
+
|
|
575
|
+
@field_validator(
|
|
576
|
+
"occurred_at",
|
|
577
|
+
"request_id",
|
|
578
|
+
"run_id",
|
|
579
|
+
"session_id",
|
|
580
|
+
)
|
|
581
|
+
@classmethod
|
|
582
|
+
def _strings_nonblank(cls, value: str, info: Any) -> str:
|
|
583
|
+
return _nonblank(value, info.field_name)
|
|
584
|
+
|
|
585
|
+
@field_validator("node_id", "tool_call_id", "code")
|
|
586
|
+
@classmethod
|
|
587
|
+
def _optional_strings_nonblank(cls, value: str | None, info: Any) -> str | None:
|
|
588
|
+
return None if value is None else _nonblank(value, info.field_name)
|
|
589
|
+
|
|
590
|
+
@field_validator("fields")
|
|
591
|
+
@classmethod
|
|
592
|
+
def _field_keys_unique(
|
|
593
|
+
cls, value: tuple[DiagnosticField, ...]
|
|
594
|
+
) -> tuple[DiagnosticField, ...]:
|
|
595
|
+
keys = [field.key for field in value]
|
|
596
|
+
if len(set(keys)) != len(keys):
|
|
597
|
+
raise ValueError("diagnostic field keys must be unique")
|
|
598
|
+
return value
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
class ToolTraceDecisionRecord(BaseModel):
|
|
602
|
+
"""Typed keyed support decision recorded with a tool trace."""
|
|
603
|
+
|
|
604
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
605
|
+
|
|
606
|
+
key: str
|
|
607
|
+
decision: ToolTraceDecision
|
|
608
|
+
|
|
609
|
+
@field_validator("key")
|
|
610
|
+
@classmethod
|
|
611
|
+
def _key_nonblank(cls, value: str) -> str:
|
|
612
|
+
return _nonblank(value, "key")
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
class ToolTraceRecord(BaseModel):
|
|
616
|
+
"""Ordered runtime trace record for a tool invocation."""
|
|
617
|
+
|
|
618
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
619
|
+
|
|
620
|
+
schema_version: Literal["1.0"]
|
|
621
|
+
sequence: int = Field(gt=0)
|
|
622
|
+
occurred_at: str
|
|
623
|
+
monotonic_offset_ms: float = Field(ge=0)
|
|
624
|
+
request_id: str
|
|
625
|
+
run_id: str
|
|
626
|
+
session_id: str
|
|
627
|
+
stage: StageIdentity
|
|
628
|
+
node_id: str
|
|
629
|
+
model_turn: int = Field(ge=0)
|
|
630
|
+
tool_call_id: str
|
|
631
|
+
model_tool_name: str
|
|
632
|
+
binding: ToolBindingRef
|
|
633
|
+
binding_resolution_status: Literal["resolved", "ambiguous", "uncompiled"] = (
|
|
634
|
+
"resolved"
|
|
635
|
+
)
|
|
636
|
+
connector_id: str | None = None
|
|
637
|
+
provider_tool_name: str | None = None
|
|
638
|
+
connector_tool_id: str | None = None
|
|
639
|
+
connector_tool_version: int | None = Field(default=None, ge=1)
|
|
640
|
+
connector_descriptor_sha256: str | None = None
|
|
641
|
+
connector_identity_sha256: str | None = None
|
|
642
|
+
discovery_snapshot_sha256: str | None = None
|
|
643
|
+
approval_policy: ConnectorApprovalPolicyValue | None = None
|
|
644
|
+
approval_decision: ConnectorApprovalDecision | None = None
|
|
645
|
+
approval_evidence: dict[str, str | int | float | bool | None] = Field(
|
|
646
|
+
default_factory=dict
|
|
647
|
+
)
|
|
648
|
+
broker_attempted: bool | None = None
|
|
649
|
+
request_sha256: str | None = None
|
|
650
|
+
response_sha256: str | None = None
|
|
651
|
+
retry_decision: Literal["retry_allowed", "retry_denied"] | None = None
|
|
652
|
+
drift_decision: ConnectorDriftDecision | None = None
|
|
653
|
+
redacted_evidence: dict[str, Any] = Field(default_factory=dict)
|
|
654
|
+
input_sha256: str
|
|
655
|
+
prerequisite_decisions: tuple[ToolTraceDecisionRecord, ...] = Field(
|
|
656
|
+
default_factory=tuple
|
|
657
|
+
)
|
|
658
|
+
capability_decisions: tuple[ToolTraceDecisionRecord, ...] = Field(
|
|
659
|
+
default_factory=tuple
|
|
660
|
+
)
|
|
661
|
+
execution_status: ToolExecutionStatus
|
|
662
|
+
retryable: bool
|
|
663
|
+
side_effect_class: ToolTraceSideEffectClass
|
|
664
|
+
idempotency: ToolTraceIdempotency
|
|
665
|
+
side_effect_certainty: SideEffectCertainty
|
|
666
|
+
side_effect_detail_code: str | None = None
|
|
667
|
+
side_effect_detail_summary: str | None = None
|
|
668
|
+
side_effect_retry_allowed: bool | None = None
|
|
669
|
+
output_sha256: str | None = None
|
|
670
|
+
duration_ms: float = Field(ge=0)
|
|
671
|
+
summary: str
|
|
672
|
+
|
|
673
|
+
@field_validator(
|
|
674
|
+
"occurred_at",
|
|
675
|
+
"request_id",
|
|
676
|
+
"run_id",
|
|
677
|
+
"session_id",
|
|
678
|
+
"node_id",
|
|
679
|
+
"tool_call_id",
|
|
680
|
+
"model_tool_name",
|
|
681
|
+
"summary",
|
|
682
|
+
)
|
|
683
|
+
@classmethod
|
|
684
|
+
def _strings_nonblank(cls, value: str, info: Any) -> str:
|
|
685
|
+
return _nonblank(value, info.field_name)
|
|
686
|
+
|
|
687
|
+
@field_validator("input_sha256")
|
|
688
|
+
@classmethod
|
|
689
|
+
def _input_sha256_valid(cls, value: str) -> str:
|
|
690
|
+
return _validate_sha256(value, "input_sha256")
|
|
691
|
+
|
|
692
|
+
@field_validator(
|
|
693
|
+
"connector_id",
|
|
694
|
+
"provider_tool_name",
|
|
695
|
+
"connector_tool_id",
|
|
696
|
+
"approval_policy",
|
|
697
|
+
"approval_decision",
|
|
698
|
+
)
|
|
699
|
+
@classmethod
|
|
700
|
+
def _optional_connector_strings_nonblank(
|
|
701
|
+
cls, value: str | None, info: Any
|
|
702
|
+
) -> str | None:
|
|
703
|
+
return None if value is None else _nonblank(value, info.field_name)
|
|
704
|
+
|
|
705
|
+
@field_validator(
|
|
706
|
+
"connector_descriptor_sha256",
|
|
707
|
+
"connector_identity_sha256",
|
|
708
|
+
"discovery_snapshot_sha256",
|
|
709
|
+
)
|
|
710
|
+
@classmethod
|
|
711
|
+
def _optional_connector_sha256_valid(
|
|
712
|
+
cls, value: str | None, info: Any
|
|
713
|
+
) -> str | None:
|
|
714
|
+
return None if value is None else _validate_sha256(value, info.field_name)
|
|
715
|
+
|
|
716
|
+
@field_validator("request_sha256", "response_sha256", "output_sha256")
|
|
717
|
+
@classmethod
|
|
718
|
+
def _optional_sha256_valid(cls, value: str | None, info: Any) -> str | None:
|
|
719
|
+
return None if value is None else _validate_sha256(value, info.field_name)
|
|
720
|
+
|
|
721
|
+
@field_validator("side_effect_detail_code", "side_effect_detail_summary")
|
|
722
|
+
@classmethod
|
|
723
|
+
def _optional_detail_nonblank(cls, value: str | None, info: Any) -> str | None:
|
|
724
|
+
return None if value is None else _nonblank(value, info.field_name)
|
|
725
|
+
|
|
726
|
+
@field_validator("prerequisite_decisions", "capability_decisions")
|
|
727
|
+
@classmethod
|
|
728
|
+
def _decision_keys_unique(
|
|
729
|
+
cls, value: tuple[ToolTraceDecisionRecord, ...], info: Any
|
|
730
|
+
) -> tuple[ToolTraceDecisionRecord, ...]:
|
|
731
|
+
keys = [field.key for field in value]
|
|
732
|
+
if len(set(keys)) != len(keys):
|
|
733
|
+
raise ValueError(f"{info.field_name} keys must be unique")
|
|
734
|
+
return value
|
|
735
|
+
|
|
736
|
+
@model_validator(mode="after")
|
|
737
|
+
def _trace_consistency(self) -> ToolTraceRecord:
|
|
738
|
+
connector_fields = (
|
|
739
|
+
self.connector_id,
|
|
740
|
+
self.provider_tool_name,
|
|
741
|
+
self.connector_tool_id,
|
|
742
|
+
self.connector_tool_version,
|
|
743
|
+
self.connector_descriptor_sha256,
|
|
744
|
+
self.connector_identity_sha256,
|
|
745
|
+
self.discovery_snapshot_sha256,
|
|
746
|
+
self.approval_policy,
|
|
747
|
+
self.approval_decision,
|
|
748
|
+
self.broker_attempted,
|
|
749
|
+
self.request_sha256,
|
|
750
|
+
self.response_sha256,
|
|
751
|
+
self.retry_decision,
|
|
752
|
+
self.drift_decision,
|
|
753
|
+
)
|
|
754
|
+
if any(item is not None for item in connector_fields):
|
|
755
|
+
if not all(item is not None for item in connector_fields):
|
|
756
|
+
raise ValueError(
|
|
757
|
+
"connector approval trace fields must be provided together"
|
|
758
|
+
)
|
|
759
|
+
if not self.approval_evidence:
|
|
760
|
+
raise ValueError("connector approval trace requires approval_evidence")
|
|
761
|
+
if not self.redacted_evidence:
|
|
762
|
+
raise ValueError("connector trace requires redacted_evidence")
|
|
763
|
+
if (
|
|
764
|
+
self.binding_resolution_status != "resolved"
|
|
765
|
+
and self.execution_status == ToolExecutionStatus.SUCCESS
|
|
766
|
+
):
|
|
767
|
+
raise ValueError(
|
|
768
|
+
"binding-resolution failures cannot be marked as successful"
|
|
769
|
+
)
|
|
770
|
+
denied = any(
|
|
771
|
+
record.decision == ToolTraceDecision.DENIED
|
|
772
|
+
for record in (*self.prerequisite_decisions, *self.capability_decisions)
|
|
773
|
+
)
|
|
774
|
+
if denied or self.execution_status == ToolExecutionStatus.NOT_EXECUTED:
|
|
775
|
+
if self.side_effect_certainty != SideEffectCertainty.NOT_ATTEMPTED:
|
|
776
|
+
raise ValueError(
|
|
777
|
+
"denied or not_executed calls must use "
|
|
778
|
+
"side_effect_certainty=not_attempted"
|
|
779
|
+
)
|
|
780
|
+
if self.output_sha256 is not None:
|
|
781
|
+
raise ValueError(
|
|
782
|
+
"denied or not_executed calls must not have output_sha256"
|
|
783
|
+
)
|
|
784
|
+
detail_fields = (
|
|
785
|
+
self.side_effect_detail_code,
|
|
786
|
+
self.side_effect_detail_summary,
|
|
787
|
+
self.side_effect_retry_allowed,
|
|
788
|
+
)
|
|
789
|
+
if any(item is not None for item in detail_fields) and not all(
|
|
790
|
+
item is not None for item in detail_fields
|
|
791
|
+
):
|
|
792
|
+
raise ValueError("side-effect detail fields must be provided together")
|
|
793
|
+
if (
|
|
794
|
+
self.side_effect_certainty == SideEffectCertainty.COMPLETION_UNKNOWN
|
|
795
|
+
and self.idempotency
|
|
796
|
+
in {ToolTraceIdempotency.NON_IDEMPOTENT, ToolTraceIdempotency.UNKNOWN}
|
|
797
|
+
and self.retryable
|
|
798
|
+
):
|
|
799
|
+
raise ValueError(
|
|
800
|
+
"completion_unknown side effects are not retryable for "
|
|
801
|
+
"non-idempotent or unknown-idempotency tool work"
|
|
802
|
+
)
|
|
803
|
+
if self.side_effect_retry_allowed is not None:
|
|
804
|
+
if self.side_effect_retry_allowed != self.retryable:
|
|
805
|
+
raise ValueError("side_effect_retry_allowed must match retryable")
|
|
806
|
+
return self
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def canonical_json_serialize(obj: Any, *, ensure_ascii: bool = True) -> str:
|
|
810
|
+
"""Serialize JSON-compatible data for deterministic SHA-256 hashing."""
|
|
811
|
+
return (
|
|
812
|
+
json.dumps(
|
|
813
|
+
obj,
|
|
814
|
+
sort_keys=True,
|
|
815
|
+
ensure_ascii=ensure_ascii,
|
|
816
|
+
allow_nan=False,
|
|
817
|
+
separators=(",", ":"),
|
|
818
|
+
).replace("\r\n", "\n")
|
|
819
|
+
+ "\n"
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def canonical_compiled_plan_bytes(plan: CompiledHarnessPlan) -> bytes:
|
|
824
|
+
"""Serialize a finalized compiled plan as canonical UTF-8 JSON bytes."""
|
|
825
|
+
verified, _computed, warnings, restored = verify_compiled_plan_sha256(
|
|
826
|
+
canonical_json_serialize(plan.model_dump(mode="json")),
|
|
827
|
+
expected_compiled_hash=plan.compiled_sha256,
|
|
828
|
+
expected_harness_id=plan.harness_id,
|
|
829
|
+
expected_harness_version=plan.harness_version,
|
|
830
|
+
)
|
|
831
|
+
if not verified or restored is None:
|
|
832
|
+
joined = (
|
|
833
|
+
"; ".join(warnings) if warnings else "compiled hash verification failed"
|
|
834
|
+
)
|
|
835
|
+
raise ValueError(joined)
|
|
836
|
+
return canonical_json_serialize(restored.model_dump(mode="json")).encode("utf-8")
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def _no_duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
840
|
+
seen: set[str] = set()
|
|
841
|
+
result: dict[str, Any] = {}
|
|
842
|
+
for key, value in pairs:
|
|
843
|
+
if key in seen:
|
|
844
|
+
raise json.JSONDecodeError(f"Duplicate key {key!r}", "", 0)
|
|
845
|
+
seen.add(key)
|
|
846
|
+
result[key] = value
|
|
847
|
+
return result
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
def parse_and_strip_compiled_plan(
|
|
851
|
+
raw: str,
|
|
852
|
+
) -> tuple[CompiledHarnessPlan, dict[str, Any]]:
|
|
853
|
+
"""Parse JSON with duplicate-key rejection and strip ``compiled_sha256``."""
|
|
854
|
+
parsed: dict[str, Any] = json.loads(raw, object_pairs_hook=_no_duplicate_keys)
|
|
855
|
+
plan = CompiledHarnessPlan.model_validate(parsed)
|
|
856
|
+
stripped = dict(parsed)
|
|
857
|
+
stripped.pop("compiled_sha256", None)
|
|
858
|
+
return plan, stripped
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def calculate_compiled_plan_sha256(plan_payload: dict[str, Any]) -> str:
|
|
862
|
+
"""Hash a complete JSON-mode compiled-plan payload without its digest field."""
|
|
863
|
+
if "compiled_sha256" not in plan_payload:
|
|
864
|
+
raise ValueError("compiled plan payload must include compiled_sha256")
|
|
865
|
+
body = dict(plan_payload)
|
|
866
|
+
body.pop("compiled_sha256")
|
|
867
|
+
return hashlib.sha256(canonical_json_serialize(body).encode("utf-8")).hexdigest()
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def finalize_compiled_plan_sha256(plan: CompiledHarnessPlan) -> CompiledHarnessPlan:
|
|
871
|
+
"""Return a fully validated plan with the canonical compiled SHA-256 set."""
|
|
872
|
+
placeholder_payload = plan.model_dump(mode="json")
|
|
873
|
+
digest = calculate_compiled_plan_sha256(placeholder_payload)
|
|
874
|
+
finalized = CompiledHarnessPlan.model_validate(
|
|
875
|
+
{**placeholder_payload, "compiled_sha256": digest}
|
|
876
|
+
)
|
|
877
|
+
verified, computed, warnings, _restored = verify_compiled_plan_sha256(
|
|
878
|
+
canonical_json_serialize(finalized.model_dump(mode="json")),
|
|
879
|
+
expected_compiled_hash=digest,
|
|
880
|
+
expected_harness_id=finalized.harness_id,
|
|
881
|
+
expected_harness_version=finalized.harness_version,
|
|
882
|
+
)
|
|
883
|
+
if not verified or computed != digest:
|
|
884
|
+
joined = (
|
|
885
|
+
"; ".join(warnings) if warnings else "compiled hash verification failed"
|
|
886
|
+
)
|
|
887
|
+
raise ValueError(joined)
|
|
888
|
+
return finalized
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def verify_compiled_plan_sha256(
|
|
892
|
+
raw: str,
|
|
893
|
+
*,
|
|
894
|
+
expected_compiled_hash: str | None = None,
|
|
895
|
+
expected_harness_id: str | None = None,
|
|
896
|
+
expected_harness_version: int | None = None,
|
|
897
|
+
) -> tuple[bool, str, list[str], CompiledHarnessPlan | None]:
|
|
898
|
+
"""Verify a serialized 02B compiled plan canonical hash and identity."""
|
|
899
|
+
warnings: list[str] = []
|
|
900
|
+
|
|
901
|
+
try:
|
|
902
|
+
parsed: dict[str, Any] = json.loads(raw, object_pairs_hook=_no_duplicate_keys)
|
|
903
|
+
except json.JSONDecodeError as exc:
|
|
904
|
+
return False, "", [f"JSON parse error: {exc}"], None
|
|
905
|
+
|
|
906
|
+
try:
|
|
907
|
+
plan = CompiledHarnessPlan.model_validate(parsed)
|
|
908
|
+
except Exception as exc:
|
|
909
|
+
return False, "", [f"Plan validation error: {exc}"], None
|
|
910
|
+
|
|
911
|
+
try:
|
|
912
|
+
computed_hash = calculate_compiled_plan_sha256(parsed)
|
|
913
|
+
except (TypeError, ValueError) as exc:
|
|
914
|
+
return False, "", [f"Canonical serialisation error: {exc}"], None
|
|
915
|
+
|
|
916
|
+
verified = True
|
|
917
|
+
|
|
918
|
+
if computed_hash != plan.compiled_sha256:
|
|
919
|
+
warnings.append(
|
|
920
|
+
f"Computed hash {computed_hash} does not match "
|
|
921
|
+
f"plan.compiled_sha256 {plan.compiled_sha256}"
|
|
922
|
+
)
|
|
923
|
+
verified = False
|
|
924
|
+
|
|
925
|
+
if expected_compiled_hash is not None and computed_hash != expected_compiled_hash:
|
|
926
|
+
warnings.append(
|
|
927
|
+
f"Computed hash {computed_hash} does not match "
|
|
928
|
+
f"expected_compiled_hash {expected_compiled_hash}"
|
|
929
|
+
)
|
|
930
|
+
verified = False
|
|
931
|
+
|
|
932
|
+
if expected_harness_id is not None and plan.harness_id != expected_harness_id:
|
|
933
|
+
warnings.append(
|
|
934
|
+
f"Plan harness_id {plan.harness_id!r} does not match "
|
|
935
|
+
f"expected_harness_id {expected_harness_id!r}"
|
|
936
|
+
)
|
|
937
|
+
verified = False
|
|
938
|
+
|
|
939
|
+
if (
|
|
940
|
+
expected_harness_version is not None
|
|
941
|
+
and plan.harness_version != expected_harness_version
|
|
942
|
+
):
|
|
943
|
+
warnings.append(
|
|
944
|
+
f"Plan harness_version {plan.harness_version!r} does not match "
|
|
945
|
+
f"expected_harness_version {expected_harness_version!r}"
|
|
946
|
+
)
|
|
947
|
+
verified = False
|
|
948
|
+
|
|
949
|
+
return verified, computed_hash, warnings, plan
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
class FileCompiledHarnessLoader:
|
|
953
|
+
"""Filesystem loader for runtime compiled-plan references."""
|
|
954
|
+
|
|
955
|
+
async def load(self, ref: Any) -> CompiledHarnessPlan:
|
|
956
|
+
"""Load and verify a compiled harness plan from ``ref.path``."""
|
|
957
|
+
path = Path(ref.path)
|
|
958
|
+
raw = path.read_text(encoding="utf-8")
|
|
959
|
+
expected_hash = getattr(ref.expected_hash, "digest", None)
|
|
960
|
+
identity = ref.identity
|
|
961
|
+
verified, _computed, warnings, plan = verify_compiled_plan_sha256(
|
|
962
|
+
raw,
|
|
963
|
+
expected_compiled_hash=expected_hash,
|
|
964
|
+
expected_harness_id=identity.harness_id,
|
|
965
|
+
expected_harness_version=identity.harness_version,
|
|
966
|
+
)
|
|
967
|
+
if not verified or plan is None:
|
|
968
|
+
joined = "; ".join(warnings) if warnings else "compiled plan invalid"
|
|
969
|
+
raise ValueError(joined)
|
|
970
|
+
return plan
|