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,2939 @@
1
+ """Trusted MCP handler coordination primitives.
2
+
3
+ The transport server is intentionally deferred. This module closes trusted
4
+ handler paths so an adapter cannot return before its receipt and evidence are
5
+ committed.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Callable, Mapping
11
+ from dataclasses import dataclass
12
+ from datetime import datetime, timezone
13
+ import hashlib
14
+ import json
15
+ from pathlib import Path
16
+ import re
17
+ import secrets
18
+ import sqlite3
19
+ from typing import Literal
20
+
21
+ import rfc8785
22
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
23
+ Ed25519PrivateKey,
24
+ )
25
+
26
+ import openworkproof.evidence as evidence
27
+ import openworkproof.repo_tools as repo_tools
28
+ import openworkproof.runtime_context as runtime_context
29
+ from openworkproof.models import (
30
+ ACTION_RECEIPT_ADAPTER,
31
+ ActionReceiptEnvelope,
32
+ AgentRequest,
33
+ EvidenceRef,
34
+ GrantIssuedReceipt,
35
+ PolicyDecision,
36
+ RepoReadArguments,
37
+ RollbackReceipt,
38
+ RunTestsArguments,
39
+ SystemEventReceipt,
40
+ TestResultEvidence,
41
+ ToolCallReceipt,
42
+ ToolRequestArguments,
43
+ WorkOrder,
44
+ request_arguments_digest,
45
+ )
46
+ from openworkproof.policy import (
47
+ AuthorizationContext,
48
+ AuthorizationLedgerPrefix,
49
+ ProspectiveExecutionFacts,
50
+ authorize_tool_call,
51
+ derive_authorization_context,
52
+ validate_rollback,
53
+ )
54
+ from openworkproof.predicates import (
55
+ EvaluationContext,
56
+ evaluate_required_predicates,
57
+ select_required_predicates,
58
+ )
59
+ from openworkproof.repo_tools import (
60
+ CandidateWorkspace,
61
+ RollbackRequest as WorkspaceRollbackRequest,
62
+ rollback_candidate_workspace,
63
+ )
64
+ from openworkproof.signing import key_id, sign_payload, verify_nested_claim
65
+
66
+
67
+ class ToolCallDenied(RuntimeError):
68
+ """The authenticated request reached policy and was denied."""
69
+
70
+ def __init__(self, decision: PolicyDecision) -> None:
71
+ super().__init__(decision.error_code or "tool call denied")
72
+ self.decision = decision
73
+
74
+
75
+ class HandlerCoordinationError(RuntimeError):
76
+ """The trusted handler coordinator could not preserve its boundary."""
77
+
78
+
79
+ def _require_current_context(
80
+ ledger_path: Path,
81
+ evidence_root: Path,
82
+ context: AuthorizationContext,
83
+ now: datetime,
84
+ lock_descriptor: int,
85
+ ) -> None:
86
+ try:
87
+ runtime_context.require_current_context(
88
+ ledger_path,
89
+ evidence_root,
90
+ context,
91
+ now,
92
+ lock_descriptor,
93
+ )
94
+ except runtime_context.RuntimeContextError as error:
95
+ raise HandlerCoordinationError(str(error)) from error
96
+
97
+
98
+ @dataclass(frozen=True, slots=True)
99
+ class RollbackCommand:
100
+ target_patch_receipt_id: str
101
+ target_patch_digest: str
102
+ before_commit: str
103
+
104
+ def __post_init__(self) -> None:
105
+ digest = re.compile(r"^[0-9a-f]{64}$")
106
+ commit = re.compile(r"^[0-9a-f]{40}$")
107
+ if (
108
+ type(self.target_patch_receipt_id) is not str
109
+ or digest.fullmatch(self.target_patch_receipt_id) is None
110
+ or type(self.target_patch_digest) is not str
111
+ or digest.fullmatch(self.target_patch_digest) is None
112
+ or type(self.before_commit) is not str
113
+ or commit.fullmatch(self.before_commit) is None
114
+ ):
115
+ raise HandlerCoordinationError("rollback command is malformed")
116
+
117
+
118
+ @dataclass(frozen=True, slots=True)
119
+ class RollbackHandlerResult:
120
+ execution_status: Literal["succeeded", "failed"]
121
+ before_commit: str
122
+ after_commit: str
123
+ after_manifest_digest: str
124
+
125
+ def __post_init__(self) -> None:
126
+ commit = re.compile(r"^[0-9a-f]{40}$")
127
+ digest = re.compile(r"^[0-9a-f]{64}$")
128
+ if (
129
+ self.execution_status not in {"succeeded", "failed"}
130
+ or type(self.before_commit) is not str
131
+ or commit.fullmatch(self.before_commit) is None
132
+ or type(self.after_commit) is not str
133
+ or commit.fullmatch(self.after_commit) is None
134
+ or type(self.after_manifest_digest) is not str
135
+ or digest.fullmatch(self.after_manifest_digest) is None
136
+ or (
137
+ self.execution_status == "succeeded"
138
+ and self.after_commit == self.before_commit
139
+ )
140
+ or (
141
+ self.execution_status == "failed"
142
+ and self.after_commit != self.before_commit
143
+ )
144
+ ):
145
+ raise HandlerCoordinationError("rollback handler result is malformed")
146
+
147
+
148
+ def make_candidate_rollback_handler(
149
+ *,
150
+ workspace: CandidateWorkspace,
151
+ failure_target_patch_receipt_id: str,
152
+ failure_target_patch_receipt_digest: str,
153
+ before_commit: str,
154
+ before_manifest_digest: str,
155
+ parent_commit: str,
156
+ parent_manifest_digest: str,
157
+ ) -> Callable[[RollbackCommand], RollbackHandlerResult]:
158
+ """Bind one trusted candidate checkpoint to the rollback coordinator."""
159
+
160
+ frozen_command = RollbackCommand(
161
+ target_patch_receipt_id=failure_target_patch_receipt_id,
162
+ target_patch_digest=failure_target_patch_receipt_digest,
163
+ before_commit=before_commit,
164
+ )
165
+ if (
166
+ type(workspace) is not CandidateWorkspace
167
+ or type(before_manifest_digest) is not str
168
+ or re.fullmatch(r"[0-9a-f]{64}", before_manifest_digest) is None
169
+ ):
170
+ raise HandlerCoordinationError(
171
+ "candidate rollback binding is malformed"
172
+ )
173
+ RollbackHandlerResult(
174
+ execution_status="succeeded",
175
+ before_commit=before_commit,
176
+ after_commit=parent_commit,
177
+ after_manifest_digest=parent_manifest_digest,
178
+ )
179
+
180
+ def handler(command: RollbackCommand) -> RollbackHandlerResult:
181
+ if type(command) is not RollbackCommand or command != frozen_command:
182
+ raise HandlerCoordinationError(
183
+ "rollback command does not match frozen workspace target"
184
+ )
185
+ result = rollback_candidate_workspace(
186
+ WorkspaceRollbackRequest(
187
+ workspace=workspace,
188
+ target_patch_receipt_id=command.target_patch_receipt_id,
189
+ target_patch_receipt_digest=command.target_patch_digest,
190
+ failure_target_patch_receipt_id=(
191
+ failure_target_patch_receipt_id
192
+ ),
193
+ failure_target_patch_receipt_digest=(
194
+ failure_target_patch_receipt_digest
195
+ ),
196
+ before_commit=command.before_commit,
197
+ before_manifest_digest=before_manifest_digest,
198
+ parent_commit=parent_commit,
199
+ parent_manifest_digest=parent_manifest_digest,
200
+ )
201
+ )
202
+ return RollbackHandlerResult(
203
+ execution_status=result.execution_status,
204
+ before_commit=result.before_commit,
205
+ after_commit=result.after_commit,
206
+ after_manifest_digest=result.after_manifest_digest,
207
+ )
208
+
209
+ return handler
210
+
211
+
212
+ _MAX_RECEIPT_BYTES = 64 * 1024
213
+ _MAX_AGENT_REQUEST_BYTES = 8_192
214
+ _MAX_AUTHORIZATION_PREFIX_BYTES = 8 * 1024 * 1024
215
+
216
+
217
+ def _digest(value: object) -> str:
218
+ return hashlib.sha256(rfc8785.dumps(value)).hexdigest()
219
+
220
+
221
+ def _journal_transaction(
222
+ ledger_path: Path,
223
+ lock_descriptor: int,
224
+ operation: Callable[[sqlite3.Connection], object],
225
+ ) -> object:
226
+ evidence._borrow_or_acquire_target_lock(
227
+ ledger_path,
228
+ lock_descriptor,
229
+ )
230
+ connection = evidence.connect_ledger(ledger_path)
231
+ try:
232
+ connection.execute("BEGIN IMMEDIATE")
233
+ result = operation(connection)
234
+ connection.execute("COMMIT")
235
+ except Exception as error:
236
+ rollback_error = evidence._best_effort_rollback(connection)
237
+ close_error = evidence._best_effort_close(connection)
238
+ causes = [error]
239
+ if rollback_error is not None:
240
+ causes.append(rollback_error)
241
+ if close_error is not None:
242
+ causes.append(close_error)
243
+ raise HandlerCoordinationError("RECOVERY_REQUIRED") from (
244
+ evidence._error_cause(
245
+ "handler execution journal transaction failed",
246
+ causes,
247
+ )
248
+ )
249
+ close_error = evidence._best_effort_close(connection)
250
+ if close_error is not None:
251
+ raise HandlerCoordinationError("RECOVERY_REQUIRED") from close_error
252
+ return result
253
+
254
+
255
+ def _handler_execution_id(
256
+ request: AgentRequest,
257
+ execution_facts: ProspectiveExecutionFacts,
258
+ ) -> str:
259
+ return _digest(
260
+ {
261
+ "domain": "openworkproof/handler-execution/v0.1",
262
+ "request_digest": request.digest,
263
+ "execution_context_id": execution_facts.execution_context_id,
264
+ "container_instance_id_digest": (
265
+ execution_facts.container_instance_id_digest
266
+ ),
267
+ "controller_id": execution_facts.controller_id,
268
+ }
269
+ )
270
+
271
+
272
+ @dataclass(frozen=True, slots=True)
273
+ class _StoredRunTestsExecution:
274
+ execution_id: str
275
+ request: AgentRequest
276
+ contract: repo_tools.RunTestsExecutionContract
277
+ execution_facts: ProspectiveExecutionFacts
278
+ authorization_prefix_digest: str
279
+ reserved_at: datetime
280
+ state: Literal["RESERVED", "STARTED_UNCONFIRMED"]
281
+
282
+
283
+ def _canonical_agent_request(request: AgentRequest) -> bytes:
284
+ if type(request) is not AgentRequest:
285
+ raise ValueError("stored AgentRequest is invalid")
286
+ encoded = rfc8785.dumps(request.model_dump(mode="json"))
287
+ if not 1 <= len(encoded) <= _MAX_AGENT_REQUEST_BYTES:
288
+ raise ValueError("stored AgentRequest exceeds its byte limit")
289
+ return encoded
290
+
291
+
292
+ def _authorization_prefix_digest(
293
+ prefix: AuthorizationLedgerPrefix,
294
+ ) -> str:
295
+ if type(prefix) is not AuthorizationLedgerPrefix:
296
+ raise ValueError("authorization prefix is invalid")
297
+ encoded = rfc8785.dumps(
298
+ {
299
+ "domain": "openworkproof/authorization-ledger-prefix/v0.1",
300
+ "effective_grants": [
301
+ grant.model_dump(mode="json")
302
+ for grant in prefix.effective_grants
303
+ ],
304
+ "grant_attempts": [
305
+ grant.model_dump(mode="json")
306
+ for grant in prefix.grant_attempts
307
+ ],
308
+ "receipts": [
309
+ receipt.model_dump(mode="json")
310
+ for receipt in prefix.receipts
311
+ ],
312
+ }
313
+ )
314
+ if not 1 <= len(encoded) <= _MAX_AUTHORIZATION_PREFIX_BYTES:
315
+ raise ValueError("authorization prefix exceeds its byte limit")
316
+ return hashlib.sha256(encoded).hexdigest()
317
+
318
+
319
+ def _decode_canonical_agent_request(raw: object) -> AgentRequest:
320
+ if type(raw) is not str:
321
+ raise ValueError("stored AgentRequest JSON is invalid")
322
+ encoded = raw.encode("utf-8")
323
+ if not 1 <= len(encoded) <= _MAX_AGENT_REQUEST_BYTES:
324
+ raise ValueError("stored AgentRequest exceeds its byte limit")
325
+
326
+ def reject_duplicates(
327
+ pairs: list[tuple[str, object]],
328
+ ) -> dict[str, object]:
329
+ result: dict[str, object] = {}
330
+ for key, value in pairs:
331
+ if key in result:
332
+ raise ValueError("stored AgentRequest has duplicate keys")
333
+ result[key] = value
334
+ return result
335
+
336
+ def reject_constant(value: str) -> None:
337
+ raise ValueError(f"invalid JSON constant: {value}")
338
+
339
+ value = json.loads(
340
+ raw,
341
+ object_pairs_hook=reject_duplicates,
342
+ parse_constant=reject_constant,
343
+ )
344
+ if rfc8785.dumps(value) != encoded:
345
+ raise ValueError("stored AgentRequest is not canonical")
346
+ request = AgentRequest.model_validate(value)
347
+ if _canonical_agent_request(request) != encoded:
348
+ raise ValueError("stored AgentRequest does not round trip")
349
+ return request
350
+
351
+
352
+ def _contract_arguments_digest(
353
+ contract: repo_tools.RunTestsExecutionContract,
354
+ ) -> str:
355
+ arguments = RunTestsArguments(
356
+ test_mode="verifier",
357
+ command_digest=contract.command_digest,
358
+ source_commit=contract.source_commit,
359
+ candidate_commit=contract.candidate_commit,
360
+ workspace_manifest_digest=contract.workspace_manifest_digest,
361
+ container_image_digest=contract.container_image_digest,
362
+ fixed_test_source_digest=contract.fixed_test_source_digest,
363
+ )
364
+ return request_arguments_digest("owp.run_tests", arguments)
365
+
366
+
367
+ def _normalized_sql(value: str) -> str:
368
+ return " ".join(value.split()).casefold()
369
+
370
+
371
+ def _ensure_handler_execution_schema(
372
+ ledger_path: Path,
373
+ lock_descriptor: int,
374
+ ) -> None:
375
+ expected = evidence._HANDLER_EXECUTION_SCHEMA
376
+
377
+ def ensure(connection: sqlite3.Connection) -> None:
378
+ row = connection.execute(
379
+ """
380
+ SELECT sql
381
+ FROM sqlite_master
382
+ WHERE type = 'table' AND name = 'handler_executions'
383
+ """
384
+ ).fetchone()
385
+ if row is None:
386
+ connection.execute(expected)
387
+ return
388
+ if len(row) != 1 or type(row[0]) is not str:
389
+ raise ValueError("handler execution journal schema is invalid")
390
+ actual = _normalized_sql(row[0])
391
+ if actual == _normalized_sql(expected):
392
+ return
393
+ predecessors = (
394
+ evidence._LEGACY_HANDLER_EXECUTION_SCHEMA,
395
+ evidence._HANDLER_EXECUTION_SCHEMA_V1,
396
+ evidence._HANDLER_EXECUTION_SCHEMA_V2,
397
+ )
398
+ if actual in {
399
+ _normalized_sql(predecessor) for predecessor in predecessors
400
+ }:
401
+ if connection.execute(
402
+ "SELECT COUNT(*) FROM handler_executions"
403
+ ).fetchone() != (0,):
404
+ raise ValueError(
405
+ "legacy handler execution journal is unresolved"
406
+ )
407
+ connection.execute("DROP TABLE handler_executions")
408
+ connection.execute(expected)
409
+ return
410
+ raise ValueError("handler execution journal schema is invalid")
411
+
412
+ _journal_transaction(ledger_path, lock_descriptor, ensure)
413
+
414
+
415
+ def _receipt_matches_handler_execution(
416
+ stored_json: str,
417
+ row: tuple[object, ...],
418
+ ) -> bool:
419
+ try:
420
+ receipt = ACTION_RECEIPT_ADAPTER.validate_json(stored_json)
421
+ except Exception:
422
+ return False
423
+ (
424
+ _,
425
+ work_order_digest,
426
+ request_digest,
427
+ nonce,
428
+ grant_id,
429
+ tool_name,
430
+ arguments_digest,
431
+ execution_context_id,
432
+ container_instance_id_digest,
433
+ controller_id,
434
+ _,
435
+ _,
436
+ ) = row
437
+ common = (
438
+ receipt.work_order_digest == work_order_digest
439
+ and receipt.nested_claim_digest == request_digest
440
+ and receipt.nonce == nonce
441
+ and getattr(receipt, "grant_id", None) == grant_id
442
+ and receipt.nested_claim.tool_name == tool_name
443
+ and receipt.nested_claim.arguments_digest == arguments_digest
444
+ and receipt.policy_decision == "allow"
445
+ and receipt.execution_status in {"succeeded", "failed"}
446
+ )
447
+ if not common:
448
+ return False
449
+ if isinstance(receipt, RollbackReceipt):
450
+ return (
451
+ tool_name == "owp.rollback_patch"
452
+ and receipt.gateway_signer_key_id == controller_id
453
+ )
454
+ factors = receipt.correlation_factors
455
+ return (
456
+ isinstance(receipt, ToolCallReceipt)
457
+ and receipt.tool_name == tool_name
458
+ and receipt.arguments_digest == arguments_digest
459
+ and factors is not None
460
+ and factors.execution_context_id == execution_context_id
461
+ and factors.container_instance_id_digest
462
+ == container_instance_id_digest
463
+ and factors.controller_id == controller_id
464
+ )
465
+
466
+
467
+ def _recover_handler_executions(
468
+ ledger_path: Path,
469
+ lock_descriptor: int,
470
+ ) -> None:
471
+ def recover(connection: sqlite3.Connection) -> None:
472
+ rows = tuple(
473
+ connection.execute(
474
+ """
475
+ SELECT
476
+ execution_id,
477
+ work_order_digest,
478
+ request_digest,
479
+ nonce,
480
+ grant_id,
481
+ tool_name,
482
+ arguments_digest,
483
+ execution_context_id,
484
+ container_instance_id_digest,
485
+ controller_id,
486
+ reserved_at,
487
+ state
488
+ FROM handler_executions
489
+ ORDER BY execution_id
490
+ """
491
+ ).fetchall()
492
+ )
493
+ if len(rows) > 1:
494
+ raise ValueError("multiple handler executions are unresolved")
495
+ if not rows:
496
+ return
497
+ row = tuple(rows[0])
498
+ if row[5] == "owp.run_tests":
499
+ raise ValueError(
500
+ "run-tests execution requires typed driver reconciliation"
501
+ )
502
+ state = row[-1]
503
+ stored = connection.execute(
504
+ "SELECT receipt_json FROM receipts WHERE nonce = ?",
505
+ (row[3],),
506
+ ).fetchone()
507
+ if state == "RESERVED" and stored is None:
508
+ connection.execute(
509
+ "DELETE FROM handler_executions WHERE execution_id = ?",
510
+ (row[0],),
511
+ )
512
+ return
513
+ if (
514
+ state == "STARTED_UNCONFIRMED"
515
+ and stored is not None
516
+ and _receipt_matches_handler_execution(stored[0], row)
517
+ ):
518
+ connection.execute(
519
+ "DELETE FROM handler_executions WHERE execution_id = ?",
520
+ (row[0],),
521
+ )
522
+ return
523
+ raise ValueError("handler execution truth is unresolved")
524
+
525
+ _journal_transaction(ledger_path, lock_descriptor, recover)
526
+
527
+
528
+ def _reserve_handler_execution(
529
+ ledger_path: Path,
530
+ lock_descriptor: int,
531
+ context: AuthorizationContext,
532
+ request: AgentRequest,
533
+ execution_facts: ProspectiveExecutionFacts,
534
+ execution_contract: repo_tools.RunTestsExecutionContract | None,
535
+ ) -> str:
536
+ execution_id = _handler_execution_id(request, execution_facts)
537
+ request_json: str | None = None
538
+ contract_json: str | None = None
539
+ contract_digest: str | None = None
540
+ authorization_prefix_digest: str | None = None
541
+ if request.tool_name == "owp.run_tests":
542
+ if type(execution_contract) is not repo_tools.RunTestsExecutionContract:
543
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
544
+ if (
545
+ execution_contract.execution_id != execution_id
546
+ or execution_contract.request_digest != request.digest
547
+ or execution_contract.arguments_digest != request.arguments_digest
548
+ or _contract_arguments_digest(execution_contract)
549
+ != request.arguments_digest
550
+ ):
551
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
552
+ try:
553
+ request_bytes = _canonical_agent_request(request)
554
+ contract_bytes = repo_tools.encode_run_tests_execution_contract(
555
+ execution_contract
556
+ )
557
+ except (TypeError, ValueError) as error:
558
+ raise HandlerCoordinationError("RECOVERY_REQUIRED") from error
559
+ if not 1 <= len(contract_bytes) <= 8_192:
560
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
561
+ request_json = request_bytes.decode("utf-8")
562
+ contract_json = contract_bytes.decode("utf-8")
563
+ contract_digest = hashlib.sha256(contract_bytes).hexdigest()
564
+ try:
565
+ authorization_prefix_digest = _authorization_prefix_digest(
566
+ context.ledger_prefix
567
+ )
568
+ except (TypeError, ValueError) as error:
569
+ raise HandlerCoordinationError("RECOVERY_REQUIRED") from error
570
+ elif request.tool_name in {"owp.rollback_patch", "owp.repo_read"}:
571
+ if execution_contract is not None:
572
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
573
+ else:
574
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
575
+
576
+ def reserve(connection: sqlite3.Connection) -> None:
577
+ if connection.execute(
578
+ "SELECT COUNT(*) FROM handler_executions"
579
+ ).fetchone() != (0,):
580
+ raise ValueError("a handler execution is already unresolved")
581
+ connection.execute(
582
+ """
583
+ INSERT INTO handler_executions (
584
+ execution_id,
585
+ work_order_digest,
586
+ request_digest,
587
+ nonce,
588
+ grant_id,
589
+ tool_name,
590
+ arguments_digest,
591
+ execution_context_id,
592
+ container_instance_id_digest,
593
+ controller_id,
594
+ reserved_at,
595
+ state,
596
+ authorization_prefix_digest,
597
+ request_json,
598
+ execution_contract_json,
599
+ execution_contract_digest
600
+ ) VALUES (
601
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'RESERVED', ?, ?, ?, ?
602
+ )
603
+ """,
604
+ (
605
+ execution_id,
606
+ context.work_order.digest,
607
+ request.digest,
608
+ request.nonce,
609
+ request.grant_id,
610
+ request.tool_name,
611
+ request.arguments_digest,
612
+ execution_facts.execution_context_id,
613
+ execution_facts.container_instance_id_digest,
614
+ execution_facts.controller_id,
615
+ context.transaction_time.strftime("%Y-%m-%dT%H:%M:%SZ"),
616
+ authorization_prefix_digest,
617
+ request_json,
618
+ contract_json,
619
+ contract_digest,
620
+ ),
621
+ )
622
+
623
+ _journal_transaction(ledger_path, lock_descriptor, reserve)
624
+ return execution_id
625
+
626
+
627
+ def _load_stored_run_tests_execution(
628
+ ledger_path: Path,
629
+ lock_descriptor: int,
630
+ ) -> _StoredRunTestsExecution | None:
631
+ def load(
632
+ connection: sqlite3.Connection,
633
+ ) -> _StoredRunTestsExecution | None:
634
+ rows = connection.execute(
635
+ """
636
+ SELECT
637
+ execution_id,
638
+ work_order_digest,
639
+ request_digest,
640
+ nonce,
641
+ grant_id,
642
+ tool_name,
643
+ arguments_digest,
644
+ execution_context_id,
645
+ container_instance_id_digest,
646
+ controller_id,
647
+ reserved_at,
648
+ state,
649
+ authorization_prefix_digest,
650
+ request_json,
651
+ execution_contract_json,
652
+ execution_contract_digest
653
+ FROM handler_executions
654
+ ORDER BY execution_id
655
+ LIMIT 2
656
+ """
657
+ ).fetchall()
658
+ if not rows:
659
+ return None
660
+ if len(rows) != 1:
661
+ raise ValueError("multiple handler executions are unresolved")
662
+ if rows[0][5] == "owp.rollback_patch":
663
+ return None
664
+ (
665
+ execution_id,
666
+ work_order_digest,
667
+ request_digest,
668
+ nonce,
669
+ grant_id,
670
+ tool_name,
671
+ arguments_digest,
672
+ execution_context_id,
673
+ container_instance_id_digest,
674
+ controller_id,
675
+ reserved_at_raw,
676
+ state,
677
+ authorization_prefix_digest,
678
+ request_json,
679
+ contract_json,
680
+ contract_digest,
681
+ ) = rows[0]
682
+ request = _decode_canonical_agent_request(request_json)
683
+ work_order = evidence.load_authoritative_work_order(connection)
684
+ if type(contract_json) is not str:
685
+ raise ValueError("stored execution contract JSON is invalid")
686
+ contract_bytes = contract_json.encode("utf-8")
687
+ if not 1 <= len(contract_bytes) <= 8_192:
688
+ raise ValueError("stored execution contract exceeds its byte limit")
689
+ contract = repo_tools.decode_run_tests_execution_contract(
690
+ contract_bytes
691
+ )
692
+ if (
693
+ type(contract_digest) is not str
694
+ or hashlib.sha256(contract_bytes).hexdigest() != contract_digest
695
+ ):
696
+ raise ValueError("stored execution contract digest is invalid")
697
+ if type(reserved_at_raw) is not str:
698
+ raise ValueError("stored reservation time is invalid")
699
+ reserved_at = datetime.strptime(
700
+ reserved_at_raw, "%Y-%m-%dT%H:%M:%SZ"
701
+ ).replace(tzinfo=timezone.utc)
702
+ if reserved_at.strftime("%Y-%m-%dT%H:%M:%SZ") != reserved_at_raw:
703
+ raise ValueError("stored reservation time is not a UTC second")
704
+ if state not in {"RESERVED", "STARTED_UNCONFIRMED"}:
705
+ raise ValueError("stored execution state is invalid")
706
+ if (
707
+ type(authorization_prefix_digest) is not str
708
+ or re.fullmatch(r"[0-9a-f]{64}", authorization_prefix_digest)
709
+ is None
710
+ ):
711
+ raise ValueError("stored authorization prefix digest is invalid")
712
+ facts = ProspectiveExecutionFacts(
713
+ execution_context_id=execution_context_id,
714
+ container_instance_id_digest=container_instance_id_digest,
715
+ controller_id=controller_id,
716
+ )
717
+ if (
718
+ tool_name != "owp.run_tests"
719
+ or not verify_nested_claim(request, work_order)
720
+ or request.work_order_digest != work_order_digest
721
+ or request.digest != request_digest
722
+ or request.nonce != nonce
723
+ or request.grant_id != grant_id
724
+ or request.tool_name != tool_name
725
+ or request.arguments_digest != arguments_digest
726
+ or _handler_execution_id(request, facts) != execution_id
727
+ or contract.execution_id != execution_id
728
+ or contract.request_digest != request_digest
729
+ or contract.arguments_digest != arguments_digest
730
+ or _contract_arguments_digest(contract) != arguments_digest
731
+ ):
732
+ raise ValueError("stored run-tests execution fields disagree")
733
+ return _StoredRunTestsExecution(
734
+ execution_id=execution_id,
735
+ request=request,
736
+ contract=contract,
737
+ execution_facts=facts,
738
+ authorization_prefix_digest=authorization_prefix_digest,
739
+ reserved_at=reserved_at,
740
+ state=state,
741
+ )
742
+
743
+ result = _journal_transaction(ledger_path, lock_descriptor, load)
744
+ if result is None or type(result) is _StoredRunTestsExecution:
745
+ return result
746
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
747
+
748
+
749
+ def _mark_handler_started(
750
+ ledger_path: Path,
751
+ lock_descriptor: int,
752
+ execution_id: str,
753
+ ) -> None:
754
+ def mark(connection: sqlite3.Connection) -> None:
755
+ cursor = connection.execute(
756
+ """
757
+ UPDATE handler_executions
758
+ SET state = 'STARTED_UNCONFIRMED'
759
+ WHERE execution_id = ? AND state = 'RESERVED'
760
+ """,
761
+ (execution_id,),
762
+ )
763
+ if cursor.rowcount != 1:
764
+ raise ValueError("handler execution reservation is unavailable")
765
+
766
+ _journal_transaction(ledger_path, lock_descriptor, mark)
767
+
768
+
769
+ def _finalize_handler_execution(
770
+ ledger_path: Path,
771
+ lock_descriptor: int,
772
+ ) -> None:
773
+ _recover_handler_executions(ledger_path, lock_descriptor)
774
+
775
+
776
+ def _delete_handler_execution(
777
+ ledger_path: Path,
778
+ lock_descriptor: int,
779
+ execution_id: str,
780
+ ) -> None:
781
+ def delete(connection: sqlite3.Connection) -> None:
782
+ cursor = connection.execute(
783
+ "DELETE FROM handler_executions WHERE execution_id = ?",
784
+ (execution_id,),
785
+ )
786
+ if cursor.rowcount != 1:
787
+ raise ValueError("handler execution journal is unavailable")
788
+
789
+ _journal_transaction(ledger_path, lock_descriptor, delete)
790
+
791
+
792
+ def _run_tests_receipt_state(
793
+ ledger_path: Path,
794
+ lock_descriptor: int,
795
+ stored: _StoredRunTestsExecution,
796
+ ) -> repo_tools.RunTestsReceiptState:
797
+ def observe(connection: sqlite3.Connection) -> str:
798
+ row = connection.execute(
799
+ "SELECT receipt_json FROM receipts WHERE nonce = ?",
800
+ (stored.request.nonce,),
801
+ ).fetchone()
802
+ if row is None:
803
+ return "ABSENT"
804
+ journal_row = connection.execute(
805
+ """
806
+ SELECT
807
+ execution_id, work_order_digest, request_digest, nonce,
808
+ grant_id, tool_name, arguments_digest,
809
+ execution_context_id, container_instance_id_digest,
810
+ controller_id, reserved_at, state
811
+ FROM handler_executions
812
+ WHERE execution_id = ?
813
+ """,
814
+ (stored.execution_id,),
815
+ ).fetchone()
816
+ if journal_row is None or type(row[0]) is not str:
817
+ raise ValueError("stored run-tests Receipt observation is invalid")
818
+ return (
819
+ "MATCH"
820
+ if _receipt_matches_handler_execution(row[0], tuple(journal_row))
821
+ else "MISMATCH"
822
+ )
823
+
824
+ result = _journal_transaction(ledger_path, lock_descriptor, observe)
825
+ if result in {"ABSENT", "MATCH", "MISMATCH"}:
826
+ return result
827
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
828
+
829
+
830
+ def _recovery_authorization_context(
831
+ ledger_path: Path,
832
+ evidence_root: Path,
833
+ context: AuthorizationContext,
834
+ stored: _StoredRunTestsExecution,
835
+ receipt_state: repo_tools.RunTestsReceiptState,
836
+ now: datetime,
837
+ lock_descriptor: int,
838
+ ) -> AuthorizationContext:
839
+ _require_current_context(
840
+ ledger_path,
841
+ evidence_root,
842
+ context,
843
+ now,
844
+ lock_descriptor,
845
+ )
846
+ if stored.request.work_order_digest != context.work_order.digest:
847
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
848
+ if receipt_state != "ABSENT":
849
+ return context
850
+ try:
851
+ current_prefix_digest = _authorization_prefix_digest(
852
+ context.ledger_prefix
853
+ )
854
+ except (TypeError, ValueError) as error:
855
+ raise HandlerCoordinationError("RECOVERY_REQUIRED") from error
856
+ if current_prefix_digest != stored.authorization_prefix_digest:
857
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
858
+ return derive_authorization_context(
859
+ context.work_order,
860
+ context.ledger_prefix,
861
+ context.committed_evidence,
862
+ context.replay_checkpoint,
863
+ stored.reserved_at,
864
+ )
865
+
866
+
867
+ def _remaining_tool_calls(context: AuthorizationContext, grant_id: str) -> int:
868
+ balances = {
869
+ (candidate_id, metric): remaining
870
+ for candidate_id, metric, remaining in context.replay_state.balances
871
+ }
872
+ try:
873
+ return balances[(grant_id, "tool_calls")]
874
+ except KeyError as error:
875
+ raise HandlerCoordinationError(
876
+ "authorized Grant balance is unavailable"
877
+ ) from error
878
+
879
+
880
+ def _test_result_payload(
881
+ arguments: RunTestsArguments,
882
+ actual_exit_code: int,
883
+ ) -> bytes:
884
+ result = TestResultEvidence(
885
+ schema_version="openworkproof-test-result/0.1",
886
+ **arguments.model_dump(mode="python"),
887
+ actual_exit_code=actual_exit_code,
888
+ )
889
+ return rfc8785.dumps(result.model_dump(mode="json"))
890
+
891
+
892
+ def _run_tests_arguments_from_contract(
893
+ contract: repo_tools.RunTestsExecutionContract,
894
+ ) -> RunTestsArguments:
895
+ return RunTestsArguments(
896
+ test_mode="verifier",
897
+ command_digest=contract.command_digest,
898
+ source_commit=contract.source_commit,
899
+ candidate_commit=contract.candidate_commit,
900
+ workspace_manifest_digest=contract.workspace_manifest_digest,
901
+ container_image_digest=contract.container_image_digest,
902
+ fixed_test_source_digest=contract.fixed_test_source_digest,
903
+ )
904
+
905
+
906
+ def _run_tests_episode(
907
+ context: AuthorizationContext,
908
+ request: AgentRequest,
909
+ arguments: RunTestsArguments,
910
+ ) -> Literal["primary_verifier", "independent_verifier"]:
911
+ """Derive the closed run-tests episode from current authority.
912
+
913
+ The episode is derived from the signed context and the agent request;
914
+ callers do not supply it. A second Verifier run after the first composition
915
+ is the independent_verifier episode; all other Verifier runs are the
916
+ primary_verifier episode. Developer mode and any non-Verifier caller are
917
+ not handled by this helper.
918
+ """
919
+ binding = next(
920
+ (
921
+ item
922
+ for item in context.work_order.key_bindings
923
+ if item.subject_id == request.actor_id
924
+ and item.key_id == request.actor_key_id
925
+ ),
926
+ None,
927
+ )
928
+ if binding is None or binding.role != "Verifier":
929
+ raise HandlerCoordinationError("run-tests actor is not the Verifier")
930
+ if arguments.test_mode != "verifier":
931
+ raise HandlerCoordinationError("run-tests mode is not verifier")
932
+ if context.current_state in {"running", "retrying"}:
933
+ return "primary_verifier"
934
+ if context.current_state == "evidence_incomplete":
935
+ return "independent_verifier"
936
+ raise HandlerCoordinationError("run-tests state is not executable")
937
+
938
+
939
+ def _next_test_reference(
940
+ context: AuthorizationContext,
941
+ arguments: RunTestsArguments,
942
+ payload: bytes,
943
+ *,
944
+ purpose: Literal["verifier_result", "verifier_independent_result", "developer_test_result"] | None = None,
945
+ ) -> EvidenceRef:
946
+ if purpose is None:
947
+ purpose = (
948
+ "verifier_result"
949
+ if arguments.test_mode == "verifier"
950
+ else "developer_test_result"
951
+ )
952
+ used_paths = {
953
+ reference.path
954
+ for receipt in context.ledger_prefix.receipts
955
+ for reference in receipt.evidence_refs
956
+ }
957
+ slot = next(
958
+ (
959
+ artifact
960
+ for artifact in context.work_order.evidence_policy.artifacts
961
+ if artifact.purpose == purpose
962
+ and f"evidence/{artifact.path}" not in used_paths
963
+ ),
964
+ None,
965
+ )
966
+ if slot is None or len(payload) > slot.max_size_bytes:
967
+ raise HandlerCoordinationError(
968
+ "EVIDENCE_SLOT_UNAVAILABLE"
969
+ )
970
+ return EvidenceRef(
971
+ path=f"evidence/{slot.path}",
972
+ sha256=hashlib.sha256(payload).hexdigest(),
973
+ media_type=slot.media_type,
974
+ size_bytes=len(payload),
975
+ )
976
+
977
+
978
+ def _predicate_results(
979
+ context: AuthorizationContext,
980
+ request: AgentRequest,
981
+ arguments: RunTestsArguments,
982
+ *,
983
+ execution_status: str,
984
+ actual_exit_code: int | None,
985
+ evidence_digest: str | None,
986
+ ):
987
+ selected = select_required_predicates(
988
+ work_order=context.work_order,
989
+ tool_name="owp.run_tests",
990
+ policy_decision="allow",
991
+ execution_status=execution_status,
992
+ test_mode=arguments.test_mode,
993
+ )
994
+ remaining_before = _remaining_tool_calls(context, request.grant_id)
995
+ tip = context.ledger_prefix.receipts[-1]
996
+ profile = next(
997
+ candidate
998
+ for candidate in context.work_order.test_profiles
999
+ if candidate.test_mode == arguments.test_mode
1000
+ )
1001
+ inputs: dict[str, object] = {}
1002
+ for spec in selected:
1003
+ if spec.name == "tool_allowed":
1004
+ value = {"actual_tool_name": "owp.run_tests"}
1005
+ elif spec.name == "quota_remaining":
1006
+ value = {
1007
+ "grant_id": request.grant_id,
1008
+ "metric": "tool_calls",
1009
+ "amount": 1,
1010
+ "grant_remaining_before": remaining_before,
1011
+ "ledger_prefix_digest": tip.digest,
1012
+ }
1013
+ elif spec.name == "tests_passed":
1014
+ value = {
1015
+ "test_mode": "verifier",
1016
+ "command_digest": arguments.command_digest,
1017
+ "expected_exit_code": profile.expected_exit_code,
1018
+ "actual_exit_code": actual_exit_code,
1019
+ "test_evidence_digest": evidence_digest,
1020
+ "source_commit": arguments.source_commit,
1021
+ "candidate_commit": arguments.candidate_commit,
1022
+ "workspace_manifest_digest": (
1023
+ arguments.workspace_manifest_digest
1024
+ ),
1025
+ "container_image_digest": arguments.container_image_digest,
1026
+ "fixed_test_source_digest": (
1027
+ arguments.fixed_test_source_digest
1028
+ ),
1029
+ }
1030
+ else:
1031
+ raise HandlerCoordinationError(
1032
+ "run-tests predicate authority is incomplete"
1033
+ )
1034
+ inputs[spec.predicate_id] = value
1035
+ return evaluate_required_predicates(
1036
+ selected,
1037
+ EvaluationContext(
1038
+ inputs=inputs,
1039
+ authoritative_inputs=inputs,
1040
+ authoritative_ledger_prefix_digests={
1041
+ request.grant_id: tip.digest,
1042
+ },
1043
+ ),
1044
+ )
1045
+
1046
+
1047
+ def _causal_parents(
1048
+ context: AuthorizationContext,
1049
+ request: AgentRequest,
1050
+ *,
1051
+ extra_parents: tuple[ActionReceiptEnvelope, ...] = (),
1052
+ ):
1053
+ receipts = context.ledger_prefix.receipts
1054
+ issuance = next(
1055
+ (
1056
+ receipt
1057
+ for receipt in receipts
1058
+ if isinstance(receipt, GrantIssuedReceipt)
1059
+ and receipt.policy_decision == "allow"
1060
+ and receipt.issued_grant_id == request.grant_id
1061
+ ),
1062
+ None,
1063
+ )
1064
+ active_patch = next(
1065
+ (
1066
+ receipt
1067
+ for receipt in receipts
1068
+ if receipt.receipt_id == context.active_patch_receipt_id
1069
+ ),
1070
+ None,
1071
+ )
1072
+ if issuance is None or active_patch is None:
1073
+ raise HandlerCoordinationError(
1074
+ "run-tests causal parents are unavailable"
1075
+ )
1076
+ parents: dict[str, ActionReceiptEnvelope] = {
1077
+ issuance.receipt_id: issuance,
1078
+ active_patch.receipt_id: active_patch,
1079
+ }
1080
+ for parent in extra_parents:
1081
+ parents[parent.receipt_id] = parent
1082
+ return tuple(
1083
+ receipt.receipt_id
1084
+ for receipt in sorted(
1085
+ parents.values(),
1086
+ key=lambda item: item.sequence,
1087
+ )
1088
+ )
1089
+
1090
+
1091
+ def _latest_proof_composed_trigger(
1092
+ receipts: tuple[ActionReceiptEnvelope, ...],
1093
+ ) -> SystemEventReceipt | None:
1094
+ for receipt in reversed(receipts):
1095
+ if (
1096
+ isinstance(receipt, SystemEventReceipt)
1097
+ and receipt.system_event_name == "proof_composed"
1098
+ ):
1099
+ return receipt
1100
+ return None
1101
+
1102
+
1103
+ def _build_run_tests_receipt(
1104
+ context: AuthorizationContext,
1105
+ request: AgentRequest,
1106
+ arguments: RunTestsArguments,
1107
+ execution_facts: ProspectiveExecutionFacts,
1108
+ sidecar_private_key: Ed25519PrivateKey,
1109
+ *,
1110
+ execution_status: str,
1111
+ execution_error_code: Literal[
1112
+ "OUTPUT_LIMIT", "TIMEOUT", "DISK_LIMIT"
1113
+ ] | None,
1114
+ actual_exit_code: int | None,
1115
+ payload: bytes | None,
1116
+ ) -> ToolCallReceipt:
1117
+ episode = _run_tests_episode(context, request, arguments)
1118
+ if episode == "independent_verifier":
1119
+ trigger = _latest_proof_composed_trigger(context.ledger_prefix.receipts)
1120
+ # A retry after an infrastructure failure causally links to the
1121
+ # previous closed failure receipt of the same episode, so the exact
1122
+ # publication-tip parent requirement is satisfied on retry.
1123
+ prior_independent = next(
1124
+ (
1125
+ receipt
1126
+ for receipt in reversed(context.ledger_prefix.receipts)
1127
+ if isinstance(receipt, ToolCallReceipt)
1128
+ and receipt.tool_name == "owp.run_tests"
1129
+ and receipt.state_before == "evidence_incomplete"
1130
+ and receipt.state_after == "evidence_incomplete"
1131
+ ),
1132
+ None,
1133
+ )
1134
+ extra_parents: tuple[ActionReceiptEnvelope, ...] = tuple(
1135
+ parent
1136
+ for parent in (trigger, prior_independent)
1137
+ if parent is not None
1138
+ )
1139
+ purpose: Literal[
1140
+ "verifier_result",
1141
+ "verifier_independent_result",
1142
+ "developer_test_result",
1143
+ ] = "verifier_independent_result"
1144
+ else:
1145
+ extra_parents = ()
1146
+ purpose = (
1147
+ "verifier_result"
1148
+ if arguments.test_mode == "verifier"
1149
+ else "developer_test_result"
1150
+ )
1151
+ if execution_status == "succeeded":
1152
+ if execution_error_code is not None:
1153
+ raise HandlerCoordinationError("run-tests outcome is malformed")
1154
+ assert payload is not None and actual_exit_code is not None
1155
+ reference = _next_test_reference(
1156
+ context, arguments, payload, purpose=purpose
1157
+ )
1158
+ evidence_refs = (reference,)
1159
+ output_digest = reference.sha256
1160
+ if episode == "independent_verifier":
1161
+ state_after = "evidence_incomplete"
1162
+ else:
1163
+ state_after = (
1164
+ "locally_verified"
1165
+ if arguments.test_mode == "verifier"
1166
+ and actual_exit_code
1167
+ == next(
1168
+ profile.expected_exit_code
1169
+ for profile in context.work_order.test_profiles
1170
+ if profile.test_mode == arguments.test_mode
1171
+ )
1172
+ else "needs_rework"
1173
+ if arguments.test_mode == "verifier"
1174
+ else context.current_state
1175
+ )
1176
+ else:
1177
+ if execution_error_code not in {
1178
+ "OUTPUT_LIMIT",
1179
+ "TIMEOUT",
1180
+ "DISK_LIMIT",
1181
+ }:
1182
+ raise HandlerCoordinationError("run-tests outcome is malformed")
1183
+ if payload is not None or actual_exit_code is not None:
1184
+ raise HandlerCoordinationError("run-tests outcome is malformed")
1185
+ evidence_refs = ()
1186
+ output_digest = _digest(
1187
+ {"status": "failed", "error_code": execution_error_code}
1188
+ )
1189
+ state_after = (
1190
+ "evidence_incomplete"
1191
+ if episode == "independent_verifier"
1192
+ else context.current_state
1193
+ )
1194
+ results = _predicate_results(
1195
+ context,
1196
+ request,
1197
+ arguments,
1198
+ execution_status=execution_status,
1199
+ actual_exit_code=actual_exit_code,
1200
+ evidence_digest=(
1201
+ None if payload is None else hashlib.sha256(payload).hexdigest()
1202
+ ),
1203
+ )
1204
+ remaining_before = _remaining_tool_calls(context, request.grant_id)
1205
+ sidecar_key_id = key_id(sidecar_private_key.public_key())
1206
+ toolchain_id = _digest(
1207
+ {
1208
+ "domain": "openworkproof/toolchain/v0.1",
1209
+ "tool_name": "owp.run_tests",
1210
+ "tool_version": "0.1",
1211
+ "container_image_digest": arguments.container_image_digest,
1212
+ "command_digest": arguments.command_digest,
1213
+ }
1214
+ )
1215
+ receipt_id = _digest(
1216
+ {
1217
+ "domain": "openworkproof/receipt-id/v0.1",
1218
+ "request_digest": request.digest,
1219
+ "entropy": secrets.token_hex(32),
1220
+ }
1221
+ )
1222
+ raw = {
1223
+ "protocol_version": "0.1",
1224
+ "receipt_id": receipt_id,
1225
+ "work_order_digest": context.work_order.digest,
1226
+ "actor_type": "agent",
1227
+ "actor_id": request.actor_id,
1228
+ "actor_key_id": request.actor_key_id,
1229
+ "nested_claim_type": "agent-request",
1230
+ "nested_claim_digest": request.digest,
1231
+ "nested_claim": request.model_dump(mode="json"),
1232
+ "gateway_signer_key_id": sidecar_key_id,
1233
+ "event_type": "tool_call",
1234
+ "policy_decision": "allow",
1235
+ "policy_error_code": None,
1236
+ "execution_status": execution_status,
1237
+ "execution_error_code": execution_error_code,
1238
+ "quota_charge": {
1239
+ "grant_id": request.grant_id,
1240
+ "metric": "tool_calls",
1241
+ "amount": 1,
1242
+ "remaining_after": remaining_before - 1,
1243
+ },
1244
+ "state_before": context.current_state,
1245
+ "state_after": state_after,
1246
+ "parent_receipt_ids": _causal_parents(
1247
+ context, request, extra_parents=extra_parents
1248
+ ),
1249
+ "correlation_factors": {
1250
+ "model_id": request.model_id,
1251
+ "model_version": request.model_version,
1252
+ "prompt_template_digest": request.prompt_template_digest,
1253
+ "context_source_digest": request.context_source_digest,
1254
+ "toolchain_id": toolchain_id,
1255
+ "execution_context_id": execution_facts.execution_context_id,
1256
+ "container_instance_id_digest": (
1257
+ execution_facts.container_instance_id_digest
1258
+ ),
1259
+ "controller_id": execution_facts.controller_id,
1260
+ "fixed_test_source_digest": arguments.fixed_test_source_digest,
1261
+ },
1262
+ "evidence_refs": [
1263
+ item.model_dump(mode="json") for item in evidence_refs
1264
+ ],
1265
+ "occurred_at": context.transaction_time.strftime(
1266
+ "%Y-%m-%dT%H:%M:%SZ"
1267
+ ),
1268
+ "sequence": len(context.ledger_prefix.receipts) + 1,
1269
+ "nonce": request.nonce,
1270
+ "previous_receipt_digest": (
1271
+ context.ledger_prefix.receipts[-1].digest
1272
+ ),
1273
+ "grant_id": request.grant_id,
1274
+ "tool_name": "owp.run_tests",
1275
+ "tool_version": "0.1",
1276
+ "request_arguments": arguments.model_dump(mode="json"),
1277
+ "arguments_digest": request.arguments_digest,
1278
+ "output_digest": output_digest,
1279
+ "predicate_results": [
1280
+ result.model_dump(mode="json") for result in results
1281
+ ],
1282
+ }
1283
+ return ACTION_RECEIPT_ADAPTER.validate_python(
1284
+ sign_payload("action-receipt", raw, sidecar_private_key)
1285
+ )
1286
+
1287
+
1288
+ def _preflight_run_tests_receipts(
1289
+ context: AuthorizationContext,
1290
+ request: AgentRequest,
1291
+ arguments: RunTestsArguments,
1292
+ execution_facts: ProspectiveExecutionFacts,
1293
+ sidecar_private_key: Ed25519PrivateKey,
1294
+ ) -> None:
1295
+ episode = _run_tests_episode(context, request, arguments)
1296
+ if episode == "independent_verifier":
1297
+ if context.causal_state.latest_composition_trigger_id is None:
1298
+ raise HandlerCoordinationError(
1299
+ "independent verifier trigger is unavailable"
1300
+ )
1301
+ if (
1302
+ context.independent_failure_terminal
1303
+ or context.causal_state.independent_result_receipt_id is not None
1304
+ ):
1305
+ raise HandlerCoordinationError(
1306
+ "independent verifier episode is sealed"
1307
+ )
1308
+ representative_receipts = []
1309
+ expected_exit_code = next(
1310
+ profile.expected_exit_code
1311
+ for profile in context.work_order.test_profiles
1312
+ if profile.test_mode == "verifier"
1313
+ )
1314
+ unexpected_exit_code = 0 if expected_exit_code != 0 else 1
1315
+ for exit_code in (expected_exit_code, unexpected_exit_code):
1316
+ payload = _test_result_payload(arguments, exit_code)
1317
+ representative_receipts.append(
1318
+ _build_run_tests_receipt(
1319
+ context,
1320
+ request,
1321
+ arguments,
1322
+ execution_facts,
1323
+ sidecar_private_key,
1324
+ execution_status="succeeded",
1325
+ execution_error_code=None,
1326
+ actual_exit_code=exit_code,
1327
+ payload=payload,
1328
+ )
1329
+ )
1330
+ for failure_code in ("OUTPUT_LIMIT", "TIMEOUT", "DISK_LIMIT"):
1331
+ representative_receipts.append(
1332
+ _build_run_tests_receipt(
1333
+ context,
1334
+ request,
1335
+ arguments,
1336
+ execution_facts,
1337
+ sidecar_private_key,
1338
+ execution_status="failed",
1339
+ execution_error_code=failure_code,
1340
+ actual_exit_code=None,
1341
+ payload=None,
1342
+ )
1343
+ )
1344
+ if any(
1345
+ len(rfc8785.dumps(receipt.model_dump(mode="json")))
1346
+ > _MAX_RECEIPT_BYTES
1347
+ for receipt in representative_receipts
1348
+ ):
1349
+ raise HandlerCoordinationError("BUNDLE_CAPACITY_EXCEEDED")
1350
+
1351
+
1352
+ def _recover_run_tests_execution(
1353
+ ledger_path: Path,
1354
+ evidence_root: Path,
1355
+ lock_descriptor: int,
1356
+ context: AuthorizationContext,
1357
+ stored: _StoredRunTestsExecution,
1358
+ sidecar_private_key: Ed25519PrivateKey,
1359
+ execution_driver: repo_tools.RunTestsExecutionDriver,
1360
+ now: datetime,
1361
+ ) -> ToolCallReceipt | None:
1362
+ if (
1363
+ key_id(sidecar_private_key.public_key())
1364
+ != stored.execution_facts.controller_id
1365
+ ):
1366
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
1367
+ receipt_state = _run_tests_receipt_state(
1368
+ ledger_path, lock_descriptor, stored
1369
+ )
1370
+ old_context = _recovery_authorization_context(
1371
+ ledger_path,
1372
+ evidence_root,
1373
+ context,
1374
+ stored,
1375
+ receipt_state,
1376
+ now,
1377
+ lock_descriptor,
1378
+ )
1379
+ try:
1380
+ outcome = execution_driver.reconcile(
1381
+ stored.contract,
1382
+ stored.state,
1383
+ receipt_state,
1384
+ )
1385
+ except Exception as error:
1386
+ raise HandlerCoordinationError("RECOVERY_REQUIRED") from error
1387
+ if outcome.action in {"WAIT_RUNNING", "UNRESOLVED"}:
1388
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
1389
+ if outcome.action == "SAFE_TO_RETRY":
1390
+ if receipt_state != "ABSENT":
1391
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
1392
+ _delete_handler_execution(
1393
+ ledger_path, lock_descriptor, stored.execution_id
1394
+ )
1395
+ return None
1396
+ if outcome.action != "CLOSED_RESULT":
1397
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
1398
+ if receipt_state == "MISMATCH":
1399
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
1400
+ if receipt_state == "MATCH":
1401
+ if outcome.result is not None:
1402
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
1403
+ _delete_handler_execution(
1404
+ ledger_path, lock_descriptor, stored.execution_id
1405
+ )
1406
+ return None
1407
+ result = outcome.result
1408
+ if result is not None:
1409
+ try:
1410
+ repo_tools.encode_run_tests_result_envelope(result)
1411
+ except (TypeError, ValueError) as error:
1412
+ raise HandlerCoordinationError("RECOVERY_REQUIRED") from error
1413
+ contract_digest = hashlib.sha256(
1414
+ repo_tools.encode_run_tests_execution_contract(stored.contract)
1415
+ ).hexdigest()
1416
+ if (
1417
+ result is None
1418
+ or result.execution_id != stored.execution_id
1419
+ or result.execution_contract_digest != contract_digest
1420
+ ):
1421
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
1422
+ arguments = _run_tests_arguments_from_contract(stored.contract)
1423
+ if result.failure_code is not None:
1424
+ receipt = _build_run_tests_receipt(
1425
+ old_context,
1426
+ stored.request,
1427
+ arguments,
1428
+ stored.execution_facts,
1429
+ sidecar_private_key,
1430
+ execution_status="failed",
1431
+ execution_error_code=result.failure_code,
1432
+ actual_exit_code=None,
1433
+ payload=None,
1434
+ )
1435
+ payloads: dict[str, bytes] = {}
1436
+ else:
1437
+ if result.actual_exit_code is None:
1438
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
1439
+ payload = _test_result_payload(arguments, result.actual_exit_code)
1440
+ receipt = _build_run_tests_receipt(
1441
+ old_context,
1442
+ stored.request,
1443
+ arguments,
1444
+ stored.execution_facts,
1445
+ sidecar_private_key,
1446
+ execution_status="succeeded",
1447
+ execution_error_code=None,
1448
+ actual_exit_code=result.actual_exit_code,
1449
+ payload=payload,
1450
+ )
1451
+ payloads = {receipt.evidence_refs[0].path: payload}
1452
+ evidence.complete_receipt_publication(
1453
+ ledger_path,
1454
+ evidence_root=evidence_root,
1455
+ receipt=receipt,
1456
+ payloads=payloads,
1457
+ clock=lambda: stored.reserved_at,
1458
+ _borrowed_lock_descriptor=lock_descriptor,
1459
+ )
1460
+ try:
1461
+ execution_driver.cleanup(stored.contract)
1462
+ except Exception as error:
1463
+ raise HandlerCoordinationError("RECOVERY_REQUIRED") from error
1464
+ _delete_handler_execution(
1465
+ ledger_path, lock_descriptor, stored.execution_id
1466
+ )
1467
+ return receipt
1468
+
1469
+
1470
+ def execute_run_tests(
1471
+ ledger_path: Path,
1472
+ *,
1473
+ evidence_root: Path,
1474
+ context: AuthorizationContext,
1475
+ request: AgentRequest,
1476
+ request_arguments: RunTestsArguments,
1477
+ execution_facts: ProspectiveExecutionFacts,
1478
+ candidate_snapshot_request: repo_tools.CandidateExecutionSnapshotRequest,
1479
+ sidecar_private_key: Ed25519PrivateKey,
1480
+ execution_driver: repo_tools.RunTestsExecutionDriver,
1481
+ clock: Callable[[], datetime],
1482
+ ) -> ToolCallReceipt:
1483
+ """Authorize, execute, sign, publish, and commit one test call."""
1484
+
1485
+ path = Path(ledger_path)
1486
+ root = Path(evidence_root)
1487
+ if (
1488
+ type(candidate_snapshot_request)
1489
+ is not repo_tools.CandidateExecutionSnapshotRequest
1490
+ or not callable(getattr(execution_driver, "prepare", None))
1491
+ or not callable(getattr(execution_driver, "start_and_wait", None))
1492
+ or not callable(getattr(execution_driver, "reconcile", None))
1493
+ or not callable(getattr(execution_driver, "cleanup", None))
1494
+ ):
1495
+ raise HandlerCoordinationError("HANDLER_UNAVAILABLE")
1496
+ evidence.recover_evidence_publications(path, evidence_root=root)
1497
+ lock_descriptor = evidence._acquire_target_lock(path)
1498
+ primary_error: Exception | None = None
1499
+ receipt: ToolCallReceipt | None = None
1500
+ try:
1501
+ _ensure_handler_execution_schema(path, lock_descriptor)
1502
+ now = evidence._freeze_trusted_utc_second(clock())
1503
+ stored = _load_stored_run_tests_execution(path, lock_descriptor)
1504
+ if stored is not None:
1505
+ receipt = _recover_run_tests_execution(
1506
+ path,
1507
+ root,
1508
+ lock_descriptor,
1509
+ context,
1510
+ stored,
1511
+ sidecar_private_key,
1512
+ execution_driver,
1513
+ now,
1514
+ )
1515
+ if receipt is not None:
1516
+ _, release_errors = evidence._release_target_lock(
1517
+ lock_descriptor
1518
+ )
1519
+ lock_descriptor = -1
1520
+ if release_errors:
1521
+ raise HandlerCoordinationError(
1522
+ "handler coordination lock release failed"
1523
+ ) from release_errors[0]
1524
+ return receipt
1525
+ if (
1526
+ key_id(sidecar_private_key.public_key())
1527
+ != execution_facts.controller_id
1528
+ ):
1529
+ raise HandlerCoordinationError(
1530
+ "Sidecar signing key does not match execution controller"
1531
+ )
1532
+ _require_current_context(
1533
+ path,
1534
+ root,
1535
+ context,
1536
+ now,
1537
+ lock_descriptor,
1538
+ )
1539
+ decision = authorize_tool_call(
1540
+ context,
1541
+ request,
1542
+ request_arguments,
1543
+ execution_facts,
1544
+ )
1545
+ if not decision.allowed:
1546
+ raise ToolCallDenied(decision)
1547
+ if (
1548
+ request_arguments.test_mode != "verifier"
1549
+ or request_arguments.command_digest
1550
+ != repo_tools.frozen_verifier_command_digest()
1551
+ or candidate_snapshot_request.source_artifact_sha256
1552
+ != context.work_order.replay_profile.source_artifact_sha256
1553
+ or candidate_snapshot_request.expected_head_commit
1554
+ != request_arguments.candidate_commit
1555
+ or candidate_snapshot_request.expected_workspace_manifest_digest
1556
+ != request_arguments.workspace_manifest_digest
1557
+ ):
1558
+ raise HandlerCoordinationError(
1559
+ "run-tests execution binding is invalid"
1560
+ )
1561
+ _preflight_run_tests_receipts(
1562
+ context,
1563
+ request,
1564
+ request_arguments,
1565
+ execution_facts,
1566
+ sidecar_private_key,
1567
+ )
1568
+ execution_id = _handler_execution_id(request, execution_facts)
1569
+ execution_contract = repo_tools.RunTestsExecutionContract(
1570
+ execution_id=execution_id,
1571
+ request_digest=request.digest,
1572
+ arguments_digest=request.arguments_digest,
1573
+ candidate_workspace_id=candidate_snapshot_request.workspace_id,
1574
+ source_artifact_sha256=(
1575
+ candidate_snapshot_request.source_artifact_sha256
1576
+ ),
1577
+ source_commit=request_arguments.source_commit,
1578
+ candidate_commit=request_arguments.candidate_commit,
1579
+ workspace_manifest_digest=(
1580
+ request_arguments.workspace_manifest_digest
1581
+ ),
1582
+ container_image_digest=(
1583
+ request_arguments.container_image_digest
1584
+ ),
1585
+ command_digest=request_arguments.command_digest,
1586
+ fixed_test_source_digest=(
1587
+ request_arguments.fixed_test_source_digest
1588
+ ),
1589
+ )
1590
+ _reserve_handler_execution(
1591
+ path,
1592
+ lock_descriptor,
1593
+ context,
1594
+ request,
1595
+ execution_facts,
1596
+ execution_contract,
1597
+ )
1598
+ try:
1599
+ snapshot = repo_tools.prepare_candidate_execution_snapshot(
1600
+ candidate_snapshot_request
1601
+ )
1602
+ if (
1603
+ snapshot.head_commit
1604
+ != candidate_snapshot_request.expected_head_commit
1605
+ or snapshot.workspace_manifest_digest
1606
+ != candidate_snapshot_request.expected_workspace_manifest_digest
1607
+ ):
1608
+ raise ValueError("candidate execution snapshot is mismatched")
1609
+ preparation = execution_driver.prepare(
1610
+ execution_contract,
1611
+ snapshot,
1612
+ )
1613
+ except Exception:
1614
+ preparation = repo_tools.RunTestsPreparationOutcome("UNRESOLVED")
1615
+ if preparation.action != "READY_TO_START":
1616
+ try:
1617
+ recovered = execution_driver.reconcile(
1618
+ execution_contract,
1619
+ "RESERVED",
1620
+ "ABSENT",
1621
+ )
1622
+ except Exception as error:
1623
+ raise HandlerCoordinationError(
1624
+ "RECOVERY_REQUIRED"
1625
+ ) from error
1626
+ if recovered.action == "SAFE_TO_RETRY":
1627
+ _delete_handler_execution(
1628
+ path, lock_descriptor, execution_id
1629
+ )
1630
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
1631
+ _mark_handler_started(
1632
+ path,
1633
+ lock_descriptor,
1634
+ execution_id,
1635
+ )
1636
+ try:
1637
+ outcome = execution_driver.start_and_wait(execution_contract)
1638
+ except Exception as error:
1639
+ raise HandlerCoordinationError("RECOVERY_REQUIRED") from error
1640
+ if outcome.action != "CLOSED_RESULT" or outcome.result is None:
1641
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
1642
+ result = outcome.result
1643
+ try:
1644
+ repo_tools.encode_run_tests_result_envelope(result)
1645
+ except (TypeError, ValueError) as error:
1646
+ raise HandlerCoordinationError("RECOVERY_REQUIRED") from error
1647
+ contract_digest = hashlib.sha256(
1648
+ repo_tools.encode_run_tests_execution_contract(execution_contract)
1649
+ ).hexdigest()
1650
+ if (
1651
+ result.execution_id != execution_id
1652
+ or result.execution_contract_digest != contract_digest
1653
+ ):
1654
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
1655
+ if result.failure_code is not None:
1656
+ receipt = _build_run_tests_receipt(
1657
+ context,
1658
+ request,
1659
+ request_arguments,
1660
+ execution_facts,
1661
+ sidecar_private_key,
1662
+ execution_status="failed",
1663
+ execution_error_code=result.failure_code,
1664
+ actual_exit_code=None,
1665
+ payload=None,
1666
+ )
1667
+ payloads = {}
1668
+ else:
1669
+ actual_exit_code = result.actual_exit_code
1670
+ if actual_exit_code is None:
1671
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
1672
+ payload = _test_result_payload(
1673
+ request_arguments,
1674
+ actual_exit_code,
1675
+ )
1676
+ receipt = _build_run_tests_receipt(
1677
+ context,
1678
+ request,
1679
+ request_arguments,
1680
+ execution_facts,
1681
+ sidecar_private_key,
1682
+ execution_status="succeeded",
1683
+ execution_error_code=None,
1684
+ actual_exit_code=actual_exit_code,
1685
+ payload=payload,
1686
+ )
1687
+ payloads = {receipt.evidence_refs[0].path: payload}
1688
+ evidence.complete_receipt_publication(
1689
+ path,
1690
+ evidence_root=root,
1691
+ receipt=receipt,
1692
+ payloads=payloads,
1693
+ clock=lambda: now,
1694
+ _borrowed_lock_descriptor=lock_descriptor,
1695
+ )
1696
+ try:
1697
+ execution_driver.cleanup(execution_contract)
1698
+ except Exception as error:
1699
+ raise HandlerCoordinationError("RECOVERY_REQUIRED") from error
1700
+ _delete_handler_execution(path, lock_descriptor, execution_id)
1701
+ except Exception as error:
1702
+ primary_error = error
1703
+ if lock_descriptor < 0:
1704
+ release_errors = ()
1705
+ else:
1706
+ _, release_errors = evidence._release_target_lock(lock_descriptor)
1707
+ if primary_error is not None:
1708
+ if release_errors:
1709
+ raise HandlerCoordinationError(
1710
+ "handler coordination and lock release both failed"
1711
+ ) from primary_error
1712
+ raise primary_error
1713
+ if release_errors:
1714
+ raise HandlerCoordinationError(
1715
+ "handler coordination lock release failed"
1716
+ ) from release_errors[0]
1717
+ assert receipt is not None
1718
+ return receipt
1719
+
1720
+
1721
+ def _rollback_parents(
1722
+ context: AuthorizationContext,
1723
+ request: AgentRequest,
1724
+ ) -> tuple[GrantIssuedReceipt, ToolCallReceipt, ToolCallReceipt]:
1725
+ receipts = context.ledger_prefix.receipts
1726
+ issuance = next(
1727
+ (
1728
+ receipt
1729
+ for receipt in receipts
1730
+ if isinstance(receipt, GrantIssuedReceipt)
1731
+ and receipt.policy_decision == "allow"
1732
+ and receipt.issued_grant_id == request.grant_id
1733
+ ),
1734
+ None,
1735
+ )
1736
+ target = next(
1737
+ (
1738
+ receipt
1739
+ for receipt in receipts
1740
+ if receipt.receipt_id == context.active_patch_receipt_id
1741
+ ),
1742
+ None,
1743
+ )
1744
+ failure = next(
1745
+ (
1746
+ receipt
1747
+ for receipt in receipts
1748
+ if receipt.receipt_id == context.causal_state.failure_receipt_id
1749
+ ),
1750
+ None,
1751
+ )
1752
+ if (
1753
+ not isinstance(issuance, GrantIssuedReceipt)
1754
+ or not isinstance(target, ToolCallReceipt)
1755
+ or not isinstance(failure, ToolCallReceipt)
1756
+ ):
1757
+ raise HandlerCoordinationError(
1758
+ "rollback causal parents are unavailable"
1759
+ )
1760
+ return issuance, target, failure
1761
+
1762
+
1763
+ def _rollback_command(
1764
+ context: AuthorizationContext,
1765
+ request: AgentRequest,
1766
+ ) -> RollbackCommand:
1767
+ _, target, _ = _rollback_parents(context, request)
1768
+ return RollbackCommand(
1769
+ target_patch_receipt_id=target.receipt_id,
1770
+ target_patch_digest=target.digest,
1771
+ before_commit=context.replay_checkpoint.head_commit,
1772
+ )
1773
+
1774
+
1775
+ def _build_rollback_receipt(
1776
+ context: AuthorizationContext,
1777
+ request: AgentRequest,
1778
+ sidecar_private_key: Ed25519PrivateKey,
1779
+ result: RollbackHandlerResult,
1780
+ ) -> RollbackReceipt:
1781
+ issuance, target, failure = _rollback_parents(context, request)
1782
+ remaining_before = _remaining_tool_calls(context, request.grant_id)
1783
+ sidecar_key_id = key_id(sidecar_private_key.public_key())
1784
+ raw = {
1785
+ "protocol_version": "0.1",
1786
+ "receipt_id": _digest(
1787
+ {
1788
+ "domain": "openworkproof/receipt-id/v0.1",
1789
+ "request_digest": request.digest,
1790
+ "entropy": secrets.token_hex(32),
1791
+ }
1792
+ ),
1793
+ "work_order_digest": context.work_order.digest,
1794
+ "actor_type": "agent",
1795
+ "actor_id": request.actor_id,
1796
+ "actor_key_id": request.actor_key_id,
1797
+ "nested_claim_type": "agent-request",
1798
+ "nested_claim_digest": request.digest,
1799
+ "nested_claim": request.model_dump(mode="json"),
1800
+ "gateway_signer_key_id": sidecar_key_id,
1801
+ "event_type": "rollback",
1802
+ "policy_decision": "allow",
1803
+ "policy_error_code": None,
1804
+ "execution_status": result.execution_status,
1805
+ "execution_error_code": (
1806
+ None
1807
+ if result.execution_status == "succeeded"
1808
+ else "HANDLER_ERROR"
1809
+ ),
1810
+ "quota_charge": {
1811
+ "grant_id": request.grant_id,
1812
+ "metric": "tool_calls",
1813
+ "amount": 1,
1814
+ "remaining_after": remaining_before - 1,
1815
+ },
1816
+ "state_before": "needs_rework",
1817
+ "state_after": "needs_rework",
1818
+ "parent_receipt_ids": [
1819
+ issuance.receipt_id,
1820
+ target.receipt_id,
1821
+ failure.receipt_id,
1822
+ ],
1823
+ "correlation_factors": None,
1824
+ "evidence_refs": [],
1825
+ "occurred_at": context.transaction_time.strftime(
1826
+ "%Y-%m-%dT%H:%M:%SZ"
1827
+ ),
1828
+ "sequence": len(context.ledger_prefix.receipts) + 1,
1829
+ "nonce": request.nonce,
1830
+ "previous_receipt_digest": (
1831
+ context.ledger_prefix.receipts[-1].digest
1832
+ ),
1833
+ "grant_id": request.grant_id,
1834
+ "target_patch_receipt_id": target.receipt_id,
1835
+ "target_patch_digest": target.digest,
1836
+ "before_commit": result.before_commit,
1837
+ "after_commit": result.after_commit,
1838
+ "after_manifest_digest": result.after_manifest_digest,
1839
+ "rollback_result": result.execution_status,
1840
+ }
1841
+ return ACTION_RECEIPT_ADAPTER.validate_python(
1842
+ sign_payload("action-receipt", raw, sidecar_private_key)
1843
+ )
1844
+
1845
+
1846
+ def _preflight_rollback_receipts(
1847
+ context: AuthorizationContext,
1848
+ request: AgentRequest,
1849
+ sidecar_private_key: Ed25519PrivateKey,
1850
+ ) -> None:
1851
+ before = context.replay_checkpoint.head_commit
1852
+ alternate = context.work_order.source_commit
1853
+ if alternate == before:
1854
+ alternate = "0" * 40 if before != "0" * 40 else "1" * 40
1855
+ representatives = (
1856
+ RollbackHandlerResult(
1857
+ execution_status="succeeded",
1858
+ before_commit=before,
1859
+ after_commit=alternate,
1860
+ after_manifest_digest="0" * 64,
1861
+ ),
1862
+ RollbackHandlerResult(
1863
+ execution_status="failed",
1864
+ before_commit=before,
1865
+ after_commit=before,
1866
+ after_manifest_digest=(
1867
+ context.replay_checkpoint.workspace_manifest_digest
1868
+ ),
1869
+ ),
1870
+ )
1871
+ if any(
1872
+ len(
1873
+ rfc8785.dumps(
1874
+ _build_rollback_receipt(
1875
+ context,
1876
+ request,
1877
+ sidecar_private_key,
1878
+ result,
1879
+ ).model_dump(mode="json")
1880
+ )
1881
+ )
1882
+ > _MAX_RECEIPT_BYTES
1883
+ for result in representatives
1884
+ ):
1885
+ raise HandlerCoordinationError("BUNDLE_CAPACITY_EXCEEDED")
1886
+
1887
+
1888
+ def execute_rollback(
1889
+ ledger_path: Path,
1890
+ *,
1891
+ evidence_root: Path,
1892
+ context: AuthorizationContext,
1893
+ request: AgentRequest,
1894
+ execution_facts: ProspectiveExecutionFacts,
1895
+ sidecar_private_key: Ed25519PrivateKey,
1896
+ handler: Callable[[RollbackCommand], RollbackHandlerResult],
1897
+ clock: Callable[[], datetime],
1898
+ ) -> RollbackReceipt:
1899
+ """Authorize, execute, sign, and commit one rollback attempt."""
1900
+
1901
+ path = Path(ledger_path)
1902
+ root = Path(evidence_root)
1903
+ if not callable(handler):
1904
+ raise HandlerCoordinationError("HANDLER_UNAVAILABLE")
1905
+ evidence.recover_evidence_publications(path, evidence_root=root)
1906
+ lock_descriptor = evidence._acquire_target_lock(path)
1907
+ primary_error: Exception | None = None
1908
+ receipt: RollbackReceipt | None = None
1909
+ try:
1910
+ _ensure_handler_execution_schema(path, lock_descriptor)
1911
+ _recover_handler_executions(path, lock_descriptor)
1912
+ now = evidence._freeze_trusted_utc_second(clock())
1913
+ if (
1914
+ key_id(sidecar_private_key.public_key())
1915
+ != execution_facts.controller_id
1916
+ ):
1917
+ raise HandlerCoordinationError(
1918
+ "Sidecar signing key does not match execution controller"
1919
+ )
1920
+ _require_current_context(
1921
+ path,
1922
+ root,
1923
+ context,
1924
+ now,
1925
+ lock_descriptor,
1926
+ )
1927
+ decision = validate_rollback(context, request)
1928
+ if not decision.allowed:
1929
+ raise ToolCallDenied(decision)
1930
+ _preflight_rollback_receipts(
1931
+ context,
1932
+ request,
1933
+ sidecar_private_key,
1934
+ )
1935
+ command = _rollback_command(context, request)
1936
+ execution_id = _reserve_handler_execution(
1937
+ path,
1938
+ lock_descriptor,
1939
+ context,
1940
+ request,
1941
+ execution_facts,
1942
+ None,
1943
+ )
1944
+ _mark_handler_started(path, lock_descriptor, execution_id)
1945
+ try:
1946
+ result = handler(command)
1947
+ except Exception as error:
1948
+ raise HandlerCoordinationError("RECOVERY_REQUIRED") from error
1949
+ if type(result) is not RollbackHandlerResult:
1950
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
1951
+ receipt = _build_rollback_receipt(
1952
+ context,
1953
+ request,
1954
+ sidecar_private_key,
1955
+ result,
1956
+ )
1957
+ evidence.complete_receipt_publication(
1958
+ path,
1959
+ evidence_root=root,
1960
+ receipt=receipt,
1961
+ payloads={},
1962
+ clock=lambda: now,
1963
+ _borrowed_lock_descriptor=lock_descriptor,
1964
+ )
1965
+ _finalize_handler_execution(path, lock_descriptor)
1966
+ except Exception as error:
1967
+ primary_error = error
1968
+ _, release_errors = evidence._release_target_lock(lock_descriptor)
1969
+ if primary_error is not None:
1970
+ if release_errors:
1971
+ raise HandlerCoordinationError(
1972
+ "handler coordination and lock release both failed"
1973
+ ) from primary_error
1974
+ raise primary_error
1975
+ if release_errors:
1976
+ raise HandlerCoordinationError(
1977
+ "handler coordination lock release failed"
1978
+ ) from release_errors[0]
1979
+ assert receipt is not None
1980
+ return receipt
1981
+
1982
+
1983
+ __all__ = [
1984
+ "HandlerCoordinationError",
1985
+ "RollbackCommand",
1986
+ "RollbackHandlerResult",
1987
+ "ToolCallDenied",
1988
+ "execute_rollback",
1989
+ "execute_run_tests",
1990
+ "make_candidate_rollback_handler",
1991
+ "produce_deny_receipt",
1992
+ ]
1993
+
1994
+
1995
+ def build_docker_run_tests_driver(
1996
+ *,
1997
+ docker_binary: Path,
1998
+ image_reference: str,
1999
+ candidate_runtime_root: Path,
2000
+ ) -> repo_tools.DockerRunTestsExecutor:
2001
+ """Construct the production Docker run-tests executor (fail closed)."""
2002
+ if (
2003
+ not isinstance(docker_binary, Path)
2004
+ or not docker_binary.is_absolute()
2005
+ or not isinstance(candidate_runtime_root, Path)
2006
+ or not candidate_runtime_root.is_absolute()
2007
+ or type(image_reference) is not str
2008
+ ):
2009
+ raise HandlerCoordinationError("HANDLER_UNAVAILABLE")
2010
+ try:
2011
+ return repo_tools.DockerRunTestsExecutor(
2012
+ docker_binary=docker_binary,
2013
+ candidate_runtime_root=candidate_runtime_root,
2014
+ image_reference=image_reference,
2015
+ )
2016
+ except ValueError as error:
2017
+ raise HandlerCoordinationError("HANDLER_UNAVAILABLE") from error
2018
+
2019
+
2020
+ def execute_run_tests_production(
2021
+ ledger_path: Path,
2022
+ *,
2023
+ evidence_root: Path,
2024
+ context: AuthorizationContext,
2025
+ request: AgentRequest,
2026
+ request_arguments: RunTestsArguments,
2027
+ execution_facts: ProspectiveExecutionFacts,
2028
+ sidecar_private_key: Ed25519PrivateKey,
2029
+ docker_binary: Path,
2030
+ image_reference: str,
2031
+ candidate_runtime_root: Path,
2032
+ clock: Callable[[], datetime],
2033
+ ) -> ToolCallReceipt:
2034
+ """Run one production test call with the real Docker executor."""
2035
+ if (
2036
+ not isinstance(docker_binary, Path)
2037
+ or not isinstance(candidate_runtime_root, Path)
2038
+ or type(image_reference) is not str
2039
+ ):
2040
+ raise HandlerCoordinationError("HANDLER_UNAVAILABLE")
2041
+ snapshot_request = repo_tools.CandidateExecutionSnapshotRequest(
2042
+ runtime_root=Path(candidate_runtime_root),
2043
+ workspace_id="c" * 64,
2044
+ source_artifact_sha256=(
2045
+ context.work_order.replay_profile.source_artifact_sha256
2046
+ ),
2047
+ expected_head_commit=request_arguments.candidate_commit,
2048
+ expected_workspace_manifest_digest=(
2049
+ request_arguments.workspace_manifest_digest
2050
+ ),
2051
+ )
2052
+ driver = build_docker_run_tests_driver(
2053
+ docker_binary=docker_binary,
2054
+ image_reference=image_reference,
2055
+ candidate_runtime_root=Path(candidate_runtime_root),
2056
+ )
2057
+ return execute_run_tests(
2058
+ ledger_path,
2059
+ evidence_root=evidence_root,
2060
+ context=context,
2061
+ request=request,
2062
+ request_arguments=request_arguments,
2063
+ execution_facts=execution_facts,
2064
+ candidate_snapshot_request=snapshot_request,
2065
+ sidecar_private_key=sidecar_private_key,
2066
+ execution_driver=driver,
2067
+ clock=clock,
2068
+ )
2069
+
2070
+
2071
+ def make_repo_pipeline_read_handler(
2072
+ *,
2073
+ max_bytes: int = 1_048_576,
2074
+ ) -> Callable[
2075
+ [repo_tools.CandidateReadRequest], repo_tools.CandidateReadResult
2076
+ ]:
2077
+ """Build a production repo-read handler backed by the repo pipeline.
2078
+
2079
+ The handler reads ``CandidateReadRequest.path`` under the candidate
2080
+ runtime root through the repo_pipeline reader (UTF-8 decode, size cap,
2081
+ permission guards) and constructs the exact ``RepoReadOutput`` with its
2082
+ content digest and the expected workspace-manifest binding.
2083
+ """
2084
+
2085
+ def handler(
2086
+ command: repo_tools.CandidateReadRequest,
2087
+ ) -> repo_tools.CandidateReadResult:
2088
+ from openworkproof.repo_pipeline.errors import RepoPipelineError
2089
+ from openworkproof.repo_pipeline.reader import (
2090
+ read_text_file,
2091
+ sha256_bytes,
2092
+ )
2093
+
2094
+ root = Path(command.runtime_root)
2095
+ path = root / command.path
2096
+ if not path.is_file():
2097
+ raise HandlerCoordinationError("REPO_READ_PATH_MISSING")
2098
+ try:
2099
+ content = read_text_file(path, max_bytes=max_bytes)
2100
+ except RepoPipelineError as error:
2101
+ raise HandlerCoordinationError("REPO_READ_READ_FAILED") from error
2102
+ raw = content.encode("utf-8")
2103
+ return repo_tools.CandidateReadResult(
2104
+ content=raw,
2105
+ output=repo_tools.RepoReadOutput(
2106
+ path=command.path,
2107
+ content_sha256=sha256_bytes(raw),
2108
+ size_bytes=len(raw),
2109
+ workspace_manifest_digest=(
2110
+ command.expected_workspace_manifest_digest
2111
+ ),
2112
+ ),
2113
+ )
2114
+
2115
+ return handler
2116
+
2117
+
2118
+ def _repo_read_predicate_results(
2119
+ context: AuthorizationContext,
2120
+ request: AgentRequest,
2121
+ arguments: RepoReadArguments,
2122
+ output_digest: str,
2123
+ ) -> tuple:
2124
+ """Construct the exact predicate results for a repo-read receipt."""
2125
+ from openworkproof.predicates import ( # noqa: PLC0415
2126
+ EvaluationContext,
2127
+ evaluate_required_predicates,
2128
+ )
2129
+ from openworkproof.repo_tools import ( # noqa: PLC0415
2130
+ ResolutionManifest,
2131
+ ResolutionManifestEntry,
2132
+ resolution_manifest_digest,
2133
+ )
2134
+
2135
+ selected = tuple(
2136
+ spec
2137
+ for spec in (
2138
+ context.work_order.preconditions
2139
+ + context.work_order.invariants
2140
+ )
2141
+ if "owp.repo_read" in spec.applies_to_tools
2142
+ )
2143
+ inputs: dict[str, object] = {}
2144
+ for spec in selected:
2145
+ if spec.name == "tool_allowed":
2146
+ inputs[spec.predicate_id] = {
2147
+ "actual_tool_name": "owp.repo_read"
2148
+ }
2149
+ elif spec.name == "quota_remaining":
2150
+ inputs[spec.predicate_id] = {
2151
+ "grant_id": request.grant_id,
2152
+ "metric": "tool_calls",
2153
+ "amount": 1,
2154
+ "grant_remaining_before": _remaining_tool_calls(
2155
+ context, request.grant_id
2156
+ ),
2157
+ "ledger_prefix_digest": (
2158
+ context.ledger_prefix.receipts[-1].digest
2159
+ ),
2160
+ }
2161
+ elif spec.name == "path_allowed":
2162
+ manifest = ResolutionManifest(
2163
+ schema_version="openworkproof-resolution-manifest/0.1",
2164
+ workspace_manifest_digest=(
2165
+ context.replay_checkpoint.workspace_manifest_digest
2166
+ ),
2167
+ requested_paths=(arguments.path,),
2168
+ resolved_entries=(
2169
+ ResolutionManifestEntry(
2170
+ requested_path=arguments.path,
2171
+ resolved_relative_path=arguments.path,
2172
+ ),
2173
+ ),
2174
+ )
2175
+ inputs[spec.predicate_id] = {
2176
+ "requested_paths": [arguments.path],
2177
+ "resolved_entries": [
2178
+ {
2179
+ "requested_path": arguments.path,
2180
+ "resolved_relative_path": arguments.path,
2181
+ }
2182
+ ],
2183
+ "resolution_manifest_digest": resolution_manifest_digest(
2184
+ manifest
2185
+ ),
2186
+ }
2187
+ else:
2188
+ raise HandlerCoordinationError(
2189
+ "repo-read predicate has no offline authority rule"
2190
+ )
2191
+ results = evaluate_required_predicates(
2192
+ selected,
2193
+ EvaluationContext(
2194
+ inputs=inputs,
2195
+ authoritative_inputs=inputs,
2196
+ authoritative_ledger_prefix_digests={
2197
+ request.grant_id: context.ledger_prefix.receipts[-1].digest,
2198
+ },
2199
+ ),
2200
+ )
2201
+ return tuple(
2202
+ result.model_dump(mode="json") for result in results
2203
+ )
2204
+
2205
+
2206
+ def _repo_read_parents(
2207
+ context: AuthorizationContext,
2208
+ request: AgentRequest,
2209
+ ) -> tuple[str, ...]:
2210
+ """Causal parents for a repo-read receipt: grant issuance plus the active
2211
+ patch when one exists (mirrors the frozen causal replay rule)."""
2212
+ receipts = context.ledger_prefix.receipts
2213
+ issuance = next(
2214
+ (
2215
+ receipt
2216
+ for receipt in receipts
2217
+ if isinstance(receipt, GrantIssuedReceipt)
2218
+ and receipt.policy_decision == "allow"
2219
+ and receipt.issued_grant_id == request.grant_id
2220
+ ),
2221
+ None,
2222
+ )
2223
+ if issuance is None:
2224
+ raise HandlerCoordinationError("repo-read causal parents are unavailable")
2225
+ parents: dict[str, ActionReceiptEnvelope] = {
2226
+ issuance.receipt_id: issuance
2227
+ }
2228
+ active_patch = next(
2229
+ (
2230
+ receipt
2231
+ for receipt in receipts
2232
+ if receipt.receipt_id == context.active_patch_receipt_id
2233
+ ),
2234
+ None,
2235
+ )
2236
+ if active_patch is not None:
2237
+ parents[active_patch.receipt_id] = active_patch
2238
+ return tuple(
2239
+ receipt.receipt_id
2240
+ for receipt in sorted(
2241
+ parents.values(), key=lambda item: item.sequence
2242
+ )
2243
+ )
2244
+
2245
+
2246
+ def _repo_read_command(
2247
+ context: AuthorizationContext,
2248
+ arguments: RepoReadArguments,
2249
+ candidate_runtime_root: Path,
2250
+ ) -> repo_tools.CandidateReadRequest:
2251
+ return repo_tools.CandidateReadRequest(
2252
+ runtime_root=Path(candidate_runtime_root),
2253
+ workspace_id="c" * 64,
2254
+ source_artifact_sha256=(
2255
+ context.work_order.replay_profile.source_artifact_sha256
2256
+ ),
2257
+ expected_head_commit=context.replay_checkpoint.head_commit,
2258
+ expected_workspace_manifest_digest=(
2259
+ context.replay_checkpoint.workspace_manifest_digest
2260
+ ),
2261
+ path=arguments.path,
2262
+ )
2263
+
2264
+
2265
+ def _build_repo_read_receipt(
2266
+ context: AuthorizationContext,
2267
+ request: AgentRequest,
2268
+ arguments: RepoReadArguments,
2269
+ result: repo_tools.CandidateReadResult,
2270
+ sidecar_private_key: Ed25519PrivateKey,
2271
+ execution_facts: ProspectiveExecutionFacts,
2272
+ ) -> ToolCallReceipt:
2273
+ remaining_before = _remaining_tool_calls(context, request.grant_id)
2274
+ sidecar_key_id = key_id(sidecar_private_key.public_key())
2275
+ output_digest = _digest(result.output.model_dump(mode="json"))
2276
+ raw = {
2277
+ "protocol_version": "0.1",
2278
+ "receipt_id": _digest(
2279
+ {
2280
+ "domain": "openworkproof/receipt-id/v0.1",
2281
+ "request_digest": request.digest,
2282
+ "entropy": secrets.token_hex(32),
2283
+ }
2284
+ ),
2285
+ "work_order_digest": context.work_order.digest,
2286
+ "actor_type": "agent",
2287
+ "actor_id": request.actor_id,
2288
+ "actor_key_id": request.actor_key_id,
2289
+ "nested_claim_type": "agent-request",
2290
+ "nested_claim_digest": request.digest,
2291
+ "nested_claim": request.model_dump(mode="json"),
2292
+ "gateway_signer_key_id": sidecar_key_id,
2293
+ "event_type": "tool_call",
2294
+ "policy_decision": "allow",
2295
+ "policy_error_code": None,
2296
+ "execution_status": "succeeded",
2297
+ "execution_error_code": None,
2298
+ "quota_charge": {
2299
+ "grant_id": request.grant_id,
2300
+ "metric": "tool_calls",
2301
+ "amount": 1,
2302
+ "remaining_after": remaining_before - 1,
2303
+ },
2304
+ "state_before": context.current_state,
2305
+ "state_after": context.current_state,
2306
+ "parent_receipt_ids": list(_repo_read_parents(context, request)),
2307
+ "correlation_factors": {
2308
+ "model_id": request.model_id,
2309
+ "model_version": request.model_version,
2310
+ "prompt_template_digest": request.prompt_template_digest,
2311
+ "context_source_digest": request.context_source_digest,
2312
+ "toolchain_id": _digest(
2313
+ {
2314
+ "domain": "openworkproof/toolchain/v0.1",
2315
+ "tool_name": "owp.repo_read",
2316
+ "tool_version": "0.1",
2317
+ }
2318
+ ),
2319
+ "execution_context_id": execution_facts.execution_context_id,
2320
+ "container_instance_id_digest": (
2321
+ execution_facts.container_instance_id_digest
2322
+ ),
2323
+ "controller_id": execution_facts.controller_id,
2324
+ "fixed_test_source_digest": None,
2325
+ },
2326
+ "evidence_refs": [],
2327
+ "occurred_at": context.transaction_time.strftime(
2328
+ "%Y-%m-%dT%H:%M:%SZ"
2329
+ ),
2330
+ "sequence": len(context.ledger_prefix.receipts) + 1,
2331
+ "nonce": request.nonce,
2332
+ "previous_receipt_digest": (
2333
+ context.ledger_prefix.receipts[-1].digest
2334
+ ),
2335
+ "grant_id": request.grant_id,
2336
+ "tool_name": "owp.repo_read",
2337
+ "tool_version": "0.1",
2338
+ "request_arguments": arguments.model_dump(mode="json"),
2339
+ "arguments_digest": request.arguments_digest,
2340
+ "output_digest": output_digest,
2341
+ "predicate_results": list(
2342
+ _repo_read_predicate_results(
2343
+ context,
2344
+ request,
2345
+ arguments,
2346
+ output_digest,
2347
+ )
2348
+ ),
2349
+ }
2350
+ return ACTION_RECEIPT_ADAPTER.validate_python(
2351
+ sign_payload("action-receipt", raw, sidecar_private_key)
2352
+ )
2353
+
2354
+
2355
+ def _preflight_repo_read_receipts(
2356
+ context: AuthorizationContext,
2357
+ request: AgentRequest,
2358
+ arguments: RepoReadArguments,
2359
+ sidecar_private_key: Ed25519PrivateKey,
2360
+ execution_facts: ProspectiveExecutionFacts,
2361
+ ) -> None:
2362
+ import base64 # noqa: PLC0415
2363
+
2364
+ entries = context.replay_checkpoint.workspace_manifest.entries
2365
+ decoded_paths = {
2366
+ base64.urlsafe_b64decode(
2367
+ (entry.path_bytes_b64url + "==").encode("ascii")
2368
+ ).decode("utf-8")
2369
+ for entry in entries
2370
+ }
2371
+ if arguments.path not in decoded_paths:
2372
+ raise HandlerCoordinationError("REPO_READ_PATH_DENIED")
2373
+ representative = repo_tools.CandidateReadResult(
2374
+ content=b"x" * 65_536,
2375
+ output=repo_tools.RepoReadOutput(
2376
+ path=arguments.path,
2377
+ content_sha256="0" * 64,
2378
+ size_bytes=65_536,
2379
+ workspace_manifest_digest=(
2380
+ context.replay_checkpoint.workspace_manifest_digest
2381
+ ),
2382
+ ),
2383
+ )
2384
+ if (
2385
+ len(
2386
+ rfc8785.dumps(
2387
+ _build_repo_read_receipt(
2388
+ context,
2389
+ request,
2390
+ arguments,
2391
+ representative,
2392
+ sidecar_private_key,
2393
+ execution_facts,
2394
+ ).model_dump(mode="json")
2395
+ )
2396
+ )
2397
+ > _MAX_RECEIPT_BYTES
2398
+ ):
2399
+ raise HandlerCoordinationError("BUNDLE_CAPACITY_EXCEEDED")
2400
+
2401
+
2402
+ def _readback_repo_read_committed(
2403
+ ledger_path: Path,
2404
+ *,
2405
+ work_order,
2406
+ receipt: ToolCallReceipt,
2407
+ ) -> bool:
2408
+ try:
2409
+ connection = evidence.connect_ledger(ledger_path)
2410
+ try:
2411
+ current_work_order, receipts, _, _ = (
2412
+ evidence._replay_receipt_publication_ledger(connection)
2413
+ )
2414
+ finally:
2415
+ connection.close()
2416
+ except Exception:
2417
+ return False
2418
+ return (
2419
+ current_work_order == work_order
2420
+ and bool(receipts)
2421
+ and receipts[-1].receipt_id == receipt.receipt_id
2422
+ and receipts[-1] == receipt
2423
+ )
2424
+
2425
+
2426
+ def execute_repo_read(
2427
+ ledger_path: Path,
2428
+ *,
2429
+ evidence_root: Path,
2430
+ context: AuthorizationContext,
2431
+ request: AgentRequest,
2432
+ request_arguments: RepoReadArguments,
2433
+ execution_facts: ProspectiveExecutionFacts,
2434
+ sidecar_private_key: Ed25519PrivateKey,
2435
+ candidate_runtime_root: Path,
2436
+ handler: Callable[[repo_tools.CandidateReadRequest], repo_tools.CandidateReadResult],
2437
+ clock: Callable[[], datetime],
2438
+ ) -> ToolCallReceipt:
2439
+ """Authorize, execute, sign, and commit one repo-read attempt."""
2440
+ if (
2441
+ not callable(handler)
2442
+ or not isinstance(candidate_runtime_root, Path)
2443
+ or not isinstance(request_arguments, RepoReadArguments)
2444
+ ):
2445
+ raise HandlerCoordinationError("HANDLER_UNAVAILABLE")
2446
+ arguments = request_arguments
2447
+ path = Path(ledger_path)
2448
+ root = Path(evidence_root)
2449
+ evidence.recover_evidence_publications(path, evidence_root=root)
2450
+ lock_descriptor = evidence._acquire_target_lock(path)
2451
+ primary_error: Exception | None = None
2452
+ receipt: ToolCallReceipt | None = None
2453
+ try:
2454
+ _ensure_handler_execution_schema(path, lock_descriptor)
2455
+ _recover_handler_executions(path, lock_descriptor)
2456
+ now = evidence._freeze_trusted_utc_second(clock())
2457
+ _require_current_context(
2458
+ path,
2459
+ root,
2460
+ context,
2461
+ now,
2462
+ lock_descriptor,
2463
+ )
2464
+ decision = authorize_tool_call(
2465
+ context,
2466
+ request,
2467
+ arguments,
2468
+ None,
2469
+ )
2470
+ if not decision.allowed:
2471
+ raise ToolCallDenied(decision)
2472
+ _preflight_repo_read_receipts(
2473
+ context,
2474
+ request,
2475
+ arguments,
2476
+ sidecar_private_key,
2477
+ execution_facts,
2478
+ )
2479
+ command = _repo_read_command(context, arguments, candidate_runtime_root)
2480
+ execution_id = _reserve_handler_execution(
2481
+ path,
2482
+ lock_descriptor,
2483
+ context,
2484
+ request,
2485
+ execution_facts,
2486
+ None,
2487
+ )
2488
+ _mark_handler_started(path, lock_descriptor, execution_id)
2489
+ try:
2490
+ result = handler(command)
2491
+ except Exception as error:
2492
+ raise HandlerCoordinationError("RECOVERY_REQUIRED") from error
2493
+ if type(result) is not repo_tools.CandidateReadResult:
2494
+ raise HandlerCoordinationError("RECOVERY_REQUIRED")
2495
+ receipt = _build_repo_read_receipt(
2496
+ context,
2497
+ request,
2498
+ arguments,
2499
+ result,
2500
+ sidecar_private_key,
2501
+ execution_facts,
2502
+ )
2503
+ evidence.complete_receipt_publication(
2504
+ path,
2505
+ evidence_root=root,
2506
+ receipt=receipt,
2507
+ payloads={},
2508
+ clock=lambda: now,
2509
+ _borrowed_lock_descriptor=lock_descriptor,
2510
+ )
2511
+ _finalize_handler_execution(path, lock_descriptor)
2512
+ except Exception as error:
2513
+ primary_error = error
2514
+ _, release_errors = evidence._release_target_lock(lock_descriptor)
2515
+ if primary_error is not None:
2516
+ if release_errors:
2517
+ raise HandlerCoordinationError(
2518
+ "handler coordination and lock release both failed"
2519
+ ) from primary_error
2520
+ raise primary_error
2521
+ if release_errors:
2522
+ raise HandlerCoordinationError(
2523
+ "handler coordination lock release failed"
2524
+ ) from release_errors[0]
2525
+ assert receipt is not None
2526
+ return receipt
2527
+
2528
+
2529
+ def produce_deny_receipt(
2530
+ ledger_path: Path,
2531
+ *,
2532
+ evidence_root: Path,
2533
+ context: AuthorizationContext,
2534
+ request: AgentRequest,
2535
+ arguments: object,
2536
+ execution_facts: ProspectiveExecutionFacts,
2537
+ sidecar_private_key: Ed25519PrivateKey,
2538
+ decision: PolicyDecision,
2539
+ clock: Callable[[], datetime],
2540
+ ) -> ToolCallReceipt:
2541
+ """Atomically record an authenticated same-state denial receipt.
2542
+
2543
+ The policy layer already denied the tool call (for example
2544
+ ROLE_DENIED / CAPABILITY_DENIED / QUOTA_EXHAUSTED). This entry point
2545
+ records an immutable, zero-charge denial receipt so the rejection
2546
+ itself becomes auditable — without starting a handler, charging
2547
+ quota, or changing task state. The nonce is derived from a dedicated
2548
+ domain so it is globally unique and independent of the request nonce.
2549
+
2550
+ It is an optional audit entry: callers that only need the denial
2551
+ error keep raising ToolCallDenied; callers that need a denial audit
2552
+ trail call this function before surfacing the error.
2553
+ """
2554
+ path = Path(ledger_path)
2555
+ root = Path(evidence_root)
2556
+ try:
2557
+ ToolRequestArguments.__class_getitem__ # noqa: B018 - type-alias marker
2558
+ except (AttributeError, TypeError):
2559
+ pass
2560
+ if not _is_tool_request_arguments(arguments):
2561
+ raise ValueError("deny receipt arguments must be a ToolRequestArguments")
2562
+ if not isinstance(decision, PolicyDecision) or decision.allowed:
2563
+ raise ValueError("deny receipt requires a non-allowed PolicyDecision")
2564
+ if (
2565
+ key_id(sidecar_private_key.public_key())
2566
+ != execution_facts.controller_id
2567
+ ):
2568
+ raise HandlerCoordinationError(
2569
+ "Sidecar signing key does not match execution controller"
2570
+ )
2571
+ evidence.recover_evidence_publications(path, evidence_root=root)
2572
+ lock_descriptor = evidence._acquire_target_lock(path)
2573
+ primary_error: Exception | None = None
2574
+ receipt: ToolCallReceipt | None = None
2575
+ try:
2576
+ _ensure_handler_execution_schema(path, lock_descriptor)
2577
+ now = evidence._freeze_trusted_utc_second(clock())
2578
+ _require_current_context(
2579
+ path,
2580
+ root,
2581
+ context,
2582
+ now,
2583
+ lock_descriptor,
2584
+ )
2585
+ state = context.current_state
2586
+ receipt_id = hashlib.sha256(
2587
+ rfc8785.dumps(
2588
+ {
2589
+ "domain": "openworkproof/deny-receipt/v0.1",
2590
+ "work_order_digest": context.work_order.digest,
2591
+ "tool_name": request.tool_name,
2592
+ "arguments_digest": request.arguments_digest,
2593
+ "policy_error_code": decision.error_code,
2594
+ "sequence_hint": len(context.ledger_prefix.receipts) + 1,
2595
+ }
2596
+ )
2597
+ ).hexdigest()
2598
+ raw = {
2599
+ "protocol_version": "0.1",
2600
+ "receipt_id": receipt_id,
2601
+ "work_order_digest": context.work_order.digest,
2602
+ "actor_type": "agent",
2603
+ "actor_id": request.actor_id,
2604
+ "actor_key_id": request.actor_key_id,
2605
+ "nested_claim_type": "agent-request",
2606
+ "nested_claim_digest": request.digest,
2607
+ "nested_claim": request.model_dump(mode="json"),
2608
+ "gateway_signer_key_id": key_id(sidecar_private_key.public_key()),
2609
+ "event_type": "tool_call",
2610
+ "policy_decision": "deny",
2611
+ "policy_error_code": decision.error_code,
2612
+ "execution_status": "denied",
2613
+ "execution_error_code": None,
2614
+ "quota_charge": None,
2615
+ "state_before": state,
2616
+ "state_after": state,
2617
+ "parent_receipt_ids": _causal_parents(context, request),
2618
+ "correlation_factors": {
2619
+ "model_id": request.model_id,
2620
+ "model_version": request.model_version,
2621
+ "prompt_template_digest": request.prompt_template_digest,
2622
+ "context_source_digest": request.context_source_digest,
2623
+ "toolchain_id": None,
2624
+ "execution_context_id": None,
2625
+ "container_instance_id_digest": None,
2626
+ "controller_id": execution_facts.controller_id,
2627
+ "fixed_test_source_digest": None,
2628
+ },
2629
+ "evidence_refs": [],
2630
+ "occurred_at": now.strftime("%Y-%m-%dT%H:%M:%SZ"),
2631
+ "sequence": len(context.ledger_prefix.receipts) + 1,
2632
+ "nonce": request.nonce,
2633
+ "previous_receipt_digest": (
2634
+ context.ledger_prefix.receipts[-1].receipt_id
2635
+ if context.ledger_prefix.receipts
2636
+ else context.work_order.digest
2637
+ ),
2638
+ "grant_id": request.grant_id,
2639
+ "tool_name": request.tool_name,
2640
+ "tool_version": "0.1",
2641
+ "request_arguments": arguments.model_dump(mode="json"),
2642
+ "arguments_digest": request.arguments_digest,
2643
+ "output_digest": None,
2644
+ "predicate_results": [],
2645
+ }
2646
+ receipt = ToolCallReceipt.model_validate(
2647
+ sign_payload("action-receipt", raw, sidecar_private_key)
2648
+ )
2649
+ receipt.validate_against_work_order(context.work_order)
2650
+ connection = evidence.connect_ledger(path)
2651
+ try:
2652
+ connection.execute("BEGIN IMMEDIATE")
2653
+ try:
2654
+ connection.execute(
2655
+ """
2656
+ INSERT INTO receipts (
2657
+ receipt_id,
2658
+ work_order_digest,
2659
+ nonce,
2660
+ sequence,
2661
+ previous_digest,
2662
+ receipt_json
2663
+ )
2664
+ VALUES (?, ?, ?, ?, ?, ?)
2665
+ """,
2666
+ (
2667
+ receipt.receipt_id,
2668
+ context.work_order.digest,
2669
+ request.nonce,
2670
+ len(context.ledger_prefix.receipts) + 1,
2671
+ (
2672
+ context.ledger_prefix.receipts[-1].receipt_id
2673
+ if context.ledger_prefix.receipts
2674
+ else context.work_order.digest
2675
+ ),
2676
+ evidence._canonical_json(
2677
+ receipt.model_dump(mode="json")
2678
+ ),
2679
+ ),
2680
+ )
2681
+ for parent_receipt_id in _causal_parents(
2682
+ context, request
2683
+ ):
2684
+ connection.execute(
2685
+ """
2686
+ INSERT INTO receipt_parents (
2687
+ child_receipt_id,
2688
+ parent_receipt_id
2689
+ )
2690
+ VALUES (?, ?)
2691
+ """,
2692
+ (receipt.receipt_id, parent_receipt_id),
2693
+ )
2694
+ connection.execute("COMMIT")
2695
+ except Exception:
2696
+ connection.execute("ROLLBACK")
2697
+ raise
2698
+ finally:
2699
+ connection.close()
2700
+ except Exception as error:
2701
+ primary_error = error
2702
+ _, release_errors = evidence._release_target_lock(lock_descriptor)
2703
+ if primary_error is not None:
2704
+ if release_errors:
2705
+ raise HandlerCoordinationError(
2706
+ "deny receipt and lock release both failed"
2707
+ ) from primary_error
2708
+ raise primary_error
2709
+ if release_errors:
2710
+ raise HandlerCoordinationError(
2711
+ "deny receipt lock release failed"
2712
+ ) from release_errors[0]
2713
+ assert receipt is not None
2714
+ return receipt
2715
+
2716
+
2717
+ def _is_tool_request_arguments(value: object) -> bool:
2718
+ """True when value is one of the registered ToolRequestArguments variants.
2719
+
2720
+ ToolRequestArguments is a pydantic Union alias, so isinstance is not
2721
+ reliable; validate against the union adapter instead.
2722
+ """
2723
+ from pydantic import TypeAdapter # noqa: PLC0415
2724
+
2725
+ adapter = TypeAdapter(ToolRequestArguments)
2726
+ try:
2727
+ adapter.validate_python(value)
2728
+ except Exception: # noqa: BLE001 - validation failure
2729
+ return False
2730
+ return True
2731
+
2732
+
2733
+ def _committed_evidence_from_ledger(
2734
+ ledger_path: Path,
2735
+ evidence_root: Path,
2736
+ work_order: WorkOrder,
2737
+ receipts,
2738
+ ) -> tuple:
2739
+ """Rebuild the committed evidence tuple from the evidence root."""
2740
+ from openworkproof.policy import CommittedEvidence # noqa: PLC0415
2741
+
2742
+ connection = evidence.connect_ledger(ledger_path)
2743
+ try:
2744
+ groups = evidence._journal_publication_groups(connection)
2745
+ finally:
2746
+ connection.close()
2747
+ committed: list[CommittedEvidence] = []
2748
+ for group in groups:
2749
+ for publication in group.publications:
2750
+ if publication.state != "COMMITTED":
2751
+ continue
2752
+ artifact_path = Path(evidence_root) / publication.final_path
2753
+ try:
2754
+ payload = artifact_path.read_bytes()
2755
+ except OSError:
2756
+ continue
2757
+ committed.append(
2758
+ CommittedEvidence(
2759
+ reference=publication.reference,
2760
+ payload=payload,
2761
+ )
2762
+ )
2763
+ committed.sort(key=lambda item: item.reference.path.encode())
2764
+ return tuple(committed)
2765
+
2766
+
2767
+ def _context_from_payload(
2768
+ ledger_path: Path,
2769
+ evidence_root: Path,
2770
+ payload: Mapping[str, object],
2771
+ now: datetime,
2772
+ ) -> AuthorizationContext:
2773
+ """Reconstruct an AuthorizationContext from a transport payload."""
2774
+ from openworkproof.policy import (
2775
+ AuthorizationLedgerPrefix,
2776
+ derive_authorization_context,
2777
+ )
2778
+ from openworkproof.repo_tools import (
2779
+ ReplayCheckpoint,
2780
+ WorkspaceManifest,
2781
+ )
2782
+
2783
+ checkpoint_data = payload["checkpoint"]
2784
+ if type(checkpoint_data) is not dict:
2785
+ raise KeyError("checkpoint")
2786
+ manifest_data = checkpoint_data.get("workspace_manifest")
2787
+ from openworkproof.repo_tools import WorkspaceManifestEntry # noqa: PLC0415
2788
+
2789
+ manifest = WorkspaceManifest(
2790
+ schema_version=manifest_data["schema_version"],
2791
+ head_commit=manifest_data["head_commit"],
2792
+ entries=tuple(
2793
+ WorkspaceManifestEntry(
2794
+ path_bytes_b64url=entry["path_bytes_b64url"],
2795
+ type=entry["type"],
2796
+ posix_mode=entry["posix_mode"],
2797
+ size_bytes=entry["size_bytes"],
2798
+ sha256=entry["sha256"],
2799
+ symlink_target_b64url=entry["symlink_target_b64url"],
2800
+ )
2801
+ for entry in manifest_data["entries"]
2802
+ ),
2803
+ )
2804
+ checkpoint = ReplayCheckpoint(
2805
+ files=(),
2806
+ head_commit=checkpoint_data["head_commit"],
2807
+ workspace_manifest=manifest,
2808
+ workspace_manifest_digest=checkpoint_data[
2809
+ "workspace_manifest_digest"
2810
+ ],
2811
+ verified_test_results=(),
2812
+ )
2813
+ connection = evidence.connect_ledger(ledger_path)
2814
+ try:
2815
+ work_order, receipts, grants, _ = (
2816
+ evidence._replay_receipt_publication_ledger(connection)
2817
+ )
2818
+ attempts = evidence._validated_grant_attempts(
2819
+ connection, work_order, receipts
2820
+ )
2821
+ finally:
2822
+ connection.close()
2823
+ prefix = AuthorizationLedgerPrefix(
2824
+ effective_grants=tuple(
2825
+ sorted(grants.values(), key=lambda item: item.grant_id)
2826
+ ),
2827
+ grant_attempts=tuple(
2828
+ sorted(attempts.values(), key=lambda item: item.digest)
2829
+ ),
2830
+ receipts=receipts,
2831
+ )
2832
+ committed = _committed_evidence_from_ledger(
2833
+ ledger_path, evidence_root, work_order, receipts
2834
+ )
2835
+ return derive_authorization_context(
2836
+ work_order,
2837
+ prefix,
2838
+ committed,
2839
+ checkpoint,
2840
+ now,
2841
+ )
2842
+
2843
+
2844
+ def _load_sidecar_key(key_hex: str) -> Ed25519PrivateKey:
2845
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
2846
+ Ed25519PrivateKey,
2847
+ )
2848
+
2849
+ raw = bytes.fromhex(key_hex)
2850
+ if len(raw) != 32:
2851
+ raise HandlerCoordinationError("SIDECAR_KEY_INVALID")
2852
+ return Ed25519PrivateKey.from_private_bytes(raw)
2853
+
2854
+
2855
+ def _run_tests_from_payload(
2856
+ ledger_path: str | Path,
2857
+ payload: Mapping[str, object],
2858
+ ) -> ToolCallReceipt:
2859
+ """Forward one run-tests execution from a transport payload."""
2860
+ from openworkproof.models import (
2861
+ AgentRequest,
2862
+ RunTestsArguments,
2863
+ )
2864
+ from openworkproof.policy import ProspectiveExecutionFacts
2865
+
2866
+ path = Path(ledger_path)
2867
+ now = evidence._freeze_trusted_utc_second(
2868
+ datetime.fromisoformat(str(payload["now"]))
2869
+ )
2870
+ context = _context_from_payload(
2871
+ path,
2872
+ Path(str(payload["evidence_root"])),
2873
+ payload,
2874
+ now,
2875
+ )
2876
+ request = AgentRequest.model_validate(payload["request"])
2877
+ arguments = RunTestsArguments.model_validate(payload["arguments"])
2878
+ facts = ProspectiveExecutionFacts(
2879
+ execution_context_id=payload["facts"]["execution_context_id"],
2880
+ container_instance_id_digest=payload["facts"][
2881
+ "container_instance_id_digest"
2882
+ ],
2883
+ controller_id=payload["facts"]["controller_id"],
2884
+ )
2885
+ sidecar_key = _load_sidecar_key(str(payload["sidecar_key_hex"]))
2886
+ return execute_run_tests_production(
2887
+ path,
2888
+ evidence_root=Path(str(payload["evidence_root"])),
2889
+ context=context,
2890
+ request=request,
2891
+ request_arguments=arguments,
2892
+ execution_facts=facts,
2893
+ sidecar_private_key=sidecar_key,
2894
+ docker_binary=Path(str(payload["docker_binary"])),
2895
+ image_reference=str(payload["image_reference"]),
2896
+ candidate_runtime_root=Path(str(payload["candidate_runtime_root"])),
2897
+ clock=lambda: now,
2898
+ )
2899
+
2900
+
2901
+ def _repo_read_from_payload(
2902
+ ledger_path: str | Path,
2903
+ payload: Mapping[str, object],
2904
+ ) -> ToolCallReceipt:
2905
+ """Forward one repo-read execution from a transport payload."""
2906
+ from openworkproof.models import AgentRequest, RepoReadArguments
2907
+
2908
+ path = Path(ledger_path)
2909
+ now = evidence._freeze_trusted_utc_second(
2910
+ datetime.fromisoformat(str(payload["now"]))
2911
+ )
2912
+ context = _context_from_payload(
2913
+ path,
2914
+ Path(str(payload["evidence_root"])),
2915
+ payload,
2916
+ now,
2917
+ )
2918
+ request = AgentRequest.model_validate(payload["request"])
2919
+ arguments = RepoReadArguments.model_validate(payload["arguments"])
2920
+ facts = ProspectiveExecutionFacts(
2921
+ execution_context_id=payload["facts"]["execution_context_id"],
2922
+ container_instance_id_digest=payload["facts"][
2923
+ "container_instance_id_digest"
2924
+ ],
2925
+ controller_id=payload["facts"]["controller_id"],
2926
+ )
2927
+ sidecar_key = _load_sidecar_key(str(payload["sidecar_key_hex"]))
2928
+ return execute_repo_read(
2929
+ path,
2930
+ evidence_root=Path(str(payload["evidence_root"])),
2931
+ context=context,
2932
+ request=request,
2933
+ request_arguments=arguments,
2934
+ execution_facts=facts,
2935
+ sidecar_private_key=sidecar_key,
2936
+ candidate_runtime_root=Path(str(payload["candidate_runtime_root"])),
2937
+ handler=make_repo_pipeline_read_handler(),
2938
+ clock=lambda: now,
2939
+ )