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.
- openworkproof/__init__.py +1 -0
- openworkproof/acceptance.py +3006 -0
- openworkproof/cli.py +164 -0
- openworkproof/composition.py +832 -0
- openworkproof/evidence.py +8281 -0
- openworkproof/execution_adapter.py +256 -0
- openworkproof/external_acceptor.py +211 -0
- openworkproof/mcp_server.py +2939 -0
- openworkproof/mcp_transport.py +72 -0
- openworkproof/models.py +3380 -0
- openworkproof/policy.py +2494 -0
- openworkproof/predicates.py +333 -0
- openworkproof/repo_pipeline/__init__.py +74 -0
- openworkproof/repo_pipeline/analysis.py +152 -0
- openworkproof/repo_pipeline/errors.py +64 -0
- openworkproof/repo_pipeline/models.py +80 -0
- openworkproof/repo_pipeline/output.py +68 -0
- openworkproof/repo_pipeline/reader.py +47 -0
- openworkproof/repo_pipeline/sources.py +120 -0
- openworkproof/repo_pipeline/traversal.py +253 -0
- openworkproof/repo_tools.py +8382 -0
- openworkproof/runtime_context.py +189 -0
- openworkproof/schema_registry.py +623 -0
- openworkproof/schemas/v0.1/acceptance-receipt.schema.json +1 -0
- openworkproof/schemas/v0.1/acceptance-rejection-receipt.schema.json +1 -0
- openworkproof/schemas/v0.1/action-receipt.schema.json +1 -0
- openworkproof/schemas/v0.1/capability-grant.schema.json +1 -0
- openworkproof/schemas/v0.1/schema-registry.json +1 -0
- openworkproof/schemas/v0.1/work-order.schema.json +1 -0
- openworkproof/signing.py +421 -0
- openworkproof/state.py +765 -0
- openworkproof/team_network_client.py +419 -0
- openworkproof/trusted_helper.py +269 -0
- openworkproof-1.0.0.dist-info/METADATA +578 -0
- openworkproof-1.0.0.dist-info/RECORD +39 -0
- openworkproof-1.0.0.dist-info/WHEEL +5 -0
- openworkproof-1.0.0.dist-info/entry_points.txt +2 -0
- openworkproof-1.0.0.dist-info/licenses/LICENSE +202 -0
- 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
|
+
)
|