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.
Files changed (116) hide show
  1. millforge/__init__.py +1174 -0
  2. millforge/_forge/LICENSE +21 -0
  3. millforge/_forge/PROVENANCE.json +295 -0
  4. millforge/_forge/UPDATE_POLICY.md +24 -0
  5. millforge/_forge/__init__.py +14 -0
  6. millforge/_forge/adapter.py +2232 -0
  7. millforge/_forge/base_runner.py +121 -0
  8. millforge/_forge/clients/__init__.py +10 -0
  9. millforge/_forge/clients/base.py +200 -0
  10. millforge/_forge/context/__init__.py +23 -0
  11. millforge/_forge/context/manager.py +178 -0
  12. millforge/_forge/context/strategies.py +335 -0
  13. millforge/_forge/core/__init__.py +16 -0
  14. millforge/_forge/core/inference.py +433 -0
  15. millforge/_forge/core/messages.py +119 -0
  16. millforge/_forge/core/runner.py +479 -0
  17. millforge/_forge/core/steps.py +108 -0
  18. millforge/_forge/core/workflow.py +400 -0
  19. millforge/_forge/errors.py +222 -0
  20. millforge/_forge/guardrails/__init__.py +21 -0
  21. millforge/_forge/guardrails/error_tracker.py +71 -0
  22. millforge/_forge/guardrails/guardrails.py +194 -0
  23. millforge/_forge/guardrails/nudge.py +47 -0
  24. millforge/_forge/guardrails/response_validator.py +119 -0
  25. millforge/_forge/guardrails/step_enforcer.py +183 -0
  26. millforge/_forge/prompts/__init__.py +16 -0
  27. millforge/_forge/prompts/nudges.py +95 -0
  28. millforge/_forge/prompts/templates.py +285 -0
  29. millforge/_version.py +3 -0
  30. millforge/artifacts.py +570 -0
  31. millforge/base/__init__.py +97 -0
  32. millforge/base/composition.py +402 -0
  33. millforge/base/context.py +285 -0
  34. millforge/base/harness.py +138 -0
  35. millforge/base/identity.py +465 -0
  36. millforge/base/options.py +34 -0
  37. millforge/base/platform.py +17 -0
  38. millforge/base/prompt.py +317 -0
  39. millforge/base/runner.py +546 -0
  40. millforge/compiled_plan.py +970 -0
  41. millforge/compiler/__init__.py +231 -0
  42. millforge/compiler/artifact_validation.py +257 -0
  43. millforge/compiler/canonicalization.py +169 -0
  44. millforge/compiler/capabilities.py +66 -0
  45. millforge/compiler/catalogs.py +500 -0
  46. millforge/compiler/diagnostics.py +491 -0
  47. millforge/compiler/graph.py +678 -0
  48. millforge/compiler/lowering.py +198 -0
  49. millforge/compiler/output.py +692 -0
  50. millforge/compiler/parsing.py +1424 -0
  51. millforge/compiler/requests.py +1180 -0
  52. millforge/compiler/schema_validation.py +272 -0
  53. millforge/compiler/semantic.py +490 -0
  54. millforge/compiler/service.py +448 -0
  55. millforge/compiler/source.py +375 -0
  56. millforge/compiler/validators.py +184 -0
  57. millforge/connectors/__init__.py +95 -0
  58. millforge/connectors/admission.py +801 -0
  59. millforge/connectors/broker.py +202 -0
  60. millforge/connectors/contracts.py +1159 -0
  61. millforge/connectors/diagnostics.py +189 -0
  62. millforge/connectors/fake.py +66 -0
  63. millforge/connectors/runtime.py +236 -0
  64. millforge/contracts.py +2860 -0
  65. millforge/custom_tools/__init__.py +67 -0
  66. millforge/custom_tools/compiler.py +724 -0
  67. millforge/custom_tools/contracts.py +1093 -0
  68. millforge/custom_tools/diagnostics.py +205 -0
  69. millforge/eval_artifacts.py +952 -0
  70. millforge/eval_boundary.py +2435 -0
  71. millforge/eval_fixtures/__init__.py +1 -0
  72. millforge/eval_fixtures/default_pack/__init__.py +1 -0
  73. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.bug_diagnosis.traceback.v1.json +52 -0
  74. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.direct_edit.import_sort.v1.json +52 -0
  75. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.evidence_discipline.no_source_change.v1.json +51 -0
  76. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.false_closure.visible_green.v1.json +52 -0
  77. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.multi_file.api_contract.v1.json +54 -0
  78. millforge/eval_fixtures/default_pack/fixtures/fixture.08a.recovery.malformed_artifact.v1.json +54 -0
  79. millforge/eval_fixtures/default_pack/manifest.json +12 -0
  80. millforge/eval_modes.py +1282 -0
  81. millforge/eval_presets.py +1398 -0
  82. millforge/eval_reports.py +2517 -0
  83. millforge/eval_suite.py +2429 -0
  84. millforge/eval_trials.py +2632 -0
  85. millforge/eval_workflow.py +794 -0
  86. millforge/exceptions.py +122 -0
  87. millforge/model_backend.py +2098 -0
  88. millforge/protocols.py +340 -0
  89. millforge/py.typed +0 -0
  90. millforge/runtime.py +1791 -0
  91. millforge/testing/__init__.py +1089 -0
  92. millforge/tools/__init__.py +83 -0
  93. millforge/tools/builtin_runtime.py +1339 -0
  94. millforge/tools/builtins.py +773 -0
  95. millforge/tools/execution.py +1545 -0
  96. millforge/tools/path_policy.py +155 -0
  97. millforge/tools/pi_compat/PI_LICENSE +21 -0
  98. millforge/tools/pi_compat/PROVENANCE.json +55 -0
  99. millforge/tools/pi_compat/UPDATE_POLICY.md +36 -0
  100. millforge/tools/pi_compat/__init__.py +34 -0
  101. millforge/tools/pi_compat/contracts.py +49 -0
  102. millforge/tools/pi_compat/editing.py +390 -0
  103. millforge/tools/pi_compat/mutations.py +57 -0
  104. millforge/tools/pi_compat/operations.py +401 -0
  105. millforge/tools/pi_compat/paths.py +155 -0
  106. millforge/tools/pi_compat/process.py +1375 -0
  107. millforge/tools/pi_compat/search.py +738 -0
  108. millforge/tools/pi_compat/truncation.py +267 -0
  109. millforge/tools/pi_compat_catalog.py +396 -0
  110. millforge/tools/pi_compat_runtime.py +460 -0
  111. millforge/tools/registry.py +553 -0
  112. millforge/tools/results.py +533 -0
  113. millforge-0.1.0.dist-info/METADATA +844 -0
  114. millforge-0.1.0.dist-info/RECORD +116 -0
  115. millforge-0.1.0.dist-info/WHEEL +4 -0
  116. millforge-0.1.0.dist-info/licenses/LICENSE +201 -0
