openworkproof 1.0.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 (39) hide show
  1. openworkproof/__init__.py +1 -0
  2. openworkproof/acceptance.py +3006 -0
  3. openworkproof/cli.py +164 -0
  4. openworkproof/composition.py +832 -0
  5. openworkproof/evidence.py +8281 -0
  6. openworkproof/execution_adapter.py +256 -0
  7. openworkproof/external_acceptor.py +211 -0
  8. openworkproof/mcp_server.py +2939 -0
  9. openworkproof/mcp_transport.py +72 -0
  10. openworkproof/models.py +3380 -0
  11. openworkproof/policy.py +2494 -0
  12. openworkproof/predicates.py +333 -0
  13. openworkproof/repo_pipeline/__init__.py +74 -0
  14. openworkproof/repo_pipeline/analysis.py +152 -0
  15. openworkproof/repo_pipeline/errors.py +64 -0
  16. openworkproof/repo_pipeline/models.py +80 -0
  17. openworkproof/repo_pipeline/output.py +68 -0
  18. openworkproof/repo_pipeline/reader.py +47 -0
  19. openworkproof/repo_pipeline/sources.py +120 -0
  20. openworkproof/repo_pipeline/traversal.py +253 -0
  21. openworkproof/repo_tools.py +8382 -0
  22. openworkproof/runtime_context.py +189 -0
  23. openworkproof/schema_registry.py +623 -0
  24. openworkproof/schemas/v0.1/acceptance-receipt.schema.json +1 -0
  25. openworkproof/schemas/v0.1/acceptance-rejection-receipt.schema.json +1 -0
  26. openworkproof/schemas/v0.1/action-receipt.schema.json +1 -0
  27. openworkproof/schemas/v0.1/capability-grant.schema.json +1 -0
  28. openworkproof/schemas/v0.1/schema-registry.json +1 -0
  29. openworkproof/schemas/v0.1/work-order.schema.json +1 -0
  30. openworkproof/signing.py +421 -0
  31. openworkproof/state.py +765 -0
  32. openworkproof/team_network_client.py +419 -0
  33. openworkproof/trusted_helper.py +269 -0
  34. openworkproof-1.0.0.dist-info/METADATA +578 -0
  35. openworkproof-1.0.0.dist-info/RECORD +39 -0
  36. openworkproof-1.0.0.dist-info/WHEEL +5 -0
  37. openworkproof-1.0.0.dist-info/entry_points.txt +2 -0
  38. openworkproof-1.0.0.dist-info/licenses/LICENSE +202 -0
  39. openworkproof-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3380 @@