millforge/contracts.py ADDED
@@ -0,0 +1,2860 @@
1
+ """Contract models for the Millforge runtime.
2
+
3
+ All models are defined using Pydantic v2 APIs with ``extra="forbid"``
4
+ (closed-world) validation. Immutable models use ``frozen=True``;
5
+ mutable working models are explicitly noted in their docstrings.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ import math
13
+ import re
14
+ from collections.abc import Mapping
15
+ from enum import Enum
16
+ from pathlib import Path
17
+ from typing import (
18
+ Annotated,
19
+ Any,
20
+ Callable,
21
+ Dict,
22
+ Literal,
23
+ Optional,
24
+ Self,
25
+ Tuple,
26
+ TypeAlias,
27
+ )
28
+ from urllib.parse import parse_qsl, urlsplit, urlunsplit
29
+
30
+ from pydantic import (
31
+ BaseModel,
32
+ ConfigDict,
33
+ Field,
34
+ StrictBool,
35
+ field_validator,
36
+ model_serializer,
37
+ model_validator,
38
+ )
39
+
40
+ from millforge.compiled_plan import (
41
+ CompiledArtifactPolicy,
42
+ DiagnosticField,
43
+ IdempotencyClass,
44
+ SessionEvent,
45
+ SideEffectCertainty,
46
+ SideEffectClass,
47
+ StageIdentity,
48
+ ToolBindingRef,
49
+ ToolExecutionStatus,
50
+ ToolTraceRecord,
51
+ canonical_json_serialize,
52
+ )
53
+
54
+ _SHA256_RE = re.compile(r"[0-9a-f]{64}")
55
+ _SANITIZED_METADATA_MAX_ITEMS = 32
56
+ _SANITIZED_METADATA_KEY_MAX_LENGTH = 64
57
+ _SANITIZED_METADATA_STRING_MAX_LENGTH = 2048
58
+ _SANITIZED_METADATA_BYTES_MAX_LENGTH = 32768
59
+ _REDACTION_DEFAULT_DEPTH = 8
60
+ _REDACTION_DEFAULT_COLLECTION_ITEMS = 64
61
+ _REDACTION_DEFAULT_STRING_LENGTH = 2048
62
+ _REDACTION_DEFAULT_TOTAL_BYTES = 32768
63
+ _REDACTION_MAX_DEPTH = 32
64
+ _REDACTION_MAX_COLLECTION_ITEMS = 1024
65
+ _REDACTION_MAX_STRING_LENGTH = 64 * 1024 * 1024
66
+ _REDACTION_MAX_TOTAL_BYTES = 64 * 1024 * 1024
67
+ _SECRET_PATTERNS = (
68
+ re.compile(
69
+ r"(?i)\b[A-Z][A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|API_KEY)[A-Z0-9_]*=([^\s]+)"
70
+ ),
71
+ re.compile(r"(?i)(api[_-]?key|token|secret|password)=([^&\s]+)"),
72
+ re.compile(r"(?i)(bearer\s+)[a-z0-9._~+/=-]+"),
73
+ re.compile(r"\b(sk|pk|org|sess)-[a-zA-Z0-9]{8,}\b"),
74
+ )
75
+ _URL_PATTERN = re.compile(r"\b[a-z][a-z0-9+.-]*://[^\s<>'\"]+", re.IGNORECASE)
76
+
77
+
78
+ def _nonblank(value: str, field_name: str) -> str:
79
+ if not value.strip():
80
+ raise ValueError(f"{field_name} must be a non-empty string")
81
+ return value
82
+
83
+
84
+ def _unique(values: tuple[str, ...], field_name: str) -> None:
85
+ if len(set(values)) != len(values):
86
+ raise ValueError(f"{field_name} values must be unique")
87
+
88
+
89
+ def _validate_sha256(value: str, field_name: str) -> str:
90
+ if not _SHA256_RE.fullmatch(value):
91
+ raise ValueError(f"{field_name} must be exactly 64 lowercase hex characters")
92
+ return value
93
+
94
+
95
+ def _is_sensitive_field_name(value: str, policy: RedactionPolicy) -> bool:
96
+ lowered = value.lower()
97
+ compact = lowered.replace("_", "").replace("-", "")
98
+ return any(
99
+ marker in lowered or marker.replace("-", "") in compact
100
+ for marker in policy.sensitive_field_markers
101
+ )
102
+
103
+
104
+ JsonScalar: TypeAlias = str | int | float | bool | None
105
+ JsonValue: TypeAlias = Any
106
+ JsonObject: TypeAlias = dict[str, JsonValue]
107
+ SanitizedMetadataValue = JsonScalar
108
+
109
+ # Public global ceilings for one invocation-local selected JSON output.
110
+ MAX_SELECTED_OUTPUT_SCHEMA_BYTES = 64 * 1024
111
+ MAX_SELECTED_OUTPUT_PAYLOAD_BYTES = 1024 * 1024
112
+ MAX_SELECTED_OUTPUT_NESTING_DEPTH = 16
113
+ MAX_SELECTED_OUTPUT_OBJECT_PROPERTIES = 64
114
+ MAX_SELECTED_OUTPUT_ARRAY_ITEMS = 1024
115
+ MAX_SELECTED_OUTPUT_STRING_LENGTH = 64 * 1024
116
+
117
+ _SELECTED_OUTPUT_TYPES = {
118
+ "object",
119
+ "array",
120
+ "string",
121
+ "integer",
122
+ "number",
123
+ "boolean",
124
+ "null",
125
+ }
126
+ _SELECTED_OUTPUT_SCHEMA_KEYWORDS = {
127
+ "object": {"type", "properties", "required", "additionalProperties"},
128
+ "array": {"type", "items", "minItems", "maxItems"},
129
+ "string": {"type", "minLength", "maxLength"},
130
+ "integer": {"type"},
131
+ "number": {"type"},
132
+ "boolean": {"type"},
133
+ "null": {"type"},
134
+ }
135
+ _SELECTED_OUTPUT_VALUE_CONSTRAINTS = {"const", "enum"}
136
+
137
+
138
+ class _FrozenSelectedOutputDict(dict[str, Any]):
139
+ """Internal recursively frozen dict that retains JSON serialization shape."""
140
+
141
+ @staticmethod
142
+ def _immutable(*_args: Any, **_kwargs: Any) -> None:
143
+ raise TypeError("selected output authority is immutable")
144
+
145
+ __setitem__ = _immutable
146
+ __delitem__ = _immutable
147
+ clear = _immutable
148
+ pop = _immutable
149
+ popitem = _immutable # type: ignore[assignment]
150
+ setdefault = _immutable
151
+ update = _immutable # type: ignore[assignment]
152
+ __ior__ = _immutable # type: ignore[assignment]
153
+
154
+
155
+ class _FrozenSelectedOutputList(list[Any]):
156
+ """Internal recursively frozen list that retains JSON serialization shape."""
157
+
158
+ @staticmethod
159
+ def _immutable(*_args: Any, **_kwargs: Any) -> None:
160
+ raise TypeError("selected output authority is immutable")
161
+
162
+ __setitem__ = _immutable
163
+ __delitem__ = _immutable
164
+ __iadd__ = _immutable # type: ignore[assignment]
165
+ __imul__ = _immutable # type: ignore[assignment]
166
+ append = _immutable
167
+ clear = _immutable
168
+ extend = _immutable
169
+ insert = _immutable
170
+ pop = _immutable
171
+ remove = _immutable
172
+ reverse = _immutable
173
+ sort = _immutable
174
+
175
+
176
+ def _freeze_selected_output_json(value: JsonValue) -> JsonValue:
177
+ if isinstance(value, dict):
178
+ return _FrozenSelectedOutputDict(
179
+ {key: _freeze_selected_output_json(item) for key, item in value.items()}
180
+ )
181
+ if isinstance(value, list):
182
+ return _FrozenSelectedOutputList(
183
+ _freeze_selected_output_json(item) for item in value
184
+ )
185
+ return value
186
+
187
+
188
+ def _reject_selected_output_duplicate_keys(
189
+ pairs: list[tuple[str, Any]],
190
+ ) -> dict[str, Any]:
191
+ result: dict[str, Any] = {}
192
+ for key, value in pairs:
193
+ if key in result:
194
+ raise ValueError(f"selected output JSON contains duplicate key {key!r}")
195
+ result[key] = value
196
+ return result
197
+
198
+
199
+ def _reject_selected_output_constant(value: str) -> None:
200
+ raise ValueError(f"selected output JSON contains non-finite number {value}")
201
+
202
+
203
+ def _parse_selected_output_float(value: str) -> float:
204
+ parsed = float(value)
205
+ if not math.isfinite(parsed):
206
+ raise ValueError("selected output JSON contains a non-finite number")
207
+ return parsed
208
+
209
+
210
+ def _parse_selected_output_json(raw: str | bytes, *, field_name: str) -> JsonValue:
211
+ if isinstance(raw, bytes):
212
+ try:
213
+ text = raw.decode("utf-8")
214
+ except UnicodeDecodeError as exc:
215
+ raise ValueError(f"{field_name} must be UTF-8 JSON") from exc
216
+ else:
217
+ text = raw
218
+ try:
219
+ return json.loads(
220
+ text,
221
+ object_pairs_hook=_reject_selected_output_duplicate_keys,
222
+ parse_constant=_reject_selected_output_constant,
223
+ parse_float=_parse_selected_output_float,
224
+ )
225
+ except (json.JSONDecodeError, ValueError) as exc:
226
+ raise ValueError(f"{field_name} must be strict JSON: {exc}") from exc
227
+
228
+
229
+ class _RawJsonObject(list[tuple[str, Any]]):
230
+ """A JSON object represented as its ordered raw key/value pairs."""
231
+
232
+
233
+ def _parse_json_objects_preserving_pairs(
234
+ raw: str | bytes | bytearray,
235
+ ) -> JsonValue | None:
236
+ """Parse JSON while retaining object key pairs for strict raw validation."""
237
+ try:
238
+ return json.loads(raw, object_pairs_hook=_RawJsonObject)
239
+ except (UnicodeDecodeError, json.JSONDecodeError, TypeError):
240
+ # Let Pydantic preserve its normal raw-JSON validation behavior.
241
+ return None
242
+
243
+
244
+ def _validate_raw_json_strictness(value: JsonValue, *, field_name: str) -> None:
245
+ """Reject duplicate object keys and non-finite numbers before conversion."""
246
+ if isinstance(value, _RawJsonObject):
247
+ seen: set[str] = set()
248
+ for key, item in value:
249
+ if key in seen:
250
+ raise ValueError(f"{field_name} contains duplicate key {key!r}")
251
+ seen.add(key)
252
+ _validate_raw_json_strictness(item, field_name=field_name)
253
+ elif isinstance(value, list):
254
+ for item in value:
255
+ _validate_raw_json_strictness(item, field_name=field_name)
256
+ elif type(value) is float and not math.isfinite(value):
257
+ raise ValueError(f"{field_name} contains a non-finite number")
258
+
259
+
260
+ def _validate_harness_request_raw_json(
261
+ raw: str | bytes | bytearray,
262
+ ) -> None:
263
+ """Reject ambiguous or non-finite values throughout a raw request."""
264
+ parsed = _parse_json_objects_preserving_pairs(raw)
265
+ if parsed is None:
266
+ return
267
+ _validate_raw_json_strictness(parsed, field_name="request JSON")
268
+
269
+
270
+ def _validate_selected_output_bound(
271
+ schema: Mapping[str, Any],
272
+ *,
273
+ minimum_key: str,
274
+ maximum_key: str,
275
+ global_maximum: int,
276
+ ) -> None:
277
+ minimum = schema.get(minimum_key, 0)
278
+ maximum = schema.get(maximum_key, global_maximum)
279
+ if type(minimum) is not int or type(maximum) is not int:
280
+ raise ValueError(f"{minimum_key} and {maximum_key} must be integers")
281
+ if minimum < 0 or maximum < 0:
282
+ raise ValueError(f"{minimum_key} and {maximum_key} must be non-negative")
283
+ if minimum > maximum:
284
+ raise ValueError(f"{minimum_key} must not exceed {maximum_key}")
285
+ if maximum > global_maximum:
286
+ raise ValueError(f"{maximum_key} exceeds the selected output global ceiling")
287
+
288
+
289
+ def _normalize_selected_output_scalar(value: Any, *, field_name: str) -> JsonScalar:
290
+ if value is None or type(value) in {bool, int}:
291
+ return value
292
+ if type(value) is float:
293
+ if not math.isfinite(value):
294
+ raise ValueError(f"{field_name} contains a non-finite number")
295
+ return value
296
+ if type(value) is str:
297
+ if len(value) > MAX_SELECTED_OUTPUT_STRING_LENGTH:
298
+ raise ValueError(f"{field_name} string exceeds the string ceiling")
299
+ return value
300
+ raise ValueError(f"{field_name} must contain strict JSON scalar values")
301
+
302
+
303
+ def _selected_output_scalar_matches_type(value: JsonScalar, schema_type: str) -> bool:
304
+ if schema_type == "string":
305
+ return isinstance(value, str)
306
+ if schema_type == "integer":
307
+ return isinstance(value, int) and not isinstance(value, bool)
308
+ if schema_type == "number":
309
+ return isinstance(value, int | float) and not isinstance(value, bool)
310
+ if schema_type == "boolean":
311
+ return isinstance(value, bool)
312
+ if schema_type == "null":
313
+ return value is None
314
+ return False
315
+
316
+
317
+ def _canonical_selected_output_scalar_bytes(value: JsonScalar) -> bytes:
318
+ return canonical_json_serialize(value).encode("utf-8")
319
+
320
+
321
+ def _normalize_selected_output_schema(
322
+ schema: Any,
323
+ *,
324
+ depth: int,
325
+ ) -> JsonObject:
326
+ if depth > MAX_SELECTED_OUTPUT_NESTING_DEPTH:
327
+ raise ValueError("selected output schema exceeds the nesting-depth ceiling")
328
+ if not isinstance(schema, Mapping):
329
+ raise ValueError("every selected output schema node must be a JSON object")
330
+ if any(not isinstance(key, str) for key in schema):
331
+ raise ValueError("selected output schema object keys must be strings")
332
+
333
+ schema_type = schema.get("type")
334
+ constraints = _SELECTED_OUTPUT_VALUE_CONSTRAINTS.intersection(schema)
335
+ if len(constraints) > 1:
336
+ raise ValueError("selected output schema cannot contain both const and enum")
337
+ if schema_type is None:
338
+ if len(constraints) != 1:
339
+ raise ValueError(
340
+ "selected output schema type is outside the admitted subset"
341
+ )
342
+ unsupported = set(schema) - constraints
343
+ if unsupported:
344
+ rendered = ", ".join(sorted(unsupported))
345
+ raise ValueError(
346
+ f"unsupported selected output schema keyword(s): {rendered}"
347
+ )
348
+ normalized: JsonObject = {}
349
+ elif not isinstance(schema_type, str) or schema_type not in _SELECTED_OUTPUT_TYPES:
350
+ raise ValueError("selected output schema type is outside the admitted subset")
351
+ else:
352
+ unsupported = set(schema) - (
353
+ _SELECTED_OUTPUT_SCHEMA_KEYWORDS[schema_type]
354
+ | _SELECTED_OUTPUT_VALUE_CONSTRAINTS
355
+ )
356
+ if unsupported:
357
+ rendered = ", ".join(sorted(unsupported))
358
+ raise ValueError(
359
+ f"unsupported selected output schema keyword(s): {rendered}"
360
+ )
361
+ normalized = {"type": schema_type}
362
+
363
+ if schema_type == "object":
364
+ if schema.get("additionalProperties") is not False:
365
+ raise ValueError("object schemas require additionalProperties=false")
366
+ properties = schema.get("properties", {})
367
+ required = schema.get("required", [])
368
+ if not isinstance(properties, Mapping):
369
+ raise ValueError("object schema properties must be a JSON object")
370
+ if any(not isinstance(key, str) for key in properties):
371
+ raise ValueError("selected output property names must be strings")
372
+ if len(properties) > MAX_SELECTED_OUTPUT_OBJECT_PROPERTIES:
373
+ raise ValueError(
374
+ "selected output schema exceeds the object-property ceiling"
375
+ )
376
+ if not isinstance(required, list) or any(
377
+ not isinstance(item, str) for item in required
378
+ ):
379
+ raise ValueError("object schema required must be an array of strings")
380
+ if len(set(required)) != len(required):
381
+ raise ValueError("object schema required values must be unique")
382
+ unknown_required = set(required) - set(properties)
383
+ if unknown_required:
384
+ raise ValueError(
385
+ "object schema required values must name declared properties"
386
+ )
387
+ for property_name in properties:
388
+ if len(property_name) > MAX_SELECTED_OUTPUT_STRING_LENGTH:
389
+ raise ValueError("selected output property name exceeds string ceiling")
390
+ normalized["properties"] = {
391
+ key: _normalize_selected_output_schema(value, depth=depth + 1)
392
+ for key, value in properties.items()
393
+ }
394
+ normalized["required"] = sorted(required)
395
+ normalized["additionalProperties"] = False
396
+ elif schema_type == "array":
397
+ if "items" not in schema:
398
+ raise ValueError("array schemas require an items schema")
399
+ _validate_selected_output_bound(
400
+ schema,
401
+ minimum_key="minItems",
402
+ maximum_key="maxItems",
403
+ global_maximum=MAX_SELECTED_OUTPUT_ARRAY_ITEMS,
404
+ )
405
+ normalized["items"] = _normalize_selected_output_schema(
406
+ schema["items"],
407
+ depth=depth + 1,
408
+ )
409
+ if "minItems" in schema:
410
+ normalized["minItems"] = schema["minItems"]
411
+ if "maxItems" in schema:
412
+ normalized["maxItems"] = schema["maxItems"]
413
+ elif schema_type == "string":
414
+ _validate_selected_output_bound(
415
+ schema,
416
+ minimum_key="minLength",
417
+ maximum_key="maxLength",
418
+ global_maximum=MAX_SELECTED_OUTPUT_STRING_LENGTH,
419
+ )
420
+ if "minLength" in schema:
421
+ normalized["minLength"] = schema["minLength"]
422
+ if "maxLength" in schema:
423
+ normalized["maxLength"] = schema["maxLength"]
424
+
425
+ if "const" in constraints:
426
+ const = _normalize_selected_output_scalar(
427
+ schema["const"], field_name="selected output const"
428
+ )
429
+ if schema_type is not None and not _selected_output_scalar_matches_type(
430
+ const, schema_type
431
+ ):
432
+ raise ValueError("selected output const does not satisfy schema type")
433
+ normalized["const"] = const
434
+ elif "enum" in constraints:
435
+ raw_enum = schema["enum"]
436
+ if not isinstance(raw_enum, list):
437
+ raise ValueError("selected output enum must be a JSON array")
438
+ if not 1 <= len(raw_enum) <= 64:
439
+ raise ValueError(
440
+ "selected output enum must contain between 1 and 64 values"
441
+ )
442
+ canonical_values: list[tuple[bytes, JsonScalar]] = []
443
+ seen: set[bytes] = set()
444
+ for value in raw_enum:
445
+ normalized_value = _normalize_selected_output_scalar(
446
+ value, field_name="selected output enum"
447
+ )
448
+ if schema_type is not None and not _selected_output_scalar_matches_type(
449
+ normalized_value, schema_type
450
+ ):
451
+ raise ValueError(
452
+ "selected output enum value does not satisfy schema type"
453
+ )
454
+ canonical = _canonical_selected_output_scalar_bytes(normalized_value)
455
+ if canonical in seen:
456
+ raise ValueError("selected output enum values must be unique")
457
+ seen.add(canonical)
458
+ canonical_values.append((canonical, normalized_value))
459
+ normalized["enum"] = [
460
+ value
461
+ for _canonical, value in sorted(canonical_values, key=lambda item: item[0])
462
+ ]
463
+ return normalized
464
+
465
+
466
+ def canonical_selected_output_schema_bytes(
467
+ schema: str | bytes | Mapping[str, Any],
468
+ ) -> bytes:
469
+ """Admit and canonically serialize the closed selected-schema subset."""
470
+ if isinstance(schema, (str, bytes)):
471
+ raw_size = (
472
+ len(schema.encode("utf-8")) if isinstance(schema, str) else len(schema)
473
+ )
474
+ if raw_size > MAX_SELECTED_OUTPUT_SCHEMA_BYTES:
475
+ raise ValueError("selected output schema exceeds the schema-byte ceiling")
476
+ parsed = _parse_selected_output_json(
477
+ schema, field_name="selected output schema"
478
+ )
479
+ else:
480
+ parsed = schema
481
+ normalized = _normalize_selected_output_schema(parsed, depth=1)
482
+ canonical = canonical_json_serialize(normalized).encode("utf-8")
483
+ if len(canonical) > MAX_SELECTED_OUTPUT_SCHEMA_BYTES:
484
+ raise ValueError("selected output schema exceeds the schema-byte ceiling")
485
+ return canonical
486
+
487
+
488
+ def selected_output_schema_sha256(
489
+ schema: str | bytes | Mapping[str, Any],
490
+ ) -> str:
491
+ """Return the SHA-256 digest of an admitted canonical selected schema."""
492
+ return hashlib.sha256(canonical_selected_output_schema_bytes(schema)).hexdigest()
493
+
494
+
495
+ def _normalize_selected_output_payload(value: Any, *, depth: int) -> JsonValue:
496
+ if depth > MAX_SELECTED_OUTPUT_NESTING_DEPTH:
497
+ raise ValueError("selected output payload exceeds the nesting-depth ceiling")
498
+ if value is None or type(value) in {bool, int}:
499
+ return value
500
+ if type(value) is float:
501
+ if not math.isfinite(value):
502
+ raise ValueError("selected output payload contains a non-finite number")
503
+ return value
504
+ if isinstance(value, str):
505
+ if len(value) > MAX_SELECTED_OUTPUT_STRING_LENGTH:
506
+ raise ValueError(
507
+ "selected output payload string exceeds the string ceiling"
508
+ )
509
+ return value
510
+ if isinstance(value, list):
511
+ if len(value) > MAX_SELECTED_OUTPUT_ARRAY_ITEMS:
512
+ raise ValueError("selected output payload exceeds the array-item ceiling")
513
+ return [
514
+ _normalize_selected_output_payload(item, depth=depth + 1) for item in value
515
+ ]
516
+ if isinstance(value, Mapping):
517
+ if any(not isinstance(key, str) for key in value):
518
+ raise ValueError("selected output payload object keys must be strings")
519
+ if len(value) > MAX_SELECTED_OUTPUT_OBJECT_PROPERTIES:
520
+ raise ValueError(
521
+ "selected output payload exceeds the object-property ceiling"
522
+ )
523
+ for key in value:
524
+ if len(key) > MAX_SELECTED_OUTPUT_STRING_LENGTH:
525
+ raise ValueError(
526
+ "selected output payload key exceeds the string ceiling"
527
+ )
528
+ return {
529
+ key: _normalize_selected_output_payload(item, depth=depth + 1)
530
+ for key, item in value.items()
531
+ }
532
+ raise ValueError("selected output payload contains a non-JSON value")
533
+
534
+
535
+ def canonical_selected_output_payload_bytes(value: JsonValue) -> bytes:
536
+ """Validate global JSON bounds and return canonical selected payload bytes."""
537
+ normalized = _normalize_selected_output_payload(value, depth=1)
538
+ canonical = canonical_json_serialize(normalized).encode("utf-8")
539
+ if len(canonical) > MAX_SELECTED_OUTPUT_PAYLOAD_BYTES:
540
+ raise ValueError("selected output payload exceeds the payload-byte ceiling")
541
+ return canonical
542
+
543
+
544
+ def parse_selected_output_payload_json(raw: str | bytes) -> JsonValue:
545
+ """Parse strict JSON, rejecting duplicate keys and all global bound violations."""
546
+ raw_size = len(raw.encode("utf-8")) if isinstance(raw, str) else len(raw)
547
+ if raw_size > MAX_SELECTED_OUTPUT_PAYLOAD_BYTES:
548
+ raise ValueError("selected output payload exceeds the payload-byte ceiling")
549
+ parsed = _parse_selected_output_json(raw, field_name="selected output payload")
550
+ canonical_selected_output_payload_bytes(parsed)
551
+ return parsed
552
+
553
+
554
+ # ---------------------------------------------------------------------------
555
+ # Closed enums
556
+ # ---------------------------------------------------------------------------
557
+
558
+
559
+ class ExecutionStatus(str, Enum):
560
+ """Closed enum of possible execution statuses for a run or session."""
561
+
562
+ COMPLETED = "completed"
563
+ FAILED = "failed"
564
+ INTERRUPTED = "interrupted"
565
+
566
+
567
+ class ExecutionResultClass(str, Enum):
568
+ """Closed enum of possible execution result classifications."""
569
+
570
+ DOMAIN_TERMINAL = "domain_terminal"
571
+ DOMAIN_REJECTED = "domain_rejected"
572
+ BINDING_REJECTED = "binding_rejected"
573
+ COMPILED_HARNESS_INVALID = "compiled_harness_invalid"
574
+ BACKEND_FAILURE = "backend_failure"
575
+ MODEL_FAILURE = "model_failure"
576
+ TOOL_FAILURE = "tool_failure"
577
+ BUDGET_EXHAUSTED = "budget_exhausted"
578
+ TIMED_OUT = "timed_out"
579
+ CANCELLED = "cancelled"
580
+ TERMINAL_RESULT_INVALID = "terminal_result_invalid"
581
+ ARTIFACT_FINALIZATION_FAILED = "artifact_finalization_failed"
582
+ INTERNAL_FAILURE = "internal_failure"
583
+
584
+
585
+ class TerminalCertainty(str, Enum):
586
+ """Certainty of terminal-result commit ordering."""
587
+
588
+ NOT_APPLICABLE = "not_applicable"
589
+ COMMITTED = "committed"
590
+ UNKNOWN = "unknown"
591
+
592
+
593
+ class GuardedSessionStatus(str, Enum):
594
+ """Closed enum of possible guarded session statuses."""
595
+
596
+ TERMINAL = "terminal"
597
+ REJECTED = "rejected"
598
+ BACKEND_FAILED = "backend_failed"
599
+ MODEL_FAILED = "model_failed"
600
+ TOOL_FAILED = "tool_failed"
601
+ BUDGET_EXHAUSTED = "budget_exhausted"
602
+ PREREQUISITE_BUDGET_EXHAUSTED = "prerequisite_budget_exhausted"
603
+ TIMED_OUT = "timed_out"
604
+ CANCELLED = "cancelled"
605
+ INVALID_TERMINAL = "invalid_terminal"
606
+
607
+
608
+ class TimeoutOrigin(str, Enum):
609
+ """Closed timeout origins that preserve the stable timeout result class."""
610
+
611
+ SESSION_DEADLINE = "session_deadline"
612
+ MODEL_CONNECT_TIMEOUT = "model_connect_timeout"
613
+ MODEL_READ_TIMEOUT = "model_read_timeout"
614
+ MODEL_WRITE_TIMEOUT = "model_write_timeout"
615
+ MODEL_POOL_TIMEOUT = "model_pool_timeout"
616
+ TOOL_TIMEOUT = "tool_timeout"
617
+ BACKEND_TIMEOUT = "backend_timeout"
618
+ ARTIFACT_FINALIZATION_TIMEOUT = "artifact_finalization_timeout"
619
+ CLEANUP_TIMEOUT = "cleanup_timeout"
620
+
621
+
622
+ # ---------------------------------------------------------------------------
623
+ # New standalone types
624
+ # ---------------------------------------------------------------------------
625
+
626
+
627
+ class Deadline(BaseModel):
628
+ """Immutable monotonic deadline specification."""
629
+
630
+ model_config = ConfigDict(extra="forbid", frozen=True)
631
+
632
+ started_monotonic: float = Field(
633
+ ge=0, description="Monotonic time when deadline evaluation started"
634
+ )
635
+ outer_deadline_monotonic: float = Field(
636
+ ge=0, description="Outer request deadline as monotonic seconds"
637
+ )
638
+ compiled_harness_deadline_monotonic: float | None = Field(
639
+ default=None,
640
+ ge=0,
641
+ description="Optional smaller compiled harness deadline as monotonic seconds",
642
+ )
643
+ effective_deadline_monotonic: float = Field(
644
+ ge=0, description="Effective deadline after all bounds are applied"
645
+ )
646
+ source: Literal["request", "compiled_harness", "request_and_harness"] = Field(
647
+ description="Source used to derive the effective deadline"
648
+ )
649
+
650
+ @model_validator(mode="after")
651
+ def _check_deadline_ordering(self) -> Deadline:
652
+ if self.outer_deadline_monotonic < self.started_monotonic:
653
+ raise ValueError("outer_deadline_monotonic must not precede start")
654
+ if self.effective_deadline_monotonic < self.started_monotonic:
655
+ raise ValueError("effective_deadline_monotonic must not precede start")
656
+ if self.effective_deadline_monotonic > self.outer_deadline_monotonic:
657
+ raise ValueError(
658
+ "effective_deadline_monotonic must not exceed outer deadline"
659
+ )
660
+ if self.compiled_harness_deadline_monotonic is not None:
661
+ if self.compiled_harness_deadline_monotonic < self.started_monotonic:
662
+ raise ValueError(
663
+ "compiled_harness_deadline_monotonic must not precede start"
664
+ )
665
+ expected = min(
666
+ self.outer_deadline_monotonic,
667
+ self.compiled_harness_deadline_monotonic,
668
+ )
669
+ if self.effective_deadline_monotonic != expected:
670
+ raise ValueError(
671
+ "effective_deadline_monotonic must equal the smaller request "
672
+ "or compiled harness deadline"
673
+ )
674
+ if self.source == "request":
675
+ raise ValueError(
676
+ "source=request is invalid when compiled harness deadline is present"
677
+ )
678
+ elif self.effective_deadline_monotonic != self.outer_deadline_monotonic:
679
+ raise ValueError(
680
+ "effective_deadline_monotonic must equal outer deadline when no "
681
+ "compiled harness deadline is present"
682
+ )
683
+ return self
684
+
685
+ @property
686
+ def request_deadline_monotonic(self) -> float:
687
+ """Return the absolute request deadline in monotonic seconds."""
688
+ return self.outer_deadline_monotonic
689
+
690
+ def remaining(self, clock: Callable[[], float] | Any) -> float:
691
+ """Return non-negative seconds remaining against the effective deadline."""
692
+ now = clock() if callable(clock) else clock.monotonic()
693
+ return max(0.0, self.effective_deadline_monotonic - float(now))
694
+
695
+ @classmethod
696
+ def from_deadlines(
697
+ cls,
698
+ *,
699
+ started_monotonic: float,
700
+ request_deadline_monotonic: float,
701
+ compiled_harness_deadline_monotonic: float | None = None,
702
+ ) -> Deadline:
703
+ """Build a deadline with effective time derived from admitted bounds."""
704
+ if compiled_harness_deadline_monotonic is None:
705
+ source: Literal["request", "compiled_harness", "request_and_harness"] = (
706
+ "request"
707
+ )
708
+ effective = request_deadline_monotonic
709
+ else:
710
+ effective = min(
711
+ request_deadline_monotonic,
712
+ compiled_harness_deadline_monotonic,
713
+ )
714
+ source = (
715
+ "compiled_harness"
716
+ if compiled_harness_deadline_monotonic < request_deadline_monotonic
717
+ else "request_and_harness"
718
+ )
719
+ return cls(
720
+ started_monotonic=started_monotonic,
721
+ outer_deadline_monotonic=request_deadline_monotonic,
722
+ compiled_harness_deadline_monotonic=compiled_harness_deadline_monotonic,
723
+ effective_deadline_monotonic=effective,
724
+ source=source,
725
+ )
726
+
727
+
728
+ class TokenUsage(BaseModel):
729
+ """Immutable token usage breakdown for a model interaction."""
730
+
731
+ model_config = ConfigDict(extra="forbid", frozen=True)
732
+
733
+ input_tokens: int = Field(ge=0, description="Number of input (prompt) tokens")
734
+ output_tokens: int = Field(ge=0, description="Number of output (completion) tokens")
735
+ total_tokens: int = Field(ge=0, description="Total tokens consumed")
736
+ provider_reported: bool = Field(
737
+ description="Whether the usage was reported by the provider"
738
+ )
739
+
740
+ @model_validator(mode="after")
741
+ def _total_matches_parts(self) -> TokenUsage:
742
+ if self.total_tokens != self.input_tokens + self.output_tokens:
743
+ raise ValueError("total_tokens must equal input_tokens + output_tokens")
744
+ return self
745
+
746
+
747
+ # ---------------------------------------------------------------------------
748
+ # Secret reference
749
+ # ---------------------------------------------------------------------------
750
+
751
+
752
+ class SecretRef(BaseModel):
753
+ """Reference to a secret stored outside the contract boundary.
754
+
755
+ Stores an opaque handle (``secret_id``) and the environment-variable
756
+ name that resolves to the secret value. **The secret value itself
757
+ must never appear in any contract field or serialization output.**
758
+ """
759
+
760
+ model_config = ConfigDict(extra="forbid", frozen=True)
761
+
762
+ secret_id: str = Field(description="Unique secret identifier")
763
+ env_var: str = Field(description="Environment variable name holding the secret")
764
+
765
+ @field_validator("secret_id")
766
+ @classmethod
767
+ def _secret_id_must_be_non_empty(cls, v: str) -> str:
768
+ if not v.strip():
769
+ raise ValueError("secret_id must be a non-empty string")
770
+ return v
771
+
772
+ @field_validator("env_var")
773
+ @classmethod
774
+ def _env_var_must_be_non_empty(cls, v: str) -> str:
775
+ if not v.strip():
776
+ raise ValueError("env_var must be a non-empty string")
777
+ return v
778
+
779
+
780
+ # ---------------------------------------------------------------------------
781
+ # Identity / Reference models (immutable snapshots)
782
+ # ---------------------------------------------------------------------------
783
+
784
+
785
+ class CompiledHarnessIdentity(BaseModel):
786
+ """Immutable identity of a compiled harness plan."""
787
+
788
+ model_config = ConfigDict(extra="forbid", frozen=True)
789
+
790
+ compiled_plan_id: str = Field(description="Unique identifier for the compiled plan")
791
+ harness_id: str = Field(description="Harness identifier")
792
+ harness_version: int = Field(gt=0, description="Positive harness version")
793
+
794
+ @field_validator("compiled_plan_id", "harness_id")
795
+ @classmethod
796
+ def _strings_nonblank(cls, value: str, info: Any) -> str:
797
+ return _nonblank(value, info.field_name)
798
+
799
+
800
+ class CompiledHarnessHash(BaseModel):
801
+ """Immutable cryptographic hash of a compiled harness."""
802
+
803
+ model_config = ConfigDict(extra="forbid", frozen=True)
804
+
805
+ algorithm: Literal["sha256"] = Field(description="Hash algorithm (sha256 only)")
806
+ digest: str = Field(description="Hex-encoded digest value")
807
+
808
+ @field_validator("digest")
809
+ @classmethod
810
+ def _digest_valid(cls, value: str) -> str:
811
+ return _validate_sha256(value, "digest")
812
+
813
+
814
+ class CompiledHarnessRef(BaseModel):
815
+ """Immutable reference to a compiled harness, including identity, path, and hash."""
816
+
817
+ model_config = ConfigDict(extra="forbid", frozen=True)
818
+
819
+ identity: CompiledHarnessIdentity = Field(description="Compiled harness identity")
820
+ path: Path = Field(description="Filesystem path to the compiled harness")
821
+ expected_hash: CompiledHarnessHash = Field(
822
+ description="Expected cryptographic hash of the harness"
823
+ )
824
+
825
+
826
+ class RunDirRef(BaseModel):
827
+ """Immutable reference to a run directory."""
828
+
829
+ model_config = ConfigDict(extra="forbid", frozen=True)
830
+
831
+ run_id: str = Field(description="Unique run identifier")
832
+ path: Path = Field(description="Absolute or relative path to the run directory")
833
+
834
+ @field_validator("run_id")
835
+ @classmethod
836
+ def _run_id_nonblank(cls, value: str) -> str:
837
+ return _nonblank(value, "run_id")
838
+
839
+
840
+ class ArtifactRef(BaseModel):
841
+ """Immutable reference to an artifact file."""
842
+
843
+ model_config = ConfigDict(extra="forbid", frozen=True)
844
+
845
+ artifact_id: str = Field(description="Unique artifact identifier")
846
+ path: Path = Field(description="Path to the artifact file")
847
+ content_type: Optional[str] = Field(
848
+ default=None, description="MIME type or content format"
849
+ )
850
+
851
+ @field_validator("artifact_id")
852
+ @classmethod
853
+ def _artifact_id_nonblank(cls, value: str) -> str:
854
+ return _nonblank(value, "artifact_id")
855
+
856
+ @field_validator("content_type")
857
+ @classmethod
858
+ def _content_type_nonblank(cls, value: str | None) -> str | None:
859
+ return None if value is None else _nonblank(value, "content_type")
860
+
861
+
862
+ class HarnessTaskInput(BaseModel):
863
+ """Exact bounded instruction supplied to an executable harness request."""
864
+
865
+ model_config = ConfigDict(
866
+ extra="forbid",
867
+ frozen=True,
868
+ hide_input_in_errors=True,
869
+ )
870
+
871
+ schema_version: Literal["1.0"] = "1.0"
872
+ instruction: str
873
+
874
+ @field_validator("instruction")
875
+ @classmethod
876
+ def _instruction_is_bounded(cls, value: str) -> str:
877
+ if not value.strip():
878
+ raise ValueError(
879
+ "instruction must be non-empty after whitespace inspection"
880
+ )
881
+ if "\x00" in value:
882
+ raise ValueError("instruction must not contain NUL")
883
+ try:
884
+ encoded = value.encode("utf-8")
885
+ except UnicodeEncodeError as exc:
886
+ raise ValueError("instruction must be valid UTF-8 text") from exc
887
+ if len(encoded) > 65_536:
888
+ raise ValueError("instruction must not exceed 65,536 UTF-8 bytes")
889
+ return value
890
+
891
+ @property
892
+ def utf8_byte_count(self) -> int:
893
+ """Return the exact UTF-8 encoded instruction size."""
894
+ return len(self.instruction.encode("utf-8"))
895
+
896
+ @property
897
+ def sha256(self) -> str:
898
+ """Return the lowercase SHA-256 of the exact UTF-8 instruction bytes."""
899
+ return hashlib.sha256(self.instruction.encode("utf-8")).hexdigest()
900
+
901
+
902
+ # ---------------------------------------------------------------------------
903
+ # Stage identity
904
+ # ---------------------------------------------------------------------------
905
+
906
+
907
+ # ---------------------------------------------------------------------------
908
+ # Capability models
909
+ # ---------------------------------------------------------------------------
910
+
911
+ CAPABILITY_GRANT_CONSTRAINTS_DESC = (
912
+ "Optional constraints applied to this capability grant"
913
+ )
914
+
915
+
916
+ class CapabilityGrant(BaseModel):
917
+ """Immutable capability grant with capability identifier and optional constraints."""
918
+
919
+ model_config = ConfigDict(extra="forbid", frozen=True)
920
+
921
+ capability_id: str = Field(description="Capability identifier")
922
+ constraints: Optional[Dict[str, Any]] = Field(
923
+ default=None, description=CAPABILITY_GRANT_CONSTRAINTS_DESC
924
+ )
925
+
926
+ @field_validator("capability_id")
927
+ @classmethod
928
+ def _capability_id_nonblank(cls, value: str) -> str:
929
+ return _nonblank(value, "capability_id")
930
+
931
+
932
+ class CapabilityEnvelope(BaseModel):
933
+ """Immutable capability grant envelope containing a tuple of grants."""
934
+
935
+ model_config = ConfigDict(extra="forbid", frozen=True)
936
+
937
+ grants: Tuple[CapabilityGrant, ...] = Field(
938
+ description="Tuple of capability grants"
939
+ )
940
+
941
+ @field_validator("grants")
942
+ @classmethod
943
+ def _grant_capabilities_unique(
944
+ cls, value: tuple[CapabilityGrant, ...]
945
+ ) -> tuple[CapabilityGrant, ...]:
946
+ _unique(tuple(grant.capability_id for grant in value), "capability_id")
947
+ return value
948
+
949
+
950
+ # ---------------------------------------------------------------------------
951
+ # Profile and reference models
952
+ # ---------------------------------------------------------------------------
953
+
954
+
955
+ class ModelProfileRef(BaseModel):
956
+ """Immutable reference to a model profile.
957
+
958
+ Contains only the profile identifier — model provider, name, and
959
+ details are resolved externally from the profile configuration.
960
+ """
961
+
962
+ model_config = ConfigDict(extra="forbid", frozen=True)
963
+
964
+ profile_id: str = Field(description="Model profile identifier")
965
+
966
+ @field_validator("profile_id")
967
+ @classmethod
968
+ def _profile_id_nonblank(cls, value: str) -> str:
969
+ return _nonblank(value, "profile_id")
970
+
971
+
972
+ class TimeoutRef(BaseModel):
973
+ """Immutable timeout reference."""
974
+
975
+ model_config = ConfigDict(extra="forbid", frozen=True)
976
+
977
+ timeout_seconds: float = Field(description="Timeout duration in seconds")
978
+ deadline: Optional[str] = Field(
979
+ default=None, description="ISO-8601 deadline timestamp"
980
+ )
981
+
982
+ @field_validator("timeout_seconds")
983
+ @classmethod
984
+ def _timeout_must_be_positive(cls, v: float) -> float:
985
+ if v <= 0:
986
+ raise ValueError("timeout_seconds must be a positive number")
987
+ return v
988
+
989
+
990
+ class CancellationRef(BaseModel):
991
+ """Immutable cancellation reference."""
992
+
993
+ model_config = ConfigDict(extra="forbid", frozen=True)
994
+
995
+ cancellation_id: str = Field(description="Cancellation identifier")
996
+
997
+ @field_validator("cancellation_id")
998
+ @classmethod
999
+ def _cancellation_id_must_be_non_empty(cls, v: str) -> str:
1000
+ if not v.strip():
1001
+ raise ValueError("cancellation_id must be a non-empty string")
1002
+ return v
1003
+
1004
+
1005
+ # ---------------------------------------------------------------------------
1006
+ class ConnectorApprovalGrant(BaseModel):
1007
+ """Runtime-only grant authorizing one exact connector approval scope."""
1008
+
1009
+ model_config = ConfigDict(extra="forbid", frozen=True)
1010
+
1011
+ connector_id: str = Field(description="Connector identity authorized by runtime")
1012
+ provider_tool_name: str = Field(description="Provider tool name authorized")
1013
+ tool_id: str = Field(description="Compiled connector tool identifier")
1014
+ tool_version: int = Field(ge=1, description="Compiled connector tool version")
1015
+ descriptor_sha256: str = Field(description="Compiled descriptor hash")
1016
+ request_id: str = Field(description="Runtime request identifier")
1017
+ run_id: str = Field(description="Runtime run identifier")
1018
+ stage: StageIdentity = Field(description="Runtime stage identity")
1019
+ approval_policy: Literal["millrace_explicit"] = Field(
1020
+ description="Approval policy authorized by the runtime"
1021
+ )
1022
+ expires_at_monotonic: float = Field(
1023
+ ge=0,
1024
+ description="Trusted monotonic timestamp after which the grant is invalid",
1025
+ )
1026
+ approval_id: str | None = Field(
1027
+ default=None, description="Operator or runtime approval identifier"
1028
+ )
1029
+ nonce: str | None = Field(
1030
+ default=None, description="Opaque runtime nonce for one approval grant"
1031
+ )
1032
+
1033
+ @field_validator(
1034
+ "connector_id",
1035
+ "provider_tool_name",
1036
+ "tool_id",
1037
+ "request_id",
1038
+ "run_id",
1039
+ "approval_id",
1040
+ "nonce",
1041
+ )
1042
+ @classmethod
1043
+ def _grant_text_nonblank(cls, value: str | None, info: Any) -> str | None:
1044
+ if value is None:
1045
+ return None
1046
+ return _nonblank(value, info.field_name)
1047
+
1048
+ @field_validator("descriptor_sha256")
1049
+ @classmethod
1050
+ def _descriptor_hash_valid(cls, value: str) -> str:
1051
+ return _validate_sha256(value, "descriptor_sha256")
1052
+
1053
+ @model_validator(mode="after")
1054
+ def _has_approval_identity(self) -> ConnectorApprovalGrant:
1055
+ if self.approval_id is None and self.nonce is None:
1056
+ raise ValueError("connector approval grant requires approval_id or nonce")
1057
+ return self
1058
+
1059
+
1060
+ # Tool execution context
1061
+ # ---------------------------------------------------------------------------
1062
+
1063
+
1064
+ class ToolExecutionContext(BaseModel):
1065
+ """Contextual information passed to ``ToolExecutor.execute()``.
1066
+
1067
+ Provides the execution environment — request identity, stage,
1068
+ run directory, capability envelope, timeout, and cancellation
1069
+ reference — as the second argument to ``ToolExecutor.execute()``.
1070
+ """
1071
+
1072
+ model_config = ConfigDict(extra="forbid", frozen=True)
1073
+
1074
+ request_id: str = Field(description="Unique request identifier")
1075
+ run_id: str = Field(description="Run this request belongs to")
1076
+ stage: StageIdentity = Field(description="Stage identity")
1077
+ run_directory: RunDirRef = Field(description="Run directory reference")
1078
+ capability_envelope: CapabilityEnvelope = Field(
1079
+ description="Capability grant envelope"
1080
+ )
1081
+ timeout: TimeoutRef = Field(description="Timeout reference")
1082
+ cancellation: CancellationRef = Field(description="Cancellation reference")
1083
+ deadline: Deadline = Field(description="Deadline specification")
1084
+ workspace_root: Path | None = Field(
1085
+ default=None, description="Trusted workspace root supplied by runtime"
1086
+ )
1087
+ artifact_root: Path | None = Field(
1088
+ default=None, description="Trusted artifact root supplied by runtime"
1089
+ )
1090
+ compiled_artifact_policy: CompiledArtifactPolicy | None = Field(
1091
+ default=None,
1092
+ description="Compiled artifact declarations supplied by the runtime",
1093
+ )
1094
+ input_artifacts: Tuple[ArtifactRef, ...] = Field(
1095
+ default_factory=tuple,
1096
+ description="Runtime-supplied request input artifact references",
1097
+ )
1098
+ work_item_id: str | None = Field(
1099
+ default=None, description="Runtime-supplied active work item identifier"
1100
+ )
1101
+ cancellation_requested: bool = Field(
1102
+ default=False, description="Trusted pre-entry cancellation state"
1103
+ )
1104
+ current_monotonic: float = Field(
1105
+ default=0.0,
1106
+ ge=0,
1107
+ description="Trusted monotonic timestamp used for pre-entry deadline checks",
1108
+ )
1109
+ connector_approval_grants: Tuple[ConnectorApprovalGrant, ...] = Field(
1110
+ default_factory=tuple,
1111
+ description="Runtime-only connector approval grants scoped to exact bindings",
1112
+ )
1113
+
1114
+
1115
+ # ---------------------------------------------------------------------------
1116
+ # Invocation-local selected output authority and admitted value
1117
+ # ---------------------------------------------------------------------------
1118
+
1119
+
1120
+ class SelectedOutputRequirement(BaseModel):
1121
+ """Immutable required/optional authority for one selected JSON output.
1122
+
1123
+ ``json_schema`` may be supplied as a strict JSON string/bytes value or as a
1124
+ Python mapping. It is normalized to the documented closed subset and its
1125
+ canonical SHA-256 digest is pinned into the serialized request contract.
1126
+ """
1127
+
1128
+ model_config = ConfigDict(
1129
+ extra="forbid",
1130
+ frozen=True,
1131
+ hide_input_in_errors=True,
1132
+ revalidate_instances="always",
1133
+ )
1134
+
1135
+ required: StrictBool = Field(
1136
+ description="Whether the invocation must admit a present selected output"
1137
+ )
1138
+ json_schema: JsonObject = Field(description="Admitted closed selected JSON schema")
1139
+ schema_sha256: str = Field(
1140
+ default="",
1141
+ description="SHA-256 digest of the canonical admitted selected schema",
1142
+ )
1143
+
1144
+ @classmethod
1145
+ def model_validate_json(
1146
+ cls,
1147
+ json_data: str | bytes | bytearray,
1148
+ *,
1149
+ strict: bool | None = None,
1150
+ extra: Any | None = None,
1151
+ context: Any | None = None,
1152
+ by_alias: bool | None = None,
1153
+ by_name: bool | None = None,
1154
+ ) -> Self:
1155
+ """Validate raw requirement JSON without losing duplicate keys first."""
1156
+ parsed = _parse_json_objects_preserving_pairs(json_data)
1157
+ if parsed is not None:
1158
+ _validate_raw_json_strictness(parsed, field_name="selected output JSON")
1159
+ return super().model_validate_json(
1160
+ json_data,
1161
+ strict=strict,
1162
+ extra=extra,
1163
+ context=context,
1164
+ by_alias=by_alias,
1165
+ by_name=by_name,
1166
+ )
1167
+
1168
+ @model_validator(mode="before")
1169
+ @classmethod
1170
+ def _admit_and_pin_schema(cls, value: Any) -> Any:
1171
+ if isinstance(value, cls):
1172
+ return value
1173
+ if not isinstance(value, Mapping) or "json_schema" not in value:
1174
+ return value
1175
+ canonical = canonical_selected_output_schema_bytes(value["json_schema"])
1176
+ admitted = json.loads(canonical)
1177
+ digest = hashlib.sha256(canonical).hexdigest()
1178
+ supplied_digest = value.get("schema_sha256")
1179
+ if supplied_digest not in (None, "", digest):
1180
+ raise ValueError("schema_sha256 does not match canonical selected schema")
1181
+ normalized = dict(value)
1182
+ normalized["json_schema"] = admitted
1183
+ normalized["schema_sha256"] = digest
1184
+ return normalized
1185
+
1186
+ @field_validator("schema_sha256")
1187
+ @classmethod
1188
+ def _schema_digest_valid(cls, value: str) -> str:
1189
+ return _validate_sha256(value, "schema_sha256")
1190
+
1191
+ @model_validator(mode="after")
1192
+ def _schema_digest_matches(self) -> SelectedOutputRequirement:
1193
+ if self.schema_sha256 != selected_output_schema_sha256(self.json_schema):
1194
+ raise ValueError("schema_sha256 does not match canonical selected schema")
1195
+ object.__setattr__(
1196
+ self,
1197
+ "json_schema",
1198
+ _freeze_selected_output_json(self.json_schema),
1199
+ )
1200
+ return self
1201
+
1202
+ @property
1203
+ def canonical_schema_bytes(self) -> bytes:
1204
+ """Return the admitted schema as canonical UTF-8 JSON bytes."""
1205
+ return canonical_selected_output_schema_bytes(self.json_schema)
1206
+
1207
+
1208
+ class TerminalSelectedOutputRequirement(BaseModel):
1209
+ """Immutable selected-output authority for one exact terminal result."""
1210
+
1211
+ model_config = ConfigDict(
1212
+ extra="forbid",
1213
+ frozen=True,
1214
+ revalidate_instances="always",
1215
+ )
1216
+
1217
+ terminal_result: str
1218
+ selected_output: SelectedOutputRequirement
1219
+
1220
+ @field_validator("terminal_result")
1221
+ @classmethod
1222
+ def _terminal_result_is_nonblank(cls, value: str) -> str:
1223
+ return _nonblank(value, "terminal_result")
1224
+
1225
+
1226
+ def _selected_output_requirements_by_terminal_result(
1227
+ requirements: tuple[TerminalSelectedOutputRequirement, ...],
1228
+ ) -> dict[str, SelectedOutputRequirement]:
1229
+ """Return the one canonical terminal-result lookup for selected output."""
1230
+
1231
+ if len(requirements) > 64:
1232
+ raise ValueError("selected output requirements exceed the 64-record ceiling")
1233
+ lookup: dict[str, SelectedOutputRequirement] = {}
1234
+ for item in requirements:
1235
+ terminal_result = _nonblank(item.terminal_result, "terminal_result")
1236
+ if terminal_result in lookup:
1237
+ raise ValueError("selected output terminal_result values must be unique")
1238
+ lookup[terminal_result] = item.selected_output
1239
+ return dict(sorted(lookup.items(), key=lambda item: item[0].encode("utf-8")))
1240
+
1241
+
1242
+ class SelectedOutputAbsent(BaseModel):
1243
+ """Explicit absence for an admitted optional selected-output authority."""
1244
+
1245
+ model_config = ConfigDict(extra="forbid", frozen=True)
1246
+
1247
+ present: Literal[False] = False
1248
+
1249
+
1250
+ class SelectedOutputPresent(BaseModel):
1251
+ """A present, globally bounded JSON value; ``value=None`` is JSON null."""
1252
+
1253
+ model_config = ConfigDict(
1254
+ extra="forbid",
1255
+ frozen=True,
1256
+ hide_input_in_errors=True,
1257
+ revalidate_instances="always",
1258
+ )
1259
+
1260
+ present: Literal[True] = True
1261
+ value: JsonValue = Field(description="Admitted JSON value, including JSON null")
1262
+
1263
+ @field_validator("value", mode="before")
1264
+ @classmethod
1265
+ def _value_is_bounded_json(cls, value: Any) -> JsonValue:
1266
+ normalized = _normalize_selected_output_payload(value, depth=1)
1267
+ canonical_selected_output_payload_bytes(normalized)
1268
+ return _freeze_selected_output_json(normalized)
1269
+
1270
+
1271
+ SelectedOutput: TypeAlias = Annotated[
1272
+ SelectedOutputAbsent | SelectedOutputPresent,
1273
+ Field(discriminator="present"),
1274
+ ]
1275
+
1276
+
1277
+ def admit_selected_output(
1278
+ requirement: SelectedOutputRequirement,
1279
+ *,
1280
+ present: bool,
1281
+ value: Any = None,
1282
+ ) -> SelectedOutput:
1283
+ """Mechanically admit one invocation-local selected-output candidate.
1284
+
1285
+ Absence is legal only for optional authorities. Present values first pass
1286
+ the global payload ceilings owned by :class:`SelectedOutputPresent`, then
1287
+ the exact closed schema carried by ``requirement``. No prose, artifact, or
1288
+ workspace fallback participates in admission.
1289
+ """
1290
+
1291
+ if not present:
1292
+ if requirement.required:
1293
+ raise ValueError("required selected output candidate is missing")
1294
+ return SelectedOutputAbsent()
1295
+
1296
+ candidate = SelectedOutputPresent(value=value)
1297
+ error = _selected_output_schema_error(
1298
+ candidate.value,
1299
+ requirement.json_schema,
1300
+ path="$",
1301
+ )
1302
+ if error is not None:
1303
+ raise ValueError(f"selected output candidate failed schema validation: {error}")
1304
+ return candidate
1305
+
1306
+
1307
+ def _selected_output_schema_error(
1308
+ value: Any,
1309
+ schema: Mapping[str, Any],
1310
+ *,
1311
+ path: str,
1312
+ ) -> str | None:
1313
+ """Return the first deterministic mismatch against the admitted subset."""
1314
+
1315
+ if "const" in schema:
1316
+ if _canonical_selected_output_scalar_bytes(value) != (
1317
+ _canonical_selected_output_scalar_bytes(schema["const"])
1318
+ ):
1319
+ return f"{path} must equal const value"
1320
+ elif "enum" in schema:
1321
+ candidate = _canonical_selected_output_scalar_bytes(value)
1322
+ if all(
1323
+ candidate != _canonical_selected_output_scalar_bytes(item)
1324
+ for item in schema["enum"]
1325
+ ):
1326
+ return f"{path} must equal an enum value"
1327
+
1328
+ schema_type = schema.get("type")
1329
+ if schema_type is None:
1330
+ return None
1331
+ if schema_type == "object":
1332
+ if not isinstance(value, Mapping):
1333
+ return f"{path} must be object"
1334
+ properties = schema["properties"]
1335
+ for key in schema["required"]:
1336
+ if key not in value:
1337
+ return f"{path}.{key} is required"
1338
+ extra = sorted(set(value) - set(properties))
1339
+ if extra:
1340
+ return f"{path}.{extra[0]} is not allowed"
1341
+ for key, item in value.items():
1342
+ error = _selected_output_schema_error(
1343
+ item,
1344
+ properties[key],
1345
+ path=f"{path}.{key}",
1346
+ )
1347
+ if error is not None:
1348
+ return error
1349
+ return None
1350
+ if schema_type == "array":
1351
+ if not isinstance(value, list):
1352
+ return f"{path} must be array"
1353
+ minimum = schema.get("minItems", 0)
1354
+ maximum = schema.get("maxItems", MAX_SELECTED_OUTPUT_ARRAY_ITEMS)
1355
+ if len(value) < minimum:
1356
+ return f"{path} has fewer than {minimum} items"
1357
+ if len(value) > maximum:
1358
+ return f"{path} has more than {maximum} items"
1359
+ for index, item in enumerate(value):
1360
+ error = _selected_output_schema_error(
1361
+ item,
1362
+ schema["items"],
1363
+ path=f"{path}[{index}]",
1364
+ )
1365
+ if error is not None:
1366
+ return error
1367
+ return None
1368
+ if schema_type == "string":
1369
+ if not isinstance(value, str):
1370
+ return f"{path} must be string"
1371
+ minimum = schema.get("minLength", 0)
1372
+ maximum = schema.get("maxLength", MAX_SELECTED_OUTPUT_STRING_LENGTH)
1373
+ if len(value) < minimum:
1374
+ return f"{path} is shorter than {minimum} characters"
1375
+ if len(value) > maximum:
1376
+ return f"{path} is longer than {maximum} characters"
1377
+ return None
1378
+ if schema_type == "integer":
1379
+ return (
1380
+ None
1381
+ if isinstance(value, int) and not isinstance(value, bool)
1382
+ else f"{path} must be integer"
1383
+ )
1384
+ if schema_type == "number":
1385
+ return (
1386
+ None
1387
+ if isinstance(value, int | float)
1388
+ and not isinstance(value, bool)
1389
+ and (not isinstance(value, float) or math.isfinite(value))
1390
+ else f"{path} must be finite number"
1391
+ )
1392
+ if schema_type == "boolean":
1393
+ return None if isinstance(value, bool) else f"{path} must be boolean"
1394
+ if schema_type == "null":
1395
+ return None if value is None else f"{path} must be null"
1396
+ raise AssertionError(f"unreachable selected output schema type: {schema_type}")
1397
+
1398
+
1399
+ # ---------------------------------------------------------------------------
1400
+ # Harness execution request (primary executable boundary)
1401
+ # ---------------------------------------------------------------------------
1402
+
1403
+
1404
+ class HarnessExecutionRequest(BaseModel):
1405
+ """Immutable executable boundary for harness execution.
1406
+
1407
+ This is the primary input contract for ``HarnessRuntime.execute()``.
1408
+ ``stage`` is the provider-local identity admitted by the selected compiled
1409
+ Millforge harness; it does not carry a caller workflow plane, node, route,
1410
+ dispatch identity, or authority. ``request_id`` and ``run_id`` are opaque
1411
+ caller-owned correlation values that Millforge validates and echoes without
1412
+ interpreting them as workflow or terminal authority. Run IDs are checked
1413
+ for consistency and collection-level duplicates are rejected at
1414
+ construction time.
1415
+ """
1416
+
1417
+ model_config = ConfigDict(
1418
+ extra="forbid",
1419
+ frozen=True,
1420
+ hide_input_in_errors=True,
1421
+ )
1422
+
1423
+ request_id: str = Field(description="Opaque caller request correlation value")
1424
+ run_id: str = Field(description="Opaque caller run correlation value")
1425
+ work_item_id: str = Field(description="Active work item identifier")
1426
+ task: HarnessTaskInput = Field(description="Exact bounded task instruction")
1427
+ stage: StageIdentity = Field(
1428
+ description="Provider-local identity of the admitted compiled harness stage"
1429
+ )
1430
+ compiled_harness: CompiledHarnessRef = Field(
1431
+ description="Reference to the compiled harness"
1432
+ )
1433
+ capability_envelope: CapabilityEnvelope = Field(
1434
+ description="Capability grant envelope"
1435
+ )
1436
+ input_artifacts: Tuple[ArtifactRef, ...] = Field(
1437
+ description="Input artifact references"
1438
+ )
1439
+ run_directory: RunDirRef = Field(description="Run directory reference")
1440
+ timeout: TimeoutRef = Field(description="Timeout reference")
1441
+ cancellation: CancellationRef = Field(description="Cancellation reference")
1442
+ secret_refs: Tuple[SecretRef, ...] = Field(
1443
+ description="Secret references (handles only, never values)"
1444
+ )
1445
+ model_profile: ModelProfileRef = Field(description="Model profile reference")
1446
+ selected_output_requirements: tuple[TerminalSelectedOutputRequirement, ...] = Field(
1447
+ default_factory=tuple,
1448
+ max_length=64,
1449
+ description="Terminal-result-scoped selected JSON output requirements",
1450
+ )
1451
+
1452
+ @field_validator("selected_output_requirements")
1453
+ @classmethod
1454
+ def _selected_output_requirements_are_canonical(
1455
+ cls,
1456
+ value: tuple[TerminalSelectedOutputRequirement, ...],
1457
+ ) -> tuple[TerminalSelectedOutputRequirement, ...]:
1458
+ canonical_lookup = _selected_output_requirements_by_terminal_result(value)
1459
+ records = {item.terminal_result: item for item in value}
1460
+ return tuple(records[terminal_result] for terminal_result in canonical_lookup)
1461
+
1462
+ @classmethod
1463
+ def model_validate_json(
1464
+ cls,
1465
+ json_data: str | bytes | bytearray,
1466
+ *,
1467
+ strict: bool | None = None,
1468
+ extra: Any | None = None,
1469
+ context: Any | None = None,
1470
+ by_alias: bool | None = None,
1471
+ by_name: bool | None = None,
1472
+ ) -> Self:
1473
+ """Validate raw request JSON before Pydantic can collapse key pairs."""
1474
+ _validate_harness_request_raw_json(json_data)
1475
+ return super().model_validate_json(
1476
+ json_data,
1477
+ strict=strict,
1478
+ extra=extra,
1479
+ context=context,
1480
+ by_alias=by_alias,
1481
+ by_name=by_name,
1482
+ )
1483
+
1484
+ # ------------------------------------------------------------------
1485
+ # Cross-field validators
1486
+ # ------------------------------------------------------------------
1487
+
1488
+ @model_validator(mode="after")
1489
+ def _check_run_id_consistency(self) -> HarnessExecutionRequest:
1490
+ """HarnessExecutionRequest.run_id must match RunDirRef.run_id."""
1491
+ if self.run_id != self.run_directory.run_id:
1492
+ raise ValueError(
1493
+ f"HarnessExecutionRequest.run_id ({self.run_id!r}) must match "
1494
+ f"RunDirRef.run_id ({self.run_directory.run_id!r})"
1495
+ )
1496
+ return self
1497
+
1498
+ @model_validator(mode="after")
1499
+ def _check_sha256_digest(self) -> HarnessExecutionRequest:
1500
+ """When algorithm is sha256, digest must be exactly 64 lowercase hex chars."""
1501
+ h = self.compiled_harness.expected_hash
1502
+ if h.algorithm == "sha256":
1503
+ if not re.fullmatch(r"[0-9a-f]{64}", h.digest):
1504
+ raise ValueError(
1505
+ f"CompiledHarnessHash digest must be exactly 64 lowercase hex "
1506
+ f"characters when algorithm is 'sha256', got {h.digest!r}"
1507
+ )
1508
+ return self
1509
+
1510
+ @model_validator(mode="after")
1511
+ def _check_duplicate_grant_capabilities(self) -> HarnessExecutionRequest:
1512
+ """Reject duplicate capability identifiers in the grants tuple."""
1513
+ seen: set[str] = set()
1514
+ for grant in self.capability_envelope.grants:
1515
+ if grant.capability_id in seen:
1516
+ raise ValueError(
1517
+ f"Duplicate capability_id {grant.capability_id!r} in CapabilityEnvelope"
1518
+ )
1519
+ seen.add(grant.capability_id)
1520
+ return self
1521
+
1522
+ @model_validator(mode="after")
1523
+ def _check_duplicate_artifact_ids(self) -> HarnessExecutionRequest:
1524
+ """Reject duplicate artifact_id values in input_artifacts."""
1525
+ seen: set[str] = set()
1526
+ for artifact in self.input_artifacts:
1527
+ if artifact.artifact_id in seen:
1528
+ raise ValueError(
1529
+ f"Duplicate artifact_id {artifact.artifact_id!r} in input_artifacts"
1530
+ )
1531
+ seen.add(artifact.artifact_id)
1532
+ return self
1533
+
1534
+ @model_validator(mode="after")
1535
+ def _check_duplicate_secret_ids(self) -> HarnessExecutionRequest:
1536
+ """Reject duplicate secret_id values in secret_refs."""
1537
+ seen: set[str] = set()
1538
+ for secret in self.secret_refs:
1539
+ if secret.secret_id in seen:
1540
+ raise ValueError(
1541
+ f"Duplicate secret_id {secret.secret_id!r} in secret_refs"
1542
+ )
1543
+ seen.add(secret.secret_id)
1544
+ return self
1545
+
1546
+ @model_validator(mode="after")
1547
+ def _check_duplicate_secret_env_vars(self) -> HarnessExecutionRequest:
1548
+ """Reject duplicate env_var values in secret_refs."""
1549
+ seen: set[str] = set()
1550
+ for secret in self.secret_refs:
1551
+ if secret.env_var in seen:
1552
+ raise ValueError(f"Duplicate env_var {secret.env_var!r} in secret_refs")
1553
+ seen.add(secret.env_var)
1554
+ return self
1555
+
1556
+
1557
+ # ---------------------------------------------------------------------------
1558
+ # Model, tool, and bridge-owned request/response models
1559
+ # ---------------------------------------------------------------------------
1560
+
1561
+
1562
+ class ModelCapabilityRequirements(BaseModel):
1563
+ """Exact model capabilities required by the 02C-02D Forge bridge."""
1564
+
1565
+ model_config = ConfigDict(extra="forbid", frozen=True)
1566
+
1567
+ tool_calls: Literal[True] = True
1568
+ parallel_tool_calls: Literal[False] = False
1569
+ structured_output: Literal[False] = False
1570
+ reasoning_controls: Literal[False] = False
1571
+ usage_reporting: Literal[False] = False
1572
+ system_messages: Literal[True] = True
1573
+ tool_result_messages: Literal[True] = True
1574
+
1575
+
1576
+ class SamplingRequest(BaseModel):
1577
+ """Canonical owned sampling controls for model calls."""
1578
+
1579
+ model_config = ConfigDict(extra="forbid", frozen=True)
1580
+
1581
+ temperature: float | None = Field(default=None, ge=0, le=2)
1582
+ top_p: float | None = Field(default=None, ge=0, le=1)
1583
+ presence_penalty: float | None = Field(default=None, ge=-2, le=2)
1584
+ frequency_penalty: float | None = Field(default=None, ge=-2, le=2)
1585
+ seed: int | None = None
1586
+ stop: tuple[str, ...] | None = None
1587
+ reasoning_mode: str | None = None
1588
+ reasoning_effort: str | None = None
1589
+
1590
+ @field_validator("stop")
1591
+ @classmethod
1592
+ def _stop_values_nonblank(
1593
+ cls, value: tuple[str, ...] | None
1594
+ ) -> tuple[str, ...] | None:
1595
+ if value is None:
1596
+ return None
1597
+ for item in value:
1598
+ _nonblank(item, "stop")
1599
+ return value
1600
+
1601
+ @field_validator("reasoning_mode", "reasoning_effort")
1602
+ @classmethod
1603
+ def _optional_strings_nonblank(cls, value: str | None, info: Any) -> str | None:
1604
+ return None if value is None else _nonblank(value, info.field_name)
1605
+
1606
+
1607
+ class SanitizedMetadata(BaseModel):
1608
+ """Bounded metadata that is safe to persist across the public boundary."""
1609
+
1610
+ model_config = ConfigDict(extra="forbid", frozen=True)
1611
+
1612
+ values: Dict[str, SanitizedMetadataValue] = Field(default_factory=dict)
1613
+
1614
+ @field_validator("values")
1615
+ @classmethod
1616
+ def _values_bounded(
1617
+ cls, value: dict[str, SanitizedMetadataValue]
1618
+ ) -> dict[str, SanitizedMetadataValue]:
1619
+ if len(value) > _SANITIZED_METADATA_MAX_ITEMS:
1620
+ raise ValueError("sanitized metadata contains too many items")
1621
+ total_bytes = 0
1622
+ for key, item in value.items():
1623
+ _nonblank(key, "sanitized metadata key")
1624
+ if len(key) > _SANITIZED_METADATA_KEY_MAX_LENGTH:
1625
+ raise ValueError("sanitized metadata key is too long")
1626
+ if isinstance(item, str):
1627
+ if len(item) > _SANITIZED_METADATA_STRING_MAX_LENGTH:
1628
+ raise ValueError("sanitized metadata string value is too long")
1629
+ total_bytes += len(item.encode("utf-8"))
1630
+ else:
1631
+ total_bytes += len(str(item).encode("utf-8"))
1632
+ if total_bytes > _SANITIZED_METADATA_BYTES_MAX_LENGTH:
1633
+ raise ValueError("sanitized metadata payload is too large")
1634
+ return value
1635
+
1636
+
1637
+ class RedactionPolicy(BaseModel):
1638
+ """Single bounded redaction policy for public summaries and diagnostics."""
1639
+
1640
+ model_config = ConfigDict(extra="forbid", frozen=True)
1641
+
1642
+ max_depth: int = Field(
1643
+ default=_REDACTION_DEFAULT_DEPTH, ge=1, le=_REDACTION_MAX_DEPTH
1644
+ )
1645
+ max_collection_items: int = Field(
1646
+ default=_REDACTION_DEFAULT_COLLECTION_ITEMS,
1647
+ ge=1,
1648
+ le=_REDACTION_MAX_COLLECTION_ITEMS,
1649
+ )
1650
+ max_string_length: int = Field(
1651
+ default=_REDACTION_DEFAULT_STRING_LENGTH,
1652
+ ge=1,
1653
+ le=_REDACTION_MAX_STRING_LENGTH,
1654
+ )
1655
+ max_total_bytes: int = Field(
1656
+ default=_REDACTION_DEFAULT_TOTAL_BYTES,
1657
+ ge=1,
1658
+ le=_REDACTION_MAX_TOTAL_BYTES,
1659
+ )
1660
+ replacement: str = "**redacted**"
1661
+ sensitive_field_markers: Tuple[str, ...] = (
1662
+ "authorization",
1663
+ "api-key",
1664
+ "apikey",
1665
+ "token",
1666
+ "secret",
1667
+ "password",
1668
+ "credential",
1669
+ "cookie",
1670
+ "set-cookie",
1671
+ )
1672
+
1673
+ @field_validator("replacement")
1674
+ @classmethod
1675
+ def _replacement_nonblank(cls, value: str) -> str:
1676
+ return _nonblank(value, "replacement")
1677
+
1678
+ @field_validator("sensitive_field_markers")
1679
+ @classmethod
1680
+ def _markers_valid(cls, value: tuple[str, ...]) -> tuple[str, ...]:
1681
+ normalized = tuple(
1682
+ _nonblank(item, "sensitive marker").lower() for item in value
1683
+ )
1684
+ _unique(normalized, "sensitive marker")
1685
+ return normalized
1686
+
1687
+
1688
+ class _RedactionBudget:
1689
+ def __init__(self, limit: int) -> None:
1690
+ self._limit = limit
1691
+ self._used = 0
1692
+
1693
+ def text(self, value: str) -> str:
1694
+ remaining = self._limit - self._used
1695
+ if remaining <= 0:
1696
+ return "[truncated]"
1697
+ encoded = value.encode("utf-8")
1698
+ if len(encoded) <= remaining:
1699
+ self._used += len(encoded)
1700
+ return value
1701
+ truncated = encoded[:remaining].decode("utf-8", errors="ignore")
1702
+ self._used = self._limit
1703
+ return f"{truncated}[truncated]"
1704
+
1705
+
1706
+ def _redact_url(value: str, policy: RedactionPolicy) -> str:
1707
+ try:
1708
+ split = urlsplit(value)
1709
+ except ValueError:
1710
+ return _redact_secret_patterns(value, policy)
1711
+ host = split.hostname or ""
1712
+ if split.port is not None:
1713
+ host = f"{host}:{split.port}"
1714
+ query_parts = parse_qsl(split.query, keep_blank_values=True)
1715
+ query = "&".join(
1716
+ f"{key}={policy.replacement}"
1717
+ if _is_sensitive_field_name(key, policy)
1718
+ else f"{key}={value}"
1719
+ for key, value in query_parts
1720
+ )
1721
+ return urlunsplit(
1722
+ (
1723
+ split.scheme,
1724
+ host,
1725
+ split.path,
1726
+ query,
1727
+ policy.replacement if split.fragment else "",
1728
+ )
1729
+ )
1730
+
1731
+
1732
+ def _redact_secret_patterns(text: str, policy: RedactionPolicy) -> str:
1733
+ text = _SECRET_PATTERNS[0].sub(
1734
+ lambda match: (
1735
+ match.group(0).split("=", 1)[0] + "=" + policy.replacement
1736
+ if match.group(1) != policy.replacement
1737
+ else match.group(0)
1738
+ ),
1739
+ text,
1740
+ )
1741
+ text = _SECRET_PATTERNS[1].sub(
1742
+ lambda match: (
1743
+ match.group(0)
1744
+ if match.group(2) == policy.replacement
1745
+ else f"{match.group(1)}{policy.replacement}"
1746
+ ),
1747
+ text,
1748
+ )
1749
+ for pattern in _SECRET_PATTERNS[2:]:
1750
+ text = pattern.sub(lambda match: f"{match.group(1)}{policy.replacement}", text)
1751
+ return text
1752
+
1753
+
1754
+ def redact_diagnostic_text(
1755
+ value: str,
1756
+ *,
1757
+ policy: RedactionPolicy | None = None,
1758
+ secret_values: tuple[str, ...] = (),
1759
+ ) -> str:
1760
+ """Apply the shared deterministic redaction policy to text."""
1761
+ active_policy = policy or RedactionPolicy()
1762
+ text = value
1763
+ for secret in secret_values:
1764
+ if secret:
1765
+ text = text.replace(secret, active_policy.replacement)
1766
+ text = _URL_PATTERN.sub(
1767
+ lambda match: _redact_url(match.group(0), active_policy), text
1768
+ )
1769
+ text = _redact_secret_patterns(text, active_policy)
1770
+ if len(text) > active_policy.max_string_length:
1771
+ text = f"{text[: active_policy.max_string_length]}[truncated]"
1772
+ return text
1773
+
1774
+
1775
+ def redact_diagnostic_value(
1776
+ value: object,
1777
+ *,
1778
+ policy: RedactionPolicy | None = None,
1779
+ secret_values: tuple[str, ...] = (),
1780
+ ) -> JsonValue:
1781
+ """Return a bounded JSON-safe diagnostic value without arbitrary repr calls."""
1782
+ active_policy = policy or RedactionPolicy()
1783
+ budget = _RedactionBudget(active_policy.max_total_bytes)
1784
+ return _redact_value(
1785
+ value,
1786
+ policy=active_policy,
1787
+ secret_values=secret_values,
1788
+ budget=budget,
1789
+ depth=0,
1790
+ seen=set(),
1791
+ sensitive_key=False,
1792
+ )
1793
+
1794
+
1795
+ def redact_diagnostic_mapping(
1796
+ values: Mapping[str, object],
1797
+ *,
1798
+ policy: RedactionPolicy | None = None,
1799
+ secret_values: tuple[str, ...] = (),
1800
+ ) -> dict[str, JsonValue]:
1801
+ """Redact a diagnostic mapping using one bounded recursive policy."""
1802
+ redacted = redact_diagnostic_value(
1803
+ values,
1804
+ policy=policy,
1805
+ secret_values=secret_values,
1806
+ )
1807
+ return redacted if isinstance(redacted, dict) else {}
1808
+
1809
+
1810
+ def _safe_text(value: object) -> str:
1811
+ if isinstance(value, str):
1812
+ return value
1813
+ if isinstance(value, int | float | bool) or value is None:
1814
+ return str(value)
1815
+ if isinstance(value, Path):
1816
+ return value.as_posix()
1817
+ if isinstance(value, BaseException):
1818
+ parts = [
1819
+ _safe_text(arg)
1820
+ for arg in value.args
1821
+ if isinstance(arg, str | int | float | bool) or arg is None
1822
+ ]
1823
+ detail = ": " + " ".join(parts) if parts else ""
1824
+ return f"{type(value).__name__}{detail}"
1825
+ return f"<{type(value).__module__}.{type(value).__qualname__}>"
1826
+
1827
+
1828
+ def _redact_key(key: object, policy: RedactionPolicy, budget: _RedactionBudget) -> str:
1829
+ text = redact_diagnostic_text(_safe_text(key), policy=policy)
1830
+ return budget.text(text[: policy.max_string_length])
1831
+
1832
+
1833
+ def _redact_value(
1834
+ value: object,
1835
+ *,
1836
+ policy: RedactionPolicy,
1837
+ secret_values: tuple[str, ...],
1838
+ budget: _RedactionBudget,
1839
+ depth: int,
1840
+ seen: set[int],
1841
+ sensitive_key: bool,
1842
+ ) -> JsonValue:
1843
+ if sensitive_key:
1844
+ return budget.text(policy.replacement)
1845
+ if depth >= policy.max_depth:
1846
+ return budget.text("[max_depth]")
1847
+ if isinstance(value, str):
1848
+ return budget.text(
1849
+ redact_diagnostic_text(
1850
+ value,
1851
+ policy=policy,
1852
+ secret_values=secret_values,
1853
+ )
1854
+ )
1855
+ if isinstance(value, int | float | bool) or value is None:
1856
+ return value
1857
+ if isinstance(value, Path | BaseException):
1858
+ return budget.text(
1859
+ redact_diagnostic_text(
1860
+ _safe_text(value),
1861
+ policy=policy,
1862
+ secret_values=secret_values,
1863
+ )
1864
+ )
1865
+ if isinstance(value, Mapping):
1866
+ identity = id(value)
1867
+ if identity in seen:
1868
+ return budget.text("[cycle]")
1869
+ seen.add(identity)
1870
+ result: dict[str, JsonValue] = {}
1871
+ for index, (key, item) in enumerate(value.items()):
1872
+ if index >= policy.max_collection_items:
1873
+ result["[truncated]"] = budget.text("[truncated]")
1874
+ break
1875
+ clean_key = _redact_key(key, policy, budget)
1876
+ lowered = clean_key.lower()
1877
+ child_sensitive = _is_sensitive_field_name(lowered, policy)
1878
+ result[clean_key] = _redact_value(
1879
+ item,
1880
+ policy=policy,
1881
+ secret_values=secret_values,
1882
+ budget=budget,
1883
+ depth=depth + 1,
1884
+ seen=seen,
1885
+ sensitive_key=child_sensitive,
1886
+ )
1887
+ seen.remove(identity)
1888
+ return result
1889
+ if isinstance(value, tuple | list | set | frozenset):
1890
+ identity = id(value)
1891
+ if identity in seen:
1892
+ return budget.text("[cycle]")
1893
+ seen.add(identity)
1894
+ sequence_result: list[JsonValue] = [
1895
+ _redact_value(
1896
+ item,
1897
+ policy=policy,
1898
+ secret_values=secret_values,
1899
+ budget=budget,
1900
+ depth=depth + 1,
1901
+ seen=seen,
1902
+ sensitive_key=False,
1903
+ )
1904
+ for index, item in enumerate(value)
1905
+ if index < policy.max_collection_items
1906
+ ]
1907
+ if len(value) > policy.max_collection_items:
1908
+ sequence_result.append(budget.text("[truncated]"))
1909
+ seen.remove(identity)
1910
+ return sequence_result
1911
+ return budget.text(
1912
+ redact_diagnostic_text(
1913
+ _safe_text(value),
1914
+ policy=policy,
1915
+ secret_values=secret_values,
1916
+ )
1917
+ )
1918
+
1919
+
1920
+ class ParsedToolArguments(BaseModel):
1921
+ """Parsed JSON object arguments for a model-requested tool call."""
1922
+
1923
+ model_config = ConfigDict(extra="forbid", frozen=True)
1924
+
1925
+ kind: Literal["parsed"] = "parsed"
1926
+ value: JsonObject = Field(default_factory=dict)
1927
+
1928
+ @property
1929
+ def values(self) -> JsonObject:
1930
+ """Compatibility accessor; ``value`` is the serialized contract field."""
1931
+ return self.value
1932
+
1933
+
1934
+ class InvalidToolArguments(BaseModel):
1935
+ """A malformed tool-argument payload for the public model bridge."""
1936
+
1937
+ model_config = ConfigDict(extra="forbid", frozen=True)
1938
+
1939
+ kind: Literal["invalid"] = "invalid"
1940
+ raw: JsonValue
1941
+ error_code: str
1942
+
1943
+ @field_validator("error_code")
1944
+ @classmethod
1945
+ def _error_code_nonblank(cls, value: str) -> str:
1946
+ return _nonblank(value, "error_code")
1947
+
1948
+
1949
+ ToolArguments = Annotated[
1950
+ ParsedToolArguments | InvalidToolArguments, Field(discriminator="kind")
1951
+ ]
1952
+
1953
+
1954
+ class ModelToolCall(BaseModel):
1955
+ """Owned typed representation of an assistant-requested tool call."""
1956
+
1957
+ model_config = ConfigDict(extra="forbid", frozen=True)
1958
+
1959
+ call_id: str
1960
+ name: str
1961
+ arguments: ToolArguments
1962
+
1963
+ @field_validator("arguments", mode="before")
1964
+ @classmethod
1965
+ def _coerce_arguments(cls, value: Any) -> Any:
1966
+ if isinstance(value, dict) and not (
1967
+ value.get("kind") in {"parsed", "invalid"} or set(value) == {"value"}
1968
+ ):
1969
+ return ParsedToolArguments(value=value)
1970
+ return value
1971
+
1972
+ @property
1973
+ def id(self) -> str:
1974
+ """Compatibility accessor; ``call_id`` is the serialized contract field."""
1975
+ return self.call_id
1976
+
1977
+ @field_validator("call_id", "name")
1978
+ @classmethod
1979
+ def _strings_nonblank(cls, value: str, info: Any) -> str:
1980
+ return _nonblank(value, info.field_name)
1981
+
1982
+
1983
+ class SystemMessage(BaseModel):
1984
+ """System instructions sent through the model bridge."""
1985
+
1986
+ model_config = ConfigDict(extra="forbid", frozen=True)
1987
+
1988
+ role: Literal["system"] = "system"
1989
+ content: str
1990
+
1991
+ @property
1992
+ def kind(self) -> str:
1993
+ """Compatibility accessor; ``role`` is the serialized discriminator."""
1994
+ return self.role
1995
+
1996
+ @field_validator("content")
1997
+ @classmethod
1998
+ def _content_nonblank(cls, value: str) -> str:
1999
+ return _nonblank(value, "content")
2000
+
2001
+
2002
+ class UserMessage(BaseModel):
2003
+ """User message sent through the model bridge."""
2004
+
2005
+ model_config = ConfigDict(extra="forbid", frozen=True)
2006
+
2007
+ role: Literal["user"] = "user"
2008
+ content: str
2009
+
2010
+ @property
2011
+ def kind(self) -> str:
2012
+ """Compatibility accessor; ``role`` is the serialized discriminator."""
2013
+ return self.role
2014
+
2015
+ @field_validator("content")
2016
+ @classmethod
2017
+ def _content_nonblank(cls, value: str) -> str:
2018
+ return _nonblank(value, "content")
2019
+
2020
+
2021
+ class AssistantMessage(BaseModel):
2022
+ """Assistant response message containing text and/or tool calls."""
2023
+
2024
+ model_config = ConfigDict(extra="forbid", frozen=True)
2025
+
2026
+ role: Literal["assistant"] = "assistant"
2027
+ content: str | None = None
2028
+ tool_calls: Tuple[ModelToolCall, ...] = Field(default_factory=tuple)
2029
+ reasoning_content: str | None = Field(default=None, repr=False)
2030
+
2031
+ @property
2032
+ def kind(self) -> str:
2033
+ """Compatibility accessor; ``role`` is the serialized discriminator."""
2034
+ return self.role
2035
+
2036
+ @field_validator("content", "reasoning_content")
2037
+ @classmethod
2038
+ def _content_nonblank(cls, value: str | None, info: Any) -> str | None:
2039
+ return None if value is None else _nonblank(value, info.field_name)
2040
+
2041
+ @model_serializer(mode="wrap")
2042
+ def _omit_absent_reasoning_content(self, handler: Any) -> dict[str, Any]:
2043
+ payload = handler(self)
2044
+ if self.reasoning_content is None:
2045
+ payload.pop("reasoning_content", None)
2046
+ return payload
2047
+
2048
+ @model_validator(mode="after")
2049
+ def _assistant_has_content_or_tools(self) -> AssistantMessage:
2050
+ _unique(tuple(call.call_id for call in self.tool_calls), "assistant call_id")
2051
+ if self.content is None and not self.tool_calls:
2052
+ raise ValueError("assistant message requires content or tool_calls")
2053
+ return self
2054
+
2055
+
2056
+ class ToolResultMessage(BaseModel):
2057
+ """Model-visible result for a prior assistant tool call."""
2058
+
2059
+ model_config = ConfigDict(extra="forbid", frozen=True)
2060
+
2061
+ role: Literal["tool"] = "tool"
2062
+ tool_call_id: str
2063
+ tool_name: str
2064
+ content: str
2065
+
2066
+ @property
2067
+ def kind(self) -> str:
2068
+ """Compatibility accessor; ``role`` is the serialized discriminator."""
2069
+ return "tool_result"
2070
+
2071
+ @field_validator("tool_call_id", "tool_name", "content")
2072
+ @classmethod
2073
+ def _strings_nonblank(cls, value: str, info: Any) -> str:
2074
+ return _nonblank(value, info.field_name)
2075
+
2076
+
2077
+ ModelMessage = Annotated[
2078
+ SystemMessage | UserMessage | AssistantMessage | ToolResultMessage,
2079
+ Field(discriminator="role"),
2080
+ ]
2081
+
2082
+
2083
+ class ModelToolDefinition(BaseModel):
2084
+ """Owned model-visible tool definition."""
2085
+
2086
+ model_config = ConfigDict(extra="forbid", frozen=True)
2087
+
2088
+ name: str
2089
+ description: str
2090
+ input_schema: JsonObject
2091
+
2092
+ @field_validator("name", "description")
2093
+ @classmethod
2094
+ def _strings_nonblank(cls, value: str, info: Any) -> str:
2095
+ return _nonblank(value, info.field_name)
2096
+
2097
+
2098
+ class ModelCompletionRequest(BaseModel):
2099
+ """Immutable validated model inference request."""
2100
+
2101
+ model_config = ConfigDict(extra="forbid", frozen=True)
2102
+
2103
+ request_id: str
2104
+ run_id: str
2105
+ model_profile_id: str
2106
+ messages: Tuple[ModelMessage, ...] = Field(description="Typed chat messages")
2107
+ tools: Tuple[ModelToolDefinition, ...] = Field(
2108
+ default_factory=tuple, description="Available tool definitions"
2109
+ )
2110
+ required_capabilities: ModelCapabilityRequirements = Field(
2111
+ default_factory=ModelCapabilityRequirements
2112
+ )
2113
+ sampling_overrides: SamplingRequest = Field(default_factory=SamplingRequest)
2114
+ maximum_output_tokens_override: int | None = Field(default=None, gt=0)
2115
+ request_options: JsonObject = Field(default_factory=dict)
2116
+ deadline: Deadline
2117
+ cancellation: CancellationRef
2118
+ secret_refs: Tuple[SecretRef, ...] = Field(default_factory=tuple)
2119
+
2120
+ @property
2121
+ def model(self) -> str:
2122
+ """Compatibility accessor; ``model_profile_id`` is the contract field."""
2123
+ return self.model_profile_id
2124
+
2125
+ @field_validator("request_id", "run_id", "model_profile_id")
2126
+ @classmethod
2127
+ def _strings_nonblank(cls, value: str, info: Any) -> str:
2128
+ return _nonblank(value, info.field_name)
2129
+
2130
+ @model_validator(mode="after")
2131
+ def _request_invariants(self) -> ModelCompletionRequest:
2132
+ protected_options = {
2133
+ "model",
2134
+ "messages",
2135
+ "tools",
2136
+ "stream",
2137
+ "endpoint",
2138
+ "authentication",
2139
+ "timeout",
2140
+ "headers",
2141
+ "host",
2142
+ "content_type",
2143
+ "user_agent",
2144
+ "max_tokens",
2145
+ "maximum_output_tokens",
2146
+ "temperature",
2147
+ "top_p",
2148
+ "presence_penalty",
2149
+ "frequency_penalty",
2150
+ "seed",
2151
+ "stop",
2152
+ }
2153
+ for option_name in self.request_options:
2154
+ if option_name in protected_options:
2155
+ raise ValueError(f"request option {option_name!r} is protected")
2156
+ _unique(tuple(tool.name for tool in self.tools), "tool name")
2157
+ _unique(tuple(secret.secret_id for secret in self.secret_refs), "secret_id")
2158
+ pending_tool_calls: dict[str, str] = {}
2159
+ answered_tool_call_ids: set[str] = set()
2160
+ for message in self.messages:
2161
+ if isinstance(message, AssistantMessage):
2162
+ for call in message.tool_calls:
2163
+ if (
2164
+ call.call_id in pending_tool_calls
2165
+ or call.call_id in answered_tool_call_ids
2166
+ ):
2167
+ raise ValueError("assistant tool-call IDs must be unique")
2168
+ pending_tool_calls[call.call_id] = call.name
2169
+ elif isinstance(message, ToolResultMessage):
2170
+ expected_tool_name = pending_tool_calls.get(message.tool_call_id)
2171
+ if expected_tool_name is None:
2172
+ raise ValueError("tool-result message has no matching tool call")
2173
+ if message.tool_name != expected_tool_name:
2174
+ raise ValueError("tool-result message tool_name does not match")
2175
+ if message.tool_call_id in answered_tool_call_ids:
2176
+ raise ValueError("tool-result message duplicates a tool call")
2177
+ answered_tool_call_ids.add(message.tool_call_id)
2178
+ return self
2179
+
2180
+
2181
+ class UsageMetadata(BaseModel):
2182
+ """Token usage metadata for a model request/response pair."""
2183
+
2184
+ model_config = ConfigDict(extra="forbid", frozen=True)
2185
+
2186
+ model_calls: int = Field(ge=0, description="Number of model calls made")
2187
+ tool_calls: int = Field(ge=0, description="Number of tool calls made")
2188
+ token_usage: TokenUsage | None = Field(
2189
+ default=None, description="Detailed token usage breakdown"
2190
+ )
2191
+
2192
+
2193
+ class ModelCompletionResponse(BaseModel):
2194
+ """Immutable validated model inference response."""
2195
+
2196
+ model_config = ConfigDict(extra="forbid", frozen=True)
2197
+
2198
+ provider_request_id: str | None = None
2199
+ model_id: str
2200
+ message: AssistantMessage
2201
+ finish_reason: Literal[
2202
+ "stop",
2203
+ "tool_calls",
2204
+ "length",
2205
+ "content_filter",
2206
+ "cancelled",
2207
+ "unknown",
2208
+ ]
2209
+ usage: TokenUsage | None = Field(default=None, description="Token usage metadata")
2210
+ provider_metadata: SanitizedMetadata | None = None
2211
+
2212
+ @property
2213
+ def model(self) -> str:
2214
+ """Compatibility accessor; ``model_id`` is the contract field."""
2215
+ return self.model_id
2216
+
2217
+ @property
2218
+ def content(self) -> str | None:
2219
+ """Compatibility accessor for the assistant message content."""
2220
+ return self.message.content
2221
+
2222
+ @property
2223
+ def tool_calls(self) -> tuple[ModelToolCall, ...]:
2224
+ """Compatibility accessor for the assistant message tool calls."""
2225
+ return self.message.tool_calls
2226
+
2227
+ @field_validator("provider_request_id", "model_id")
2228
+ @classmethod
2229
+ def _strings_nonblank(cls, value: str | None, info: Any) -> str | None:
2230
+ return None if value is None else _nonblank(value, info.field_name)
2231
+
2232
+ @model_validator(mode="after")
2233
+ def _finish_reason_matches_message(self) -> ModelCompletionResponse:
2234
+ if self.message.tool_calls and self.finish_reason != "tool_calls":
2235
+ raise ValueError("tool call responses require finish_reason='tool_calls'")
2236
+ return self
2237
+
2238
+
2239
+ class ValidatedToolCall(BaseModel):
2240
+ """Immutable validated tool call from a model response."""
2241
+
2242
+ model_config = ConfigDict(extra="forbid", frozen=True)
2243
+
2244
+ call_id: str = Field(description="Unique tool call identifier")
2245
+ node_id: str = Field(description="Compiled node identifier")
2246
+ binding: ToolBindingRef = Field(description="Resolved tool binding")
2247
+ arguments: JsonObject = Field(description="Canonical JSON object arguments")
2248
+
2249
+ @property
2250
+ def id(self) -> str:
2251
+ """Compatibility accessor; ``call_id`` is the serialized contract field."""
2252
+ return self.call_id
2253
+
2254
+ @property
2255
+ def name(self) -> str:
2256
+ """Compatibility accessor for legacy fakes; not a serialized field."""
2257
+ return self.binding.tool_id
2258
+
2259
+ @field_validator("call_id", "node_id")
2260
+ @classmethod
2261
+ def _strings_nonblank(cls, value: str, info: Any) -> str:
2262
+ return _nonblank(value, info.field_name)
2263
+
2264
+
2265
+ class ToolExecutionResult(BaseModel):
2266
+ """Immutable validated tool execution result."""
2267
+
2268
+ model_config = ConfigDict(extra="forbid", frozen=True)
2269
+
2270
+ call_id: str = Field(description="Identifier of the originating tool call")
2271
+ status: ToolExecutionStatus = Field(description="Closed tool execution status")
2272
+ summary: str = Field(description="Bounded model-visible summary")
2273
+ structured_data: JsonValue = None
2274
+ artifact_refs: Tuple[ArtifactRef, ...] = Field(default_factory=tuple)
2275
+ error_code: str | None = Field(
2276
+ default=None, description="Stable error code when execution failed"
2277
+ )
2278
+ retryable: bool = Field(
2279
+ default=False, description="Whether retrying this tool call is safe"
2280
+ )
2281
+ side_effect_class: SideEffectClass
2282
+ idempotency: IdempotencyClass
2283
+ side_effect_certainty: SideEffectCertainty
2284
+ side_effect_record: SideEffectRecord | None = Field(
2285
+ default=None,
2286
+ description="Typed side-effect detail when certainty needs explanation",
2287
+ )
2288
+ input_sha256: str
2289
+ output_sha256: str | None = Field(
2290
+ default=None, description="SHA-256 hash of the serialized safe output"
2291
+ )
2292
+ timing: TimingMetadata = Field(description="Canonical timing metadata")
2293
+
2294
+ @property
2295
+ def duration_ms(self) -> float:
2296
+ """Compatibility accessor; ``timing`` is the serialized contract field."""
2297
+ return self.timing.duration_ms
2298
+
2299
+ @field_validator("call_id", "summary")
2300
+ @classmethod
2301
+ def _strings_nonblank(cls, value: str, info: Any) -> str:
2302
+ return _nonblank(value, info.field_name)
2303
+
2304
+ @field_validator("error_code")
2305
+ @classmethod
2306
+ def _error_code_nonblank(cls, value: str | None) -> str | None:
2307
+ return None if value is None else _nonblank(value, "error_code")
2308
+
2309
+ @field_validator("input_sha256")
2310
+ @classmethod
2311
+ def _input_sha256_valid(cls, value: str) -> str:
2312
+ return _validate_sha256(value, "input_sha256")
2313
+
2314
+ @field_validator("output_sha256")
2315
+ @classmethod
2316
+ def _output_sha256_valid(cls, value: str | None) -> str | None:
2317
+ return None if value is None else _validate_sha256(value, "output_sha256")
2318
+
2319
+ @model_validator(mode="after")
2320
+ def _result_consistency(self) -> ToolExecutionResult:
2321
+ if self.status == ToolExecutionStatus.SUCCESS:
2322
+ if self.error_code is not None:
2323
+ raise ValueError("successful tool results must not include error_code")
2324
+ if self.retryable:
2325
+ raise ValueError("successful tool results must not be retryable")
2326
+ else:
2327
+ if self.error_code is None:
2328
+ raise ValueError("failed tool results require error_code")
2329
+ if (
2330
+ self.side_effect_certainty == SideEffectCertainty.COMPLETION_UNKNOWN
2331
+ and self.idempotency
2332
+ in {IdempotencyClass.NON_IDEMPOTENT, IdempotencyClass.UNKNOWN}
2333
+ and self.retryable
2334
+ ):
2335
+ raise ValueError(
2336
+ "completion_unknown side effects are not retryable for "
2337
+ "non-idempotent or unknown-idempotency tool work"
2338
+ )
2339
+ if self.side_effect_record is not None:
2340
+ if self.side_effect_record.certainty != self.side_effect_certainty:
2341
+ raise ValueError(
2342
+ "side_effect_record certainty must match side_effect_certainty"
2343
+ )
2344
+ if self.side_effect_record.retry_allowed != self.retryable:
2345
+ raise ValueError(
2346
+ "side_effect_record retry_allowed must match retryable"
2347
+ )
2348
+ return self
2349
+
2350
+
2351
+ class SideEffectRecord(BaseModel):
2352
+ """Typed side-effect detail for uncertain, rolled back, or absent tool effects."""
2353
+
2354
+ model_config = ConfigDict(extra="forbid", frozen=True)
2355
+
2356
+ certainty: SideEffectCertainty
2357
+ detail_code: str
2358
+ summary: str
2359
+ retry_allowed: bool
2360
+
2361
+ @field_validator("detail_code", "summary")
2362
+ @classmethod
2363
+ def _strings_nonblank(cls, value: str, info: Any) -> str:
2364
+ return _nonblank(value, info.field_name)
2365
+
2366
+
2367
+ # ---------------------------------------------------------------------------
2368
+ # Session models (mutable working models)
2369
+ # ---------------------------------------------------------------------------
2370
+
2371
+
2372
+ class GuardedSessionRequest(BaseModel):
2373
+ """Request wrapped in a guarded session.
2374
+
2375
+ **Immutable** — once constructed, the session request is not modified.
2376
+ Includes a deadline for session-level time bounds.
2377
+ """
2378
+
2379
+ model_config = ConfigDict(extra="forbid", frozen=True)
2380
+
2381
+ session_id: str = Field(description="Unique session identifier")
2382
+ execution_request: HarnessExecutionRequest = Field(
2383
+ description="The harness execution request"
2384
+ )
2385
+ deadline: Deadline = Field(description="Deadline for the guarded session")
2386
+ tool_execution_context: ToolExecutionContext | None = Field(
2387
+ default=None,
2388
+ description="Runtime-owned context passed through to tool execution",
2389
+ )
2390
+
2391
+
2392
+ class GuardedSessionResult(BaseModel):
2393
+ """Result from a guarded session.
2394
+
2395
+ **Immutable** — once produced, the result is not modified.
2396
+ Includes structured event and tool trace records alongside
2397
+ the existing terminal intent, usage, timing, and diagnostic fields.
2398
+ """
2399
+
2400
+ model_config = ConfigDict(extra="forbid", frozen=True)
2401
+
2402
+ session_id: str = Field(description="Unique session identifier")
2403
+ status: GuardedSessionStatus = Field(description="Session status")
2404
+ terminal_intent: TerminalIntent | None = Field(
2405
+ default=None, description="Terminal intent if session completed"
2406
+ )
2407
+ artifact_refs: Tuple[ArtifactRef, ...] = Field(
2408
+ default_factory=tuple, description="Artifact references produced"
2409
+ )
2410
+ usage: UsageMetadata | None = Field(
2411
+ default=None, description="Token usage metadata"
2412
+ )
2413
+ timing: TimingMetadata | None = Field(default=None, description="Timing metadata")
2414
+ diagnostic: DiagnosticMetadata | None = Field(
2415
+ default=None, description="Diagnostic metadata"
2416
+ )
2417
+ events: Tuple[SessionEvent, ...] = Field(
2418
+ default_factory=tuple, description="Session events recorded"
2419
+ )
2420
+ tool_trace: Tuple[ToolTraceRecord, ...] = Field(
2421
+ default_factory=tuple, description="Tool trace records"
2422
+ )
2423
+
2424
+
2425
+ # ---------------------------------------------------------------------------
2426
+ # Intent and result models (immutable snapshots)
2427
+ # ---------------------------------------------------------------------------
2428
+
2429
+
2430
+ class TerminalIntent(BaseModel):
2431
+ """Terminal intent expressing a provider-local stage disposition.
2432
+
2433
+ Extended for 02B shape — includes request identity, stage
2434
+ identity, terminal node, closed disposition, summary, and
2435
+ artifact references. Immutable snapshot — once emitted, the
2436
+ intent is not modified. Correlation values are echoed from the execution
2437
+ request and do not grant caller workflow or terminal authority.
2438
+ """
2439
+
2440
+ model_config = ConfigDict(extra="forbid", frozen=True)
2441
+
2442
+ request_id: str = Field(description="Unique request identifier")
2443
+ run_id: str = Field(description="Run this request belongs to")
2444
+ stage: StageIdentity = Field(description="Stage identity")
2445
+ terminal_node_id: str = Field(description="Terminal node identifier")
2446
+ terminal_result: str = Field(description="Terminal result string")
2447
+ disposition: Literal["success", "blocked", "rejected", "escalated"] = Field(
2448
+ description="Terminal disposition (closed)"
2449
+ )
2450
+ summary: str = Field(description="Human-readable summary")
2451
+ artifact_refs: Tuple[ArtifactRef, ...] = Field(
2452
+ default_factory=tuple, description="Artifact references"
2453
+ )
2454
+ selected_output: SelectedOutput | None = Field(
2455
+ default=None,
2456
+ exclude_if=lambda value: value is None,
2457
+ description="Admitted selected JSON output, explicitly present or absent",
2458
+ )
2459
+ selected_output_schema_sha256: str | None = Field(
2460
+ default=None,
2461
+ exclude_if=lambda value: value is None,
2462
+ description="Digest of the invocation-local selected schema authority",
2463
+ )
2464
+
2465
+ @field_validator("request_id")
2466
+ @classmethod
2467
+ def _request_id_must_be_non_empty(cls, v: str) -> str:
2468
+ if not v.strip():
2469
+ raise ValueError("request_id must be a non-empty string")
2470
+ return v
2471
+
2472
+ @field_validator("terminal_node_id")
2473
+ @classmethod
2474
+ def _terminal_node_id_must_be_non_empty(cls, v: str) -> str:
2475
+ if not v.strip():
2476
+ raise ValueError("terminal_node_id must be a non-empty string")
2477
+ return v
2478
+
2479
+ @field_validator("terminal_result")
2480
+ @classmethod
2481
+ def _terminal_result_must_be_non_empty(cls, v: str) -> str:
2482
+ if not v.strip():
2483
+ raise ValueError("terminal_result must be a non-empty string")
2484
+ return v
2485
+
2486
+ @field_validator("summary")
2487
+ @classmethod
2488
+ def _summary_must_be_non_empty(cls, v: str) -> str:
2489
+ if not v.strip():
2490
+ raise ValueError("summary must be a non-empty string")
2491
+ return v
2492
+
2493
+ @field_validator("selected_output_schema_sha256")
2494
+ @classmethod
2495
+ def _selected_schema_digest_valid(cls, value: str | None) -> str | None:
2496
+ if value is None:
2497
+ return None
2498
+ return _validate_sha256(value, "selected_output_schema_sha256")
2499
+
2500
+ @model_validator(mode="after")
2501
+ def _selected_output_authority_is_paired(self) -> TerminalIntent:
2502
+ if (self.selected_output is None) != (
2503
+ self.selected_output_schema_sha256 is None
2504
+ ):
2505
+ raise ValueError(
2506
+ "selected_output and selected_output_schema_sha256 must be paired"
2507
+ )
2508
+ return self
2509
+
2510
+
2511
+ class HarnessExecutionResult(BaseModel):
2512
+ """Result of a harness execution.
2513
+
2514
+ Immutable snapshot with semantic result classification and
2515
+ structured metadata — 02B semantic shape replaces legacy
2516
+ process-shaped fields. Its stage and terminal intent remain provider-local;
2517
+ request and run identifiers remain opaque caller correlation values.
2518
+ """
2519
+
2520
+ model_config = ConfigDict(extra="forbid", frozen=True)
2521
+
2522
+ status: ExecutionStatus = Field(description="Execution status")
2523
+ result_class: ExecutionResultClass = Field(description="Result classification")
2524
+ request_id: str = Field(description="Unique request identifier")
2525
+ run_id: str = Field(description="Run this request belongs to")
2526
+ stage: StageIdentity = Field(description="Stage identity")
2527
+ terminal_intent: TerminalIntent | None = Field(
2528
+ default=None, description="Terminal intent if session completed"
2529
+ )
2530
+ artifact_refs: Tuple[ArtifactRef, ...] = Field(
2531
+ default_factory=tuple, description="Artifact references produced"
2532
+ )
2533
+ compiled_harness: CompiledHarnessRef = Field(
2534
+ description="Reference to the compiled harness"
2535
+ )
2536
+ usage: UsageMetadata | None = Field(
2537
+ default=None, description="Token usage metadata"
2538
+ )
2539
+ timing: TimingMetadata = Field(description="Timing metadata")
2540
+ diagnostic: DiagnosticMetadata | None = Field(
2541
+ default=None, description="Diagnostic metadata"
2542
+ )
2543
+ terminal_certainty: TerminalCertainty = Field(
2544
+ default=TerminalCertainty.NOT_APPLICABLE,
2545
+ description="Certainty of terminal-result commit ordering",
2546
+ )
2547
+ selected_output: SelectedOutput | None = Field(
2548
+ default=None,
2549
+ exclude_if=lambda value: value is None,
2550
+ description="Admitted selected JSON output, explicitly present or absent",
2551
+ )
2552
+ selected_output_schema_sha256: str | None = Field(
2553
+ default=None,
2554
+ exclude_if=lambda value: value is None,
2555
+ description="Digest of the invocation-local selected schema authority",
2556
+ )
2557
+
2558
+ @field_validator("selected_output_schema_sha256")
2559
+ @classmethod
2560
+ def _selected_schema_digest_valid(cls, value: str | None) -> str | None:
2561
+ if value is None:
2562
+ return None
2563
+ return _validate_sha256(value, "selected_output_schema_sha256")
2564
+
2565
+ @model_validator(mode="after")
2566
+ def _check_result_class_invariants(self) -> HarnessExecutionResult:
2567
+ if (self.selected_output is None) != (
2568
+ self.selected_output_schema_sha256 is None
2569
+ ):
2570
+ raise ValueError(
2571
+ "selected_output and selected_output_schema_sha256 must be paired"
2572
+ )
2573
+ completed_classes = {
2574
+ ExecutionResultClass.DOMAIN_TERMINAL,
2575
+ ExecutionResultClass.DOMAIN_REJECTED,
2576
+ }
2577
+ if self.status == ExecutionStatus.COMPLETED:
2578
+ if self.result_class not in completed_classes:
2579
+ raise ValueError(
2580
+ "status=completed is only valid for domain_terminal "
2581
+ "or domain_rejected"
2582
+ )
2583
+ elif self.result_class in completed_classes:
2584
+ raise ValueError("domain result classes must use status=completed")
2585
+
2586
+ if self.terminal_intent is not None:
2587
+ if (
2588
+ self.status != ExecutionStatus.COMPLETED
2589
+ or self.result_class not in completed_classes
2590
+ ):
2591
+ raise ValueError(
2592
+ "terminal_intent is only valid for completed domain results"
2593
+ )
2594
+ if self.terminal_intent.request_id != self.request_id:
2595
+ raise ValueError("terminal_intent.request_id must match result")
2596
+ if self.terminal_intent.run_id != self.run_id:
2597
+ raise ValueError("terminal_intent.run_id must match result")
2598
+ if self.terminal_intent.stage != self.stage:
2599
+ raise ValueError("terminal_intent.stage must match result")
2600
+ if self.terminal_intent.selected_output != self.selected_output:
2601
+ raise ValueError("terminal_intent.selected_output must match result")
2602
+ if (
2603
+ self.terminal_intent.selected_output_schema_sha256
2604
+ != self.selected_output_schema_sha256
2605
+ ):
2606
+ raise ValueError(
2607
+ "terminal_intent selected schema digest must match result"
2608
+ )
2609
+ elif (
2610
+ self.terminal_certainty == TerminalCertainty.COMMITTED
2611
+ and self.result_class
2612
+ not in {
2613
+ ExecutionResultClass.DOMAIN_TERMINAL,
2614
+ ExecutionResultClass.DOMAIN_REJECTED,
2615
+ }
2616
+ ):
2617
+ raise ValueError("committed terminal_certainty requires a domain result")
2618
+ if self.selected_output is not None and self.terminal_intent is None:
2619
+ raise ValueError(
2620
+ "selected_output authority requires a matching terminal_intent"
2621
+ )
2622
+ return self
2623
+
2624
+
2625
+ # ---------------------------------------------------------------------------
2626
+ # Timing and diagnostic models (immutable snapshots)
2627
+ # ---------------------------------------------------------------------------
2628
+
2629
+
2630
+ class TimingMetadata(BaseModel):
2631
+ """Timing and duration metadata.
2632
+
2633
+ All fields are required — ``completed_at`` is now mandatory.
2634
+ """
2635
+
2636
+ model_config = ConfigDict(extra="forbid", frozen=True)
2637
+
2638
+ started_at: str = Field(description="ISO-8601 start timestamp")
2639
+ completed_at: str = Field(description="ISO-8601 completion timestamp")
2640
+ duration_ms: float = Field(ge=0, description="Duration in milliseconds")
2641
+
2642
+
2643
+ class DiagnosticMetadata(BaseModel):
2644
+ """Immutable diagnostic metadata with structured field-level diagnostics."""
2645
+
2646
+ model_config = ConfigDict(extra="forbid", frozen=True)
2647
+
2648
+ error_code: str = Field(description="Top-level error code identifier")
2649
+ category: Literal[
2650
+ "binding",
2651
+ "compiled_harness",
2652
+ "backend",
2653
+ "model",
2654
+ "tool",
2655
+ "budget",
2656
+ "timeout",
2657
+ "cancellation",
2658
+ "artifact",
2659
+ "internal",
2660
+ ] = Field(description="Closed diagnostic category")
2661
+ message: str = Field(description="Human-readable diagnostic message")
2662
+ retryable: bool = Field(description="Whether retrying may resolve this diagnostic")
2663
+ origin: str | TimeoutOrigin = Field(description="Failure origin or subsystem")
2664
+ fields: tuple[DiagnosticField, ...] = Field(
2665
+ default_factory=tuple,
2666
+ description="Tuple of bounded scalar diagnostic entries",
2667
+ )
2668
+
2669
+ @model_validator(mode="after")
2670
+ def _field_keys_unique(self) -> DiagnosticMetadata:
2671
+ keys = [field.key for field in self.fields]
2672
+ if len(set(keys)) != len(keys):
2673
+ raise ValueError("Diagnostic field keys must be unique")
2674
+ return self
2675
+
2676
+
2677
+ class TerminalResultArtifact(BaseModel):
2678
+ """Validated ``terminal_result.json`` artifact payload."""
2679
+
2680
+ model_config = ConfigDict(extra="forbid", frozen=True)
2681
+
2682
+ schema_version: Literal["1.0"]
2683
+ request_id: str
2684
+ run_id: str
2685
+ stage: StageIdentity
2686
+ terminal_result: str
2687
+ result_class: Literal[
2688
+ ExecutionResultClass.DOMAIN_TERMINAL,
2689
+ ExecutionResultClass.DOMAIN_REJECTED,
2690
+ ]
2691
+ summary_artifact_paths: Tuple[str, ...] = Field(default_factory=tuple)
2692
+ compiled_harness_sha256: str
2693
+ terminal_certainty: TerminalCertainty = TerminalCertainty.COMMITTED
2694
+
2695
+ @field_validator("request_id", "run_id", "terminal_result")
2696
+ @classmethod
2697
+ def _strings_nonblank(cls, value: str, info: Any) -> str:
2698
+ if not value.strip():
2699
+ raise ValueError(f"{info.field_name} must be a non-empty string")
2700
+ return value
2701
+
2702
+ @field_validator("summary_artifact_paths")
2703
+ @classmethod
2704
+ def _summary_paths_relative(cls, value: Tuple[str, ...]) -> Tuple[str, ...]:
2705
+ seen: set[str] = set()
2706
+ for item in value:
2707
+ if not item.strip():
2708
+ raise ValueError("summary_artifact_paths values must be non-empty")
2709
+ path = Path(item)
2710
+ if path.is_absolute() or ".." in path.parts:
2711
+ raise ValueError(
2712
+ "summary_artifact_paths values must be safe relative paths"
2713
+ )
2714
+ if item in seen:
2715
+ raise ValueError("summary_artifact_paths values must be unique")
2716
+ seen.add(item)
2717
+ return value
2718
+
2719
+ @field_validator("compiled_harness_sha256")
2720
+ @classmethod
2721
+ def _compiled_harness_sha256_valid(cls, value: str) -> str:
2722
+ if not re.fullmatch(r"[0-9a-f]{64}", value):
2723
+ raise ValueError(
2724
+ "compiled_harness_sha256 must be exactly 64 lowercase hex characters"
2725
+ )
2726
+ return value
2727
+
2728
+
2729
+ class ExecutionSummaryArtifact(BaseModel):
2730
+ """Validated ``execution_summary.json`` artifact payload."""
2731
+
2732
+ model_config = ConfigDict(extra="forbid", frozen=True)
2733
+
2734
+ schema_version: Literal["1.0"]
2735
+ request_id: str
2736
+ run_id: str
2737
+ stage: StageIdentity
2738
+ status: ExecutionStatus
2739
+ result_class: ExecutionResultClass
2740
+ diagnostic_error_code: str | None = None
2741
+ terminal_certainty: TerminalCertainty = TerminalCertainty.NOT_APPLICABLE
2742
+
2743
+ @field_validator("request_id", "run_id", "diagnostic_error_code")
2744
+ @classmethod
2745
+ def _optional_strings_nonblank(cls, value: str | None, info: Any) -> str | None:
2746
+ if value is not None and not value.strip():
2747
+ raise ValueError(f"{info.field_name} must be a non-empty string")
2748
+ return value
2749
+
2750
+
2751
+ class MetricsArtifact(BaseModel):
2752
+ """Validated ``metrics.json`` artifact payload."""
2753
+
2754
+ model_config = ConfigDict(extra="forbid", frozen=True)
2755
+
2756
+ schema_version: Literal["1.0"]
2757
+ request_id: str
2758
+ run_id: str
2759
+ session_id: str | None = None
2760
+ status: GuardedSessionStatus | ExecutionStatus
2761
+ usage: UsageMetadata | None = None
2762
+
2763
+ @field_validator("request_id", "run_id", "session_id")
2764
+ @classmethod
2765
+ def _optional_strings_nonblank(cls, value: str | None, info: Any) -> str | None:
2766
+ if value is not None and not value.strip():
2767
+ raise ValueError(f"{info.field_name} must be a non-empty string")
2768
+ return value
2769
+
2770
+
2771
+ class DiagnosticArtifact(BaseModel):
2772
+ """Validated sanitized ``diagnostic.json`` artifact payload."""
2773
+
2774
+ model_config = ConfigDict(extra="forbid", frozen=True)
2775
+
2776
+ schema_version: Literal["1.0"]
2777
+ diagnostic: DiagnosticMetadata
2778
+
2779
+
2780
+ class ArtifactManifestEntry(BaseModel):
2781
+ """Single validated artifact entry in ``artifact_manifest.json``."""
2782
+
2783
+ model_config = ConfigDict(extra="forbid", frozen=True)
2784
+
2785
+ artifact_id: str
2786
+ path: str
2787
+ media_type: str
2788
+ byte_size: int = Field(ge=0)
2789
+ sha256_hex: str
2790
+ complete: bool
2791
+ producer: str
2792
+ failure_code: str | None = Field(
2793
+ default=None,
2794
+ description="Stable failure code when this artifact is incomplete",
2795
+ )
2796
+
2797
+ @field_validator("artifact_id", "path", "media_type", "producer")
2798
+ @classmethod
2799
+ def _strings_nonblank(cls, value: str, info: Any) -> str:
2800
+ if not value.strip():
2801
+ raise ValueError(f"{info.field_name} must be a non-empty string")
2802
+ return value
2803
+
2804
+ @field_validator("failure_code")
2805
+ @classmethod
2806
+ def _failure_code_nonblank(cls, value: str | None) -> str | None:
2807
+ return None if value is None else _nonblank(value, "failure_code")
2808
+
2809
+ @field_validator("path")
2810
+ @classmethod
2811
+ def _path_relative_safe(cls, value: str) -> str:
2812
+ path = Path(value)
2813
+ if path.is_absolute() or ".." in path.parts:
2814
+ raise ValueError("manifest artifact paths must be safe relative paths")
2815
+ return value
2816
+
2817
+ @field_validator("sha256_hex")
2818
+ @classmethod
2819
+ def _sha256_valid(cls, value: str) -> str:
2820
+ if not re.fullmatch(r"[0-9a-f]{64}", value):
2821
+ raise ValueError("sha256_hex must be exactly 64 lowercase hex characters")
2822
+ return value
2823
+
2824
+ @model_validator(mode="after")
2825
+ def _completion_failure_consistent(self) -> ArtifactManifestEntry:
2826
+ if self.complete and self.failure_code is not None:
2827
+ raise ValueError("complete artifacts must not include failure_code")
2828
+ if not self.complete and self.failure_code is None:
2829
+ raise ValueError("incomplete artifacts require failure_code")
2830
+ return self
2831
+
2832
+
2833
+ class ArtifactManifestArtifact(BaseModel):
2834
+ """Validated ``artifact_manifest.json`` payload."""
2835
+
2836
+ model_config = ConfigDict(extra="forbid", frozen=True)
2837
+
2838
+ schema_version: Literal["1.0"]
2839
+ request_id: str
2840
+ run_id: str
2841
+ artifacts: Tuple[ArtifactManifestEntry, ...]
2842
+
2843
+ @field_validator("request_id", "run_id")
2844
+ @classmethod
2845
+ def _strings_nonblank(cls, value: str, info: Any) -> str:
2846
+ if not value.strip():
2847
+ raise ValueError(f"{info.field_name} must be a non-empty string")
2848
+ return value
2849
+
2850
+ @field_validator("artifacts")
2851
+ @classmethod
2852
+ def _artifact_ids_unique(
2853
+ cls, value: Tuple[ArtifactManifestEntry, ...]
2854
+ ) -> Tuple[ArtifactManifestEntry, ...]:
2855
+ artifact_ids = [entry.artifact_id for entry in value]
2856
+ if len(set(artifact_ids)) != len(artifact_ids):
2857
+ raise ValueError("manifest artifact_id values must be unique")
2858
+ if "artifact_manifest" in artifact_ids:
2859
+ raise ValueError("artifact_manifest must not reference itself")
2860
+ return value