1
+ """Closed, immutable protocol models for OpenWorkProof v0.1."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import binascii
7
+ import hashlib
8
+ import json
9
+ import re
10
+ from collections.abc import Iterator, Mapping
11
+ from datetime import UTC, datetime, timedelta
12
+ from types import MappingProxyType
13
+ from typing import Annotated, Any, Literal
14
+
15
+ import rfc8785
16
+ from pydantic import (
17
+ BaseModel,
18
+ BeforeValidator,
19
+ ConfigDict,
20
+ Field,
21
+ PlainSerializer,
22
+ TypeAdapter,
23
+ ValidationError,
24
+ field_validator,
25
+ model_serializer,
26
+ model_validator,
27
+ )
28
+ from pydantic_core import core_schema
29
+
30
+
31
+ MAX_SAFE_INTEGER = 9_007_199_254_740_991
32
+ BUNDLE_FINALIZATION_GRACE_SECONDS = 3600
33
+ MAX_PATCH_BYTES = 65_536
34
+ MAX_ARTIFACT_BYTES = 8 * 1024 * 1024
35
+ MAX_TOTAL_DECLARED_EVIDENCE_BYTES = 16 * 1024 * 1024
36
+
37
+ _LOWER_HEX_64 = re.compile(r"^[0-9a-f]{64}$")
38
+ _LOWER_HEX_40 = re.compile(r"^[0-9a-f]{40}$")
39
+ _IMAGE_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
40
+ _KEY_ID = re.compile(r"^ed25519:[0-9a-f]{64}$")
41
+ _UTC_SECONDS = re.compile(
42
+ r"^([0-9]{4})-([0-9]{2})-([0-9]{2})T"
43
+ r"([0-9]{2}):([0-9]{2}):([0-9]{2})Z$"
44
+ )
45
+ _ROOT_PATH = re.compile(r"^[A-Za-z0-9._/-]+$")
46
+ _BASE64URL = re.compile(r"^[A-Za-z0-9_-]+$")
47
+
48
+ _ALL_TOOLS = frozenset(
49
+ {
50
+ "owp.activate_root_grant",
51
+ "owp.apply_patch",
52
+ "owp.compose_proof",
53
+ "owp.create_pr_proposal",
54
+ "owp.delegate_grant",
55
+ "owp.repo_read",
56
+ "owp.request_acceptance",
57
+ "owp.request_pr_proposal",
58
+ "owp.revoke_grant",
59
+ "owp.rollback_patch",
60
+ "owp.run_tests",
61
+ "owp.start_retry",
62
+ }
63
+ )
64
+ _MANAGER_DIRECT_TOOLS = frozenset(
65
+ {
66
+ "owp.activate_root_grant",
67
+ "owp.compose_proof",
68
+ "owp.create_pr_proposal",
69
+ "owp.delegate_grant",
70
+ "owp.request_acceptance",
71
+ "owp.request_pr_proposal",
72
+ "owp.revoke_grant",
73
+ "owp.start_retry",
74
+ }
75
+ )
76
+ _DELEGABLE_CHILD_TOOLS = frozenset(
77
+ {
78
+ "owp.apply_patch",
79
+ "owp.repo_read",
80
+ "owp.rollback_patch",
81
+ "owp.run_tests",
82
+ }
83
+ )
84
+ _PREDICATE_TOOLS = frozenset(
85
+ {
86
+ "owp.repo_read",
87
+ "owp.apply_patch",
88
+ "owp.run_tests",
89
+ "owp.create_pr_proposal",
90
+ "owp.compose_proof",
91
+ }
92
+ )
93
+ _VERIFIER_ARGV = (
94
+ "/opt/venv/bin/python",
95
+ "-I",
96
+ "-m",
97
+ "pytest",
98
+ "-q",
99
+ "-c",
100
+ "/dev/null",
101
+ "--rootdir=/fixed-tests",
102
+ "--confcutdir=/fixed-tests",
103
+ "/fixed-tests/verifier_test.py",
104
+ )
105
+ _FIXED_ENV = {
106
+ "HOME": "/nonexistent",
107
+ "LC_ALL": "C.UTF-8",
108
+ "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1",
109
+ "TZ": "UTC",
110
+ }
111
+ _EVIDENCE_DIMENSIONS = (
112
+ "authority",
113
+ "scope",
114
+ "execution",
115
+ "result",
116
+ "independent_result",
117
+ )
118
+ _KEY_ROLES = (
119
+ "Maintainer",
120
+ "Manager",
121
+ "Developer",
122
+ "Verifier",
123
+ "Sidecar",
124
+ "Acceptor",
125
+ )
126
+ _PURPOSE_ORDER = {
127
+ "patch_input": 0,
128
+ "patch_result": 1,
129
+ "patch_denial_audit": 2,
130
+ "developer_test_result": 3,
131
+ "verifier_result": 4,
132
+ "verifier_independent_result": 5,
133
+ }
134
+ _PURPOSE_BINDINGS = {
135
+ "patch_input": ("text/x-diff", "scope"),
136
+ "patch_result": ("application/json", "execution"),
137
+ "patch_denial_audit": ("text/x-diff", "none"),
138
+ "developer_test_result": ("application/json", "execution"),
139
+ "verifier_result": ("application/json", "result"),
140
+ "verifier_independent_result": ("application/json", "independent_result"),
141
+ }
142
+ _RESERVED_EVIDENCE_ROOTS = frozenset(
143
+ {
144
+ ".pending",
145
+ "evidence",
146
+ "manifest.json",
147
+ "manifest.sig",
148
+ "keys",
149
+ "protocol",
150
+ "source",
151
+ "fixed-tests",
152
+ "schemas",
153
+ }
154
+ )
155
+
156
+
157
+ def _require_exact_int(value: Any, *, positive: bool) -> int:
158
+ if type(value) is not int:
159
+ raise ValueError("value must be a strict JSON integer")
160
+ minimum = 1 if positive else 0
161
+ if not minimum <= value <= MAX_SAFE_INTEGER:
162
+ raise ValueError(f"value must be in {minimum}..{MAX_SAFE_INTEGER}")
163
+ return value
164
+
165
+
166
+ SafeNonNegativeInt = Annotated[
167
+ int, BeforeValidator(lambda value: _require_exact_int(value, positive=False))
168
+ ]
169
+ SafePositiveInt = Annotated[
170
+ int, BeforeValidator(lambda value: _require_exact_int(value, positive=True))
171
+ ]
172
+
173
+
174
+ def _parse_canonical_time(value: Any) -> datetime:
175
+ if type(value) is not str:
176
+ raise ValueError("timestamp must be a canonical RFC 3339 string")
177
+ match = _UTC_SECONDS.fullmatch(value)
178
+ if match is None:
179
+ raise ValueError("timestamp must use YYYY-MM-DDTHH:MM:SSZ")
180
+ year, month, day, hour, minute, second = map(int, match.groups())
181
+ if second > 59:
182
+ raise ValueError("leap seconds are not permitted")
183
+ try:
184
+ parsed = datetime(year, month, day, hour, minute, second, tzinfo=UTC)
185
+ except ValueError as error:
186
+ raise ValueError("timestamp is not a valid UTC second") from error
187
+ if parsed < datetime(1970, 1, 1, tzinfo=UTC):
188
+ raise ValueError("timestamp precedes the Unix epoch")
189
+ return parsed
190
+
191
+
192
+ def _serialize_canonical_time(value: datetime) -> str:
193
+ return value.strftime("%Y-%m-%dT%H:%M:%SZ")
194
+
195
+
196
+ CanonicalUTCTime = Annotated[
197
+ datetime,
198
+ BeforeValidator(_parse_canonical_time),
199
+ PlainSerializer(_serialize_canonical_time, return_type=str),
200
+ ]
201
+
202
+
203
+ def _strict_text(value: Any, *, max_bytes: int, field_name: str) -> str:
204
+ if type(value) is not str:
205
+ raise ValueError(f"{field_name} must be a strict string")
206
+ encoded = value.encode("utf-8")
207
+ if not encoded or len(encoded) > max_bytes:
208
+ raise ValueError(f"{field_name} must contain 1..{max_bytes} UTF-8 bytes")
209
+ return value
210
+
211
+
212
+ def _identifier(value: Any) -> str:
213
+ return _strict_text(value, max_bytes=128, field_name="identifier")
214
+
215
+
216
+ def _ordinary_string(value: Any) -> str:
217
+ return _strict_text(value, max_bytes=4096, field_name="string")
218
+
219
+
220
+ def _digest64(value: Any) -> str:
221
+ if type(value) is not str or _LOWER_HEX_64.fullmatch(value) is None:
222
+ raise ValueError("digest must be 64 lowercase hexadecimal characters")
223
+ return value
224
+
225
+
226
+ def _oid40(value: Any) -> str:
227
+ if type(value) is not str or _LOWER_HEX_40.fullmatch(value) is None:
228
+ raise ValueError("object id must be 40 lowercase hexadecimal characters")
229
+ return value
230
+
231
+
232
+ def _image_digest(value: Any) -> str:
233
+ if type(value) is not str or _IMAGE_DIGEST.fullmatch(value) is None:
234
+ raise ValueError("image digest must use sha256:<64 lowercase hex>")
235
+ return value
236
+
237
+
238
+ def _key_id(value: Any) -> str:
239
+ if type(value) is not str or _KEY_ID.fullmatch(value) is None:
240
+ raise ValueError("key id must use ed25519:<64 lowercase hex>")
241
+ return value
242
+
243
+
244
+ Identifier = Annotated[str, BeforeValidator(_identifier)]
245
+ ProtocolString = Annotated[str, BeforeValidator(_ordinary_string)]
246
+ Digest64 = Annotated[str, BeforeValidator(_digest64)]
247
+ ObjectId40 = Annotated[str, BeforeValidator(_oid40)]
248
+ ImageDigest = Annotated[str, BeforeValidator(_image_digest)]
249
+ KeyId = Annotated[str, BeforeValidator(_key_id)]
250
+
251
+
252
+ def _strict_bounded_text(max_bytes: int, field_name: str):
253
+ return BeforeValidator(
254
+ lambda value: _strict_text(
255
+ value, max_bytes=max_bytes, field_name=field_name
256
+ )
257
+ )
258
+
259
+
260
+ class FrozenDict(Mapping[str, Any]):
261
+ """A JSON-object mapping that cannot be changed after validation."""
262
+
263
+ __slots__ = ("_data",)
264
+
265
+ def __init__(self, value: Mapping[str, Any]) -> None:
266
+ frozen = _freeze_json(dict(value), freeze_mapping=False)
267
+ object.__setattr__(self, "_data", MappingProxyType(frozen))
268
+
269
+ def __setattr__(self, name: str, value: Any) -> None:
270
+ del name, value
271
+ raise TypeError("FrozenDict is immutable")
272
+
273
+ def __getitem__(self, key: str) -> Any:
274
+ return self._data[key]
275
+
276
+ def __iter__(self) -> Iterator[str]:
277
+ return iter(self._data)
278
+
279
+ def __len__(self) -> int:
280
+ return len(self._data)
281
+
282
+ def __repr__(self) -> str:
283
+ return f"FrozenDict({self._data!r})"
284
+
285
+ def __eq__(self, other: object) -> bool:
286
+ if isinstance(other, Mapping):
287
+ return dict(self.items()) == dict(other.items())
288
+ return False
289
+
290
+ def __deepcopy__(self, memo: dict[int, Any]) -> FrozenDict:
291
+ return self
292
+
293
+ @classmethod
294
+ def _from_frozen(cls, value: dict[str, Any]) -> FrozenDict:
295
+ instance = object.__new__(cls)
296
+ object.__setattr__(instance, "_data", MappingProxyType(value))
297
+ return instance
298
+
299
+ @classmethod
300
+ def __get_pydantic_core_schema__(
301
+ cls, source_type: Any, handler: Any
302
+ ) -> core_schema.CoreSchema:
303
+ del source_type, handler
304
+ return core_schema.no_info_plain_validator_function(
305
+ cls._validate,
306
+ serialization=core_schema.plain_serializer_function_ser_schema(
307
+ lambda value: _jsonable(value),
308
+ info_arg=False,
309
+ return_schema=core_schema.dict_schema(),
310
+ when_used="json",
311
+ ),
312
+ )
313
+
314
+ @classmethod
315
+ def __get_pydantic_json_schema__(
316
+ cls, schema: core_schema.CoreSchema, handler: Any
317
+ ) -> dict[str, Any]:
318
+ del cls, schema, handler
319
+ return {"type": "object", "additionalProperties": True}
320
+
321
+ @classmethod
322
+ def _validate(cls, value: Any) -> FrozenDict:
323
+ if isinstance(value, cls):
324
+ return value
325
+ if not isinstance(value, Mapping):
326
+ raise ValueError("value must be a JSON object")
327
+ return cls(value)
328
+
329
+
330
+ def _freeze_json(
331
+ value: Any, depth: int = 1, *, freeze_mapping: bool = True
332
+ ) -> Any:
333
+ if depth > 16:
334
+ raise ValueError("JSON nesting depth exceeds 16")
335
+ if isinstance(value, BaseModel):
336
+ raise ValueError("nested BaseModel values are not strict JSON")
337
+ if isinstance(value, Mapping):
338
+ if len(value) > 64:
339
+ raise ValueError("JSON map exceeds 64 entries")
340
+ frozen: dict[str, Any] = {}
341
+ for key, item in value.items():
342
+ if type(key) is not str:
343
+ raise ValueError("JSON map keys must be strings")
344
+ if len(key.encode("utf-8")) > 4096:
345
+ raise ValueError("JSON map key exceeds 4096 UTF-8 bytes")
346
+ frozen[key] = _freeze_json(item, depth + 1)
347
+ return FrozenDict._from_frozen(frozen) if freeze_mapping else frozen
348
+ if isinstance(value, (list, tuple)):
349
+ if len(value) > 64:
350
+ raise ValueError("JSON array exceeds 64 items")
351
+ return tuple(_freeze_json(item, depth + 1) for item in value)
352
+ if type(value) is str:
353
+ if len(value.encode("utf-8")) > 4096:
354
+ raise ValueError("JSON string exceeds 4096 UTF-8 bytes")
355
+ return value
356
+ if value is None or type(value) is bool:
357
+ return value
358
+ if type(value) is int:
359
+ return _require_exact_int(value, positive=False)
360
+ raise ValueError("unsupported or non-strict JSON scalar")
361
+
362
+
363
+ def _jsonable(value: Any) -> Any:
364
+ if isinstance(value, BaseModel):
365
+ return value.model_dump(mode="json")
366
+ if isinstance(value, Mapping):
367
+ return {key: _jsonable(item) for key, item in value.items()}
368
+ if isinstance(value, (tuple, list)):
369
+ return [_jsonable(item) for item in value]
370
+ if isinstance(value, datetime):
371
+ return _serialize_canonical_time(value)
372
+ return value
373
+
374
+
375
+ def _jcs_digest(value: Any) -> str:
376
+ return hashlib.sha256(rfc8785.dumps(_jsonable(value))).hexdigest()
377
+
378
+
379
+ def _decode_unpadded_base64url(value: Any, expected_bytes: int) -> bytes:
380
+ if type(value) is not str or not value or "=" in value:
381
+ raise ValueError("value must be unpadded base64url")
382
+ if _BASE64URL.fullmatch(value) is None:
383
+ raise ValueError("value must contain only base64url characters")
384
+ try:
385
+ raw = base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
386
+ except (ValueError, binascii.Error) as error:
387
+ raise ValueError("value is not valid base64url") from error
388
+ canonical = base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
389
+ if canonical != value or len(raw) != expected_bytes:
390
+ raise ValueError(f"value must canonically encode {expected_bytes} bytes")
391
+ return raw
392
+
393
+
394
+ def _signature(value: Any) -> str:
395
+ _decode_unpadded_base64url(value, 64)
396
+ return value
397
+
398
+
399
+ Signature = Annotated[str, BeforeValidator(_signature)]
400
+
401
+
402
+ def _is_utf8_sorted_unique(values: tuple[str, ...]) -> bool:
403
+ return list(values) == sorted(set(values), key=lambda item: item.encode("utf-8"))
404
+
405
+
406
+ def _validate_root(value: str) -> str:
407
+ if type(value) is not str:
408
+ raise ValueError("root must be a strict string")
409
+ try:
410
+ encoded = value.encode("ascii")
411
+ except UnicodeEncodeError as error:
412
+ raise ValueError("root must be ASCII") from error
413
+ if not encoded or len(encoded) > 512:
414
+ raise ValueError("root must contain 1..512 ASCII bytes")
415
+ if (
416
+ value.startswith("/")
417
+ or value.endswith("/")
418
+ or "\\" in value
419
+ or any(token in value for token in ("*", "?", "[", "]"))
420
+ or _ROOT_PATH.fullmatch(value) is None
421
+ ):
422
+ raise ValueError("root is not a canonical relative POSIX path")
423
+ segments = value.split("/")
424
+ if any(segment in {"", ".", ".."} for segment in segments):
425
+ raise ValueError("root contains a forbidden segment")
426
+ if segments[0] == ".git":
427
+ raise ValueError(".git is a protected root")
428
+ return value
429
+
430
+
431
+ CanonicalRoot = Annotated[str, BeforeValidator(_validate_root)]
432
+
433
+
434
+ def _validate_sorted_roots(values: tuple[str, ...], *, allow_empty: bool) -> None:
435
+ if not allow_empty and not values:
436
+ raise ValueError("at least one root is required")
437
+ if not _is_utf8_sorted_unique(values):
438
+ raise ValueError("roots must be UTF-8 sorted and unique")
439
+
440
+
441
+ def _root_covers(parent: str, child: str) -> bool:
442
+ return child == parent or child.startswith(parent + "/")
443
+
444
+
445
+ def _write_roots_within_read(
446
+ read_roots: tuple[str, ...], write_roots: tuple[str, ...]
447
+ ) -> bool:
448
+ return all(
449
+ any(_root_covers(read_root, write_root) for read_root in read_roots)
450
+ for write_root in write_roots
451
+ )
452
+
453
+
454
+ def _validate_tools(values: tuple[str, ...], *, allow_empty: bool = False) -> None:
455
+ if not allow_empty and not values:
456
+ raise ValueError("at least one tool is required")
457
+ if not _is_utf8_sorted_unique(values):
458
+ raise ValueError("tools must be UTF-8 sorted and unique")
459
+ if any(value not in _ALL_TOOLS for value in values):
460
+ raise ValueError("unknown tool")
461
+
462
+
463
+ class ProtocolModel(BaseModel):
464
+ """Base for all closed and immutable v0.1 protocol shapes."""
465
+
466
+ model_config = ConfigDict(
467
+ extra="forbid",
468
+ frozen=True,
469
+ strict=True,
470
+ validate_assignment=True,
471
+ )
472
+
473
+ @model_validator(mode="before")
474
+ @classmethod
475
+ def _validate_json_shape(cls, value: Any) -> Any:
476
+ if isinstance(value, cls):
477
+ return value
478
+ if not isinstance(value, Mapping):
479
+ raise ValueError("protocol object must be a JSON object")
480
+ return _freeze_json(dict(value), freeze_mapping=False)
481
+
482
+
483
+ class SignedProtocolModel(ProtocolModel):
484
+ digest: Digest64
485
+ signature_alg: Literal["Ed25519"]
486
+ signer_key_id: KeyId
487
+ signature: Signature
488
+
489
+
490
+ class Quota(ProtocolModel):
491
+ tool_calls: SafeNonNegativeInt
492
+ repair_rounds: SafeNonNegativeInt
493
+
494
+
495
+ class ApprovalGate(ProtocolModel):
496
+ gate_id: Digest64
497
+ tool_name: Literal["owp.create_pr_proposal"]
498
+ required_role: Literal["Maintainer"]
499
+ max_validity_seconds: SafePositiveInt
500
+ scope_schema: Literal["openworkproof/pr-proposal-scope/0.1"]
501
+
502
+ @model_validator(mode="after")
503
+ def _validate_gate(self) -> ApprovalGate:
504
+ if self.max_validity_seconds > 3600:
505
+ raise ValueError("gate validity must not exceed 3600 seconds")
506
+ expected = _jcs_digest(
507
+ {
508
+ "domain": "openworkproof/approval-gate-id/v0.1",
509
+ "tool_name": self.tool_name,
510
+ "required_role": self.required_role,
511
+ "max_validity_seconds": self.max_validity_seconds,
512
+ "scope_schema": self.scope_schema,
513
+ }
514
+ )
515
+ if self.gate_id != expected:
516
+ raise ValueError("gate_id does not match the canonical gate")
517
+ return self
518
+
519
+
520
+ class PredicateSpec(ProtocolModel):
521
+ predicate_id: Digest64
522
+ name: Literal[
523
+ "path_allowed",
524
+ "tool_allowed",
525
+ "quota_remaining",
526
+ "tests_passed",
527
+ "artifact_digest_matches",
528
+ "human_signature_present",
529
+ ]
530
+ version: Literal["0.1"]
531
+ applies_to_tools: tuple[str, ...]
532
+ arguments: FrozenDict
533
+
534
+ @classmethod
535
+ def from_parts(
536
+ cls,
537
+ *,
538
+ name: str,
539
+ version: str,
540
+ applies_to_tools: tuple[str, ...],
541
+ arguments: Mapping[str, Any],
542
+ ) -> PredicateSpec:
543
+ tools = tuple(
544
+ sorted(set(applies_to_tools), key=lambda value: value.encode("utf-8"))
545
+ )
546
+ frozen_arguments = FrozenDict(arguments)
547
+ predicate_id = _jcs_digest(
548
+ {
549
+ "domain": "openworkproof/predicate-id/v0.1",
550
+ "name": name,
551
+ "version": version,
552
+ "applies_to_tools": tools,
553
+ "arguments": frozen_arguments,
554
+ }
555
+ )
556
+ return cls.model_validate(
557
+ {
558
+ "predicate_id": predicate_id,
559
+ "name": name,
560
+ "version": version,
561
+ "applies_to_tools": tools,
562
+ "arguments": frozen_arguments,
563
+ }
564
+ )
565
+
566
+ @model_validator(mode="after")
567
+ def _validate_frozen_registry(self) -> PredicateSpec:
568
+ tools = self.applies_to_tools
569
+ if not _is_utf8_sorted_unique(tools):
570
+ raise ValueError("predicate tools must be sorted and unique")
571
+ if any(value not in _PREDICATE_TOOLS for value in tools):
572
+ raise ValueError("predicate references an unsupported tool")
573
+ if self.name == "human_signature_present":
574
+ if tools:
575
+ raise ValueError(
576
+ "human signature predicate cannot apply to tools"
577
+ )
578
+ elif not tools:
579
+ raise ValueError("tool predicate must apply to at least one tool")
580
+
581
+ arguments = self.arguments
582
+ argument_keys = {
583
+ "path_allowed": frozenset({"allowed_roots"}),
584
+ "tool_allowed": frozenset({"allowed_tools", "tool_name"}),
585
+ "quota_remaining": frozenset({"metric", "amount"}),
586
+ "tests_passed": frozenset(
587
+ {
588
+ "test_mode",
589
+ "command_digest",
590
+ "expected_exit_code",
591
+ "fixed_test_source_digest",
592
+ }
593
+ ),
594
+ "artifact_digest_matches": frozenset(
595
+ {"artifact_path", "expected_digest"}
596
+ ),
597
+ "human_signature_present": frozenset({"required_role"}),
598
+ }
599
+ if set(arguments) != argument_keys[self.name]:
600
+ raise ValueError("predicate arguments do not match the registry")
601
+
602
+ if self.name == "path_allowed":
603
+ allowed_roots = arguments["allowed_roots"]
604
+ if (
605
+ type(allowed_roots) is not tuple
606
+ or not 1 <= len(allowed_roots) <= 32
607
+ or any(type(root) is not str for root in allowed_roots)
608
+ ):
609
+ raise ValueError("allowed_roots must contain 1..32 roots")
610
+ for root in allowed_roots:
611
+ _validate_root(root)
612
+ _validate_sorted_roots(allowed_roots, allow_empty=False)
613
+ elif self.name == "tool_allowed":
614
+ allowed_tools = arguments["allowed_tools"]
615
+ tool_name = arguments["tool_name"]
616
+ if (
617
+ type(allowed_tools) is not tuple
618
+ or not allowed_tools
619
+ or any(type(tool) is not str for tool in allowed_tools)
620
+ or not _is_utf8_sorted_unique(allowed_tools)
621
+ or any(tool not in _PREDICATE_TOOLS for tool in allowed_tools)
622
+ ):
623
+ raise ValueError(
624
+ "allowed_tools must be a non-empty canonical tool array"
625
+ )
626
+ if (
627
+ type(tool_name) is not str
628
+ or tool_name not in _PREDICATE_TOOLS
629
+ or tool_name not in allowed_tools
630
+ ):
631
+ raise ValueError("tool_name must name an allowed predicate tool")
632
+ elif self.name == "quota_remaining":
633
+ metric = arguments["metric"]
634
+ if (
635
+ type(metric) is not str
636
+ or metric not in {"tool_calls", "repair_rounds"}
637
+ ):
638
+ raise ValueError("quota metric is not registered")
639
+ _require_exact_int(arguments["amount"], positive=True)
640
+ elif self.name == "tests_passed":
641
+ if arguments["test_mode"] != "verifier":
642
+ raise ValueError("tests_passed requires verifier mode")
643
+ _digest64(arguments["command_digest"])
644
+ _digest64(arguments["fixed_test_source_digest"])
645
+ expected_exit_code = _require_exact_int(
646
+ arguments["expected_exit_code"],
647
+ positive=False,
648
+ )
649
+ if expected_exit_code > 255:
650
+ raise ValueError("expected_exit_code must be in 0..255")
651
+ elif self.name == "artifact_digest_matches":
652
+ _validate_root(arguments["artifact_path"])
653
+ _digest64(arguments["expected_digest"])
654
+ elif arguments["required_role"] != "Maintainer":
655
+ raise ValueError(
656
+ "human signature predicate requires the Maintainer role"
657
+ )
658
+
659
+ expected = _jcs_digest(
660
+ {
661
+ "domain": "openworkproof/predicate-id/v0.1",
662
+ "name": self.name,
663
+ "version": self.version,
664
+ "applies_to_tools": self.applies_to_tools,
665
+ "arguments": self.arguments,
666
+ }
667
+ )
668
+ if self.predicate_id != expected:
669
+ raise ValueError("predicate_id does not match predicate fields")
670
+ return self
671
+
672
+
673
+ class ResolvedPathEntry(ProtocolModel):
674
+ requested_path: CanonicalRoot
675
+ resolved_relative_path: CanonicalRoot | None
676
+
677
+
678
+ class PathAllowedPredicateInput(ProtocolModel):
679
+ requested_paths: tuple[CanonicalRoot, ...]
680
+ resolved_entries: tuple[ResolvedPathEntry, ...]
681
+ resolution_manifest_digest: Digest64 | None
682
+
683
+ @model_validator(mode="after")
684
+ def _validate_paths(self) -> PathAllowedPredicateInput:
685
+ if (
686
+ not self.requested_paths
687
+ or len(self.requested_paths) > 32
688
+ or not _is_utf8_sorted_unique(self.requested_paths)
689
+ or len(self.resolved_entries) != len(self.requested_paths)
690
+ or tuple(entry.requested_path for entry in self.resolved_entries)
691
+ != self.requested_paths
692
+ ):
693
+ raise ValueError("path predicate vectors are not aligned/canonical")
694
+ if self.resolution_manifest_digest is None and any(
695
+ entry.resolved_relative_path is not None
696
+ for entry in self.resolved_entries
697
+ ):
698
+ raise ValueError("resolved paths require a resolution manifest")
699
+ return self
700
+
701
+
702
+ class ToolAllowedPredicateInput(ProtocolModel):
703
+ actual_tool_name: Literal[
704
+ "owp.repo_read",
705
+ "owp.apply_patch",
706
+ "owp.run_tests",
707
+ "owp.create_pr_proposal",
708
+ "owp.compose_proof",
709
+ ]
710
+
711
+
712
+ class QuotaRemainingPredicateInput(ProtocolModel):
713
+ grant_id: Digest64
714
+ metric: Literal["tool_calls", "repair_rounds"]
715
+ amount: SafePositiveInt
716
+ grant_remaining_before: SafeNonNegativeInt
717
+ ledger_prefix_digest: Digest64
718
+
719
+
720
+ class TestsPassedPredicateInput(ProtocolModel):
721
+ test_mode: Literal["verifier"]
722
+ command_digest: Digest64
723
+ expected_exit_code: SafeNonNegativeInt
724
+ actual_exit_code: SafeNonNegativeInt | None
725
+ test_evidence_digest: Digest64 | None
726
+ source_commit: ObjectId40
727
+ candidate_commit: ObjectId40
728
+ workspace_manifest_digest: Digest64
729
+ container_image_digest: ImageDigest
730
+ fixed_test_source_digest: Digest64
731
+
732
+ @model_validator(mode="after")
733
+ def _validate_exit(self) -> TestsPassedPredicateInput:
734
+ if self.expected_exit_code > 255 or (
735
+ self.actual_exit_code is not None and self.actual_exit_code > 255
736
+ ):
737
+ raise ValueError("test exit code must be in 0..255")
738
+ if (self.actual_exit_code is None) != (self.test_evidence_digest is None):
739
+ raise ValueError("test exit and evidence digest nullability must match")
740
+ return self
741
+
742
+
743
+ class ArtifactDigestPredicateInput(ProtocolModel):
744
+ artifact_path: CanonicalRoot
745
+ expected_digest: Digest64
746
+ actual_digest: Digest64 | None
747
+ size_bytes: SafeNonNegativeInt | None
748
+ workspace_manifest_digest: Digest64 | None
749
+
750
+ @model_validator(mode="after")
751
+ def _validate_actual(self) -> ArtifactDigestPredicateInput:
752
+ present = (
753
+ self.actual_digest is not None,
754
+ self.size_bytes is not None,
755
+ self.workspace_manifest_digest is not None,
756
+ )
757
+ if len(set(present)) != 1:
758
+ raise ValueError("artifact actual fields must be all-null or all-present")
759
+ return self
760
+
761
+
762
+ class HumanSignaturePredicateInput(ProtocolModel):
763
+ decision_type: Literal[
764
+ "approval_decision", "termination_decision", "final_acceptance"
765
+ ]
766
+ required_role: Literal["Maintainer"]
767
+ actor_key_id: KeyId
768
+ claim_digest: Digest64
769
+ decision: Literal["approved", "denied", "rejected", "accepted"]
770
+ expires_at: CanonicalUTCTime | None
771
+ validated_at: CanonicalUTCTime
772
+
773
+ @model_validator(mode="after")
774
+ def _validate_expiry(self) -> HumanSignaturePredicateInput:
775
+ if (self.decision_type == "termination_decision") != (
776
+ self.expires_at is None
777
+ ):
778
+ raise ValueError("human-signature expiry does not match decision type")
779
+ return self
780
+
781
+
782
+ PredicateInput = (
783
+ PathAllowedPredicateInput
784
+ | ToolAllowedPredicateInput
785
+ | QuotaRemainingPredicateInput
786
+ | TestsPassedPredicateInput
787
+ | ArtifactDigestPredicateInput
788
+ | HumanSignaturePredicateInput
789
+ )
790
+
791
+
792
+ class PredicateResult(ProtocolModel):
793
+ predicate_id: Digest64
794
+ name: Literal[
795
+ "path_allowed",
796
+ "tool_allowed",
797
+ "quota_remaining",
798
+ "tests_passed",
799
+ "artifact_digest_matches",
800
+ "human_signature_present",
801
+ ]
802
+ version: Identifier
803
+ arguments_digest: Digest64
804
+ input: PredicateInput
805
+ input_digest: Digest64
806
+ passed: bool
807
+ error_code: Literal["PREDICATE_FALSE", "FAIL_CLOSED"] | None
808
+
809
+ @model_validator(mode="after")
810
+ def _validate_result(self) -> PredicateResult:
811
+ expected_types = {
812
+ "path_allowed": PathAllowedPredicateInput,
813
+ "tool_allowed": ToolAllowedPredicateInput,
814
+ "quota_remaining": QuotaRemainingPredicateInput,
815
+ "tests_passed": TestsPassedPredicateInput,
816
+ "artifact_digest_matches": ArtifactDigestPredicateInput,
817
+ "human_signature_present": HumanSignaturePredicateInput,
818
+ }
819
+ if not isinstance(self.input, expected_types[self.name]):
820
+ raise ValueError("predicate input does not match predicate name")
821
+ expected = _jcs_digest(
822
+ {
823
+ "domain": "openworkproof/predicate-input/v0.1",
824
+ "predicate_id": self.predicate_id,
825
+ "input": self.input,
826
+ }
827
+ )
828
+ if self.input_digest != expected:
829
+ raise ValueError("input_digest does not match predicate input")
830
+ if self.passed != (self.error_code is None):
831
+ raise ValueError("passed and error_code are inconsistent")
832
+ return self
833
+
834
+ def matches_spec(self, spec: PredicateSpec) -> bool:
835
+ return (
836
+ self.predicate_id == spec.predicate_id
837
+ and self.name == spec.name
838
+ and self.version == spec.version
839
+ and self.arguments_digest == _jcs_digest(spec.arguments)
840
+ )
841
+
842
+ def validate_against(self, spec: PredicateSpec) -> PredicateResult:
843
+ if not self.matches_spec(spec):
844
+ raise ValueError("PredicateResult does not match PredicateSpec")
845
+ return self
846
+
847
+
848
+ class KeyBinding(ProtocolModel):
849
+ role: Literal[
850
+ "Maintainer",
851
+ "Manager",
852
+ "Developer",
853
+ "Verifier",
854
+ "Sidecar",
855
+ "Acceptor",
856
+ ]
857
+ subject_id: Identifier
858
+ key_id: KeyId
859
+ public_key_b64url: str
860
+
861
+ @model_validator(mode="after")
862
+ def _validate_binding(self) -> KeyBinding:
863
+ raw = _decode_unpadded_base64url(self.public_key_b64url, 32)
864
+ expected = f"ed25519:{hashlib.sha256(raw).hexdigest()}"
865
+ if self.key_id != expected:
866
+ raise ValueError("key_id does not match the public key")
867
+ return self
868
+
869
+
870
+ class SourceArtifact(ProtocolModel):
871
+ path: Literal["source/base.owpsrc"]
872
+ media_type: Literal["application/vnd.openworkproof.source+zip"]
873
+ sha256: Digest64
874
+ size_bytes: SafePositiveInt
875
+
876
+ @model_validator(mode="after")
877
+ def _validate_size(self) -> SourceArtifact:
878
+ if self.size_bytes > MAX_ARTIFACT_BYTES:
879
+ raise ValueError("source artifact exceeds 8 MiB")
880
+ return self
881
+
882
+
883
+ class ReplayProfile(ProtocolModel):
884
+ schema_version: Literal["openworkproof-replay-profile/0.1"]
885
+ patch_profile_id: Literal["openworkproof/canonical-text-patch/0.1"]
886
+ object_format: Literal["sha1"]
887
+ source_artifact_sha256: Digest64
888
+ trusted_helper_image_digest: ImageDigest
889
+ author_name: Literal["OpenWorkProof Sidecar"]
890
+ author_email: Literal["sidecar@openworkproof.invalid"]
891
+ commit_message_prefix: Literal["OpenWorkProof patch "]
892
+ timestamp_rule: Literal["receipt-occurred-at-utc-seconds"]
893
+ worktree_profile: Literal["linux-posix-case-sensitive-v0.1"]
894
+
895
+
896
+ class Command(ProtocolModel):
897
+ argv: tuple[str, ...]
898
+ working_directory: Literal["/workspace"]
899
+ env: FrozenDict
900
+
901
+ @model_validator(mode="after")
902
+ def _validate_command(self) -> Command:
903
+ if not 1 <= len(self.argv) <= 16:
904
+ raise ValueError("argv must contain 1..16 items")
905
+ for argument in self.argv:
906
+ _strict_text(argument, max_bytes=256, field_name="argv item")
907
+ if not self.argv[0].startswith("/"):
908
+ raise ValueError("argv[0] must be an absolute executable path")
909
+ executable_name = self.argv[0].rsplit("/", 1)[-1]
910
+ if executable_name in {
911
+ "sh",
912
+ "bash",
913
+ "zsh",
914
+ "dash",
915
+ "ksh",
916
+ "fish",
917
+ "csh",
918
+ "tcsh",
919
+ "env",
920
+ }:
921
+ raise ValueError("shell and PATH-dispatch executables are forbidden")
922
+ if self.env != _FIXED_ENV:
923
+ raise ValueError("command environment is not the fixed v0.1 environment")
924
+ return self
925
+
926
+
927
+ class FixedTestSource(ProtocolModel):
928
+ path: Literal["fixed-tests/verifier_test.py"]
929
+ media_type: Literal["text/x-python"]
930
+ sha256: Digest64
931
+ size_bytes: SafePositiveInt
932
+
933
+ @model_validator(mode="after")
934
+ def _validate_size(self) -> FixedTestSource:
935
+ if self.size_bytes > 65_536:
936
+ raise ValueError("fixed test source exceeds 64 KiB")
937
+ return self
938
+
939
+
940
+ def validate_fixed_test_source_bytes(
941
+ source: FixedTestSource, content: bytes
942
+ ) -> FixedTestSource:
943
+ if type(content) is not bytes:
944
+ raise ValueError("fixed test content must be exact bytes")
945
+ if len(content) > 65_536:
946
+ raise ValueError("fixed test content exceeds 64 KiB")
947
+ if len(content) != source.size_bytes:
948
+ raise ValueError("fixed test content size does not match metadata")
949
+ if hashlib.sha256(content).hexdigest() != source.sha256:
950
+ raise ValueError("fixed test content SHA-256 does not match metadata")
951
+ if content.startswith(b"\xef\xbb\xbf"):
952
+ raise ValueError("fixed test content must not contain a UTF-8 BOM")
953
+ if b"\x00" in content:
954
+ raise ValueError("fixed test content must not contain NUL")
955
+ if b"\r" in content:
956
+ raise ValueError("fixed test content must not contain CR")
957
+ if not content.endswith(b"\n"):
958
+ raise ValueError("fixed test content must end with LF")
959
+ return source
960
+
961
+
962
+ class TestProfile(ProtocolModel):
963
+ test_mode: Literal["developer", "verifier"]
964
+ command: Command
965
+ command_digest: Digest64
966
+ expected_exit_code: SafeNonNegativeInt
967
+ container_image_digest: ImageDigest
968
+ fixed_test_source: FixedTestSource | None
969
+ fixed_test_source_digest: Digest64 | None
970
+
971
+ @model_validator(mode="after")
972
+ def _validate_profile(self) -> TestProfile:
973
+ if self.expected_exit_code > 255:
974
+ raise ValueError("expected_exit_code must be in 0..255")
975
+ expected_command_digest = _jcs_digest(
976
+ {
977
+ "domain": "openworkproof/test-command/v0.1",
978
+ "command": self.command,
979
+ }
980
+ )
981
+ if self.command_digest != expected_command_digest:
982
+ raise ValueError("command_digest does not match command")
983
+ if self.test_mode == "developer":
984
+ if (
985
+ self.fixed_test_source is not None
986
+ or self.fixed_test_source_digest is not None
987
+ ):
988
+ raise ValueError("developer fixed test fields must both be null")
989
+ else:
990
+ if self.command.argv != _VERIFIER_ARGV:
991
+ raise ValueError("verifier argv is not the fixed v0.1 command")
992
+ if self.fixed_test_source is None:
993
+ raise ValueError("verifier fixed_test_source is required")
994
+ if self.fixed_test_source_digest != self.fixed_test_source.sha256:
995
+ raise ValueError("fixed test source digest does not match source")
996
+ return self
997
+
998
+
999
+ class Artifact(ProtocolModel):
1000
+ name: Identifier
1001
+ path: str
1002
+ media_type: Literal["text/x-diff", "application/json"]
1003
+ max_size_bytes: SafePositiveInt
1004
+ evidence_dimension: Literal[
1005
+ "none", "scope", "execution", "result", "independent_result"
1006
+ ]
1007
+ purpose: Literal[
1008
+ "patch_input",
1009
+ "patch_result",
1010
+ "patch_denial_audit",
1011
+ "developer_test_result",
1012
+ "verifier_result",
1013
+ "verifier_independent_result",
1014
+ ]
1015
+ ordinal: SafePositiveInt
1016
+
1017
+ @model_validator(mode="after")
1018
+ def _validate_artifact(self) -> Artifact:
1019
+ try:
1020
+ encoded = self.path.encode("ascii")
1021
+ except UnicodeEncodeError as error:
1022
+ raise ValueError("artifact path must be ASCII") from error
1023
+ if (
1024
+ not encoded
1025
+ or len(encoded) > 503
1026
+ or len(b"evidence/" + encoded) > 512
1027
+ or self.path.startswith("/")
1028
+ or self.path.endswith("/")
1029
+ or "\\" in self.path
1030
+ or any(token in self.path for token in ("*", "?", "[", "]"))
1031
+ ):
1032
+ raise ValueError("artifact path is not canonical")
1033
+ segments = self.path.split("/")
1034
+ if any(segment in {"", ".", ".."} for segment in segments):
1035
+ raise ValueError("artifact path contains a forbidden segment")
1036
+ if segments[0] in _RESERVED_EVIDENCE_ROOTS:
1037
+ raise ValueError("artifact path collides with a reserved root")
1038
+ expected_media, expected_dimension = _PURPOSE_BINDINGS[self.purpose]
1039
+ if (self.media_type, self.evidence_dimension) != (
1040
+ expected_media,
1041
+ expected_dimension,
1042
+ ):
1043
+ raise ValueError("artifact purpose/media/dimension binding is invalid")
1044
+ if self.max_size_bytes > MAX_ARTIFACT_BYTES:
1045
+ raise ValueError("artifact maximum exceeds 8 MiB")
1046
+ if (
1047
+ self.purpose in {"patch_input", "patch_denial_audit"}
1048
+ and self.max_size_bytes > MAX_PATCH_BYTES
1049
+ ):
1050
+ raise ValueError("patch evidence maximum exceeds 64 KiB")
1051
+ return self
1052
+
1053
+
1054
+ class EvidencePolicy(ProtocolModel):
1055
+ evidence_root: Literal["evidence"]
1056
+ redaction_policy_id: Literal["owp-public-evidence-v0.1"]
1057
+ artifacts: tuple[Artifact, ...]
1058
+
1059
+ @model_validator(mode="after")
1060
+ def _validate_inventory(self) -> EvidencePolicy:
1061
+ artifacts = self.artifacts
1062
+ names = [item.name for item in artifacts]
1063
+ paths = [item.path for item in artifacts]
1064
+ purpose_ordinals = [(item.purpose, item.ordinal) for item in artifacts]
1065
+ if len(names) != len(set(names)):
1066
+ raise ValueError("artifact names must be unique")
1067
+ if len(paths) != len(set(paths)):
1068
+ raise ValueError("artifact paths must be unique")
1069
+ if len(purpose_ordinals) != len(set(purpose_ordinals)):
1070
+ raise ValueError("artifact purpose/ordinal pairs must be unique")
1071
+ order = [(_PURPOSE_ORDER[item.purpose], item.ordinal) for item in artifacts]
1072
+ if order != sorted(order):
1073
+ raise ValueError("artifact inventory is not purpose/ordinal sorted")
1074
+
1075
+ by_purpose: dict[str, list[int]] = {
1076
+ purpose: [] for purpose in _PURPOSE_ORDER
1077
+ }
1078
+ for artifact in artifacts:
1079
+ by_purpose[artifact.purpose].append(artifact.ordinal)
1080
+ for ordinals in by_purpose.values():
1081
+ if ordinals and ordinals != list(range(1, len(ordinals) + 1)):
1082
+ raise ValueError("artifact ordinals must be contiguous from one")
1083
+
1084
+ patch_inputs = by_purpose["patch_input"]
1085
+ patch_results = by_purpose["patch_result"]
1086
+ denials = by_purpose["patch_denial_audit"]
1087
+ developer = by_purpose["developer_test_result"]
1088
+ verifier = by_purpose["verifier_result"]
1089
+ independent = by_purpose["verifier_independent_result"]
1090
+ if not 1 <= len(patch_inputs) <= 4 or patch_inputs != patch_results:
1091
+ raise ValueError("patch input/result inventories must pair 1..4 ordinals")
1092
+ if denials != [1, 2]:
1093
+ raise ValueError("exactly two denial audit slots are required")
1094
+ if len(developer) > 4:
1095
+ raise ValueError("developer result inventory exceeds four")
1096
+ if not 1 <= len(verifier) <= 4:
1097
+ raise ValueError("verifier result inventory must contain 1..4 slots")
1098
+ if len(independent) > 1:
1099
+ raise ValueError("independent result inventory exceeds one")
1100
+ expected_range = (6, 19) if independent else (5, 18)
1101
+ if not expected_range[0] <= len(artifacts) <= expected_range[1]:
1102
+ raise ValueError("artifact inventory count is invalid")
1103
+ if sum(item.max_size_bytes for item in artifacts) > (
1104
+ MAX_TOTAL_DECLARED_EVIDENCE_BYTES
1105
+ ):
1106
+ raise ValueError("declared evidence exceeds 16 MiB")
1107
+ return self
1108
+
1109
+
1110
+ class RootGrantTemplate(ProtocolModel):
1111
+ grant_id: Digest64
1112
+ parent_grant_id: None
1113
+ issuer_key_id: KeyId
1114
+ subject_agent_id: Identifier
1115
+ subject_key_id: KeyId
1116
+ allowed_tools: tuple[str, ...]
1117
+ allowed_read_roots: tuple[CanonicalRoot, ...]
1118
+ allowed_write_roots: tuple[CanonicalRoot, ...]
1119
+ usage_mode: Literal["single_use", "metered"]
1120
+ quota: Quota
1121
+ valid_from: CanonicalUTCTime
1122
+ expires_at: CanonicalUTCTime
1123
+ may_delegate: bool
1124
+ issued_at: CanonicalUTCTime
1125
+
1126
+ @model_validator(mode="after")
1127
+ def _validate_root_template(self) -> RootGrantTemplate:
1128
+ _validate_tools(self.allowed_tools)
1129
+ _validate_sorted_roots(self.allowed_read_roots, allow_empty=False)
1130
+ _validate_sorted_roots(self.allowed_write_roots, allow_empty=True)
1131
+ if not _write_roots_within_read(
1132
+ self.allowed_read_roots, self.allowed_write_roots
1133
+ ):
1134
+ raise ValueError("write roots are not contained in read roots")
1135
+ if self.usage_mode != "metered":
1136
+ raise ValueError("root grant must use metered usage")
1137
+ if self.quota.tool_calls == 0 or self.quota.repair_rounds not in {0, 1}:
1138
+ raise ValueError("root grant quota is structurally invalid")
1139
+ if self.may_delegate is not True:
1140
+ raise ValueError("root grant must permit delegation")
1141
+ if not self.issued_at <= self.valid_from < self.expires_at:
1142
+ raise ValueError("root grant times are not ordered")
1143
+ return self
1144
+
1145
+
1146
+ class CapabilityGrant(SignedProtocolModel):
1147
+ grant_id: Digest64
1148
+ work_order_digest: Digest64
1149
+ parent_grant_id: Digest64 | None
1150
+ issuer_key_id: KeyId
1151
+ subject_agent_id: Identifier
1152
+ subject_key_id: KeyId
1153
+ allowed_tools: tuple[str, ...]
1154
+ allowed_read_roots: tuple[CanonicalRoot, ...]
1155
+ allowed_write_roots: tuple[CanonicalRoot, ...]
1156
+ usage_mode: Literal["single_use", "metered"]
1157
+ quota: Quota
1158
+ valid_from: CanonicalUTCTime
1159
+ expires_at: CanonicalUTCTime
1160
+ may_delegate: bool
1161
+ issued_at: CanonicalUTCTime
1162
+
1163
+ @model_validator(mode="after")
1164
+ def _validate_grant(self) -> CapabilityGrant:
1165
+ _validate_tools(self.allowed_tools)
1166
+ _validate_sorted_roots(self.allowed_read_roots, allow_empty=False)
1167
+ _validate_sorted_roots(self.allowed_write_roots, allow_empty=True)
1168
+ if not _write_roots_within_read(
1169
+ self.allowed_read_roots, self.allowed_write_roots
1170
+ ):
1171
+ raise ValueError("write roots are not contained in read roots")
1172
+ if self.signer_key_id != self.issuer_key_id:
1173
+ raise ValueError("grant signer must equal grant issuer")
1174
+ if not self.issued_at <= self.valid_from < self.expires_at:
1175
+ raise ValueError("grant times are not ordered")
1176
+ if self.quota.tool_calls == 0:
1177
+ raise ValueError("grant tool_calls must be positive")
1178
+ if self.parent_grant_id is None:
1179
+ if (
1180
+ self.usage_mode != "metered"
1181
+ or self.may_delegate is not True
1182
+ or self.quota.repair_rounds not in {0, 1}
1183
+ ):
1184
+ raise ValueError("root grant structure is invalid")
1185
+ elif self.may_delegate is not False or self.quota.repair_rounds != 0:
1186
+ raise ValueError("child grant structure is invalid")
1187
+ return self
1188
+
1189
+
1190
+ class WorkOrder(SignedProtocolModel):
1191
+ work_order_id: Digest64
1192
+ protocol_version: Literal["0.1"]
1193
+ issuer_id: Identifier
1194
+ acceptor_key_ids: tuple[KeyId, ...]
1195
+ objective: Annotated[str, _strict_bounded_text(4096, "objective")]
1196
+ preconditions: tuple[PredicateSpec, ...]
1197
+ invariants: tuple[PredicateSpec, ...]
1198
+ repository: Annotated[str, _strict_bounded_text(1024, "repository")]
1199
+ branch: Annotated[str, _strict_bounded_text(255, "branch")]
1200
+ allowed_read_roots: tuple[CanonicalRoot, ...]
1201
+ allowed_write_roots: tuple[CanonicalRoot, ...]
1202
+ source_commit: ObjectId40
1203
+ source_artifact: SourceArtifact
1204
+ patch_profile_id: Literal["openworkproof/canonical-text-patch/0.1"]
1205
+ replay_profile: ReplayProfile
1206
+ replay_profile_digest: Digest64
1207
+ test_profiles: tuple[TestProfile, ...]
1208
+ allowed_tools: tuple[str, ...]
1209
+ quota_ceiling: Quota
1210
+ deadline: CanonicalUTCTime
1211
+ retention_until: CanonicalUTCTime
1212
+ acceptance_criteria: Annotated[
1213
+ str, _strict_bounded_text(4096, "acceptance_criteria")
1214
+ ]
1215
+ postconditions: tuple[PredicateSpec, ...]
1216
+ approval_gates: tuple[ApprovalGate, ...]
1217
+ required_evidence_dimensions: tuple[
1218
+ Literal[
1219
+ "authority", "scope", "execution", "result", "independent_result"
1220
+ ],
1221
+ ...,
1222
+ ]
1223
+ independence_policy: Literal[
1224
+ "disclose_only", "independent_test_source_required"
1225
+ ]
1226
+ evidence_policy: EvidencePolicy
1227
+ root_grant_template: RootGrantTemplate
1228
+ key_bindings: tuple[KeyBinding, ...]
1229
+ issued_at: CanonicalUTCTime
1230
+
1231
+ @model_validator(mode="after")
1232
+ def _validate_work_order(self) -> WorkOrder:
1233
+ if not self.issued_at < self.deadline:
1234
+ raise ValueError("WorkOrder issued_at must precede deadline")
1235
+ if self.deadline + timedelta(
1236
+ seconds=BUNDLE_FINALIZATION_GRACE_SECONDS
1237
+ ) > self.retention_until:
1238
+ raise ValueError("retention does not include finalization grace")
1239
+
1240
+ _validate_tools(self.allowed_tools)
1241
+ _validate_sorted_roots(self.allowed_read_roots, allow_empty=False)
1242
+ _validate_sorted_roots(self.allowed_write_roots, allow_empty=True)
1243
+ if not _write_roots_within_read(
1244
+ self.allowed_read_roots, self.allowed_write_roots
1245
+ ):
1246
+ raise ValueError("WorkOrder write roots are not contained in read roots")
1247
+
1248
+ if (
1249
+ self.replay_profile.source_artifact_sha256
1250
+ != self.source_artifact.sha256
1251
+ ):
1252
+ raise ValueError("replay profile is not bound to source artifact")
1253
+ expected_replay_digest = _jcs_digest(
1254
+ {
1255
+ "domain": "openworkproof/replay-profile/v0.1",
1256
+ "profile": self.replay_profile,
1257
+ }
1258
+ )
1259
+ if self.replay_profile_digest != expected_replay_digest:
1260
+ raise ValueError("replay_profile_digest does not match profile")
1261
+
1262
+ if not 1 <= len(self.test_profiles) <= 2:
1263
+ raise ValueError("one verifier and at most one developer profile required")
1264
+ modes = tuple(profile.test_mode for profile in self.test_profiles)
1265
+ if modes not in {("verifier",), ("developer", "verifier")}:
1266
+ raise ValueError("test profiles must be ordered developer, verifier")
1267
+ verifier_profile = next(
1268
+ profile
1269
+ for profile in self.test_profiles
1270
+ if profile.test_mode == "verifier"
1271
+ )
1272
+ has_developer_profile = any(
1273
+ profile.test_mode == "developer" for profile in self.test_profiles
1274
+ )
1275
+
1276
+ self._validate_conditions(verifier_profile)
1277
+
1278
+ if len(self.approval_gates) != 1:
1279
+ raise ValueError("exactly one PR proposal approval gate is required")
1280
+ if self.required_evidence_dimensions not in {
1281
+ _EVIDENCE_DIMENSIONS[:4],
1282
+ _EVIDENCE_DIMENSIONS,
1283
+ }:
1284
+ raise ValueError("evidence dimensions are incomplete or out of order")
1285
+ has_independent_dimension = (
1286
+ "independent_result" in self.required_evidence_dimensions
1287
+ )
1288
+ expected_policy = (
1289
+ "independent_test_source_required"
1290
+ if has_independent_dimension
1291
+ else "disclose_only"
1292
+ )
1293
+ if self.independence_policy != expected_policy:
1294
+ raise ValueError("independence policy does not match dimensions")
1295
+ independent_slots = sum(
1296
+ artifact.purpose == "verifier_independent_result"
1297
+ for artifact in self.evidence_policy.artifacts
1298
+ )
1299
+ if independent_slots != int(has_independent_dimension):
1300
+ raise ValueError("independent evidence inventory does not match policy")
1301
+ developer_slots = sum(
1302
+ artifact.purpose == "developer_test_result"
1303
+ for artifact in self.evidence_policy.artifacts
1304
+ )
1305
+ if has_developer_profile != (developer_slots > 0):
1306
+ raise ValueError(
1307
+ "developer profile and developer evidence inventory must coexist"
1308
+ )
1309
+
1310
+ roles = tuple(binding.role for binding in self.key_bindings)
1311
+ if roles != _KEY_ROLES:
1312
+ raise ValueError("key bindings must use the fixed role order")
1313
+ subject_ids = tuple(binding.subject_id for binding in self.key_bindings)
1314
+ key_ids = tuple(binding.key_id for binding in self.key_bindings)
1315
+ if (
1316
+ len(set(subject_ids)) != 6
1317
+ or len(set(key_ids)) != 6
1318
+ ):
1319
+ raise ValueError("key binding subjects and keys must be unique")
1320
+ maintainer, manager, _, _, _, acceptor = self.key_bindings
1321
+ if (
1322
+ self.issuer_id != maintainer.subject_id
1323
+ or self.signer_key_id != maintainer.key_id
1324
+ or self.acceptor_key_ids != (acceptor.key_id,)
1325
+ ):
1326
+ raise ValueError(
1327
+ "Maintainer must issue/sign and Acceptor must accept"
1328
+ )
1329
+
1330
+ template = self.root_grant_template
1331
+ if (
1332
+ template.issuer_key_id != maintainer.key_id
1333
+ or template.subject_agent_id != manager.subject_id
1334
+ or template.subject_key_id != manager.key_id
1335
+ or template.issued_at != self.issued_at
1336
+ or template.expires_at != self.deadline
1337
+ ):
1338
+ raise ValueError("root grant template identity/time binding is invalid")
1339
+ if not set(template.allowed_tools).issubset(self.allowed_tools):
1340
+ raise ValueError("root template tools exceed WorkOrder tools")
1341
+ required_root_tools = _MANAGER_DIRECT_TOOLS | (
1342
+ set(self.allowed_tools) & _DELEGABLE_CHILD_TOOLS
1343
+ )
1344
+ if not required_root_tools.issubset(template.allowed_tools):
1345
+ raise ValueError("root template omits required Manager or child tools")
1346
+ if not all(
1347
+ any(_root_covers(parent, child) for parent in self.allowed_read_roots)
1348
+ for child in template.allowed_read_roots
1349
+ ):
1350
+ raise ValueError("root template read roots exceed WorkOrder roots")
1351
+ if not all(
1352
+ any(_root_covers(parent, child) for parent in self.allowed_write_roots)
1353
+ for child in template.allowed_write_roots
1354
+ ):
1355
+ raise ValueError("root template write roots exceed WorkOrder roots")
1356
+ if (
1357
+ template.quota.tool_calls > self.quota_ceiling.tool_calls
1358
+ or template.quota.repair_rounds > self.quota_ceiling.repair_rounds
1359
+ ):
1360
+ raise ValueError("root template quota exceeds WorkOrder ceiling")
1361
+ return self
1362
+
1363
+ def _validate_conditions(self, verifier_profile: TestProfile) -> None:
1364
+ partitions = (
1365
+ ("preconditions", self.preconditions, {"path_allowed", "tool_allowed", "quota_remaining"}),
1366
+ ("invariants", self.invariants, {"path_allowed", "tool_allowed"}),
1367
+ ("postconditions", self.postconditions, {"tests_passed", "artifact_digest_matches"}),
1368
+ )
1369
+ all_ids: list[str] = []
1370
+ for name, conditions, allowed_names in partitions:
1371
+ if len(conditions) > 32:
1372
+ raise ValueError(f"{name} exceeds 32 predicates")
1373
+ ids = [condition.predicate_id for condition in conditions]
1374
+ if ids != sorted(ids):
1375
+ raise ValueError(f"{name} is not predicate_id sorted")
1376
+ if any(condition.name not in allowed_names for condition in conditions):
1377
+ raise ValueError(f"{name} contains a predicate in the wrong phase")
1378
+ for condition in conditions:
1379
+ if not set(condition.applies_to_tools).intersection(
1380
+ self.allowed_tools
1381
+ ):
1382
+ raise ValueError("predicate does not apply to an allowed tool")
1383
+ all_ids.extend(ids)
1384
+ if len(all_ids) != len(set(all_ids)):
1385
+ raise ValueError("predicate ids must be globally unique")
1386
+ all_conditions = self.preconditions + self.invariants + self.postconditions
1387
+ for tool_name in _PREDICATE_TOOLS:
1388
+ applicable_ids = {
1389
+ condition.predicate_id
1390
+ for condition in all_conditions
1391
+ if tool_name in condition.applies_to_tools
1392
+ }
1393
+ if len(applicable_ids) > 64:
1394
+ raise ValueError("applicable predicate union exceeds 64")
1395
+
1396
+ for condition in self.preconditions + self.invariants:
1397
+ if condition.name == "path_allowed" and not set(
1398
+ condition.applies_to_tools
1399
+ ).issubset({"owp.repo_read", "owp.apply_patch"}):
1400
+ raise ValueError("path_allowed references a tool without path projection")
1401
+ for condition in self.postconditions:
1402
+ if condition.applies_to_tools != ("owp.run_tests",):
1403
+ raise ValueError("postconditions are verifier run_tests only")
1404
+ tests_passed = [
1405
+ condition
1406
+ for condition in self.postconditions
1407
+ if condition.name == "tests_passed"
1408
+ ]
1409
+ if len(tests_passed) != 1:
1410
+ raise ValueError("exactly one tests_passed postcondition is required")
1411
+ expected_arguments = {
1412
+ "test_mode": "verifier",
1413
+ "command_digest": verifier_profile.command_digest,
1414
+ "expected_exit_code": verifier_profile.expected_exit_code,
1415
+ "fixed_test_source_digest": verifier_profile.fixed_test_source_digest,
1416
+ }
1417
+ if tests_passed[0].arguments != expected_arguments:
1418
+ raise ValueError("tests_passed does not match verifier profile")
1419
+
1420
+
1421
+ class EvidenceRef(ProtocolModel):
1422
+ path: CanonicalRoot
1423
+ sha256: Digest64
1424
+ media_type: Literal["text/x-diff", "application/json"]
1425
+ size_bytes: SafePositiveInt
1426
+
1427
+ @model_validator(mode="after")
1428
+ def _validate_size(self) -> EvidenceRef:
1429
+ if self.size_bytes > MAX_ARTIFACT_BYTES:
1430
+ raise ValueError("evidence reference exceeds 8 MiB")
1431
+ return self
1432
+
1433
+
1434
+ class QuotaCharge(ProtocolModel):
1435
+ grant_id: Digest64
1436
+ metric: Literal["tool_calls", "repair_rounds"]
1437
+ amount: SafePositiveInt
1438
+ remaining_after: SafeNonNegativeInt
1439
+
1440
+
1441
+ class CorrelationFactors(ProtocolModel):
1442
+ model_id: ProtocolString
1443
+ model_version: ProtocolString
1444
+ prompt_template_digest: Digest64
1445
+ context_source_digest: Digest64
1446
+ toolchain_id: Digest64 | None
1447
+ execution_context_id: Digest64 | None
1448
+ container_instance_id_digest: Digest64 | None
1449
+ controller_id: KeyId
1450
+ fixed_test_source_digest: Digest64 | None
1451
+
1452
+
1453
+ class AgentRequest(SignedProtocolModel):
1454
+ claim_type: Literal["agent-request"]
1455
+ work_order_digest: Digest64
1456
+ grant_id: Digest64
1457
+ actor_id: Identifier
1458
+ actor_key_id: KeyId
1459
+ tool_name: Literal[
1460
+ "owp.activate_root_grant",
1461
+ "owp.apply_patch",
1462
+ "owp.compose_proof",
1463
+ "owp.create_pr_proposal",
1464
+ "owp.delegate_grant",
1465
+ "owp.repo_read",
1466
+ "owp.request_acceptance",
1467
+ "owp.request_pr_proposal",
1468
+ "owp.revoke_grant",
1469
+ "owp.rollback_patch",
1470
+ "owp.run_tests",
1471
+ "owp.start_retry",
1472
+ ]
1473
+ arguments_digest: Digest64
1474
+ nonce: Digest64
1475
+ requested_at: CanonicalUTCTime
1476
+ authentication_method: Literal["agent_signature"]
1477
+ model_id: ProtocolString
1478
+ model_version: ProtocolString
1479
+ prompt_template_digest: Digest64
1480
+ context_source_digest: Digest64
1481
+
1482
+ @model_validator(mode="after")
1483
+ def _validate_signer_identity(self) -> AgentRequest:
1484
+ if self.signer_key_id != self.actor_key_id:
1485
+ raise ValueError("AgentRequest signer must equal actor key")
1486
+ return self
1487
+
1488
+
1489
+ class ApprovalHumanDecision(SignedProtocolModel):
1490
+ claim_type: Literal["human-decision"]
1491
+ decision_type: Literal["approval_decision"]
1492
+ work_order_digest: Digest64
1493
+ decision: Literal["approved", "denied"]
1494
+ reason: Literal["APPROVAL_GRANTED", "APPROVAL_DENIED"]
1495
+ decided_at: CanonicalUTCTime
1496
+ actor_id: Identifier
1497
+ actor_key_id: KeyId
1498
+ request_receipt_id: Digest64
1499
+ request_receipt_digest: Digest64
1500
+ approved_scope: FrozenDict
1501
+ expires_at: CanonicalUTCTime
1502
+
1503
+ @model_validator(mode="after")
1504
+ def _validate_decision(self) -> ApprovalHumanDecision:
1505
+ if self.signer_key_id != self.actor_key_id:
1506
+ raise ValueError("HumanDecision signer must equal actor key")
1507
+ expected_reason = (
1508
+ "APPROVAL_GRANTED" if self.decision == "approved" else "APPROVAL_DENIED"
1509
+ )
1510
+ if self.reason != expected_reason:
1511
+ raise ValueError("approval decision reason does not match decision")
1512
+ return self
1513
+
1514
+
1515
+ class TerminationHumanDecision(SignedProtocolModel):
1516
+ claim_type: Literal["human-decision"]
1517
+ decision_type: Literal["termination_decision"]
1518
+ work_order_digest: Digest64
1519
+ decision: Literal["rejected"]
1520
+ reason: Literal["MAINTAINER_REJECTED"]
1521
+ decided_at: CanonicalUTCTime
1522
+ actor_id: Identifier
1523
+ actor_key_id: KeyId
1524
+ target_work_order_digest: Digest64
1525
+
1526
+ @model_validator(mode="after")
1527
+ def _validate_decision(self) -> TerminationHumanDecision:
1528
+ if self.signer_key_id != self.actor_key_id:
1529
+ raise ValueError("HumanDecision signer must equal actor key")
1530
+ if self.target_work_order_digest != self.work_order_digest:
1531
+ raise ValueError("termination target must equal WorkOrder digest")
1532
+ return self
1533
+
1534
+
1535
+ HumanDecision = Annotated[
1536
+ ApprovalHumanDecision | TerminationHumanDecision,
1537
+ Field(discriminator="decision_type"),
1538
+ ]
1539
+
1540
+
1541
+ class CompositionCause(ProtocolModel):
1542
+ initiator_receipt_digest: Digest64
1543
+ composition_report_digest: Digest64
1544
+ state_version_before: SafeNonNegativeInt
1545
+
1546
+
1547
+ class ContractExpiredCause(ProtocolModel):
1548
+ deadline: CanonicalUTCTime
1549
+ observed_at: CanonicalUTCTime
1550
+ tip_receipt_digest: Digest64 | None
1551
+
1552
+ @model_validator(mode="after")
1553
+ def _validate_observation(self) -> ContractExpiredCause:
1554
+ if self.observed_at <= self.deadline:
1555
+ raise ValueError("contract expiry observation must be after deadline")
1556
+ return self
1557
+
1558
+
1559
+ SystemEventCause = CompositionCause | ContractExpiredCause
1560
+
1561
+
1562
+ class SidecarEvent(ProtocolModel):
1563
+ claim_type: Literal["sidecar-event"]
1564
+ work_order_digest: Digest64
1565
+ event_name: Literal[
1566
+ "proof_composed", "contract_expired", "security_violation"
1567
+ ]
1568
+ cause: SystemEventCause
1569
+ input_digest: Digest64
1570
+ occurred_at: CanonicalUTCTime
1571
+
1572
+ @model_validator(mode="after")
1573
+ def _validate_event(self) -> SidecarEvent:
1574
+ if self.event_name == "contract_expired":
1575
+ if not isinstance(self.cause, ContractExpiredCause):
1576
+ raise ValueError("contract_expired requires its closed cause")
1577
+ if self.cause.observed_at != self.occurred_at:
1578
+ raise ValueError("expiry observation must equal event time")
1579
+ elif not isinstance(self.cause, CompositionCause):
1580
+ raise ValueError("composition event requires its closed cause")
1581
+ expected = _jcs_digest(
1582
+ {
1583
+ "domain": "openworkproof/system-event-input/v0.1",
1584
+ "event_name": self.event_name,
1585
+ "work_order_digest": self.work_order_digest,
1586
+ "cause": self.cause,
1587
+ }
1588
+ )
1589
+ if self.input_digest != expected:
1590
+ raise ValueError("system-event input digest does not match cause")
1591
+ return self
1592
+
1593
+
1594
+ class RepoReadArguments(ProtocolModel):
1595
+ path: CanonicalRoot
1596
+
1597
+
1598
+ class ApplyPatchArguments(ProtocolModel):
1599
+ target_paths: tuple[CanonicalRoot, ...]
1600
+ patch_digest: Digest64
1601
+ patch_size_bytes: SafePositiveInt
1602
+
1603
+ @model_validator(mode="after")
1604
+ def _validate_arguments(self) -> ApplyPatchArguments:
1605
+ if not 1 <= len(self.target_paths) <= 32:
1606
+ raise ValueError("target_paths must contain 1..32 paths")
1607
+ if not _is_utf8_sorted_unique(self.target_paths):
1608
+ raise ValueError("target_paths must be UTF-8 sorted and unique")
1609
+ if self.patch_size_bytes > MAX_PATCH_BYTES:
1610
+ raise ValueError("patch exceeds MAX_PATCH_BYTES")
1611
+ return self
1612
+
1613
+
1614
+ class RunTestsArguments(ProtocolModel):
1615
+ test_mode: Literal["developer", "verifier"]
1616
+ command_digest: Digest64
1617
+ source_commit: ObjectId40
1618
+ candidate_commit: ObjectId40
1619
+ workspace_manifest_digest: Digest64
1620
+ container_image_digest: ImageDigest
1621
+ fixed_test_source_digest: Digest64 | None
1622
+
1623
+ @model_validator(mode="after")
1624
+ def _validate_mode(self) -> RunTestsArguments:
1625
+ if (self.test_mode == "verifier") != (
1626
+ self.fixed_test_source_digest is not None
1627
+ ):
1628
+ raise ValueError("fixed test source does not match test mode")
1629
+ return self
1630
+
1631
+
1632
+ class CreatePrProposalArguments(ProtocolModel):
1633
+ target_patch_digest: Digest64
1634
+ approval_receipt_id: Digest64
1635
+ approval_receipt_digest: Digest64
1636
+
1637
+
1638
+ class ComposeProofArguments(ProtocolModel):
1639
+ expected_state_version: SafeNonNegativeInt
1640
+ previous_report_digest: Digest64 | None
1641
+
1642
+
1643
+ ToolRequestArguments = (
1644
+ RepoReadArguments
1645
+ | ApplyPatchArguments
1646
+ | RunTestsArguments
1647
+ | CreatePrProposalArguments
1648
+ | ComposeProofArguments
1649
+ )
1650
+
1651
+
1652
+ class PatchResultEvidence(ProtocolModel):
1653
+ schema_version: Literal["openworkproof-patch-result/0.1"]
1654
+ parent_commit: ObjectId40
1655
+ parent_manifest_digest: Digest64
1656
+ candidate_commit: ObjectId40
1657
+ workspace_manifest_digest: Digest64
1658
+ patch_digest: Digest64
1659
+ patch_size_bytes: SafePositiveInt
1660
+ replay_profile_digest: Digest64
1661
+
1662
+ @model_validator(mode="after")
1663
+ def _validate_size(self) -> PatchResultEvidence:
1664
+ if self.patch_size_bytes > MAX_PATCH_BYTES:
1665
+ raise ValueError("patch result exceeds MAX_PATCH_BYTES")
1666
+ return self
1667
+
1668
+
1669
+ class TestResultEvidence(ProtocolModel):
1670
+ schema_version: Literal["openworkproof-test-result/0.1"]
1671
+ test_mode: Literal["developer", "verifier"]
1672
+ command_digest: Digest64
1673
+ source_commit: ObjectId40
1674
+ candidate_commit: ObjectId40
1675
+ workspace_manifest_digest: Digest64
1676
+ container_image_digest: ImageDigest
1677
+ fixed_test_source_digest: Digest64 | None
1678
+ actual_exit_code: SafeNonNegativeInt
1679
+
1680
+ @model_validator(mode="after")
1681
+ def _validate_result(self) -> TestResultEvidence:
1682
+ if self.actual_exit_code > 255:
1683
+ raise ValueError("actual_exit_code must be in 0..255")
1684
+ if (self.test_mode == "verifier") != (
1685
+ self.fixed_test_source_digest is not None
1686
+ ):
1687
+ raise ValueError("fixed test source does not match test mode")
1688
+ return self
1689
+
1690
+
1691
+ class ReportDiagnostic(ProtocolModel):
1692
+ code: Literal[
1693
+ "SHARED_MODEL",
1694
+ "SHARED_PROMPT_TEMPLATE",
1695
+ "SHARED_CONTEXT_SOURCE",
1696
+ "SHARED_TOOLCHAIN",
1697
+ "SHARED_EXECUTION_CONTEXT",
1698
+ "SHARED_CONTROLLER",
1699
+ "SHARED_TEST_SOURCE",
1700
+ "MISSING_EVIDENCE_DIMENSION",
1701
+ "CAUSAL_INCOMPLETE",
1702
+ "INDEPENDENCE_UNSATISFIED",
1703
+ "GLOBAL_POSTCONDITION_FAILED",
1704
+ ]
1705
+ subject_ref: ProtocolString
1706
+
1707
+
1708
+ class CorrelationReference(ProtocolModel):
1709
+ receipt_digest: Digest64
1710
+ factors: CorrelationFactors
1711
+
1712
+
1713
+ _SHARED_FACTOR_ORDER = (
1714
+ "model",
1715
+ "prompt_template",
1716
+ "context_source",
1717
+ "toolchain",
1718
+ "execution_context",
1719
+ "controller",
1720
+ "test_source",
1721
+ )
1722
+
1723
+
1724
+ class IndependenceAssessment(ProtocolModel):
1725
+ policy: Literal["disclose_only", "independent_test_source_required"]
1726
+ developer_reference: CorrelationReference
1727
+ verifier_reference: CorrelationReference | None
1728
+ shared_factors: tuple[
1729
+ Literal[
1730
+ "model",
1731
+ "prompt_template",
1732
+ "context_source",
1733
+ "toolchain",
1734
+ "execution_context",
1735
+ "controller",
1736
+ "test_source",
1737
+ ],
1738
+ ...,
1739
+ ]
1740
+ satisfied: bool
1741
+
1742
+ @model_validator(mode="after")
1743
+ def _validate_assessment(self) -> IndependenceAssessment:
1744
+ positions = [_SHARED_FACTOR_ORDER.index(value) for value in self.shared_factors]
1745
+ if positions != sorted(set(positions)):
1746
+ raise ValueError("shared factors must be registry ordered and unique")
1747
+ if self.satisfied and self.verifier_reference is None:
1748
+ raise ValueError("satisfied assessment requires a verifier reference")
1749
+ if self.verifier_reference is None and self.shared_factors:
1750
+ raise ValueError("shared factors require a verifier reference")
1751
+ expected, _ = _recompute_shared_factors(self)
1752
+ if self.shared_factors != expected:
1753
+ raise ValueError("shared factors do not match correlation references")
1754
+ if self.satisfied and self.policy == "independent_test_source_required":
1755
+ developer = self.developer_reference.factors
1756
+ verifier = self.verifier_reference
1757
+ if verifier is None or (
1758
+ developer.execution_context_id
1759
+ == verifier.factors.execution_context_id
1760
+ or developer.container_instance_id_digest
1761
+ == verifier.factors.container_instance_id_digest
1762
+ or developer.fixed_test_source_digest is not None
1763
+ or verifier.factors.fixed_test_source_digest is None
1764
+ ):
1765
+ raise ValueError("independent verifier context is not fresh/bound")
1766
+ return self
1767
+
1768
+
1769
+ def _recompute_shared_factors(
1770
+ assessment: IndependenceAssessment,
1771
+ ) -> tuple[tuple[str, ...], dict[str, Any]]:
1772
+ verifier_reference = assessment.verifier_reference
1773
+ if verifier_reference is None:
1774
+ return (), {}
1775
+ developer = assessment.developer_reference.factors
1776
+ verifier = verifier_reference.factors
1777
+ comparisons: tuple[tuple[str, Any, bool], ...] = (
1778
+ (
1779
+ "model",
1780
+ {"model_id": developer.model_id, "model_version": developer.model_version},
1781
+ (developer.model_id, developer.model_version)
1782
+ == (verifier.model_id, verifier.model_version),
1783
+ ),
1784
+ (
1785
+ "prompt_template",
1786
+ developer.prompt_template_digest,
1787
+ developer.prompt_template_digest == verifier.prompt_template_digest,
1788
+ ),
1789
+ (
1790
+ "context_source",
1791
+ developer.context_source_digest,
1792
+ developer.context_source_digest == verifier.context_source_digest,
1793
+ ),
1794
+ (
1795
+ "toolchain",
1796
+ developer.toolchain_id,
1797
+ developer.toolchain_id is not None
1798
+ and developer.toolchain_id == verifier.toolchain_id,
1799
+ ),
1800
+ (
1801
+ "execution_context",
1802
+ {
1803
+ "execution_context_id": developer.execution_context_id,
1804
+ "container_instance_id_digest": (
1805
+ developer.container_instance_id_digest
1806
+ ),
1807
+ },
1808
+ developer.execution_context_id is not None
1809
+ and developer.container_instance_id_digest is not None
1810
+ and (
1811
+ developer.execution_context_id,
1812
+ developer.container_instance_id_digest,
1813
+ )
1814
+ == (
1815
+ verifier.execution_context_id,
1816
+ verifier.container_instance_id_digest,
1817
+ ),
1818
+ ),
1819
+ (
1820
+ "controller",
1821
+ developer.controller_id,
1822
+ developer.controller_id == verifier.controller_id,
1823
+ ),
1824
+ (
1825
+ "test_source",
1826
+ developer.fixed_test_source_digest,
1827
+ developer.fixed_test_source_digest is not None
1828
+ and developer.fixed_test_source_digest
1829
+ == verifier.fixed_test_source_digest,
1830
+ ),
1831
+ )
1832
+ values = {name: value for name, value, shared in comparisons if shared}
1833
+ return tuple(name for name, _, shared in comparisons if shared), values
1834
+
1835
+
1836
+ class FinalArtifact(ProtocolModel):
1837
+ active_patch_receipt_digest: Digest64
1838
+ candidate_commit: ObjectId40
1839
+ workspace_manifest_digest: Digest64
1840
+
1841
+
1842
+ _MCP_ERROR_CODES = Literal[
1843
+ "REQUEST_INTEGRITY_INVALID",
1844
+ "ROOT_ACTIVATION_INVALID",
1845
+ "RECOVERY_REQUIRED",
1846
+ "HANDLER_UNAVAILABLE",
1847
+ "EVIDENCE_SLOT_UNAVAILABLE",
1848
+ "INDEPENDENT_RESULT_NOT_READY",
1849
+ "EVIDENCE_FAILURE_SEALED",
1850
+ "COMPLETION_RESERVE_UNAVAILABLE",
1851
+ "DENIAL_AUDIT_LIMIT_EXCEEDED",
1852
+ "BUNDLE_CAPACITY_EXCEEDED",
1853
+ "CONTRACT_EXPIRED",
1854
+ ]
1855
+
1856
+
1857
+ class McpErrorEnvelope(ProtocolModel):
1858
+ schema_version: Literal["openworkproof-mcp-error/0.1"]
1859
+ code: _MCP_ERROR_CODES
1860
+ work_order_digest: Digest64 | None
1861
+ nonce: Digest64 | None
1862
+
1863
+ @classmethod
1864
+ def from_untrusted(
1865
+ cls,
1866
+ *,
1867
+ code: _MCP_ERROR_CODES,
1868
+ work_order_digest: Any,
1869
+ nonce: Any,
1870
+ ) -> McpErrorEnvelope:
1871
+ return cls(
1872
+ schema_version="openworkproof-mcp-error/0.1",
1873
+ code=code,
1874
+ work_order_digest=(
1875
+ work_order_digest
1876
+ if type(work_order_digest) is str
1877
+ and _LOWER_HEX_64.fullmatch(work_order_digest)
1878
+ else None
1879
+ ),
1880
+ nonce=(
1881
+ nonce
1882
+ if type(nonce) is str and _LOWER_HEX_64.fullmatch(nonce)
1883
+ else None
1884
+ ),
1885
+ )
1886
+
1887
+ @classmethod
1888
+ def startup_expiry(cls, work_order_digest: str) -> McpErrorEnvelope:
1889
+ return cls(
1890
+ schema_version="openworkproof-mcp-error/0.1",
1891
+ code="CONTRACT_EXPIRED",
1892
+ work_order_digest=work_order_digest,
1893
+ nonce=None,
1894
+ )
1895
+
1896
+ @classmethod
1897
+ def request_expiry(
1898
+ cls, work_order_digest: str, nonce: str
1899
+ ) -> McpErrorEnvelope:
1900
+ return cls(
1901
+ schema_version="openworkproof-mcp-error/0.1",
1902
+ code="CONTRACT_EXPIRED",
1903
+ work_order_digest=work_order_digest,
1904
+ nonce=nonce,
1905
+ )
1906
+
1907
+
1908
+ _TASK_STATES = Literal[
1909
+ "issued",
1910
+ "running",
1911
+ "needs_rework",
1912
+ "retrying",
1913
+ "locally_verified",
1914
+ "evidence_incomplete",
1915
+ "proof_ready",
1916
+ "awaiting_human",
1917
+ "accepted",
1918
+ "rejected",
1919
+ "frozen",
1920
+ ]
1921
+
1922
+ _POLICY_ERROR_CODES = Literal[
1923
+ "STATE_DENIED",
1924
+ "ROLE_DENIED",
1925
+ "CAPABILITY_DENIED",
1926
+ "APPROVAL_DENIED",
1927
+ "PREDICATE_DENIED",
1928
+ "QUOTA_EXHAUSTED",
1929
+ ]
1930
+
1931
+ _EXECUTION_ERROR_CODES = Literal[
1932
+ "HANDLER_ERROR",
1933
+ "OUTPUT_LIMIT",
1934
+ "TIMEOUT",
1935
+ "DISK_LIMIT",
1936
+ "WORKSPACE_INTEGRITY_FAILED",
1937
+ "EVIDENCE_POLICY_VIOLATION",
1938
+ "ROLLBACK_FAILED",
1939
+ ]
1940
+
1941
+ _ALLOWED_RECEIPT_OUTCOMES = {
1942
+ "grant_issued": {("allow", "succeeded"), ("deny", "denied")},
1943
+ "grant_consumed": {("allow", "succeeded"), ("deny", "denied")},
1944
+ "grant_revoked": {("allow", "succeeded"), ("deny", "denied")},
1945
+ "tool_call": {
1946
+ ("allow", "succeeded"),
1947
+ ("allow", "failed"),
1948
+ ("deny", "denied"),
1949
+ },
1950
+ "system_event": {("not_applicable", "succeeded")},
1951
+ "approval_requested": {("allow", "succeeded"), ("deny", "denied")},
1952
+ "approval_decision": {("allow", "succeeded"), ("deny", "denied")},
1953
+ "termination_decision": {("allow", "succeeded"), ("deny", "denied")},
1954
+ "rollback": {
1955
+ ("allow", "succeeded"),
1956
+ ("allow", "failed"),
1957
+ ("deny", "denied"),
1958
+ },
1959
+ }
1960
+
1961
+
1962
+ def request_arguments_digest(tool_name: str, arguments: Any) -> str:
1963
+ if tool_name not in _ALL_TOOLS:
1964
+ raise ValueError("unknown Agent request tool")
1965
+ return _jcs_digest(
1966
+ {
1967
+ "domain": "openworkproof/agent-arguments/v0.1",
1968
+ "tool_name": tool_name,
1969
+ "arguments": arguments,
1970
+ }
1971
+ )
1972
+
1973
+
1974
+ class ActionReceiptEnvelope(SignedProtocolModel):
1975
+ protocol_version: Literal["0.1"]
1976
+ receipt_id: Digest64
1977
+ work_order_digest: Digest64
1978
+ actor_type: Literal["agent", "human", "sidecar"]
1979
+ actor_id: Identifier
1980
+ actor_key_id: KeyId
1981
+ nested_claim_type: Literal["agent-request", "human-decision", "sidecar-event"]
1982
+ nested_claim_digest: Digest64
1983
+ nested_claim: (
1984
+ AgentRequest
1985
+ | ApprovalHumanDecision
1986
+ | TerminationHumanDecision
1987
+ | SidecarEvent
1988
+ )
1989
+ gateway_signer_key_id: KeyId
1990
+ event_type: Literal[
1991
+ "grant_issued",
1992
+ "grant_consumed",
1993
+ "grant_revoked",
1994
+ "tool_call",
1995
+ "system_event",
1996
+ "approval_requested",
1997
+ "approval_decision",
1998
+ "termination_decision",
1999
+ "rollback",
2000
+ ]
2001
+ policy_decision: Literal["allow", "deny", "not_applicable"]
2002
+ policy_error_code: _POLICY_ERROR_CODES | None
2003
+ execution_status: Literal["succeeded", "failed", "denied"]
2004
+ execution_error_code: _EXECUTION_ERROR_CODES | None
2005
+ quota_charge: QuotaCharge | None
2006
+ state_before: _TASK_STATES
2007
+ state_after: _TASK_STATES
2008
+ parent_receipt_ids: tuple[Digest64, ...]
2009
+ correlation_factors: CorrelationFactors | None
2010
+ evidence_refs: tuple[EvidenceRef, ...]
2011
+ occurred_at: CanonicalUTCTime
2012
+ sequence: SafePositiveInt
2013
+ nonce: Digest64
2014
+ previous_receipt_digest: Digest64 | None
2015
+
2016
+ @model_validator(mode="after")
2017
+ def _validate_envelope(self) -> ActionReceiptEnvelope:
2018
+ if self.signer_key_id != self.gateway_signer_key_id:
2019
+ raise ValueError("receipt signer must equal gateway signer")
2020
+ if len(self.parent_receipt_ids) > 16 or len(
2021
+ set(self.parent_receipt_ids)
2022
+ ) != len(self.parent_receipt_ids):
2023
+ raise ValueError("receipt parents exceed bounds or contain duplicates")
2024
+ paths = [reference.path for reference in self.evidence_refs]
2025
+ if (
2026
+ len(paths) > 8
2027
+ or paths != sorted(paths, key=lambda value: value.encode("utf-8"))
2028
+ or len(set(paths)) != len(paths)
2029
+ ):
2030
+ raise ValueError("evidence refs must be path-sorted, unique, and bounded")
2031
+ pair = (self.policy_decision, self.execution_status)
2032
+ if pair not in _ALLOWED_RECEIPT_OUTCOMES[self.event_type]:
2033
+ raise ValueError("receipt outcome is not legal for event")
2034
+ denied = pair == ("deny", "denied")
2035
+ failed = pair == ("allow", "failed")
2036
+ if denied != (self.policy_error_code is not None):
2037
+ raise ValueError("policy error nullability does not match outcome")
2038
+ if failed != (self.execution_error_code is not None):
2039
+ raise ValueError("execution error nullability does not match outcome")
2040
+ if denied and self.state_before != self.state_after:
2041
+ raise ValueError("denied receipt must be same-state")
2042
+ if self.event_type != "tool_call" and self.correlation_factors is not None:
2043
+ raise ValueError("correlation factors are tool-call-only")
2044
+ return self
2045
+
2046
+ def validate_against_work_order(
2047
+ self, work_order: WorkOrder
2048
+ ) -> ActionReceiptEnvelope:
2049
+ bindings = {binding.role: binding for binding in work_order.key_bindings}
2050
+ sidecar = bindings["Sidecar"]
2051
+ if (
2052
+ self.work_order_digest != work_order.digest
2053
+ or self.gateway_signer_key_id != sidecar.key_id
2054
+ or self.signer_key_id != sidecar.key_id
2055
+ ):
2056
+ raise ValueError("receipt gateway is not bound to WorkOrder Sidecar")
2057
+ if self.actor_type == "sidecar":
2058
+ allowed = (sidecar,)
2059
+ elif self.actor_type == "human":
2060
+ allowed = (bindings["Maintainer"],)
2061
+ else:
2062
+ allowed = (
2063
+ bindings["Manager"],
2064
+ bindings["Developer"],
2065
+ bindings["Verifier"],
2066
+ )
2067
+ if not any(
2068
+ self.actor_id == binding.subject_id
2069
+ and self.actor_key_id == binding.key_id
2070
+ for binding in allowed
2071
+ ):
2072
+ raise ValueError("receipt actor is not bound to WorkOrder")
2073
+ return self
2074
+
2075
+
2076
+ class AgentReceiptEnvelope(ActionReceiptEnvelope):
2077
+ actor_type: Literal["agent"]
2078
+ nested_claim_type: Literal["agent-request"]
2079
+ nested_claim: AgentRequest
2080
+
2081
+ def _expected_agent_request(self) -> tuple[str, dict[str, Any], str]:
2082
+ raise NotImplementedError
2083
+
2084
+ @model_validator(mode="after")
2085
+ def _validate_agent_claim(self) -> AgentReceiptEnvelope:
2086
+ claim = self.nested_claim
2087
+ if (
2088
+ self.nested_claim_digest != claim.digest
2089
+ or self.work_order_digest != claim.work_order_digest
2090
+ or self.actor_id != claim.actor_id
2091
+ or self.actor_key_id != claim.actor_key_id
2092
+ or self.nonce != claim.nonce
2093
+ ):
2094
+ raise ValueError("outer Agent receipt does not match nested claim")
2095
+ expected_tool, arguments, expected_grant = self._expected_agent_request()
2096
+ if claim.grant_id != expected_grant or claim.tool_name != expected_tool:
2097
+ raise ValueError("Agent claim Grant or tool does not match branch")
2098
+ expected_digest = request_arguments_digest(expected_tool, arguments)
2099
+ if claim.arguments_digest != expected_digest:
2100
+ raise ValueError("Agent semantic arguments do not match branch")
2101
+ return self
2102
+
2103
+
2104
+ class HumanReceiptEnvelope(ActionReceiptEnvelope):
2105
+ actor_type: Literal["human"]
2106
+ nested_claim_type: Literal["human-decision"]
2107
+ nested_claim: ApprovalHumanDecision | TerminationHumanDecision
2108
+
2109
+ @model_validator(mode="after")
2110
+ def _validate_human_claim(self) -> HumanReceiptEnvelope:
2111
+ claim = self.nested_claim
2112
+ if (
2113
+ self.nested_claim_digest != claim.digest
2114
+ or self.work_order_digest != claim.work_order_digest
2115
+ or self.actor_id != claim.actor_id
2116
+ or self.actor_key_id != claim.actor_key_id
2117
+ ):
2118
+ raise ValueError("outer Human receipt does not match nested claim")
2119
+ if self.quota_charge is not None:
2120
+ raise ValueError("human decisions never charge quota")
2121
+ return self
2122
+
2123
+
2124
+ class GrantIssuedReceipt(AgentReceiptEnvelope):
2125
+ event_type: Literal["grant_issued"]
2126
+ authorizing_grant_id: Digest64
2127
+ candidate_grant_digest: Digest64
2128
+ parent_grant_id: Digest64 | None
2129
+ issued_grant_id: Digest64 | None = None
2130
+
2131
+ def _expected_agent_request(self) -> tuple[str, dict[str, Any], str]:
2132
+ root = self.parent_grant_id is None
2133
+ arguments = {
2134
+ "operation": "activate_root" if root else "delegate_child",
2135
+ "authorizing_grant_id": self.authorizing_grant_id,
2136
+ "candidate_grant_digest": self.candidate_grant_digest,
2137
+ }
2138
+ return (
2139
+ "owp.activate_root_grant" if root else "owp.delegate_grant",
2140
+ arguments,
2141
+ self.authorizing_grant_id,
2142
+ )
2143
+
2144
+ @model_validator(mode="after")
2145
+ def _validate_issuance(self) -> GrantIssuedReceipt:
2146
+ denied = self.policy_decision == "deny"
2147
+ field_present = "issued_grant_id" in self.model_fields_set
2148
+ if denied:
2149
+ if self.parent_grant_id is None or field_present:
2150
+ raise ValueError("root denial or denied issued_grant_id is forbidden")
2151
+ elif not field_present or self.issued_grant_id is None:
2152
+ raise ValueError("successful issuance requires issued_grant_id")
2153
+ if self.parent_grant_id is None and (
2154
+ self.authorizing_grant_id != self.issued_grant_id
2155
+ ):
2156
+ raise ValueError("root authorizer and issued Grant must match")
2157
+ if self.parent_grant_id is not None and (
2158
+ self.parent_grant_id != self.authorizing_grant_id
2159
+ ):
2160
+ raise ValueError("child parent and authorizer must match")
2161
+ if self.quota_charge is not None:
2162
+ raise ValueError("grant issuance does not carry a direct charge")
2163
+ return self
2164
+
2165
+ def validate_candidate(self, candidate: CapabilityGrant) -> GrantIssuedReceipt:
2166
+ if (
2167
+ candidate.digest != self.candidate_grant_digest
2168
+ or candidate.parent_grant_id != self.parent_grant_id
2169
+ or (
2170
+ self.policy_decision == "allow"
2171
+ and candidate.grant_id != self.issued_grant_id
2172
+ )
2173
+ ):
2174
+ raise ValueError("candidate Grant does not match issuance receipt")
2175
+ return self
2176
+
2177
+ @model_serializer(mode="wrap")
2178
+ def _serialize_conditional_fields(self, handler: Any) -> dict[str, Any]:
2179
+ data = handler(self)
2180
+ if "issued_grant_id" not in self.model_fields_set:
2181
+ data.pop("issued_grant_id", None)
2182
+ return data
2183
+
2184
+
2185
+ class GrantConsumedReceipt(AgentReceiptEnvelope):
2186
+ event_type: Literal["grant_consumed"]
2187
+ grant_id: Digest64
2188
+ metric: Literal["repair_rounds"]
2189
+ amount: SafePositiveInt
2190
+ remaining_after: SafeNonNegativeInt | None
2191
+
2192
+ def _expected_agent_request(self) -> tuple[str, dict[str, Any], str]:
2193
+ return (
2194
+ "owp.start_retry",
2195
+ {"grant_id": self.grant_id, "metric": self.metric, "amount": self.amount},
2196
+ self.grant_id,
2197
+ )
2198
+
2199
+ @model_validator(mode="after")
2200
+ def _validate_consumption(self) -> GrantConsumedReceipt:
2201
+ if self.policy_decision == "deny":
2202
+ if self.remaining_after is not None or self.quota_charge is not None:
2203
+ raise ValueError("denied consumption cannot charge quota")
2204
+ else:
2205
+ charge = self.quota_charge
2206
+ if charge is None or (
2207
+ charge.grant_id,
2208
+ charge.metric,
2209
+ charge.amount,
2210
+ charge.remaining_after,
2211
+ ) != (
2212
+ self.grant_id,
2213
+ self.metric,
2214
+ self.amount,
2215
+ self.remaining_after,
2216
+ ):
2217
+ raise ValueError("grant consumption charge does not match branch")
2218
+ return self
2219
+
2220
+
2221
+ class GrantRevokedReceipt(AgentReceiptEnvelope):
2222
+ event_type: Literal["grant_revoked"]
2223
+ authorizing_grant_id: Digest64
2224
+ revoked_grant_id: Digest64
2225
+ revocation_reason: Literal["LEAST_PRIVILEGE", "SUPERSEDED", "WORK_STOPPED"]
2226
+
2227
+ def _expected_agent_request(self) -> tuple[str, dict[str, Any], str]:
2228
+ return (
2229
+ "owp.revoke_grant",
2230
+ {
2231
+ "authorizing_grant_id": self.authorizing_grant_id,
2232
+ "revoked_grant_id": self.revoked_grant_id,
2233
+ "revocation_reason": self.revocation_reason,
2234
+ },
2235
+ self.authorizing_grant_id,
2236
+ )
2237
+
2238
+ @model_validator(mode="after")
2239
+ def _validate_revocation(self) -> GrantRevokedReceipt:
2240
+ if self.quota_charge is not None:
2241
+ raise ValueError("grant revocation does not carry a direct charge")
2242
+ return self
2243
+
2244
+
2245
+ class ToolCallReceipt(AgentReceiptEnvelope):
2246
+ event_type: Literal["tool_call"]
2247
+ grant_id: Digest64
2248
+ tool_name: Literal[
2249
+ "owp.repo_read",
2250
+ "owp.apply_patch",
2251
+ "owp.run_tests",
2252
+ "owp.create_pr_proposal",
2253
+ "owp.compose_proof",
2254
+ ]
2255
+ tool_version: Literal["0.1"]
2256
+ request_arguments: ToolRequestArguments
2257
+ arguments_digest: Digest64
2258
+ output_digest: Digest64 | None
2259
+ predicate_results: tuple[PredicateResult, ...]
2260
+ approval_receipt_id: Digest64 | None = None
2261
+ approval_receipt_digest: Digest64 | None = None
2262
+
2263
+ def _expected_agent_request(self) -> tuple[str, dict[str, Any], str]:
2264
+ return (
2265
+ self.tool_name,
2266
+ self.request_arguments.model_dump(mode="json"),
2267
+ self.grant_id,
2268
+ )
2269
+
2270
+ @model_validator(mode="after")
2271
+ def _validate_tool_call(self) -> ToolCallReceipt:
2272
+ expected_types = {
2273
+ "owp.repo_read": RepoReadArguments,
2274
+ "owp.apply_patch": ApplyPatchArguments,
2275
+ "owp.run_tests": RunTestsArguments,
2276
+ "owp.create_pr_proposal": CreatePrProposalArguments,
2277
+ "owp.compose_proof": ComposeProofArguments,
2278
+ }
2279
+ if not isinstance(self.request_arguments, expected_types[self.tool_name]):
2280
+ raise ValueError("request arguments do not match registered tool")
2281
+ if len(rfc8785.dumps(_jsonable(self.request_arguments))) > 65_536:
2282
+ raise ValueError("request_arguments exceeds 64 KiB")
2283
+ expected_arguments_digest = request_arguments_digest(
2284
+ self.tool_name, self.request_arguments
2285
+ )
2286
+ if (
2287
+ self.arguments_digest != expected_arguments_digest
2288
+ or self.nested_claim.arguments_digest != self.arguments_digest
2289
+ ):
2290
+ raise ValueError("tool arguments digest does not match request")
2291
+ ids = [result.predicate_id for result in self.predicate_results]
2292
+ if ids != sorted(ids) or len(ids) != len(set(ids)):
2293
+ raise ValueError("predicate results must be sorted and unique")
2294
+
2295
+ high_risk = self.tool_name == "owp.create_pr_proposal"
2296
+ approval_fields = {
2297
+ "approval_receipt_id",
2298
+ "approval_receipt_digest",
2299
+ }
2300
+ present = approval_fields.issubset(self.model_fields_set)
2301
+ any_present = bool(approval_fields.intersection(self.model_fields_set))
2302
+ if high_risk:
2303
+ args = self.request_arguments
2304
+ if (
2305
+ not present
2306
+ or self.approval_receipt_id is None
2307
+ or self.approval_receipt_digest is None
2308
+ or not isinstance(args, CreatePrProposalArguments)
2309
+ or args.approval_receipt_id != self.approval_receipt_id
2310
+ or args.approval_receipt_digest != self.approval_receipt_digest
2311
+ ):
2312
+ raise ValueError("high-risk tool requires exact approval references")
2313
+ elif any_present:
2314
+ raise ValueError("low-risk tool forbids approval references")
2315
+
2316
+ denied = self.policy_decision == "deny"
2317
+ failed = self.execution_status == "failed"
2318
+ factors = self.correlation_factors
2319
+ if factors is None:
2320
+ raise ValueError("tool call requires correlation factors")
2321
+ if (
2322
+ factors.model_id,
2323
+ factors.model_version,
2324
+ factors.prompt_template_digest,
2325
+ factors.context_source_digest,
2326
+ ) != (
2327
+ self.nested_claim.model_id,
2328
+ self.nested_claim.model_version,
2329
+ self.nested_claim.prompt_template_digest,
2330
+ self.nested_claim.context_source_digest,
2331
+ ):
2332
+ raise ValueError("correlation disclosure does not match AgentRequest")
2333
+ control_tool = self.tool_name in {
2334
+ "owp.create_pr_proposal",
2335
+ "owp.compose_proof",
2336
+ }
2337
+ derived = (
2338
+ factors.toolchain_id,
2339
+ factors.execution_context_id,
2340
+ factors.container_instance_id_digest,
2341
+ )
2342
+ if denied or control_tool:
2343
+ if any(value is not None for value in derived) or (
2344
+ factors.fixed_test_source_digest is not None
2345
+ ):
2346
+ raise ValueError("non-started/control tool cannot claim container context")
2347
+ if failed:
2348
+ raise ValueError("control tools cannot produce failed receipts")
2349
+ elif any(value is None for value in derived):
2350
+ raise ValueError("started container tool requires runtime correlation")
2351
+
2352
+ if isinstance(self.request_arguments, RunTestsArguments):
2353
+ if (
2354
+ factors.fixed_test_source_digest
2355
+ != self.request_arguments.fixed_test_source_digest
2356
+ ):
2357
+ raise ValueError("fixed test correlation does not match request")
2358
+ elif factors.fixed_test_source_digest is not None:
2359
+ raise ValueError("fixed test digest is run_tests-only")
2360
+
2361
+ if denied:
2362
+ if self.output_digest is not None or self.quota_charge is not None:
2363
+ raise ValueError("denied tool has no output or quota charge")
2364
+ else:
2365
+ charge = self.quota_charge
2366
+ if charge is None or (
2367
+ charge.grant_id != self.grant_id
2368
+ or charge.metric != "tool_calls"
2369
+ or charge.amount != 1
2370
+ ):
2371
+ raise ValueError("started tool requires exact tool_calls charge")
2372
+ if self.output_digest is None:
2373
+ raise ValueError("started tool requires a closed output digest")
2374
+ if failed:
2375
+ expected = _jcs_digest(
2376
+ {
2377
+ "status": "failed",
2378
+ "error_code": self.execution_error_code,
2379
+ }
2380
+ )
2381
+ if self.output_digest != expected:
2382
+ raise ValueError("failed tool output digest is not redacted form")
2383
+ elif self.tool_name == "owp.create_pr_proposal":
2384
+ expected = _jcs_digest(
2385
+ {
2386
+ "status": "local_pr_proposal_created",
2387
+ "target_patch_digest": self.request_arguments.target_patch_digest,
2388
+ }
2389
+ )
2390
+ if self.output_digest != expected:
2391
+ raise ValueError("PR proposal output digest does not match")
2392
+ elif self.tool_name == "owp.compose_proof":
2393
+ if self.output_digest != _jcs_digest(
2394
+ {"status": "composition_request_accepted"}
2395
+ ):
2396
+ raise ValueError("composition output digest does not match")
2397
+
2398
+ refs = self.evidence_refs
2399
+ if self.tool_name == "owp.apply_patch":
2400
+ expected_ref_count = 1 if denied or failed else 2
2401
+ if len(refs) != expected_ref_count:
2402
+ raise ValueError("apply_patch evidence publication shape is invalid")
2403
+ diff_refs = [ref for ref in refs if ref.media_type == "text/x-diff"]
2404
+ json_refs = [ref for ref in refs if ref.media_type == "application/json"]
2405
+ if len(diff_refs) != 1 or (
2406
+ not (denied or failed) and len(json_refs) != 1
2407
+ ) or ((denied or failed) and json_refs):
2408
+ raise ValueError("apply_patch evidence media pair is invalid")
2409
+ args = self.request_arguments
2410
+ if not isinstance(args, ApplyPatchArguments) or (
2411
+ diff_refs[0].sha256 != args.patch_digest
2412
+ or diff_refs[0].size_bytes != args.patch_size_bytes
2413
+ ):
2414
+ raise ValueError("patch EvidenceRef does not match request")
2415
+ if not (denied or failed) and self.output_digest != json_refs[0].sha256:
2416
+ raise ValueError("patch output digest does not match result ref")
2417
+ elif self.tool_name == "owp.run_tests":
2418
+ if self.execution_status == "succeeded":
2419
+ if (
2420
+ len(refs) != 1
2421
+ or refs[0].media_type != "application/json"
2422
+ or self.output_digest != refs[0].sha256
2423
+ ):
2424
+ raise ValueError("run_tests result EvidenceRef is invalid")
2425
+ elif refs:
2426
+ raise ValueError("non-successful run_tests publishes no result")
2427
+ elif refs:
2428
+ raise ValueError("tool does not publish evidence")
2429
+ return self
2430
+
2431
+ def validate_against_work_order(self, work_order: WorkOrder) -> ToolCallReceipt:
2432
+ super().validate_against_work_order(work_order)
2433
+ sidecar = next(
2434
+ binding
2435
+ for binding in work_order.key_bindings
2436
+ if binding.role == "Sidecar"
2437
+ )
2438
+ factors = self.correlation_factors
2439
+ if (
2440
+ factors is None
2441
+ or factors.controller_id != sidecar.key_id
2442
+ or self.gateway_signer_key_id != factors.controller_id
2443
+ ):
2444
+ raise ValueError(
2445
+ "tool controller and gateway signer must equal WorkOrder Sidecar"
2446
+ )
2447
+ return self
2448
+
2449
+ def validate_predicates_against(self, work_order: WorkOrder) -> ToolCallReceipt:
2450
+ pre = tuple(
2451
+ spec
2452
+ for spec in work_order.preconditions + work_order.invariants
2453
+ if self.tool_name in spec.applies_to_tools
2454
+ )
2455
+ post: tuple[PredicateSpec, ...] = ()
2456
+ if (
2457
+ self.tool_name == "owp.run_tests"
2458
+ and isinstance(self.request_arguments, RunTestsArguments)
2459
+ and self.request_arguments.test_mode == "verifier"
2460
+ and self.policy_decision == "allow"
2461
+ ):
2462
+ post = tuple(
2463
+ spec
2464
+ for spec in work_order.postconditions
2465
+ if self.tool_name in spec.applies_to_tools
2466
+ )
2467
+ expected = sorted(pre + post, key=lambda spec: spec.predicate_id)
2468
+ if [item.predicate_id for item in self.predicate_results] != [
2469
+ item.predicate_id for item in expected
2470
+ ]:
2471
+ raise ValueError("predicate stage set does not match WorkOrder")
2472
+ for result, spec in zip(self.predicate_results, expected, strict=True):
2473
+ result.validate_against(spec)
2474
+ if spec in pre and self.policy_decision == "allow" and not result.passed:
2475
+ raise ValueError("failed precondition/invariant cannot authorize")
2476
+ if (
2477
+ spec in post
2478
+ and self.execution_status == "failed"
2479
+ and (result.passed or result.error_code != "FAIL_CLOSED")
2480
+ ):
2481
+ raise ValueError("failed Verifier postconditions must fail closed")
2482
+ return self
2483
+
2484
+ def validate_handler_output(self, output: Any) -> ToolCallReceipt:
2485
+ if self.execution_status != "succeeded" or self.output_digest is None:
2486
+ raise ValueError("only successful tool output can be validated")
2487
+ if self.tool_name == "owp.repo_read":
2488
+ parsed = RepoReadOutput.model_validate(output)
2489
+ expected = _jcs_digest(
2490
+ {
2491
+ "domain": "openworkproof/repo-read-output/v0.1",
2492
+ "output": parsed,
2493
+ }
2494
+ )
2495
+ elif self.tool_name == "owp.create_pr_proposal":
2496
+ if not isinstance(output, Mapping):
2497
+ raise ValueError("PR proposal output must be a closed object")
2498
+ parsed = FrozenDict(output)
2499
+ expected_object = {
2500
+ "status": "local_pr_proposal_created",
2501
+ "target_patch_digest": self.request_arguments.target_patch_digest,
2502
+ }
2503
+ if dict(parsed) != expected_object:
2504
+ raise ValueError("PR proposal output is not the closed result")
2505
+ expected = _jcs_digest(parsed)
2506
+ elif self.tool_name == "owp.compose_proof":
2507
+ if output != {"status": "composition_request_accepted"}:
2508
+ raise ValueError("composition output is not the closed result")
2509
+ expected = _jcs_digest(output)
2510
+ else:
2511
+ raise ValueError("tool output is validated through evidence bytes")
2512
+ if self.output_digest != expected:
2513
+ raise ValueError("handler output digest does not match receipt")
2514
+ return self
2515
+
2516
+ def validate_evidence_payloads(
2517
+ self,
2518
+ payloads: Mapping[str, bytes],
2519
+ work_order: WorkOrder,
2520
+ ) -> ToolCallReceipt:
2521
+ if set(payloads) != {reference.path for reference in self.evidence_refs}:
2522
+ raise ValueError("evidence payload set does not match receipt refs")
2523
+ policy_slots = {
2524
+ f"{work_order.evidence_policy.evidence_root}/{artifact.path}": artifact
2525
+ for artifact in work_order.evidence_policy.artifacts
2526
+ }
2527
+ referenced_slots: dict[str, Artifact] = {}
2528
+ for reference in self.evidence_refs:
2529
+ slot = policy_slots.get(reference.path)
2530
+ if slot is None or (
2531
+ slot.media_type != reference.media_type
2532
+ or reference.size_bytes > slot.max_size_bytes
2533
+ ):
2534
+ raise ValueError("evidence ref is not bound to a WorkOrder slot")
2535
+ referenced_slots[reference.path] = slot
2536
+ payload = payloads[reference.path]
2537
+ if type(payload) is not bytes:
2538
+ raise ValueError("evidence payload must be exact bytes")
2539
+ if (
2540
+ len(payload) != reference.size_bytes
2541
+ or hashlib.sha256(payload).hexdigest() != reference.sha256
2542
+ ):
2543
+ raise ValueError("evidence payload does not match reference")
2544
+ if self.tool_name == "owp.apply_patch":
2545
+ diff_ref = next(
2546
+ ref for ref in self.evidence_refs if ref.media_type == "text/x-diff"
2547
+ )
2548
+ diff_slot = referenced_slots[diff_ref.path]
2549
+ expected_diff_purpose = (
2550
+ "patch_denial_audit"
2551
+ if self.policy_decision == "deny"
2552
+ else "patch_input"
2553
+ )
2554
+ if diff_slot.purpose != expected_diff_purpose:
2555
+ raise ValueError("patch evidence ref has the wrong slot purpose")
2556
+
2557
+ if self.tool_name == "owp.apply_patch" and self.execution_status == "succeeded":
2558
+ json_ref = next(
2559
+ ref
2560
+ for ref in self.evidence_refs
2561
+ if ref.media_type == "application/json"
2562
+ )
2563
+ result_slot = referenced_slots[json_ref.path]
2564
+ if (
2565
+ result_slot.purpose != "patch_result"
2566
+ or result_slot.ordinal != diff_slot.ordinal
2567
+ ):
2568
+ raise ValueError("patch result slot must match patch input ordinal")
2569
+ result = PatchResultEvidence.model_validate(
2570
+ _load_canonical_json(payloads[json_ref.path])
2571
+ )
2572
+ args = self.request_arguments
2573
+ if not isinstance(args, ApplyPatchArguments) or (
2574
+ result.patch_digest != args.patch_digest
2575
+ or result.patch_size_bytes != args.patch_size_bytes
2576
+ or result.replay_profile_digest != work_order.replay_profile_digest
2577
+ or hashlib.sha256(payloads[diff_ref.path]).hexdigest()
2578
+ != result.patch_digest
2579
+ ):
2580
+ raise ValueError("PatchResultEvidence does not match request/context")
2581
+ elif self.tool_name == "owp.run_tests" and self.execution_status == "succeeded":
2582
+ result_ref = self.evidence_refs[0]
2583
+ args = self.request_arguments
2584
+ if not isinstance(args, RunTestsArguments):
2585
+ raise ValueError("run_tests request arguments are invalid")
2586
+ expected_purpose = (
2587
+ "verifier_result"
2588
+ if args.test_mode == "verifier"
2589
+ else "developer_test_result"
2590
+ )
2591
+ # The slot-to-episode mapping is strictly one-to-one: an
2592
+ # evidence_incomplete prior state is the independent episode
2593
+ # signature and must use the verifier_independent_result slot
2594
+ # exclusively; every primary-episode Verifier receipt must use
2595
+ # the verifier_result slot.
2596
+ if args.test_mode == "verifier":
2597
+ if self.state_before == "evidence_incomplete":
2598
+ allowed = {"verifier_independent_result"}
2599
+ else:
2600
+ allowed = {"verifier_result"}
2601
+ if referenced_slots[result_ref.path].purpose not in allowed:
2602
+ raise ValueError(
2603
+ "test result evidence has the wrong slot purpose"
2604
+ )
2605
+ else:
2606
+ if referenced_slots[result_ref.path].purpose != expected_purpose:
2607
+ raise ValueError(
2608
+ "test result evidence has the wrong slot purpose"
2609
+ )
2610
+ result = TestResultEvidence.model_validate(
2611
+ _load_canonical_json(payloads[result_ref.path])
2612
+ )
2613
+ profile = next(
2614
+ (
2615
+ profile
2616
+ for profile in work_order.test_profiles
2617
+ if profile.test_mode == args.test_mode
2618
+ ),
2619
+ None,
2620
+ )
2621
+ if profile is None or (
2622
+ result.test_mode,
2623
+ result.command_digest,
2624
+ result.source_commit,
2625
+ result.candidate_commit,
2626
+ result.workspace_manifest_digest,
2627
+ result.container_image_digest,
2628
+ result.fixed_test_source_digest,
2629
+ ) != (
2630
+ args.test_mode,
2631
+ args.command_digest,
2632
+ args.source_commit,
2633
+ args.candidate_commit,
2634
+ args.workspace_manifest_digest,
2635
+ args.container_image_digest,
2636
+ args.fixed_test_source_digest,
2637
+ ) or (
2638
+ args.command_digest != profile.command_digest
2639
+ or args.source_commit != work_order.source_commit
2640
+ or args.container_image_digest != profile.container_image_digest
2641
+ or args.fixed_test_source_digest != profile.fixed_test_source_digest
2642
+ ):
2643
+ raise ValueError("TestResultEvidence does not match request/profile")
2644
+ return self
2645
+
2646
+ @model_serializer(mode="wrap")
2647
+ def _serialize_conditional_fields(self, handler: Any) -> dict[str, Any]:
2648
+ data = handler(self)
2649
+ for field_name in ("approval_receipt_id", "approval_receipt_digest"):
2650
+ if field_name not in self.model_fields_set:
2651
+ data.pop(field_name, None)
2652
+ return data
2653
+
2654
+
2655
+ def _load_canonical_json(payload: bytes) -> Any:
2656
+ def reject_duplicates(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
2657
+ result: dict[str, Any] = {}
2658
+ for key, value in pairs:
2659
+ if key in result:
2660
+ raise ValueError("duplicate JSON object key")
2661
+ result[key] = value
2662
+ return result
2663
+
2664
+ try:
2665
+ value = json.loads(payload, object_pairs_hook=reject_duplicates)
2666
+ except (UnicodeDecodeError, json.JSONDecodeError) as error:
2667
+ raise ValueError("evidence JSON is invalid") from error
2668
+ if rfc8785.dumps(value) != payload:
2669
+ raise ValueError("evidence JSON is not canonical JCS")
2670
+ return value
2671
+
2672
+
2673
+ class RepoReadOutput(ProtocolModel):
2674
+ path: CanonicalRoot
2675
+ content_sha256: Digest64
2676
+ size_bytes: SafeNonNegativeInt
2677
+ workspace_manifest_digest: Digest64
2678
+
2679
+ @model_validator(mode="after")
2680
+ def _validate_size(self) -> RepoReadOutput:
2681
+ if self.size_bytes > 65_536:
2682
+ raise ValueError("repo_read output exceeds 64 KiB")
2683
+ return self
2684
+
2685
+
2686
+ class SystemEventReceipt(ActionReceiptEnvelope):
2687
+ event_type: Literal["system_event"]
2688
+ actor_type: Literal["sidecar"]
2689
+ nested_claim_type: Literal["sidecar-event"]
2690
+ nested_claim: SidecarEvent
2691
+ system_event_name: Literal[
2692
+ "proof_composed", "contract_expired", "security_violation"
2693
+ ]
2694
+ cause: SystemEventCause
2695
+ input_digest: Digest64
2696
+ error_code: Literal["CONTRACT_EXPIRED", "SECURITY_VIOLATION"] | None
2697
+
2698
+ @model_validator(mode="after")
2699
+ def _validate_system_event(self) -> SystemEventReceipt:
2700
+ claim = self.nested_claim
2701
+ if (
2702
+ self.actor_key_id != self.gateway_signer_key_id
2703
+ or self.nested_claim_digest != claim.input_digest
2704
+ or self.work_order_digest != claim.work_order_digest
2705
+ or self.system_event_name != claim.event_name
2706
+ or self.cause != claim.cause
2707
+ or self.input_digest != claim.input_digest
2708
+ or self.occurred_at != claim.occurred_at
2709
+ or self.quota_charge is not None
2710
+ or self.correlation_factors is not None
2711
+ ):
2712
+ raise ValueError("system event does not match Sidecar claim")
2713
+ if self.system_event_name == "proof_composed":
2714
+ if (
2715
+ self.error_code is not None
2716
+ or not isinstance(self.cause, CompositionCause)
2717
+ or self.state_after not in {"evidence_incomplete", "proof_ready"}
2718
+ or self.state_before not in {"locally_verified", "evidence_incomplete"}
2719
+ ):
2720
+ raise ValueError("proof_composed name/error/state matrix is invalid")
2721
+ elif self.system_event_name == "security_violation":
2722
+ if (
2723
+ self.error_code != "SECURITY_VIOLATION"
2724
+ or not isinstance(self.cause, CompositionCause)
2725
+ or self.state_before
2726
+ not in {"locally_verified", "evidence_incomplete"}
2727
+ or self.state_after != "frozen"
2728
+ ):
2729
+ raise ValueError("security_violation matrix is invalid")
2730
+ elif (
2731
+ self.error_code != "CONTRACT_EXPIRED"
2732
+ or not isinstance(self.cause, ContractExpiredCause)
2733
+ or self.state_before
2734
+ not in {
2735
+ "issued",
2736
+ "running",
2737
+ "needs_rework",
2738
+ "retrying",
2739
+ "locally_verified",
2740
+ "evidence_incomplete",
2741
+ "proof_ready",
2742
+ "awaiting_human",
2743
+ }
2744
+ or self.state_after != "frozen"
2745
+ ):
2746
+ raise ValueError("contract_expired matrix is invalid")
2747
+ if self.state_before == self.state_after:
2748
+ raise ValueError("system events cannot be same-state")
2749
+ return self
2750
+
2751
+
2752
+ class ApprovalRequestedReceipt(AgentReceiptEnvelope):
2753
+ event_type: Literal["approval_requested"]
2754
+ grant_id: Digest64
2755
+ request_kind: Literal["high_risk_action", "final_acceptance"]
2756
+ target_action_digest: Digest64
2757
+ required_role: Literal["Maintainer", "Acceptor"]
2758
+ requested_scope: FrozenDict
2759
+ expires_at: CanonicalUTCTime
2760
+
2761
+ def _expected_agent_request(self) -> tuple[str, dict[str, Any], str]:
2762
+ return (
2763
+ (
2764
+ "owp.request_acceptance"
2765
+ if self.request_kind == "final_acceptance"
2766
+ else "owp.request_pr_proposal"
2767
+ ),
2768
+ {
2769
+ "request_kind": self.request_kind,
2770
+ "target_action_digest": self.target_action_digest,
2771
+ "required_role": self.required_role,
2772
+ "requested_scope": self.requested_scope,
2773
+ "expires_at": _serialize_canonical_time(self.expires_at),
2774
+ },
2775
+ self.grant_id,
2776
+ )
2777
+
2778
+ @model_validator(mode="after")
2779
+ def _validate_request(self) -> ApprovalRequestedReceipt:
2780
+ scope = dict(self.requested_scope)
2781
+ if self.request_kind == "high_risk_action":
2782
+ if set(scope) != {
2783
+ "work_order_digest",
2784
+ "operation",
2785
+ "target_patch_digest",
2786
+ } or scope.get("work_order_digest") != self.work_order_digest or scope.get(
2787
+ "operation"
2788
+ ) != "create_local_pr_proposal":
2789
+ raise ValueError("high-risk approval scope is invalid")
2790
+ expected = _jcs_digest(
2791
+ {
2792
+ "domain": "openworkproof/high-risk-action/v0.1",
2793
+ "tool_name": "owp.create_pr_proposal",
2794
+ "requested_scope": self.requested_scope,
2795
+ }
2796
+ )
2797
+ else:
2798
+ if set(scope) != {
2799
+ "work_order_digest",
2800
+ "operation",
2801
+ "composition_report_digest",
2802
+ } or scope.get("work_order_digest") != self.work_order_digest or scope.get(
2803
+ "operation"
2804
+ ) != "submit_final_acceptance":
2805
+ raise ValueError("final acceptance scope is invalid")
2806
+ expected = _jcs_digest(
2807
+ {
2808
+ "domain": "openworkproof/final-acceptance-action/v0.1",
2809
+ "requested_scope": self.requested_scope,
2810
+ }
2811
+ )
2812
+ if self.target_action_digest != expected:
2813
+ raise ValueError("approval target_action_digest does not match scope")
2814
+ expected_role = (
2815
+ "Maintainer"
2816
+ if self.request_kind == "high_risk_action"
2817
+ else "Acceptor"
2818
+ )
2819
+ if self.required_role != expected_role:
2820
+ raise ValueError("approval request role does not match request kind")
2821
+ if self.policy_decision == "deny":
2822
+ if self.quota_charge is not None:
2823
+ raise ValueError("denied approval request cannot charge quota")
2824
+ else:
2825
+ charge = self.quota_charge
2826
+ if charge is None or (
2827
+ charge.grant_id != self.grant_id
2828
+ or charge.metric != "tool_calls"
2829
+ or charge.amount != 1
2830
+ ):
2831
+ raise ValueError("successful approval request requires exact charge")
2832
+ return self
2833
+
2834
+
2835
+ class ApprovalDecisionReceipt(HumanReceiptEnvelope):
2836
+ event_type: Literal["approval_decision"]
2837
+ nested_claim: ApprovalHumanDecision
2838
+ request_receipt_id: Digest64
2839
+ request_receipt_digest: Digest64
2840
+ decision: Literal["approved", "denied"]
2841
+ approved_scope: FrozenDict
2842
+ expires_at: CanonicalUTCTime
2843
+ decision_reason: Literal["APPROVAL_GRANTED", "APPROVAL_DENIED"]
2844
+ decided_at: CanonicalUTCTime
2845
+
2846
+ @model_validator(mode="after")
2847
+ def _validate_approval(self) -> ApprovalDecisionReceipt:
2848
+ claim = self.nested_claim
2849
+ if (
2850
+ claim.decision_type != self.event_type
2851
+ or claim.request_receipt_id != self.request_receipt_id
2852
+ or claim.request_receipt_digest != self.request_receipt_digest
2853
+ or claim.decision != self.decision
2854
+ or claim.approved_scope != self.approved_scope
2855
+ or claim.expires_at != self.expires_at
2856
+ or claim.reason != self.decision_reason
2857
+ or claim.decided_at != self.decided_at
2858
+ ):
2859
+ raise ValueError("approval branch does not match HumanDecision")
2860
+ return self
2861
+
2862
+
2863
+ class TerminationDecisionReceipt(HumanReceiptEnvelope):
2864
+ event_type: Literal["termination_decision"]
2865
+ nested_claim: TerminationHumanDecision
2866
+ target_work_order_digest: Digest64
2867
+ decision: Literal["rejected"]
2868
+ termination_reason: Literal["MAINTAINER_REJECTED"]
2869
+ decided_at: CanonicalUTCTime
2870
+
2871
+ @model_validator(mode="after")
2872
+ def _validate_termination(self) -> TerminationDecisionReceipt:
2873
+ claim = self.nested_claim
2874
+ if (
2875
+ claim.decision_type != self.event_type
2876
+ or self.target_work_order_digest != self.work_order_digest
2877
+ or claim.target_work_order_digest != self.target_work_order_digest
2878
+ or claim.decision != self.decision
2879
+ or claim.reason != self.termination_reason
2880
+ or claim.decided_at != self.decided_at
2881
+ ):
2882
+ raise ValueError("termination branch does not match HumanDecision")
2883
+ return self
2884
+
2885
+
2886
+ class RollbackReceipt(AgentReceiptEnvelope):
2887
+ event_type: Literal["rollback"]
2888
+ grant_id: Digest64
2889
+ target_patch_receipt_id: Digest64
2890
+ target_patch_digest: Digest64
2891
+ before_commit: ObjectId40
2892
+ after_commit: ObjectId40
2893
+ after_manifest_digest: Digest64 | None
2894
+ rollback_result: Literal["succeeded", "failed", "denied"]
2895
+
2896
+ def _expected_agent_request(self) -> tuple[str, dict[str, Any], str]:
2897
+ return (
2898
+ "owp.rollback_patch",
2899
+ {
2900
+ "target_patch_receipt_id": self.target_patch_receipt_id,
2901
+ "target_patch_digest": self.target_patch_digest,
2902
+ "before_commit": self.before_commit,
2903
+ },
2904
+ self.grant_id,
2905
+ )
2906
+
2907
+ @model_validator(mode="after")
2908
+ def _validate_rollback(self) -> RollbackReceipt:
2909
+ if self.rollback_result != self.execution_status:
2910
+ raise ValueError("rollback_result must mirror execution_status")
2911
+ if self.execution_status == "denied":
2912
+ if (
2913
+ self.after_commit != self.before_commit
2914
+ or self.after_manifest_digest is not None
2915
+ or self.quota_charge is not None
2916
+ ):
2917
+ raise ValueError("denied rollback must not start")
2918
+ else:
2919
+ charge = self.quota_charge
2920
+ if charge is None or (
2921
+ charge.grant_id != self.grant_id
2922
+ or charge.metric != "tool_calls"
2923
+ or charge.amount != 1
2924
+ ):
2925
+ raise ValueError("started rollback requires exact charge")
2926
+ if self.execution_status == "failed" and (
2927
+ self.after_commit != self.before_commit
2928
+ or self.after_manifest_digest is None
2929
+ ):
2930
+ raise ValueError("failed rollback must preserve candidate")
2931
+ if self.execution_status == "succeeded" and (
2932
+ self.after_commit == self.before_commit
2933
+ or self.after_manifest_digest is None
2934
+ ):
2935
+ raise ValueError("successful rollback must restore parent")
2936
+ return self
2937
+
2938
+ def validate_target_patch(
2939
+ self,
2940
+ target: ToolCallReceipt,
2941
+ result: PatchResultEvidence,
2942
+ ) -> RollbackReceipt:
2943
+ if (
2944
+ target.tool_name != "owp.apply_patch"
2945
+ or target.policy_decision != "allow"
2946
+ or target.execution_status != "succeeded"
2947
+ or self.target_patch_receipt_id != target.receipt_id
2948
+ or self.target_patch_digest != target.digest
2949
+ or self.before_commit != result.candidate_commit
2950
+ ):
2951
+ raise ValueError("rollback target is not the referenced successful patch")
2952
+ if self.execution_status == "failed" and (
2953
+ self.after_commit != result.candidate_commit
2954
+ or self.after_manifest_digest != result.workspace_manifest_digest
2955
+ ):
2956
+ raise ValueError("failed rollback does not preserve candidate context")
2957
+ if self.execution_status == "succeeded" and (
2958
+ self.after_commit != result.parent_commit
2959
+ or self.after_manifest_digest != result.parent_manifest_digest
2960
+ ):
2961
+ raise ValueError("successful rollback does not restore parent context")
2962
+ return self
2963
+
2964
+
2965
+ ActionReceipt = Annotated[
2966
+ GrantIssuedReceipt
2967
+ | GrantConsumedReceipt
2968
+ | GrantRevokedReceipt
2969
+ | ToolCallReceipt
2970
+ | SystemEventReceipt
2971
+ | ApprovalRequestedReceipt
2972
+ | ApprovalDecisionReceipt
2973
+ | TerminationDecisionReceipt
2974
+ | RollbackReceipt,
2975
+ Field(discriminator="event_type"),
2976
+ ]
2977
+
2978
+ # Low-level structural parser only. Protocol entrypoints must additionally bind
2979
+ # the parsed receipt to its WorkOrder via validate_receipt_or_error().
2980
+ ACTION_RECEIPT_ADAPTER = TypeAdapter(ActionReceipt)
2981
+
2982
+
2983
+ def validate_receipt_or_error(
2984
+ value: Any,
2985
+ *,
2986
+ work_order: WorkOrder | None = None,
2987
+ ) -> ActionReceipt | McpErrorEnvelope:
2988
+ try:
2989
+ receipt = ACTION_RECEIPT_ADAPTER.validate_python(value)
2990
+ if not isinstance(work_order, WorkOrder):
2991
+ raise ValueError("WorkOrder context is required")
2992
+ receipt.validate_against_work_order(work_order)
2993
+ if isinstance(receipt, ToolCallReceipt):
2994
+ receipt.validate_predicates_against(work_order)
2995
+ return receipt
2996
+ except (ValidationError, ValueError):
2997
+ source = value if isinstance(value, Mapping) else {}
2998
+ return McpErrorEnvelope.from_untrusted(
2999
+ code="REQUEST_INTEGRITY_INVALID",
3000
+ work_order_digest=source.get("work_order_digest"),
3001
+ nonce=source.get("nonce"),
3002
+ )
3003
+
3004
+
3005
+ _WARNING_CODES = {
3006
+ "SHARED_MODEL",
3007
+ "SHARED_PROMPT_TEMPLATE",
3008
+ "SHARED_CONTEXT_SOURCE",
3009
+ "SHARED_TOOLCHAIN",
3010
+ "SHARED_EXECUTION_CONTEXT",
3011
+ "SHARED_CONTROLLER",
3012
+ "SHARED_TEST_SOURCE",
3013
+ }
3014
+
3015
+
3016
+ class CompositionReport(ProtocolModel):
3017
+ schema_version: Literal["openworkproof-composition-report/0.1"]
3018
+ work_order_digest: Digest64
3019
+ initiator_receipt_id: Digest64
3020
+ initiator_receipt_digest: Digest64
3021
+ final_artifact: FinalArtifact
3022
+ artifact_digests: tuple[EvidenceRef, ...]
3023
+ evidence_snapshot_digest: Digest64
3024
+ receipt_digests: tuple[Digest64, ...]
3025
+ causal_graph_root: Digest64
3026
+ causal_complete: bool
3027
+ evidence_coverage: FrozenDict
3028
+ independence_assessment: IndependenceAssessment
3029
+ test_evidence_refs: tuple[EvidenceRef, ...]
3030
+ unresolved_failures: tuple[ReportDiagnostic, ...]
3031
+ warnings: tuple[ReportDiagnostic, ...]
3032
+ global_postconditions: tuple[PredicateResult, ...]
3033
+ global_postconditions_satisfied: bool
3034
+ verifier_conclusion: Literal["proof_ready", "evidence_incomplete"]
3035
+ composed_at: CanonicalUTCTime
3036
+
3037
+ @model_validator(mode="after")
3038
+ def _validate_report(self) -> CompositionReport:
3039
+ coverage = dict(self.evidence_coverage)
3040
+ if (
3041
+ not coverage
3042
+ or not set(coverage).issubset(_EVIDENCE_DIMENSIONS)
3043
+ or any(type(value) is not bool for value in coverage.values())
3044
+ ):
3045
+ raise ValueError("report evidence coverage must be closed")
3046
+ for values, key in (
3047
+ (self.artifact_digests, lambda item: item.path.encode("utf-8")),
3048
+ (self.test_evidence_refs, lambda item: item.path.encode("utf-8")),
3049
+ (
3050
+ self.warnings,
3051
+ lambda item: (
3052
+ item.code.encode("utf-8"),
3053
+ item.subject_ref.encode("utf-8"),
3054
+ ),
3055
+ ),
3056
+ (
3057
+ self.unresolved_failures,
3058
+ lambda item: (
3059
+ item.code.encode("utf-8"),
3060
+ item.subject_ref.encode("utf-8"),
3061
+ ),
3062
+ ),
3063
+ (
3064
+ self.global_postconditions,
3065
+ lambda item: item.predicate_id.encode("utf-8"),
3066
+ ),
3067
+ ):
3068
+ keys = [key(item) for item in values]
3069
+ if keys != sorted(keys) or len(keys) != len(set(keys)):
3070
+ raise ValueError("report arrays must be sorted and unique")
3071
+ if (
3072
+ not self.receipt_digests
3073
+ or len(set(self.receipt_digests)) != len(self.receipt_digests)
3074
+ ):
3075
+ raise ValueError("report receipt digests must be non-empty and unique")
3076
+ if any(item.code not in _WARNING_CODES for item in self.warnings):
3077
+ raise ValueError("report warnings use the warning registry only")
3078
+ shared_factors, shared_values = _recompute_shared_factors(
3079
+ self.independence_assessment
3080
+ )
3081
+ warning_codes = {
3082
+ "model": "SHARED_MODEL",
3083
+ "prompt_template": "SHARED_PROMPT_TEMPLATE",
3084
+ "context_source": "SHARED_CONTEXT_SOURCE",
3085
+ "toolchain": "SHARED_TOOLCHAIN",
3086
+ "execution_context": "SHARED_EXECUTION_CONTEXT",
3087
+ "controller": "SHARED_CONTROLLER",
3088
+ "test_source": "SHARED_TEST_SOURCE",
3089
+ }
3090
+ expected_warnings = tuple(
3091
+ sorted(
3092
+ (
3093
+ ReportDiagnostic(
3094
+ code=warning_codes[factor],
3095
+ subject_ref=_jcs_digest(
3096
+ {
3097
+ "domain": "openworkproof/shared-factor-ref/v0.1",
3098
+ "factor": factor,
3099
+ "value": shared_values[factor],
3100
+ }
3101
+ ),
3102
+ )
3103
+ for factor in shared_factors
3104
+ ),
3105
+ key=lambda item: (
3106
+ item.code.encode("utf-8"),
3107
+ item.subject_ref.encode("utf-8"),
3108
+ ),
3109
+ )
3110
+ )
3111
+ if self.warnings != expected_warnings:
3112
+ raise ValueError("report warnings do not match shared factors")
3113
+ if self.verifier_conclusion == "proof_ready":
3114
+ if (
3115
+ not self.causal_complete
3116
+ or self.unresolved_failures
3117
+ or not self.independence_assessment.satisfied
3118
+ or not self.global_postconditions_satisfied
3119
+ or any(
3120
+ type(value) is not bool or not value
3121
+ for value in coverage.values()
3122
+ )
3123
+ ):
3124
+ raise ValueError("proof-ready report is not closed")
3125
+ else:
3126
+ if not self.unresolved_failures:
3127
+ raise ValueError("incomplete report requires closed diagnostics")
3128
+ return self
3129
+
3130
+
3131
+ class AcceptanceReceipt(SignedProtocolModel):
3132
+ protocol_version: Literal["0.1"]
3133
+ acceptance_id: Digest64
3134
+ work_order_digest: Digest64
3135
+ acceptance_request_receipt_id: Digest64
3136
+ acceptance_request_receipt_digest: Digest64
3137
+ composition_report_digest: Digest64
3138
+ final_artifact: FinalArtifact
3139
+ artifact_digests: tuple[EvidenceRef, ...]
3140
+ evidence_snapshot_digest: Digest64
3141
+ receipt_digests: tuple[Digest64, ...]
3142
+ causal_graph_root: Digest64
3143
+ causal_complete: Literal[True]
3144
+ evidence_coverage: FrozenDict
3145
+ independence_assessment: IndependenceAssessment
3146
+ test_evidence_refs: tuple[EvidenceRef, ...]
3147
+ decision: Literal["accepted"]
3148
+ unresolved_failures: tuple[ReportDiagnostic, ...]
3149
+ warnings: tuple[ReportDiagnostic, ...]
3150
+ global_postconditions: tuple[PredicateResult, ...]
3151
+ global_postconditions_satisfied: Literal[True]
3152
+ verifier_conclusion: Literal["proof_ready"]
3153
+ accepted_at: CanonicalUTCTime
3154
+
3155
+ @model_validator(mode="after")
3156
+ def _validate_acceptance(self) -> AcceptanceReceipt:
3157
+ coverage = dict(self.evidence_coverage)
3158
+ if (
3159
+ not coverage
3160
+ or not set(coverage).issubset(_EVIDENCE_DIMENSIONS)
3161
+ or any(type(value) is not bool or not value for value in coverage.values())
3162
+ ):
3163
+ raise ValueError("acceptance evidence coverage must be closed and true")
3164
+ if not self.independence_assessment.satisfied:
3165
+ raise ValueError("acceptance requires satisfied independence")
3166
+ if self.unresolved_failures:
3167
+ raise ValueError("acceptance cannot contain unresolved failures")
3168
+ for values, key in (
3169
+ (self.artifact_digests, lambda item: item.path.encode("utf-8")),
3170
+ (self.test_evidence_refs, lambda item: item.path.encode("utf-8")),
3171
+ (
3172
+ self.warnings,
3173
+ lambda item: (
3174
+ item.code.encode("utf-8"),
3175
+ item.subject_ref.encode("utf-8"),
3176
+ ),
3177
+ ),
3178
+ (
3179
+ self.global_postconditions,
3180
+ lambda item: item.predicate_id.encode("utf-8"),
3181
+ ),
3182
+ ):
3183
+ keys = [key(item) for item in values]
3184
+ if keys != sorted(keys) or len(keys) != len(set(keys)):
3185
+ raise ValueError("acceptance arrays must be sorted and unique")
3186
+ if any(item.code not in _WARNING_CODES for item in self.warnings):
3187
+ raise ValueError("acceptance warnings use the warning registry only")
3188
+ shared_factors, shared_values = _recompute_shared_factors(
3189
+ self.independence_assessment
3190
+ )
3191
+ warning_codes = {
3192
+ "model": "SHARED_MODEL",
3193
+ "prompt_template": "SHARED_PROMPT_TEMPLATE",
3194
+ "context_source": "SHARED_CONTEXT_SOURCE",
3195
+ "toolchain": "SHARED_TOOLCHAIN",
3196
+ "execution_context": "SHARED_EXECUTION_CONTEXT",
3197
+ "controller": "SHARED_CONTROLLER",
3198
+ "test_source": "SHARED_TEST_SOURCE",
3199
+ }
3200
+ expected_warnings = tuple(
3201
+ sorted(
3202
+ (
3203
+ ReportDiagnostic(
3204
+ code=warning_codes[factor],
3205
+ subject_ref=_jcs_digest(
3206
+ {
3207
+ "domain": "openworkproof/shared-factor-ref/v0.1",
3208
+ "factor": factor,
3209
+ "value": shared_values[factor],
3210
+ }
3211
+ ),
3212
+ )
3213
+ for factor in shared_factors
3214
+ ),
3215
+ key=lambda item: (
3216
+ item.code.encode("utf-8"),
3217
+ item.subject_ref.encode("utf-8"),
3218
+ ),
3219
+ )
3220
+ )
3221
+ if self.warnings != expected_warnings:
3222
+ raise ValueError("acceptance warnings do not match shared factors")
3223
+ if (
3224
+ not self.receipt_digests
3225
+ or len(set(self.receipt_digests)) != len(self.receipt_digests)
3226
+ ):
3227
+ raise ValueError("receipt digests must be non-empty and unique")
3228
+ if not self.global_postconditions or any(
3229
+ not result.passed for result in self.global_postconditions
3230
+ ):
3231
+ raise ValueError("acceptance requires passed global postconditions")
3232
+ return self
3233
+
3234
+ def validate_against_work_order(self, work_order: WorkOrder) -> AcceptanceReceipt:
3235
+ maintainer = work_order.key_bindings[0]
3236
+ acceptor = work_order.key_bindings[5]
3237
+ if (
3238
+ self.work_order_digest != work_order.digest
3239
+ or self.signer_key_id != acceptor.key_id
3240
+ or self.signer_key_id == maintainer.key_id
3241
+ or set(self.evidence_coverage)
3242
+ != set(work_order.required_evidence_dimensions)
3243
+ or [result.predicate_id for result in self.global_postconditions]
3244
+ != [result.predicate_id for result in work_order.postconditions]
3245
+ ):
3246
+ raise ValueError("AcceptanceReceipt does not match WorkOrder")
3247
+ for result, spec in zip(
3248
+ self.global_postconditions, work_order.postconditions, strict=True
3249
+ ):
3250
+ result.validate_against(spec)
3251
+ return self
3252
+
3253
+
3254
+ class AcceptanceRejectionReceipt(SignedProtocolModel):
3255
+ protocol_version: Literal["0.1"]
3256
+ rejection_id: Digest64
3257
+ work_order_digest: Digest64
3258
+ acceptance_request_receipt_id: Digest64
3259
+ acceptance_request_receipt_digest: Digest64
3260
+ composition_report_digest: Digest64
3261
+ evidence_snapshot_digest: Digest64
3262
+ receipt_digests: tuple[Digest64, ...]
3263
+ causal_graph_root: Digest64
3264
+ reason_code: Literal[
3265
+ "EVIDENCE_INSUFFICIENT",
3266
+ "INDEPENDENCE_UNSATISFIED",
3267
+ "GLOBAL_POSTCONDITION_FAILED",
3268
+ "BUSINESS_DECISION",
3269
+ ]
3270
+ reason_detail: str
3271
+ decision: Literal["rejected"]
3272
+ rejected_at: CanonicalUTCTime
3273
+
3274
+ @model_validator(mode="after")
3275
+ def _validate_rejection(self) -> AcceptanceRejectionReceipt:
3276
+ if type(self.reason_detail) is not str or len(self.reason_detail) > 1024:
3277
+ raise ValueError("rejection reason detail is invalid")
3278
+ if (
3279
+ not self.receipt_digests
3280
+ or len(set(self.receipt_digests)) != len(self.receipt_digests)
3281
+ ):
3282
+ raise ValueError("receipt digests must be non-empty and unique")
3283
+ return self
3284
+
3285
+ def validate_against_work_order(
3286
+ self, work_order: WorkOrder
3287
+ ) -> AcceptanceRejectionReceipt:
3288
+ maintainer = work_order.key_bindings[0]
3289
+ acceptor = work_order.key_bindings[5]
3290
+ if (
3291
+ self.work_order_digest != work_order.digest
3292
+ or self.signer_key_id != acceptor.key_id
3293
+ or self.signer_key_id == maintainer.key_id
3294
+ ):
3295
+ raise ValueError(
3296
+ "AcceptanceRejectionReceipt does not match WorkOrder"
3297
+ )
3298
+ return self
3299
+
3300
+
3301
+ class PolicyDecision(ProtocolModel):
3302
+ allowed: bool
3303
+ decision: Literal["allow", "deny"]
3304
+ error_code: ProtocolString | None
3305
+ reason: ProtocolString
3306
+
3307
+ @model_validator(mode="after")
3308
+ def _validate_decision(self) -> PolicyDecision:
3309
+ if self.allowed != (self.decision == "allow"):
3310
+ raise ValueError("allowed and decision are inconsistent")
3311
+ if self.allowed != (self.error_code is None):
3312
+ raise ValueError("allowed and error_code are inconsistent")
3313
+ return self
3314
+
3315
+
3316
+ class TransitionDecision(ProtocolModel):
3317
+ allowed: bool
3318
+ error_code: ProtocolString | None
3319
+ reason: ProtocolString
3320
+
3321
+ @model_validator(mode="after")
3322
+ def _validate_decision(self) -> TransitionDecision:
3323
+ if self.allowed != (self.error_code is None):
3324
+ raise ValueError("allowed and error_code are inconsistent")
3325
+ return self
3326
+
3327
+
3328
+ __all__ = [
3329
+ "ACTION_RECEIPT_ADAPTER",
3330
+ "AcceptanceReceipt",
3331
+ "ActionReceipt",
3332
+ "AgentRequest",
3333
+ "ApprovalGate",
3334
+ "ApprovalDecisionReceipt",
3335
+ "ApprovalHumanDecision",
3336
+ "ApprovalRequestedReceipt",
3337
+ "Artifact",
3338
+ "BUNDLE_FINALIZATION_GRACE_SECONDS",
3339
+ "CapabilityGrant",
3340
+ "Command",
3341
+ "CompositionReport",
3342
+ "CorrelationFactors",
3343
+ "EvidencePolicy",
3344
+ "EvidenceRef",
3345
+ "FixedTestSource",
3346
+ "GrantConsumedReceipt",
3347
+ "GrantIssuedReceipt",
3348
+ "GrantRevokedReceipt",
3349
+ "HumanDecision",
3350
+ "IndependenceAssessment",
3351
+ "KeyBinding",
3352
+ "McpErrorEnvelope",
3353
+ "PatchResultEvidence",
3354
+ "PolicyDecision",
3355
+ "PredicateResult",
3356
+ "PredicateSpec",
3357
+ "ProtocolModel",
3358
+ "QuotaCharge",
3359
+ "ReportDiagnostic",
3360
+ "ReplayProfile",
3361
+ "RepoReadOutput",
3362
+ "RootGrantTemplate",
3363
+ "RollbackReceipt",
3364
+ "SafeNonNegativeInt",
3365
+ "SafePositiveInt",
3366
+ "SidecarEvent",
3367
+ "SignedProtocolModel",
3368
+ "SourceArtifact",
3369
+ "SystemEventReceipt",
3370
+ "TestProfile",
3371
+ "TestResultEvidence",
3372
+ "TerminationDecisionReceipt",
3373
+ "TerminationHumanDecision",
3374
+ "ToolCallReceipt",
3375
+ "TransitionDecision",
3376
+ "WorkOrder",
3377
+ "request_arguments_digest",
3378
+ "validate_receipt_or_error",
3379
+ "validate_fixed_test_source_bytes",
3380
+ ]