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
openworkproof/policy.py
ADDED
|
@@ -0,0 +1,2494 @@
|
|
|
1
|
+
"""Immutable authorization policy replay for bounded signed histories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable, Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
|
+
import hashlib
|
|
9
|
+
from itertools import islice
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
import rfc8785
|
|
14
|
+
|
|
15
|
+
from openworkproof.composition import (
|
|
16
|
+
AuthorizationCausalSnapshot,
|
|
17
|
+
AuthorizationCausalState,
|
|
18
|
+
AuthorizationCausalityError,
|
|
19
|
+
replay_authorization_causality,
|
|
20
|
+
validate_authorization_causal_bindings,
|
|
21
|
+
)
|
|
22
|
+
from openworkproof.models import (
|
|
23
|
+
ActionReceiptEnvelope,
|
|
24
|
+
AgentRequest,
|
|
25
|
+
ApplyPatchArguments,
|
|
26
|
+
ApprovalHumanDecision,
|
|
27
|
+
ApprovalDecisionReceipt,
|
|
28
|
+
ApprovalRequestedReceipt,
|
|
29
|
+
CapabilityGrant,
|
|
30
|
+
EvidenceRef,
|
|
31
|
+
GrantConsumedReceipt,
|
|
32
|
+
GrantIssuedReceipt,
|
|
33
|
+
GrantRevokedReceipt,
|
|
34
|
+
KeyBinding,
|
|
35
|
+
PatchResultEvidence,
|
|
36
|
+
PolicyDecision,
|
|
37
|
+
RepoReadArguments,
|
|
38
|
+
RollbackReceipt,
|
|
39
|
+
RunTestsArguments,
|
|
40
|
+
SystemEventReceipt,
|
|
41
|
+
TerminationHumanDecision,
|
|
42
|
+
TerminationDecisionReceipt,
|
|
43
|
+
ToolCallReceipt,
|
|
44
|
+
ToolRequestArguments,
|
|
45
|
+
WorkOrder,
|
|
46
|
+
ComposeProofArguments,
|
|
47
|
+
CreatePrProposalArguments,
|
|
48
|
+
request_arguments_digest,
|
|
49
|
+
)
|
|
50
|
+
from openworkproof.repo_tools import (
|
|
51
|
+
ReplayCheckpoint,
|
|
52
|
+
workspace_manifest_digest,
|
|
53
|
+
)
|
|
54
|
+
from openworkproof.predicates import (
|
|
55
|
+
EvaluationContext,
|
|
56
|
+
evaluate_required_predicates,
|
|
57
|
+
select_required_predicates,
|
|
58
|
+
)
|
|
59
|
+
from openworkproof.signing import (
|
|
60
|
+
decode_and_verify_key_binding,
|
|
61
|
+
verify_payload,
|
|
62
|
+
)
|
|
63
|
+
from openworkproof.state import (
|
|
64
|
+
TaskState,
|
|
65
|
+
_agent_direct_call_is_authorized,
|
|
66
|
+
_validate_termination,
|
|
67
|
+
append_receipt,
|
|
68
|
+
apply_state_transition,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
MAX_EFFECTIVE_GRANTS = 8
|
|
73
|
+
MAX_GRANT_ATTEMPTS = 8
|
|
74
|
+
MAX_AUTHORIZATION_RECEIPTS = 64
|
|
75
|
+
DENIAL_PRECEDENCE = (
|
|
76
|
+
"STATE_DENIED",
|
|
77
|
+
"ROLE_DENIED",
|
|
78
|
+
"CAPABILITY_DENIED",
|
|
79
|
+
"APPROVAL_DENIED",
|
|
80
|
+
"PREDICATE_DENIED",
|
|
81
|
+
"QUOTA_EXHAUSTED",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class AuthorizationPolicyError(RuntimeError):
|
|
86
|
+
"""The signed history cannot satisfy the frozen authorization policy."""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True, slots=True)
|
|
90
|
+
class AuthorizationLedgerPrefix:
|
|
91
|
+
"""Canonical bounded inputs for one live authorization transaction."""
|
|
92
|
+
|
|
93
|
+
effective_grants: tuple[CapabilityGrant, ...]
|
|
94
|
+
grant_attempts: tuple[CapabilityGrant, ...]
|
|
95
|
+
receipts: tuple[ActionReceiptEnvelope, ...]
|
|
96
|
+
|
|
97
|
+
def __post_init__(self) -> None:
|
|
98
|
+
if any(
|
|
99
|
+
type(value) is not tuple
|
|
100
|
+
for value in (
|
|
101
|
+
self.effective_grants,
|
|
102
|
+
self.grant_attempts,
|
|
103
|
+
self.receipts,
|
|
104
|
+
)
|
|
105
|
+
):
|
|
106
|
+
raise AuthorizationPolicyError(
|
|
107
|
+
"live policy collections must be exact tuples"
|
|
108
|
+
)
|
|
109
|
+
if (
|
|
110
|
+
len(self.effective_grants) > MAX_EFFECTIVE_GRANTS
|
|
111
|
+
or len(self.grant_attempts) > MAX_GRANT_ATTEMPTS
|
|
112
|
+
or len(self.receipts) > MAX_AUTHORIZATION_RECEIPTS
|
|
113
|
+
):
|
|
114
|
+
raise AuthorizationPolicyError(
|
|
115
|
+
"live policy collection exceeds its bounded capacity"
|
|
116
|
+
)
|
|
117
|
+
if any(
|
|
118
|
+
not isinstance(grant, CapabilityGrant)
|
|
119
|
+
for grant in self.effective_grants + self.grant_attempts
|
|
120
|
+
) or any(
|
|
121
|
+
not isinstance(receipt, ActionReceiptEnvelope)
|
|
122
|
+
for receipt in self.receipts
|
|
123
|
+
):
|
|
124
|
+
raise AuthorizationPolicyError(
|
|
125
|
+
"live policy collection contains a malformed entry"
|
|
126
|
+
)
|
|
127
|
+
grant_ids = tuple(
|
|
128
|
+
grant.grant_id for grant in self.effective_grants
|
|
129
|
+
)
|
|
130
|
+
attempt_digests = tuple(
|
|
131
|
+
grant.digest for grant in self.grant_attempts
|
|
132
|
+
)
|
|
133
|
+
if (
|
|
134
|
+
grant_ids != tuple(sorted(grant_ids))
|
|
135
|
+
or len(set(grant_ids)) != len(grant_ids)
|
|
136
|
+
or attempt_digests != tuple(sorted(attempt_digests))
|
|
137
|
+
or len(set(attempt_digests)) != len(attempt_digests)
|
|
138
|
+
):
|
|
139
|
+
raise AuthorizationPolicyError(
|
|
140
|
+
"live policy Grants are not canonical and unique"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass(frozen=True, slots=True)
|
|
145
|
+
class CommittedEvidence:
|
|
146
|
+
"""Exact committed bytes paired with their signed evidence reference."""
|
|
147
|
+
|
|
148
|
+
reference: EvidenceRef
|
|
149
|
+
payload: bytes
|
|
150
|
+
|
|
151
|
+
def __post_init__(self) -> None:
|
|
152
|
+
if not isinstance(self.reference, EvidenceRef) or type(
|
|
153
|
+
self.payload
|
|
154
|
+
) is not bytes:
|
|
155
|
+
raise AuthorizationPolicyError(
|
|
156
|
+
"committed evidence entry is malformed"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass(frozen=True)
|
|
161
|
+
class AuthorizationReplayState:
|
|
162
|
+
"""Immutable policy facts reconstructed from the signed receipt history."""
|
|
163
|
+
|
|
164
|
+
active_patch_receipt_id: str | None
|
|
165
|
+
balances: tuple[tuple[str, str, int], ...]
|
|
166
|
+
revoked_grant_ids: tuple[str, ...]
|
|
167
|
+
used_single_use_grant_ids: tuple[str, ...]
|
|
168
|
+
approval_decision_by_request: tuple[tuple[str, str], ...]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True, slots=True)
|
|
172
|
+
class AuthorizationContext:
|
|
173
|
+
"""Verified immutable facts used by one live authorization decision."""
|
|
174
|
+
|
|
175
|
+
work_order: WorkOrder
|
|
176
|
+
ledger_prefix: AuthorizationLedgerPrefix
|
|
177
|
+
committed_evidence: tuple[CommittedEvidence, ...]
|
|
178
|
+
replay_checkpoint: ReplayCheckpoint
|
|
179
|
+
transaction_time: datetime
|
|
180
|
+
causal_state: AuthorizationCausalState
|
|
181
|
+
replay_state: AuthorizationReplayState
|
|
182
|
+
current_state: str
|
|
183
|
+
routine_capacity_remaining: int
|
|
184
|
+
denial_count: int
|
|
185
|
+
independent_failure_terminal: bool
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def active_patch_receipt_id(self) -> str | None:
|
|
189
|
+
return self.causal_state.active_patch_receipt_id
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass(frozen=True, slots=True)
|
|
193
|
+
class ProspectiveExecutionFacts:
|
|
194
|
+
"""Sidecar-observed execution identity for a prospective test call."""
|
|
195
|
+
|
|
196
|
+
execution_context_id: str
|
|
197
|
+
container_instance_id_digest: str
|
|
198
|
+
controller_id: str
|
|
199
|
+
|
|
200
|
+
def __post_init__(self) -> None:
|
|
201
|
+
digest = re.compile(r"^[0-9a-f]{64}$")
|
|
202
|
+
key = re.compile(r"^ed25519:[0-9a-f]{64}$")
|
|
203
|
+
if (
|
|
204
|
+
type(self.execution_context_id) is not str
|
|
205
|
+
or digest.fullmatch(self.execution_context_id) is None
|
|
206
|
+
or type(self.container_instance_id_digest) is not str
|
|
207
|
+
or digest.fullmatch(self.container_instance_id_digest) is None
|
|
208
|
+
or type(self.controller_id) is not str
|
|
209
|
+
or key.fullmatch(self.controller_id) is None
|
|
210
|
+
):
|
|
211
|
+
raise AuthorizationPolicyError(
|
|
212
|
+
"prospective execution facts are malformed"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@dataclass(frozen=True)
|
|
217
|
+
class _GrantReplayState:
|
|
218
|
+
remaining_tool_calls: int
|
|
219
|
+
remaining_repair_rounds: int
|
|
220
|
+
use_count: int
|
|
221
|
+
revoked: bool
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass(frozen=True)
|
|
225
|
+
class _GrantReplayContext:
|
|
226
|
+
states_before_receipt: tuple[
|
|
227
|
+
tuple[str, tuple[tuple[str, _GrantReplayState], ...]],
|
|
228
|
+
...,
|
|
229
|
+
]
|
|
230
|
+
final_states: tuple[tuple[str, _GrantReplayState], ...]
|
|
231
|
+
|
|
232
|
+
def state_before(
|
|
233
|
+
self,
|
|
234
|
+
receipt_id: str,
|
|
235
|
+
grant_id: str,
|
|
236
|
+
) -> _GrantReplayState | None:
|
|
237
|
+
for candidate_id, states in self.states_before_receipt:
|
|
238
|
+
if candidate_id != receipt_id:
|
|
239
|
+
continue
|
|
240
|
+
return dict(states).get(grant_id)
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
def final_by_grant(self) -> dict[str, _GrantReplayState]:
|
|
244
|
+
return dict(self.final_states)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _fail(message: str) -> AuthorizationPolicyError:
|
|
248
|
+
return AuthorizationPolicyError(message)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _validate_tool_authorization_inputs(
|
|
252
|
+
context: AuthorizationContext,
|
|
253
|
+
request: AgentRequest,
|
|
254
|
+
request_arguments: ToolRequestArguments,
|
|
255
|
+
execution_facts: ProspectiveExecutionFacts | None,
|
|
256
|
+
) -> tuple[CapabilityGrant, KeyBinding]:
|
|
257
|
+
"""Verify signed prospective inputs before any policy decision."""
|
|
258
|
+
|
|
259
|
+
argument_tools = {
|
|
260
|
+
RepoReadArguments: "owp.repo_read",
|
|
261
|
+
ApplyPatchArguments: "owp.apply_patch",
|
|
262
|
+
RunTestsArguments: "owp.run_tests",
|
|
263
|
+
CreatePrProposalArguments: "owp.create_pr_proposal",
|
|
264
|
+
ComposeProofArguments: "owp.compose_proof",
|
|
265
|
+
}
|
|
266
|
+
if type(context) is not AuthorizationContext:
|
|
267
|
+
raise _fail("authorization context is malformed")
|
|
268
|
+
if type(request) is not AgentRequest:
|
|
269
|
+
raise _fail("Agent request is malformed")
|
|
270
|
+
tool_name = argument_tools.get(type(request_arguments))
|
|
271
|
+
if tool_name is None:
|
|
272
|
+
raise _fail("tool request arguments are malformed")
|
|
273
|
+
if execution_facts is not None and type(
|
|
274
|
+
execution_facts
|
|
275
|
+
) is not ProspectiveExecutionFacts:
|
|
276
|
+
raise _fail("prospective execution facts are malformed")
|
|
277
|
+
if isinstance(request_arguments, RunTestsArguments):
|
|
278
|
+
if execution_facts is None:
|
|
279
|
+
raise _fail("test execution facts are required")
|
|
280
|
+
elif execution_facts is not None:
|
|
281
|
+
raise _fail("execution facts are forbidden for this tool")
|
|
282
|
+
if (
|
|
283
|
+
request.work_order_digest != context.work_order.digest
|
|
284
|
+
or request.tool_name != tool_name
|
|
285
|
+
or request.arguments_digest
|
|
286
|
+
!= request_arguments_digest(tool_name, request_arguments)
|
|
287
|
+
):
|
|
288
|
+
raise _fail("Agent request integrity binding is invalid")
|
|
289
|
+
|
|
290
|
+
grants = {
|
|
291
|
+
grant.grant_id: grant
|
|
292
|
+
for grant in context.ledger_prefix.effective_grants
|
|
293
|
+
}
|
|
294
|
+
grant = grants.get(request.grant_id)
|
|
295
|
+
bindings = {
|
|
296
|
+
binding.key_id: binding
|
|
297
|
+
for binding in context.work_order.key_bindings
|
|
298
|
+
}
|
|
299
|
+
binding = bindings.get(request.actor_key_id)
|
|
300
|
+
if (
|
|
301
|
+
grant is None
|
|
302
|
+
or binding is None
|
|
303
|
+
or request.actor_id != binding.subject_id
|
|
304
|
+
or grant.subject_key_id != binding.key_id
|
|
305
|
+
or grant.subject_agent_id != binding.subject_id
|
|
306
|
+
):
|
|
307
|
+
raise _fail("Agent request Grant or actor binding is invalid")
|
|
308
|
+
try:
|
|
309
|
+
public_key = decode_and_verify_key_binding(binding)
|
|
310
|
+
except Exception as error:
|
|
311
|
+
raise _fail("Agent request key binding is invalid") from error
|
|
312
|
+
if not verify_payload(
|
|
313
|
+
"agent-request",
|
|
314
|
+
request.model_dump(mode="json"),
|
|
315
|
+
public_key,
|
|
316
|
+
):
|
|
317
|
+
raise _fail("Agent request signature is invalid")
|
|
318
|
+
|
|
319
|
+
request_age = context.transaction_time - request.requested_at
|
|
320
|
+
if request_age < timedelta(0) or request_age > timedelta(seconds=300):
|
|
321
|
+
raise _fail("Agent request is outside the freshness window")
|
|
322
|
+
|
|
323
|
+
if execution_facts is not None:
|
|
324
|
+
sidecar_key_ids = {
|
|
325
|
+
candidate.key_id
|
|
326
|
+
for candidate in context.work_order.key_bindings
|
|
327
|
+
if candidate.role == "Sidecar"
|
|
328
|
+
}
|
|
329
|
+
if execution_facts.controller_id not in sidecar_key_ids:
|
|
330
|
+
raise _fail("prospective execution controller is not the Sidecar")
|
|
331
|
+
return grant, binding
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _denial_code(
|
|
335
|
+
*,
|
|
336
|
+
state_allowed: bool,
|
|
337
|
+
role_allowed: bool,
|
|
338
|
+
capability_allowed: bool,
|
|
339
|
+
approval_allowed: bool = True,
|
|
340
|
+
predicate_allowed: bool = True,
|
|
341
|
+
quota_allowed: bool = True,
|
|
342
|
+
) -> str | None:
|
|
343
|
+
allowed = (
|
|
344
|
+
state_allowed,
|
|
345
|
+
role_allowed,
|
|
346
|
+
capability_allowed,
|
|
347
|
+
approval_allowed,
|
|
348
|
+
predicate_allowed,
|
|
349
|
+
quota_allowed,
|
|
350
|
+
)
|
|
351
|
+
return next(
|
|
352
|
+
(
|
|
353
|
+
error_code
|
|
354
|
+
for error_code, condition in zip(
|
|
355
|
+
DENIAL_PRECEDENCE,
|
|
356
|
+
allowed,
|
|
357
|
+
strict=True,
|
|
358
|
+
)
|
|
359
|
+
if not condition
|
|
360
|
+
),
|
|
361
|
+
None,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _remaining_tool_calls(
|
|
366
|
+
context: AuthorizationContext,
|
|
367
|
+
grant_id: str,
|
|
368
|
+
) -> int:
|
|
369
|
+
balances = {
|
|
370
|
+
(candidate_id, metric): remaining
|
|
371
|
+
for candidate_id, metric, remaining in context.replay_state.balances
|
|
372
|
+
}
|
|
373
|
+
try:
|
|
374
|
+
return balances[(grant_id, "tool_calls")]
|
|
375
|
+
except KeyError as error:
|
|
376
|
+
raise _fail("effective Grant balance is unavailable") from error
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _policy_decision(error_code: str | None) -> PolicyDecision:
|
|
380
|
+
return PolicyDecision(
|
|
381
|
+
allowed=error_code is None,
|
|
382
|
+
decision="allow" if error_code is None else "deny",
|
|
383
|
+
error_code=error_code,
|
|
384
|
+
reason=(
|
|
385
|
+
"TOOL_CALL_AUTHORIZED"
|
|
386
|
+
if error_code is None
|
|
387
|
+
else "TOOL_CALL_DENIED"
|
|
388
|
+
),
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _prospective_state_allowed(
|
|
393
|
+
context: AuthorizationContext,
|
|
394
|
+
tool_name: str,
|
|
395
|
+
arguments: ToolRequestArguments,
|
|
396
|
+
) -> bool:
|
|
397
|
+
if tool_name == "owp.repo_read":
|
|
398
|
+
return context.current_state in {"running", "retrying"}
|
|
399
|
+
if tool_name == "owp.apply_patch":
|
|
400
|
+
return (
|
|
401
|
+
context.current_state in {"running", "retrying"}
|
|
402
|
+
and context.active_patch_receipt_id is None
|
|
403
|
+
)
|
|
404
|
+
if tool_name == "owp.run_tests":
|
|
405
|
+
return context.current_state in {"running", "retrying"} or (
|
|
406
|
+
context.current_state == "evidence_incomplete"
|
|
407
|
+
and isinstance(arguments, RunTestsArguments)
|
|
408
|
+
and arguments.test_mode == "verifier"
|
|
409
|
+
)
|
|
410
|
+
if tool_name == "owp.create_pr_proposal":
|
|
411
|
+
return (
|
|
412
|
+
context.current_state in {"running", "retrying"}
|
|
413
|
+
and context.active_patch_receipt_id is not None
|
|
414
|
+
)
|
|
415
|
+
if tool_name == "owp.compose_proof":
|
|
416
|
+
return (
|
|
417
|
+
context.current_state
|
|
418
|
+
in {"locally_verified", "evidence_incomplete"}
|
|
419
|
+
and context.active_patch_receipt_id is not None
|
|
420
|
+
)
|
|
421
|
+
return False
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _prospective_role_allowed(
|
|
425
|
+
role: str,
|
|
426
|
+
tool_name: str,
|
|
427
|
+
arguments: ToolRequestArguments,
|
|
428
|
+
) -> bool:
|
|
429
|
+
if role == "Manager":
|
|
430
|
+
return tool_name in {
|
|
431
|
+
"owp.compose_proof",
|
|
432
|
+
"owp.create_pr_proposal",
|
|
433
|
+
}
|
|
434
|
+
if role == "Developer":
|
|
435
|
+
return tool_name in {"owp.apply_patch", "owp.repo_read"} or (
|
|
436
|
+
tool_name == "owp.run_tests"
|
|
437
|
+
and isinstance(arguments, RunTestsArguments)
|
|
438
|
+
and arguments.test_mode == "developer"
|
|
439
|
+
)
|
|
440
|
+
if role == "Verifier":
|
|
441
|
+
return (
|
|
442
|
+
tool_name == "owp.run_tests"
|
|
443
|
+
and isinstance(arguments, RunTestsArguments)
|
|
444
|
+
and arguments.test_mode == "verifier"
|
|
445
|
+
)
|
|
446
|
+
return False
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _static_predicates_allowed(
|
|
450
|
+
context: AuthorizationContext,
|
|
451
|
+
grant: CapabilityGrant,
|
|
452
|
+
arguments: ToolRequestArguments,
|
|
453
|
+
execution_facts: ProspectiveExecutionFacts | None,
|
|
454
|
+
) -> bool:
|
|
455
|
+
if isinstance(arguments, RepoReadArguments):
|
|
456
|
+
return _roots_within((arguments.path,), grant.allowed_read_roots)
|
|
457
|
+
if isinstance(arguments, ApplyPatchArguments):
|
|
458
|
+
return _roots_within(
|
|
459
|
+
arguments.target_paths,
|
|
460
|
+
grant.allowed_write_roots,
|
|
461
|
+
)
|
|
462
|
+
if not isinstance(arguments, RunTestsArguments):
|
|
463
|
+
return True
|
|
464
|
+
if execution_facts is None or context.active_patch_receipt_id is None:
|
|
465
|
+
return False
|
|
466
|
+
profiles = tuple(
|
|
467
|
+
profile
|
|
468
|
+
for profile in context.work_order.test_profiles
|
|
469
|
+
if profile.test_mode == arguments.test_mode
|
|
470
|
+
)
|
|
471
|
+
if len(profiles) != 1:
|
|
472
|
+
return False
|
|
473
|
+
profile = profiles[0]
|
|
474
|
+
if (
|
|
475
|
+
arguments.command_digest != profile.command_digest
|
|
476
|
+
or arguments.source_commit != context.work_order.source_commit
|
|
477
|
+
or arguments.candidate_commit
|
|
478
|
+
!= context.replay_checkpoint.head_commit
|
|
479
|
+
or arguments.workspace_manifest_digest
|
|
480
|
+
!= context.replay_checkpoint.workspace_manifest_digest
|
|
481
|
+
or arguments.container_image_digest
|
|
482
|
+
!= profile.container_image_digest
|
|
483
|
+
or arguments.fixed_test_source_digest
|
|
484
|
+
!= profile.fixed_test_source_digest
|
|
485
|
+
):
|
|
486
|
+
return False
|
|
487
|
+
used_execution_contexts: set[str] = set()
|
|
488
|
+
used_container_instances: set[str] = set()
|
|
489
|
+
for receipt in context.ledger_prefix.receipts:
|
|
490
|
+
if not isinstance(receipt, ToolCallReceipt):
|
|
491
|
+
continue
|
|
492
|
+
if receipt.tool_name != "owp.run_tests":
|
|
493
|
+
continue
|
|
494
|
+
factors = receipt.correlation_factors
|
|
495
|
+
if factors.execution_context_id is not None:
|
|
496
|
+
used_execution_contexts.add(factors.execution_context_id)
|
|
497
|
+
if factors.container_instance_id_digest is not None:
|
|
498
|
+
used_container_instances.add(
|
|
499
|
+
factors.container_instance_id_digest
|
|
500
|
+
)
|
|
501
|
+
if (
|
|
502
|
+
execution_facts.execution_context_id in used_execution_contexts
|
|
503
|
+
or execution_facts.container_instance_id_digest
|
|
504
|
+
in used_container_instances
|
|
505
|
+
):
|
|
506
|
+
return False
|
|
507
|
+
return not (
|
|
508
|
+
arguments.test_mode == "verifier"
|
|
509
|
+
and context.causal_state.independent_result_receipt_id is not None
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _prospective_approval_allowed(
|
|
514
|
+
context: AuthorizationContext,
|
|
515
|
+
tool_name: str,
|
|
516
|
+
arguments: ToolRequestArguments,
|
|
517
|
+
) -> bool:
|
|
518
|
+
if tool_name != "owp.create_pr_proposal":
|
|
519
|
+
return True
|
|
520
|
+
if not isinstance(arguments, CreatePrProposalArguments):
|
|
521
|
+
return False
|
|
522
|
+
by_id = {
|
|
523
|
+
receipt.receipt_id: receipt
|
|
524
|
+
for receipt in context.ledger_prefix.receipts
|
|
525
|
+
}
|
|
526
|
+
patch = by_id.get(context.active_patch_receipt_id)
|
|
527
|
+
decision = by_id.get(arguments.approval_receipt_id)
|
|
528
|
+
request = (
|
|
529
|
+
by_id.get(decision.request_receipt_id)
|
|
530
|
+
if isinstance(decision, ApprovalDecisionReceipt)
|
|
531
|
+
else None
|
|
532
|
+
)
|
|
533
|
+
decisions = dict(context.replay_state.approval_decision_by_request)
|
|
534
|
+
return (
|
|
535
|
+
isinstance(patch, ToolCallReceipt)
|
|
536
|
+
and patch.tool_name == "owp.apply_patch"
|
|
537
|
+
and isinstance(decision, ApprovalDecisionReceipt)
|
|
538
|
+
and isinstance(request, ApprovalRequestedReceipt)
|
|
539
|
+
and decisions.get(request.receipt_id) == decision.receipt_id
|
|
540
|
+
and arguments.target_patch_digest == patch.digest
|
|
541
|
+
and arguments.approval_receipt_digest == decision.digest
|
|
542
|
+
and decision.request_receipt_digest == request.digest
|
|
543
|
+
and decision.decision == "approved"
|
|
544
|
+
and decision.policy_decision == "allow"
|
|
545
|
+
and decision.execution_status == "succeeded"
|
|
546
|
+
and request.request_kind == "high_risk_action"
|
|
547
|
+
and request.requested_scope["work_order_digest"]
|
|
548
|
+
== context.work_order.digest
|
|
549
|
+
and request.requested_scope["target_patch_digest"]
|
|
550
|
+
== patch.digest
|
|
551
|
+
and decision.approved_scope == request.requested_scope
|
|
552
|
+
and context.transaction_time <= request.expires_at
|
|
553
|
+
and context.transaction_time <= decision.expires_at
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _compose_arguments_allowed(
|
|
558
|
+
context: AuthorizationContext,
|
|
559
|
+
tool_name: str,
|
|
560
|
+
arguments: ToolRequestArguments,
|
|
561
|
+
) -> bool:
|
|
562
|
+
if tool_name != "owp.compose_proof":
|
|
563
|
+
return True
|
|
564
|
+
if not isinstance(arguments, ComposeProofArguments):
|
|
565
|
+
return False
|
|
566
|
+
from openworkproof import evidence as _evidence
|
|
567
|
+
|
|
568
|
+
expected_version = _evidence._derive_protocol_transaction_version(
|
|
569
|
+
action_receipts=context.ledger_prefix.receipts,
|
|
570
|
+
acceptance_receipts=(),
|
|
571
|
+
)
|
|
572
|
+
if arguments.expected_state_version != expected_version:
|
|
573
|
+
return False
|
|
574
|
+
if context.current_state == "locally_verified":
|
|
575
|
+
return arguments.previous_report_digest is None
|
|
576
|
+
if context.current_state != "evidence_incomplete":
|
|
577
|
+
return False
|
|
578
|
+
by_id = {
|
|
579
|
+
receipt.receipt_id: receipt
|
|
580
|
+
for receipt in context.ledger_prefix.receipts
|
|
581
|
+
}
|
|
582
|
+
trigger = by_id.get(
|
|
583
|
+
context.causal_state.latest_composition_trigger_id
|
|
584
|
+
)
|
|
585
|
+
independent = by_id.get(
|
|
586
|
+
context.causal_state.independent_result_receipt_id
|
|
587
|
+
)
|
|
588
|
+
return (
|
|
589
|
+
isinstance(trigger, SystemEventReceipt)
|
|
590
|
+
and trigger.system_event_name == "proof_composed"
|
|
591
|
+
and isinstance(independent, ToolCallReceipt)
|
|
592
|
+
and independent.tool_name == "owp.run_tests"
|
|
593
|
+
and arguments.previous_report_digest
|
|
594
|
+
== trigger.cause.composition_report_digest
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def authorize_tool_call(
|
|
599
|
+
context: AuthorizationContext,
|
|
600
|
+
request: AgentRequest,
|
|
601
|
+
request_arguments: ToolRequestArguments,
|
|
602
|
+
execution_facts: ProspectiveExecutionFacts | None = None,
|
|
603
|
+
) -> PolicyDecision:
|
|
604
|
+
"""Decide prospective handler eligibility without mutating authority."""
|
|
605
|
+
|
|
606
|
+
grant, binding = _validate_tool_authorization_inputs(
|
|
607
|
+
context,
|
|
608
|
+
request,
|
|
609
|
+
request_arguments,
|
|
610
|
+
execution_facts,
|
|
611
|
+
)
|
|
612
|
+
if context.transaction_time > context.work_order.deadline:
|
|
613
|
+
raise _fail("contract expired before tool authorization")
|
|
614
|
+
if context.routine_capacity_remaining <= 0:
|
|
615
|
+
raise _fail("routine Receipt capacity is exhausted")
|
|
616
|
+
if context.independent_failure_terminal:
|
|
617
|
+
raise _fail("EVIDENCE_FAILURE_SEALED")
|
|
618
|
+
|
|
619
|
+
capability_allowed = (
|
|
620
|
+
request.tool_name in context.work_order.allowed_tools
|
|
621
|
+
and request.tool_name in grant.allowed_tools
|
|
622
|
+
and grant.grant_id not in context.replay_state.revoked_grant_ids
|
|
623
|
+
and grant.valid_from
|
|
624
|
+
<= context.transaction_time
|
|
625
|
+
<= min(grant.expires_at, context.work_order.deadline)
|
|
626
|
+
)
|
|
627
|
+
error_code = _denial_code(
|
|
628
|
+
state_allowed=_prospective_state_allowed(
|
|
629
|
+
context,
|
|
630
|
+
request.tool_name,
|
|
631
|
+
request_arguments,
|
|
632
|
+
),
|
|
633
|
+
role_allowed=_prospective_role_allowed(
|
|
634
|
+
binding.role,
|
|
635
|
+
request.tool_name,
|
|
636
|
+
request_arguments,
|
|
637
|
+
),
|
|
638
|
+
capability_allowed=capability_allowed,
|
|
639
|
+
approval_allowed=_prospective_approval_allowed(
|
|
640
|
+
context,
|
|
641
|
+
request.tool_name,
|
|
642
|
+
request_arguments,
|
|
643
|
+
),
|
|
644
|
+
predicate_allowed=(
|
|
645
|
+
_static_predicates_allowed(
|
|
646
|
+
context,
|
|
647
|
+
grant,
|
|
648
|
+
request_arguments,
|
|
649
|
+
execution_facts,
|
|
650
|
+
)
|
|
651
|
+
and _compose_arguments_allowed(
|
|
652
|
+
context,
|
|
653
|
+
request.tool_name,
|
|
654
|
+
request_arguments,
|
|
655
|
+
)
|
|
656
|
+
),
|
|
657
|
+
quota_allowed=_remaining_tool_calls(context, grant.grant_id) > 0,
|
|
658
|
+
)
|
|
659
|
+
return _policy_decision(error_code)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def validate_human_decision(
|
|
663
|
+
context: AuthorizationContext,
|
|
664
|
+
claim: ApprovalHumanDecision | TerminationHumanDecision,
|
|
665
|
+
) -> PolicyDecision:
|
|
666
|
+
"""Decide a signed human claim without mutating authority."""
|
|
667
|
+
|
|
668
|
+
if type(context) is not AuthorizationContext or type(claim) not in {
|
|
669
|
+
ApprovalHumanDecision,
|
|
670
|
+
TerminationHumanDecision,
|
|
671
|
+
}:
|
|
672
|
+
raise _fail("human decision inputs are malformed")
|
|
673
|
+
if claim.work_order_digest != context.work_order.digest:
|
|
674
|
+
raise _fail("human decision WorkOrder binding is invalid")
|
|
675
|
+
|
|
676
|
+
bindings = tuple(
|
|
677
|
+
binding
|
|
678
|
+
for binding in context.work_order.key_bindings
|
|
679
|
+
if binding.key_id == claim.actor_key_id
|
|
680
|
+
)
|
|
681
|
+
if (
|
|
682
|
+
len(bindings) != 1
|
|
683
|
+
or claim.actor_id != bindings[0].subject_id
|
|
684
|
+
or claim.signer_key_id != bindings[0].key_id
|
|
685
|
+
):
|
|
686
|
+
raise _fail("human decision actor binding is invalid")
|
|
687
|
+
binding = bindings[0]
|
|
688
|
+
try:
|
|
689
|
+
public_key = decode_and_verify_key_binding(binding)
|
|
690
|
+
except Exception as error:
|
|
691
|
+
raise _fail("human decision key binding is invalid") from error
|
|
692
|
+
if not verify_payload(
|
|
693
|
+
"human-decision",
|
|
694
|
+
claim.model_dump(mode="json"),
|
|
695
|
+
public_key,
|
|
696
|
+
):
|
|
697
|
+
raise _fail("human decision signature is invalid")
|
|
698
|
+
|
|
699
|
+
ingestion_age = context.transaction_time - claim.decided_at
|
|
700
|
+
if ingestion_age < timedelta(0) or ingestion_age > timedelta(seconds=300):
|
|
701
|
+
raise _fail("human decision freshness window is invalid")
|
|
702
|
+
|
|
703
|
+
if isinstance(claim, TerminationHumanDecision):
|
|
704
|
+
if (
|
|
705
|
+
claim.target_work_order_digest != context.work_order.digest
|
|
706
|
+
or claim.decided_at < context.work_order.issued_at
|
|
707
|
+
):
|
|
708
|
+
raise _fail("human termination binding is invalid")
|
|
709
|
+
error_code = _denial_code(
|
|
710
|
+
state_allowed=context.current_state
|
|
711
|
+
not in {"accepted", "rejected"},
|
|
712
|
+
role_allowed=binding.role == "Maintainer",
|
|
713
|
+
capability_allowed=True,
|
|
714
|
+
)
|
|
715
|
+
return _policy_decision(error_code)
|
|
716
|
+
|
|
717
|
+
request = next(
|
|
718
|
+
(
|
|
719
|
+
receipt
|
|
720
|
+
for receipt in context.ledger_prefix.receipts
|
|
721
|
+
if receipt.receipt_id == claim.request_receipt_id
|
|
722
|
+
),
|
|
723
|
+
None,
|
|
724
|
+
)
|
|
725
|
+
if (
|
|
726
|
+
not isinstance(request, ApprovalRequestedReceipt)
|
|
727
|
+
or request.policy_decision != "allow"
|
|
728
|
+
or request.execution_status != "succeeded"
|
|
729
|
+
or request.request_kind != "high_risk_action"
|
|
730
|
+
or request.digest != claim.request_receipt_digest
|
|
731
|
+
or request.requested_scope != claim.approved_scope
|
|
732
|
+
or request.expires_at != claim.expires_at
|
|
733
|
+
or claim.decided_at < request.occurred_at
|
|
734
|
+
or claim.decided_at > request.expires_at
|
|
735
|
+
):
|
|
736
|
+
raise _fail("human approval binding is invalid")
|
|
737
|
+
if claim.request_receipt_id in dict(
|
|
738
|
+
context.replay_state.approval_decision_by_request
|
|
739
|
+
):
|
|
740
|
+
raise _fail("human approval request already has a decision")
|
|
741
|
+
|
|
742
|
+
error_code = _denial_code(
|
|
743
|
+
state_allowed=context.current_state in {"running", "retrying"},
|
|
744
|
+
role_allowed=binding.role == request.required_role,
|
|
745
|
+
capability_allowed=True,
|
|
746
|
+
approval_allowed=(
|
|
747
|
+
claim.decision == "approved"
|
|
748
|
+
and context.transaction_time <= claim.expires_at
|
|
749
|
+
),
|
|
750
|
+
)
|
|
751
|
+
return _policy_decision(error_code)
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def validate_rollback(
|
|
755
|
+
context: AuthorizationContext,
|
|
756
|
+
request: AgentRequest,
|
|
757
|
+
) -> PolicyDecision:
|
|
758
|
+
"""Decide prospective rollback eligibility without mutating authority."""
|
|
759
|
+
|
|
760
|
+
if (
|
|
761
|
+
type(context) is not AuthorizationContext
|
|
762
|
+
or type(request) is not AgentRequest
|
|
763
|
+
):
|
|
764
|
+
raise _fail("rollback authorization inputs are malformed")
|
|
765
|
+
grants = {
|
|
766
|
+
grant.grant_id: grant
|
|
767
|
+
for grant in context.ledger_prefix.effective_grants
|
|
768
|
+
}
|
|
769
|
+
bindings = {
|
|
770
|
+
binding.key_id: binding
|
|
771
|
+
for binding in context.work_order.key_bindings
|
|
772
|
+
}
|
|
773
|
+
grant = grants.get(request.grant_id)
|
|
774
|
+
binding = bindings.get(request.actor_key_id)
|
|
775
|
+
if (
|
|
776
|
+
request.tool_name != "owp.rollback_patch"
|
|
777
|
+
or request.work_order_digest != context.work_order.digest
|
|
778
|
+
or grant is None
|
|
779
|
+
or binding is None
|
|
780
|
+
or request.actor_id != binding.subject_id
|
|
781
|
+
or request.signer_key_id != binding.key_id
|
|
782
|
+
or grant.subject_agent_id != binding.subject_id
|
|
783
|
+
or grant.subject_key_id != binding.key_id
|
|
784
|
+
):
|
|
785
|
+
raise _fail("rollback request Grant or actor binding is invalid")
|
|
786
|
+
try:
|
|
787
|
+
public_key = decode_and_verify_key_binding(binding)
|
|
788
|
+
except Exception as error:
|
|
789
|
+
raise _fail("rollback request key binding is invalid") from error
|
|
790
|
+
if not verify_payload(
|
|
791
|
+
"agent-request",
|
|
792
|
+
request.model_dump(mode="json"),
|
|
793
|
+
public_key,
|
|
794
|
+
):
|
|
795
|
+
raise _fail("rollback request signature is invalid")
|
|
796
|
+
request_age = context.transaction_time - request.requested_at
|
|
797
|
+
if request_age < timedelta(0) or request_age > timedelta(seconds=300):
|
|
798
|
+
raise _fail("rollback request is outside the freshness window")
|
|
799
|
+
target = next(
|
|
800
|
+
(
|
|
801
|
+
receipt
|
|
802
|
+
for receipt in context.ledger_prefix.receipts
|
|
803
|
+
if receipt.receipt_id == context.active_patch_receipt_id
|
|
804
|
+
),
|
|
805
|
+
None,
|
|
806
|
+
)
|
|
807
|
+
if (
|
|
808
|
+
not isinstance(target, ToolCallReceipt)
|
|
809
|
+
or target.tool_name != "owp.apply_patch"
|
|
810
|
+
or target.policy_decision != "allow"
|
|
811
|
+
or target.execution_status != "succeeded"
|
|
812
|
+
):
|
|
813
|
+
raise _fail("rollback target patch is unavailable")
|
|
814
|
+
expected_arguments = {
|
|
815
|
+
"target_patch_receipt_id": target.receipt_id,
|
|
816
|
+
"target_patch_digest": target.digest,
|
|
817
|
+
"before_commit": context.replay_checkpoint.head_commit,
|
|
818
|
+
}
|
|
819
|
+
if request.arguments_digest != request_arguments_digest(
|
|
820
|
+
"owp.rollback_patch",
|
|
821
|
+
expected_arguments,
|
|
822
|
+
):
|
|
823
|
+
raise _fail("rollback target binding is invalid")
|
|
824
|
+
if context.transaction_time > context.work_order.deadline:
|
|
825
|
+
raise _fail("contract expired before rollback authorization")
|
|
826
|
+
if context.routine_capacity_remaining <= 0:
|
|
827
|
+
raise _fail("routine Receipt capacity is exhausted")
|
|
828
|
+
capability_allowed = (
|
|
829
|
+
request.tool_name in context.work_order.allowed_tools
|
|
830
|
+
and request.tool_name in grant.allowed_tools
|
|
831
|
+
and grant.grant_id not in context.replay_state.revoked_grant_ids
|
|
832
|
+
and (
|
|
833
|
+
grant.usage_mode != "single_use"
|
|
834
|
+
or grant.grant_id
|
|
835
|
+
not in context.replay_state.used_single_use_grant_ids
|
|
836
|
+
)
|
|
837
|
+
and grant.valid_from
|
|
838
|
+
<= context.transaction_time
|
|
839
|
+
<= min(grant.expires_at, context.work_order.deadline)
|
|
840
|
+
)
|
|
841
|
+
return _policy_decision(
|
|
842
|
+
_denial_code(
|
|
843
|
+
state_allowed=(
|
|
844
|
+
context.current_state == "needs_rework"
|
|
845
|
+
and context.causal_state.failure_receipt_id is not None
|
|
846
|
+
and context.causal_state.rollback_receipt_id is None
|
|
847
|
+
),
|
|
848
|
+
role_allowed=binding.role == "Developer",
|
|
849
|
+
capability_allowed=capability_allowed,
|
|
850
|
+
quota_allowed=_remaining_tool_calls(
|
|
851
|
+
context,
|
|
852
|
+
grant.grant_id,
|
|
853
|
+
)
|
|
854
|
+
> 0,
|
|
855
|
+
)
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def _bounded_mapping(
|
|
860
|
+
value: Mapping[str, CapabilityGrant],
|
|
861
|
+
*,
|
|
862
|
+
cap: int,
|
|
863
|
+
label: str,
|
|
864
|
+
) -> dict[str, CapabilityGrant]:
|
|
865
|
+
if not isinstance(value, Mapping):
|
|
866
|
+
raise _fail(f"{label} must be a mapping")
|
|
867
|
+
try:
|
|
868
|
+
items = tuple(islice(value.items(), cap + 1))
|
|
869
|
+
except AuthorizationPolicyError:
|
|
870
|
+
raise
|
|
871
|
+
except Exception as error:
|
|
872
|
+
raise _fail(f"{label} input is unavailable") from error
|
|
873
|
+
if len(items) > cap:
|
|
874
|
+
raise _fail(f"{label} exceeds its bounded capacity")
|
|
875
|
+
snapshot: dict[str, CapabilityGrant] = {}
|
|
876
|
+
for key, grant in items:
|
|
877
|
+
if (
|
|
878
|
+
type(key) is not str
|
|
879
|
+
or not isinstance(grant, CapabilityGrant)
|
|
880
|
+
or key in snapshot
|
|
881
|
+
):
|
|
882
|
+
raise _fail(f"{label} contains a malformed entry")
|
|
883
|
+
snapshot[key] = grant
|
|
884
|
+
return snapshot
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
def _bounded_receipts(
|
|
888
|
+
receipts: Iterable[ActionReceiptEnvelope],
|
|
889
|
+
) -> tuple[ActionReceiptEnvelope, ...]:
|
|
890
|
+
try:
|
|
891
|
+
bounded = tuple(
|
|
892
|
+
islice(receipts, MAX_AUTHORIZATION_RECEIPTS + 1)
|
|
893
|
+
)
|
|
894
|
+
except AuthorizationPolicyError:
|
|
895
|
+
raise
|
|
896
|
+
except Exception as error:
|
|
897
|
+
raise _fail("receipt history input is unavailable") from error
|
|
898
|
+
if len(bounded) > MAX_AUTHORIZATION_RECEIPTS:
|
|
899
|
+
raise _fail("receipt history exceeds its bounded capacity")
|
|
900
|
+
if any(
|
|
901
|
+
not isinstance(receipt, ActionReceiptEnvelope)
|
|
902
|
+
for receipt in bounded
|
|
903
|
+
):
|
|
904
|
+
raise _fail("receipt history contains a malformed receipt")
|
|
905
|
+
return bounded
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
def _validate_grant_signatures(
|
|
909
|
+
work_order: WorkOrder,
|
|
910
|
+
grants: Mapping[str, CapabilityGrant],
|
|
911
|
+
attempts: Mapping[str, CapabilityGrant],
|
|
912
|
+
) -> None:
|
|
913
|
+
try:
|
|
914
|
+
bindings_by_key = {
|
|
915
|
+
binding.key_id: (
|
|
916
|
+
binding,
|
|
917
|
+
decode_and_verify_key_binding(binding),
|
|
918
|
+
)
|
|
919
|
+
for binding in work_order.key_bindings
|
|
920
|
+
}
|
|
921
|
+
except Exception as error:
|
|
922
|
+
raise _fail("Grant signature key bindings are unavailable") from error
|
|
923
|
+
if len(bindings_by_key) != len(work_order.key_bindings):
|
|
924
|
+
raise _fail("Grant signature key bindings are not unique")
|
|
925
|
+
|
|
926
|
+
def validate_grant(
|
|
927
|
+
grant: CapabilityGrant,
|
|
928
|
+
*,
|
|
929
|
+
issuer_roles: set[str],
|
|
930
|
+
subject_roles: set[str],
|
|
931
|
+
) -> None:
|
|
932
|
+
issuer_entry = bindings_by_key.get(grant.issuer_key_id)
|
|
933
|
+
subject_entry = bindings_by_key.get(grant.subject_key_id)
|
|
934
|
+
if (
|
|
935
|
+
grant.work_order_digest != work_order.digest
|
|
936
|
+
or issuer_entry is None
|
|
937
|
+
or issuer_entry[0].role not in issuer_roles
|
|
938
|
+
or grant.signer_key_id != grant.issuer_key_id
|
|
939
|
+
or subject_entry is None
|
|
940
|
+
or subject_entry[0].subject_id
|
|
941
|
+
!= grant.subject_agent_id
|
|
942
|
+
or subject_entry[0].role not in subject_roles
|
|
943
|
+
or not verify_payload(
|
|
944
|
+
"capability-grant",
|
|
945
|
+
grant.model_dump(mode="json"),
|
|
946
|
+
issuer_entry[1],
|
|
947
|
+
)
|
|
948
|
+
):
|
|
949
|
+
raise _fail("Grant signature or role binding is invalid")
|
|
950
|
+
|
|
951
|
+
for grant in grants.values():
|
|
952
|
+
validate_grant(
|
|
953
|
+
grant,
|
|
954
|
+
issuer_roles={
|
|
955
|
+
"Maintainer"
|
|
956
|
+
if grant.parent_grant_id is None
|
|
957
|
+
else "Manager"
|
|
958
|
+
},
|
|
959
|
+
subject_roles=(
|
|
960
|
+
{"Manager"}
|
|
961
|
+
if grant.parent_grant_id is None
|
|
962
|
+
else {"Developer", "Verifier"}
|
|
963
|
+
),
|
|
964
|
+
)
|
|
965
|
+
for grant in attempts.values():
|
|
966
|
+
validate_grant(
|
|
967
|
+
grant,
|
|
968
|
+
issuer_roles=(
|
|
969
|
+
{"Maintainer"}
|
|
970
|
+
if grant.parent_grant_id is None
|
|
971
|
+
else {"Manager", "Developer", "Verifier"}
|
|
972
|
+
),
|
|
973
|
+
subject_roles=(
|
|
974
|
+
{"Manager"}
|
|
975
|
+
if grant.parent_grant_id is None
|
|
976
|
+
else {"Developer", "Verifier"}
|
|
977
|
+
),
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def _root_contains(root: str, candidate: str) -> bool:
|
|
982
|
+
return candidate == root or candidate.startswith(f"{root}/")
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def _roots_within(
|
|
986
|
+
candidates: tuple[str, ...],
|
|
987
|
+
allowed: tuple[str, ...],
|
|
988
|
+
) -> bool:
|
|
989
|
+
return all(
|
|
990
|
+
any(_root_contains(root, candidate) for root in allowed)
|
|
991
|
+
for candidate in candidates
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
def _child_signing_binding(
|
|
996
|
+
work_order: WorkOrder,
|
|
997
|
+
candidate: CapabilityGrant,
|
|
998
|
+
):
|
|
999
|
+
matches = tuple(
|
|
1000
|
+
binding
|
|
1001
|
+
for binding in work_order.key_bindings
|
|
1002
|
+
if binding.key_id == candidate.signer_key_id
|
|
1003
|
+
)
|
|
1004
|
+
if len(matches) != 1:
|
|
1005
|
+
raise _fail("child Grant signer binding is unavailable")
|
|
1006
|
+
return matches[0]
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def _request_matches_candidate_issuer(request, binding) -> bool:
|
|
1010
|
+
return (
|
|
1011
|
+
request.actor_id == binding.subject_id
|
|
1012
|
+
and request.actor_key_id == binding.key_id
|
|
1013
|
+
and request.signer_key_id == binding.key_id
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
def _child_policy_decision(
|
|
1018
|
+
work_order: WorkOrder,
|
|
1019
|
+
parent: CapabilityGrant,
|
|
1020
|
+
parent_state: _GrantReplayState | None,
|
|
1021
|
+
candidate: CapabilityGrant,
|
|
1022
|
+
request,
|
|
1023
|
+
signing_binding,
|
|
1024
|
+
bindings: Mapping[str, object],
|
|
1025
|
+
now,
|
|
1026
|
+
*,
|
|
1027
|
+
state_allowed: bool,
|
|
1028
|
+
) -> tuple[bool, str | None]:
|
|
1029
|
+
manager = bindings["Manager"]
|
|
1030
|
+
developer = bindings["Developer"]
|
|
1031
|
+
verifier = bindings["Verifier"]
|
|
1032
|
+
matching_subjects = tuple(
|
|
1033
|
+
binding
|
|
1034
|
+
for binding in (developer, verifier)
|
|
1035
|
+
if (
|
|
1036
|
+
candidate.subject_agent_id == binding.subject_id
|
|
1037
|
+
and candidate.subject_key_id == binding.key_id
|
|
1038
|
+
)
|
|
1039
|
+
)
|
|
1040
|
+
authority_valid = (
|
|
1041
|
+
signing_binding.role == "Manager"
|
|
1042
|
+
and signing_binding.key_id == manager.key_id
|
|
1043
|
+
and parent.subject_agent_id == manager.subject_id
|
|
1044
|
+
and parent.subject_key_id == manager.key_id
|
|
1045
|
+
and request.actor_id == manager.subject_id
|
|
1046
|
+
and request.actor_key_id == manager.key_id
|
|
1047
|
+
)
|
|
1048
|
+
role_allowed = authority_valid and len(matching_subjects) == 1
|
|
1049
|
+
tools_valid = (
|
|
1050
|
+
set(candidate.allowed_tools) <= set(parent.allowed_tools)
|
|
1051
|
+
and set(candidate.allowed_tools) <= set(work_order.allowed_tools)
|
|
1052
|
+
)
|
|
1053
|
+
roots_valid = (
|
|
1054
|
+
_roots_within(
|
|
1055
|
+
candidate.allowed_read_roots,
|
|
1056
|
+
parent.allowed_read_roots,
|
|
1057
|
+
)
|
|
1058
|
+
and _roots_within(
|
|
1059
|
+
candidate.allowed_read_roots,
|
|
1060
|
+
work_order.allowed_read_roots,
|
|
1061
|
+
)
|
|
1062
|
+
and _roots_within(
|
|
1063
|
+
candidate.allowed_write_roots,
|
|
1064
|
+
parent.allowed_write_roots,
|
|
1065
|
+
)
|
|
1066
|
+
and _roots_within(
|
|
1067
|
+
candidate.allowed_write_roots,
|
|
1068
|
+
work_order.allowed_write_roots,
|
|
1069
|
+
)
|
|
1070
|
+
)
|
|
1071
|
+
subject = matching_subjects[0] if len(matching_subjects) == 1 else None
|
|
1072
|
+
role_tools = (
|
|
1073
|
+
{"owp.run_tests"}
|
|
1074
|
+
if subject is not None and subject.role == "Verifier"
|
|
1075
|
+
else {
|
|
1076
|
+
"owp.apply_patch",
|
|
1077
|
+
"owp.repo_read",
|
|
1078
|
+
"owp.rollback_patch",
|
|
1079
|
+
*(
|
|
1080
|
+
("owp.run_tests",)
|
|
1081
|
+
if any(
|
|
1082
|
+
profile.test_mode == "developer"
|
|
1083
|
+
for profile in work_order.test_profiles
|
|
1084
|
+
)
|
|
1085
|
+
else ()
|
|
1086
|
+
),
|
|
1087
|
+
}
|
|
1088
|
+
)
|
|
1089
|
+
time_valid = (
|
|
1090
|
+
work_order.issued_at
|
|
1091
|
+
<= candidate.issued_at
|
|
1092
|
+
<= candidate.valid_from
|
|
1093
|
+
< candidate.expires_at
|
|
1094
|
+
== work_order.deadline
|
|
1095
|
+
and parent.valid_from
|
|
1096
|
+
<= candidate.issued_at
|
|
1097
|
+
<= candidate.valid_from
|
|
1098
|
+
< candidate.expires_at
|
|
1099
|
+
<= parent.expires_at
|
|
1100
|
+
and parent.valid_from <= now <= parent.expires_at
|
|
1101
|
+
and 0 <= (now - candidate.issued_at).total_seconds() <= 300
|
|
1102
|
+
)
|
|
1103
|
+
rights_valid = (
|
|
1104
|
+
candidate.may_delegate is False
|
|
1105
|
+
and candidate.usage_mode in {"single_use", "metered"}
|
|
1106
|
+
)
|
|
1107
|
+
quota_valid = (
|
|
1108
|
+
parent_state is not None
|
|
1109
|
+
and candidate.quota.tool_calls
|
|
1110
|
+
<= parent_state.remaining_tool_calls
|
|
1111
|
+
and candidate.quota.repair_rounds
|
|
1112
|
+
<= parent_state.remaining_repair_rounds
|
|
1113
|
+
)
|
|
1114
|
+
capability_valid = (
|
|
1115
|
+
parent_state is not None
|
|
1116
|
+
and not parent_state.revoked
|
|
1117
|
+
and subject is not None
|
|
1118
|
+
and tools_valid
|
|
1119
|
+
and set(candidate.allowed_tools) <= role_tools
|
|
1120
|
+
and roots_valid
|
|
1121
|
+
and not (
|
|
1122
|
+
subject is not None
|
|
1123
|
+
and subject.role == "Verifier"
|
|
1124
|
+
and candidate.allowed_write_roots
|
|
1125
|
+
)
|
|
1126
|
+
and time_valid
|
|
1127
|
+
and rights_valid
|
|
1128
|
+
)
|
|
1129
|
+
error_code = _denial_code(
|
|
1130
|
+
state_allowed=state_allowed,
|
|
1131
|
+
role_allowed=role_allowed,
|
|
1132
|
+
capability_allowed=capability_valid,
|
|
1133
|
+
quota_allowed=quota_valid,
|
|
1134
|
+
)
|
|
1135
|
+
return error_code is None, error_code
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
def _replay_grant_context(
|
|
1139
|
+
work_order: WorkOrder,
|
|
1140
|
+
receipts: Iterable[ActionReceiptEnvelope],
|
|
1141
|
+
grants: Mapping[str, CapabilityGrant],
|
|
1142
|
+
) -> _GrantReplayContext:
|
|
1143
|
+
mutable: dict[str, dict[str, int | bool]] = {}
|
|
1144
|
+
states_before_receipt: list[
|
|
1145
|
+
tuple[str, tuple[tuple[str, _GrantReplayState], ...]]
|
|
1146
|
+
] = []
|
|
1147
|
+
|
|
1148
|
+
def frozen_states() -> tuple[tuple[str, _GrantReplayState], ...]:
|
|
1149
|
+
return tuple(
|
|
1150
|
+
(
|
|
1151
|
+
grant_id,
|
|
1152
|
+
_GrantReplayState(
|
|
1153
|
+
remaining_tool_calls=int(state["tool_calls"]),
|
|
1154
|
+
remaining_repair_rounds=int(
|
|
1155
|
+
state["repair_rounds"]
|
|
1156
|
+
),
|
|
1157
|
+
use_count=int(state["use_count"]),
|
|
1158
|
+
revoked=bool(state["revoked"]),
|
|
1159
|
+
),
|
|
1160
|
+
)
|
|
1161
|
+
for grant_id, state in sorted(mutable.items())
|
|
1162
|
+
)
|
|
1163
|
+
|
|
1164
|
+
def add_grant(grant: CapabilityGrant) -> None:
|
|
1165
|
+
if grant.grant_id in mutable:
|
|
1166
|
+
raise _fail("Grant quota replay encountered duplicate authority")
|
|
1167
|
+
mutable[grant.grant_id] = {
|
|
1168
|
+
"tool_calls": grant.quota.tool_calls,
|
|
1169
|
+
"repair_rounds": grant.quota.repair_rounds,
|
|
1170
|
+
"use_count": 0,
|
|
1171
|
+
"revoked": False,
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
def consume_authority(grant: CapabilityGrant) -> None:
|
|
1175
|
+
state = mutable.get(grant.grant_id)
|
|
1176
|
+
if state is None or state["revoked"] is True:
|
|
1177
|
+
raise _fail("Grant quota replay references inactive authority")
|
|
1178
|
+
if grant.usage_mode == "single_use" and state["use_count"] != 0:
|
|
1179
|
+
raise _fail("single-use Grant was consumed more than once")
|
|
1180
|
+
state["use_count"] = int(state["use_count"]) + 1
|
|
1181
|
+
|
|
1182
|
+
def validate_direct_call(receipt, *, require_active: bool):
|
|
1183
|
+
grant = grants.get(receipt.grant_id)
|
|
1184
|
+
state = mutable.get(receipt.grant_id)
|
|
1185
|
+
request = receipt.nested_claim
|
|
1186
|
+
elapsed = (
|
|
1187
|
+
receipt.occurred_at - request.requested_at
|
|
1188
|
+
).total_seconds()
|
|
1189
|
+
if (
|
|
1190
|
+
grant is None
|
|
1191
|
+
or state is None
|
|
1192
|
+
or receipt.actor_id != grant.subject_agent_id
|
|
1193
|
+
or receipt.actor_key_id != grant.subject_key_id
|
|
1194
|
+
or request.actor_id != grant.subject_agent_id
|
|
1195
|
+
or request.actor_key_id != grant.subject_key_id
|
|
1196
|
+
or request.signer_key_id != grant.subject_key_id
|
|
1197
|
+
or request.grant_id != grant.grant_id
|
|
1198
|
+
or request.work_order_digest != work_order.digest
|
|
1199
|
+
or request.requested_at < work_order.issued_at
|
|
1200
|
+
or request.requested_at > work_order.deadline
|
|
1201
|
+
or receipt.occurred_at < work_order.issued_at
|
|
1202
|
+
or receipt.occurred_at > work_order.deadline
|
|
1203
|
+
or elapsed < 0
|
|
1204
|
+
or elapsed > 300
|
|
1205
|
+
):
|
|
1206
|
+
raise _fail("Grant call binding failed semantic replay")
|
|
1207
|
+
role_authorized = _agent_direct_call_is_authorized(
|
|
1208
|
+
receipt,
|
|
1209
|
+
work_order,
|
|
1210
|
+
)
|
|
1211
|
+
if require_active and not role_authorized:
|
|
1212
|
+
raise _fail("Grant direct-call role failed semantic replay")
|
|
1213
|
+
if require_active and (
|
|
1214
|
+
state["revoked"] is True
|
|
1215
|
+
or request.tool_name not in grant.allowed_tools
|
|
1216
|
+
or request.requested_at < grant.valid_from
|
|
1217
|
+
or request.requested_at > grant.expires_at
|
|
1218
|
+
or receipt.occurred_at < grant.valid_from
|
|
1219
|
+
or receipt.occurred_at > grant.expires_at
|
|
1220
|
+
):
|
|
1221
|
+
raise _fail("Grant charge authority failed semantic replay")
|
|
1222
|
+
return grant, state, role_authorized
|
|
1223
|
+
|
|
1224
|
+
charge_types = (
|
|
1225
|
+
GrantConsumedReceipt,
|
|
1226
|
+
ToolCallReceipt,
|
|
1227
|
+
ApprovalRequestedReceipt,
|
|
1228
|
+
RollbackReceipt,
|
|
1229
|
+
)
|
|
1230
|
+
for receipt in receipts:
|
|
1231
|
+
states_before_receipt.append(
|
|
1232
|
+
(receipt.receipt_id, frozen_states())
|
|
1233
|
+
)
|
|
1234
|
+
if isinstance(receipt, GrantIssuedReceipt):
|
|
1235
|
+
if (
|
|
1236
|
+
receipt.policy_decision != "allow"
|
|
1237
|
+
or receipt.execution_status != "succeeded"
|
|
1238
|
+
):
|
|
1239
|
+
continue
|
|
1240
|
+
grant = grants.get(receipt.issued_grant_id)
|
|
1241
|
+
if grant is None:
|
|
1242
|
+
raise _fail("Grant quota replay cannot resolve issuance")
|
|
1243
|
+
if grant.parent_grant_id is None:
|
|
1244
|
+
add_grant(grant)
|
|
1245
|
+
continue
|
|
1246
|
+
parent = grants.get(grant.parent_grant_id)
|
|
1247
|
+
parent_state = mutable.get(grant.parent_grant_id)
|
|
1248
|
+
if parent is None or parent_state is None:
|
|
1249
|
+
raise _fail(
|
|
1250
|
+
"Grant quota replay references non-prior parent"
|
|
1251
|
+
)
|
|
1252
|
+
consume_authority(parent)
|
|
1253
|
+
for metric in ("tool_calls", "repair_rounds"):
|
|
1254
|
+
remaining = int(parent_state[metric]) - getattr(
|
|
1255
|
+
grant.quota,
|
|
1256
|
+
metric,
|
|
1257
|
+
)
|
|
1258
|
+
if remaining < 0:
|
|
1259
|
+
raise _fail("Grant quota replay over-allocates parent")
|
|
1260
|
+
parent_state[metric] = remaining
|
|
1261
|
+
add_grant(grant)
|
|
1262
|
+
continue
|
|
1263
|
+
if isinstance(receipt, GrantRevokedReceipt):
|
|
1264
|
+
if (
|
|
1265
|
+
receipt.policy_decision != "allow"
|
|
1266
|
+
or receipt.execution_status != "succeeded"
|
|
1267
|
+
):
|
|
1268
|
+
if receipt.quota_charge is not None:
|
|
1269
|
+
raise _fail("denied Grant receipt cannot charge quota")
|
|
1270
|
+
continue
|
|
1271
|
+
authorizer = grants.get(receipt.authorizing_grant_id)
|
|
1272
|
+
target_state = mutable.get(receipt.revoked_grant_id)
|
|
1273
|
+
if authorizer is None or target_state is None:
|
|
1274
|
+
raise _fail(
|
|
1275
|
+
"Grant quota replay cannot resolve revocation"
|
|
1276
|
+
)
|
|
1277
|
+
consume_authority(authorizer)
|
|
1278
|
+
target_state["revoked"] = True
|
|
1279
|
+
continue
|
|
1280
|
+
if not isinstance(receipt, charge_types):
|
|
1281
|
+
if (
|
|
1282
|
+
receipt.policy_decision == "deny"
|
|
1283
|
+
and receipt.quota_charge is not None
|
|
1284
|
+
):
|
|
1285
|
+
raise _fail("denied Grant receipt cannot charge quota")
|
|
1286
|
+
continue
|
|
1287
|
+
if receipt.policy_decision == "deny":
|
|
1288
|
+
if receipt.quota_charge is not None:
|
|
1289
|
+
raise _fail("denied Grant receipt cannot charge quota")
|
|
1290
|
+
if (
|
|
1291
|
+
isinstance(receipt, GrantConsumedReceipt)
|
|
1292
|
+
and receipt.amount != 1
|
|
1293
|
+
):
|
|
1294
|
+
raise _fail(
|
|
1295
|
+
"start_retry must consume exactly one repair round"
|
|
1296
|
+
)
|
|
1297
|
+
validate_direct_call(
|
|
1298
|
+
receipt,
|
|
1299
|
+
require_active=False,
|
|
1300
|
+
)
|
|
1301
|
+
continue
|
|
1302
|
+
charge = receipt.quota_charge
|
|
1303
|
+
if charge is None:
|
|
1304
|
+
raise _fail("started Grant receipt must charge quota")
|
|
1305
|
+
if (
|
|
1306
|
+
isinstance(receipt, GrantConsumedReceipt)
|
|
1307
|
+
and receipt.amount != 1
|
|
1308
|
+
):
|
|
1309
|
+
raise _fail("start_retry must consume exactly one repair round")
|
|
1310
|
+
grant, state, _ = validate_direct_call(
|
|
1311
|
+
receipt,
|
|
1312
|
+
require_active=True,
|
|
1313
|
+
)
|
|
1314
|
+
if charge.grant_id != grant.grant_id:
|
|
1315
|
+
raise _fail("Grant charge does not match direct call")
|
|
1316
|
+
remaining = int(state[charge.metric])
|
|
1317
|
+
if charge.amount > remaining:
|
|
1318
|
+
raise _fail("Grant quota is exhausted")
|
|
1319
|
+
expected_after = remaining - charge.amount
|
|
1320
|
+
if charge.remaining_after != expected_after:
|
|
1321
|
+
raise _fail(
|
|
1322
|
+
"Grant quota remaining_after failed semantic replay"
|
|
1323
|
+
)
|
|
1324
|
+
consume_authority(grant)
|
|
1325
|
+
state[charge.metric] = expected_after
|
|
1326
|
+
if set(mutable) != set(grants):
|
|
1327
|
+
raise _fail("Grant quota replay is incomplete")
|
|
1328
|
+
return _GrantReplayContext(
|
|
1329
|
+
states_before_receipt=tuple(states_before_receipt),
|
|
1330
|
+
final_states=frozen_states(),
|
|
1331
|
+
)
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
def _replay_grant_quota_history(
|
|
1335
|
+
work_order: WorkOrder,
|
|
1336
|
+
receipts: Iterable[ActionReceiptEnvelope],
|
|
1337
|
+
grants: Mapping[str, CapabilityGrant],
|
|
1338
|
+
) -> dict[str, _GrantReplayState]:
|
|
1339
|
+
return _replay_grant_context(
|
|
1340
|
+
work_order,
|
|
1341
|
+
receipts,
|
|
1342
|
+
grants,
|
|
1343
|
+
).final_by_grant()
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
def _grant_history_replay_context(
|
|
1347
|
+
work_order: WorkOrder,
|
|
1348
|
+
receipts: Iterable[ActionReceiptEnvelope],
|
|
1349
|
+
grants: Mapping[str, CapabilityGrant],
|
|
1350
|
+
attempts: Mapping[str, CapabilityGrant],
|
|
1351
|
+
) -> _GrantReplayContext:
|
|
1352
|
+
history = tuple(receipts)
|
|
1353
|
+
replay_context = _replay_grant_context(
|
|
1354
|
+
work_order,
|
|
1355
|
+
history,
|
|
1356
|
+
grants,
|
|
1357
|
+
)
|
|
1358
|
+
bindings = {
|
|
1359
|
+
binding.role: binding for binding in work_order.key_bindings
|
|
1360
|
+
}
|
|
1361
|
+
manager = bindings["Manager"]
|
|
1362
|
+
prior_effective: dict[str, CapabilityGrant] = {}
|
|
1363
|
+
seen_attempts: set[str] = set()
|
|
1364
|
+
revoked_grants: set[str] = set()
|
|
1365
|
+
root_seen = False
|
|
1366
|
+
for receipt in history:
|
|
1367
|
+
if isinstance(receipt, GrantRevokedReceipt):
|
|
1368
|
+
authorizer = prior_effective.get(
|
|
1369
|
+
receipt.authorizing_grant_id
|
|
1370
|
+
)
|
|
1371
|
+
target = prior_effective.get(receipt.revoked_grant_id)
|
|
1372
|
+
request = receipt.nested_claim
|
|
1373
|
+
binding_invalid = (
|
|
1374
|
+
authorizer is None
|
|
1375
|
+
or authorizer.parent_grant_id is not None
|
|
1376
|
+
or target is None
|
|
1377
|
+
or target.parent_grant_id != authorizer.grant_id
|
|
1378
|
+
or target.grant_id in revoked_grants
|
|
1379
|
+
or receipt.state_before != receipt.state_after
|
|
1380
|
+
or receipt.actor_id != manager.subject_id
|
|
1381
|
+
or receipt.actor_key_id != manager.key_id
|
|
1382
|
+
or request.actor_id != manager.subject_id
|
|
1383
|
+
or request.actor_key_id != manager.key_id
|
|
1384
|
+
or request.signer_key_id != manager.key_id
|
|
1385
|
+
or request.work_order_digest != work_order.digest
|
|
1386
|
+
or request.grant_id != authorizer.grant_id
|
|
1387
|
+
or request.tool_name != "owp.revoke_grant"
|
|
1388
|
+
or request.requested_at < work_order.issued_at
|
|
1389
|
+
or request.requested_at > work_order.deadline
|
|
1390
|
+
or receipt.occurred_at < request.requested_at
|
|
1391
|
+
or (
|
|
1392
|
+
receipt.occurred_at - request.requested_at
|
|
1393
|
+
).total_seconds()
|
|
1394
|
+
> 300
|
|
1395
|
+
or receipt.occurred_at > work_order.deadline
|
|
1396
|
+
)
|
|
1397
|
+
allowed_invalid = (
|
|
1398
|
+
receipt.policy_decision == "allow"
|
|
1399
|
+
and (
|
|
1400
|
+
receipt.policy_error_code is not None
|
|
1401
|
+
or receipt.execution_status != "succeeded"
|
|
1402
|
+
or receipt.execution_error_code is not None
|
|
1403
|
+
or request.requested_at < authorizer.valid_from
|
|
1404
|
+
or request.requested_at > authorizer.expires_at
|
|
1405
|
+
or receipt.occurred_at < authorizer.valid_from
|
|
1406
|
+
or receipt.occurred_at > authorizer.expires_at
|
|
1407
|
+
)
|
|
1408
|
+
)
|
|
1409
|
+
denied_invalid = (
|
|
1410
|
+
receipt.policy_decision == "deny"
|
|
1411
|
+
and (
|
|
1412
|
+
receipt.policy_error_code is None
|
|
1413
|
+
or receipt.execution_status != "denied"
|
|
1414
|
+
or receipt.execution_error_code is not None
|
|
1415
|
+
or receipt.quota_charge is not None
|
|
1416
|
+
)
|
|
1417
|
+
)
|
|
1418
|
+
if (
|
|
1419
|
+
binding_invalid
|
|
1420
|
+
or allowed_invalid
|
|
1421
|
+
or denied_invalid
|
|
1422
|
+
or receipt.policy_decision not in {"allow", "deny"}
|
|
1423
|
+
):
|
|
1424
|
+
raise _fail(
|
|
1425
|
+
"Grant revocation history failed semantic replay"
|
|
1426
|
+
)
|
|
1427
|
+
if receipt.policy_decision == "allow":
|
|
1428
|
+
revoked_grants.add(target.grant_id)
|
|
1429
|
+
continue
|
|
1430
|
+
if not isinstance(receipt, GrantIssuedReceipt):
|
|
1431
|
+
continue
|
|
1432
|
+
if receipt.parent_grant_id is None:
|
|
1433
|
+
root = grants.get(receipt.issued_grant_id)
|
|
1434
|
+
request = receipt.nested_claim
|
|
1435
|
+
if (
|
|
1436
|
+
root_seen
|
|
1437
|
+
or receipt.sequence != 1
|
|
1438
|
+
or root is None
|
|
1439
|
+
or root.parent_grant_id is not None
|
|
1440
|
+
or receipt.candidate_grant_digest != root.digest
|
|
1441
|
+
or receipt.actor_id != manager.subject_id
|
|
1442
|
+
or receipt.actor_key_id != manager.key_id
|
|
1443
|
+
or not _request_matches_candidate_issuer(
|
|
1444
|
+
request,
|
|
1445
|
+
manager,
|
|
1446
|
+
)
|
|
1447
|
+
or request.work_order_digest != work_order.digest
|
|
1448
|
+
or request.grant_id != root.grant_id
|
|
1449
|
+
or request.tool_name != "owp.activate_root_grant"
|
|
1450
|
+
or request.requested_at < work_order.issued_at
|
|
1451
|
+
or request.requested_at > work_order.deadline
|
|
1452
|
+
or request.requested_at < root.valid_from
|
|
1453
|
+
or request.requested_at > root.expires_at
|
|
1454
|
+
or receipt.occurred_at < request.requested_at
|
|
1455
|
+
or (
|
|
1456
|
+
receipt.occurred_at - request.requested_at
|
|
1457
|
+
).total_seconds()
|
|
1458
|
+
> 300
|
|
1459
|
+
or receipt.occurred_at < root.valid_from
|
|
1460
|
+
or receipt.occurred_at > root.expires_at
|
|
1461
|
+
):
|
|
1462
|
+
raise _fail(
|
|
1463
|
+
"root activation history failed semantic replay"
|
|
1464
|
+
)
|
|
1465
|
+
root_seen = True
|
|
1466
|
+
prior_effective[root.grant_id] = root
|
|
1467
|
+
continue
|
|
1468
|
+
parent = prior_effective.get(receipt.parent_grant_id)
|
|
1469
|
+
candidate = (
|
|
1470
|
+
grants.get(receipt.issued_grant_id)
|
|
1471
|
+
if receipt.policy_decision == "allow"
|
|
1472
|
+
else attempts.get(receipt.candidate_grant_digest)
|
|
1473
|
+
)
|
|
1474
|
+
if parent is None or candidate is None:
|
|
1475
|
+
raise _fail("Grant history references non-prior authority")
|
|
1476
|
+
signing_binding = _child_signing_binding(
|
|
1477
|
+
work_order,
|
|
1478
|
+
candidate,
|
|
1479
|
+
)
|
|
1480
|
+
request = receipt.nested_claim
|
|
1481
|
+
if (
|
|
1482
|
+
receipt.actor_id != signing_binding.subject_id
|
|
1483
|
+
or receipt.actor_key_id != signing_binding.key_id
|
|
1484
|
+
or not _request_matches_candidate_issuer(
|
|
1485
|
+
request,
|
|
1486
|
+
signing_binding,
|
|
1487
|
+
)
|
|
1488
|
+
or candidate.parent_grant_id != parent.grant_id
|
|
1489
|
+
or request.work_order_digest != work_order.digest
|
|
1490
|
+
or request.grant_id != parent.grant_id
|
|
1491
|
+
or request.tool_name != "owp.delegate_grant"
|
|
1492
|
+
or request.requested_at < work_order.issued_at
|
|
1493
|
+
or request.requested_at > work_order.deadline
|
|
1494
|
+
or receipt.occurred_at < request.requested_at
|
|
1495
|
+
or (
|
|
1496
|
+
receipt.occurred_at - request.requested_at
|
|
1497
|
+
).total_seconds()
|
|
1498
|
+
> 300
|
|
1499
|
+
):
|
|
1500
|
+
raise _fail(
|
|
1501
|
+
"Grant history request binding failed semantic replay"
|
|
1502
|
+
)
|
|
1503
|
+
allowed, error_code = _child_policy_decision(
|
|
1504
|
+
work_order,
|
|
1505
|
+
parent,
|
|
1506
|
+
replay_context.state_before(
|
|
1507
|
+
receipt.receipt_id,
|
|
1508
|
+
parent.grant_id,
|
|
1509
|
+
),
|
|
1510
|
+
candidate,
|
|
1511
|
+
request,
|
|
1512
|
+
signing_binding,
|
|
1513
|
+
bindings,
|
|
1514
|
+
receipt.occurred_at,
|
|
1515
|
+
state_allowed=(
|
|
1516
|
+
receipt.state_before == receipt.state_after
|
|
1517
|
+
and receipt.state_before
|
|
1518
|
+
in {"running", "retrying", "needs_rework"}
|
|
1519
|
+
),
|
|
1520
|
+
)
|
|
1521
|
+
if receipt.policy_decision == "allow":
|
|
1522
|
+
if (
|
|
1523
|
+
not allowed
|
|
1524
|
+
or error_code is not None
|
|
1525
|
+
or receipt.policy_error_code is not None
|
|
1526
|
+
or receipt.execution_status != "succeeded"
|
|
1527
|
+
or receipt.issued_grant_id != candidate.grant_id
|
|
1528
|
+
):
|
|
1529
|
+
raise _fail(
|
|
1530
|
+
"allowed Grant history fails policy replay"
|
|
1531
|
+
)
|
|
1532
|
+
prior_effective[candidate.grant_id] = candidate
|
|
1533
|
+
else:
|
|
1534
|
+
if (
|
|
1535
|
+
allowed
|
|
1536
|
+
or error_code is None
|
|
1537
|
+
or receipt.policy_error_code != error_code
|
|
1538
|
+
or receipt.execution_status != "denied"
|
|
1539
|
+
):
|
|
1540
|
+
raise _fail(
|
|
1541
|
+
"denied Grant history fails policy replay"
|
|
1542
|
+
)
|
|
1543
|
+
seen_attempts.add(candidate.digest)
|
|
1544
|
+
if (
|
|
1545
|
+
not root_seen
|
|
1546
|
+
or set(prior_effective) != set(grants)
|
|
1547
|
+
or seen_attempts != set(attempts)
|
|
1548
|
+
):
|
|
1549
|
+
raise _fail("Grant history replay is incomplete")
|
|
1550
|
+
return replay_context
|
|
1551
|
+
|
|
1552
|
+
|
|
1553
|
+
def _validate_grant_history_semantics(
|
|
1554
|
+
work_order: WorkOrder,
|
|
1555
|
+
receipts: Iterable[ActionReceiptEnvelope],
|
|
1556
|
+
grants: Mapping[str, CapabilityGrant],
|
|
1557
|
+
attempts: Mapping[str, CapabilityGrant],
|
|
1558
|
+
) -> dict[str, _GrantReplayState]:
|
|
1559
|
+
return _grant_history_replay_context(
|
|
1560
|
+
work_order,
|
|
1561
|
+
receipts,
|
|
1562
|
+
grants,
|
|
1563
|
+
attempts,
|
|
1564
|
+
).final_by_grant()
|
|
1565
|
+
|
|
1566
|
+
|
|
1567
|
+
def _validate_state_history(
|
|
1568
|
+
work_order: WorkOrder,
|
|
1569
|
+
receipts: tuple[ActionReceiptEnvelope, ...],
|
|
1570
|
+
causal_state: AuthorizationCausalState,
|
|
1571
|
+
) -> None:
|
|
1572
|
+
try:
|
|
1573
|
+
public_keys = {
|
|
1574
|
+
binding.key_id: decode_and_verify_key_binding(binding)
|
|
1575
|
+
for binding in work_order.key_bindings
|
|
1576
|
+
}
|
|
1577
|
+
except Exception as error:
|
|
1578
|
+
raise _fail(
|
|
1579
|
+
"WorkOrder key bindings are unavailable for state replay"
|
|
1580
|
+
) from error
|
|
1581
|
+
for receipt in receipts:
|
|
1582
|
+
receipt_causal_state = causal_state.snapshot_for(
|
|
1583
|
+
receipt.receipt_id
|
|
1584
|
+
)
|
|
1585
|
+
state_before = TaskState(receipt.state_before)
|
|
1586
|
+
state_after = TaskState(receipt.state_after)
|
|
1587
|
+
decision = (
|
|
1588
|
+
append_receipt(
|
|
1589
|
+
work_order=work_order,
|
|
1590
|
+
state=state_before,
|
|
1591
|
+
receipt=receipt,
|
|
1592
|
+
public_keys=public_keys,
|
|
1593
|
+
now=receipt.occurred_at,
|
|
1594
|
+
allow_independent_verifier_rerun=True,
|
|
1595
|
+
)
|
|
1596
|
+
if state_before is state_after
|
|
1597
|
+
else apply_state_transition(
|
|
1598
|
+
work_order=work_order,
|
|
1599
|
+
state_before=state_before,
|
|
1600
|
+
state_after=state_after,
|
|
1601
|
+
trigger_receipt=receipt,
|
|
1602
|
+
acceptance_receipt=None,
|
|
1603
|
+
public_keys=public_keys,
|
|
1604
|
+
now=receipt.occurred_at,
|
|
1605
|
+
)
|
|
1606
|
+
)
|
|
1607
|
+
if decision.allowed:
|
|
1608
|
+
continue
|
|
1609
|
+
if decision.error_code in {
|
|
1610
|
+
"COMPOSITION_VALIDATOR_UNAVAILABLE",
|
|
1611
|
+
"TRANSITION_CONTEXT_UNAVAILABLE",
|
|
1612
|
+
}:
|
|
1613
|
+
if (
|
|
1614
|
+
isinstance(receipt, ToolCallReceipt)
|
|
1615
|
+
and _tool_state_context_exception(
|
|
1616
|
+
work_order,
|
|
1617
|
+
receipt,
|
|
1618
|
+
receipt_causal_state,
|
|
1619
|
+
)
|
|
1620
|
+
):
|
|
1621
|
+
continue
|
|
1622
|
+
if isinstance(
|
|
1623
|
+
receipt,
|
|
1624
|
+
(
|
|
1625
|
+
GrantConsumedReceipt,
|
|
1626
|
+
RollbackReceipt,
|
|
1627
|
+
SystemEventReceipt,
|
|
1628
|
+
),
|
|
1629
|
+
):
|
|
1630
|
+
continue
|
|
1631
|
+
if (
|
|
1632
|
+
isinstance(receipt, TerminationDecisionReceipt)
|
|
1633
|
+
and _validate_termination(
|
|
1634
|
+
receipt,
|
|
1635
|
+
work_order,
|
|
1636
|
+
state_before,
|
|
1637
|
+
receipt.occurred_at,
|
|
1638
|
+
)
|
|
1639
|
+
):
|
|
1640
|
+
continue
|
|
1641
|
+
raise _fail("receipt is denied by the frozen state machine")
|
|
1642
|
+
|
|
1643
|
+
|
|
1644
|
+
def _tool_predicates(
|
|
1645
|
+
work_order: WorkOrder,
|
|
1646
|
+
receipt: ToolCallReceipt,
|
|
1647
|
+
grant: CapabilityGrant,
|
|
1648
|
+
state: _GrantReplayState,
|
|
1649
|
+
) -> tuple[tuple, bool]:
|
|
1650
|
+
test_mode = (
|
|
1651
|
+
receipt.request_arguments.test_mode
|
|
1652
|
+
if isinstance(receipt.request_arguments, RunTestsArguments)
|
|
1653
|
+
else "developer"
|
|
1654
|
+
)
|
|
1655
|
+
selected = select_required_predicates(
|
|
1656
|
+
work_order=work_order,
|
|
1657
|
+
tool_name=receipt.tool_name,
|
|
1658
|
+
policy_decision=receipt.policy_decision,
|
|
1659
|
+
execution_status=receipt.execution_status,
|
|
1660
|
+
test_mode=test_mode,
|
|
1661
|
+
)
|
|
1662
|
+
supplied = {
|
|
1663
|
+
result.predicate_id: result
|
|
1664
|
+
for result in receipt.predicate_results
|
|
1665
|
+
}
|
|
1666
|
+
authoritative: dict[str, object] = {}
|
|
1667
|
+
child_scope_allowed = True
|
|
1668
|
+
for spec in selected:
|
|
1669
|
+
result = supplied.get(spec.predicate_id)
|
|
1670
|
+
if result is None:
|
|
1671
|
+
raise _fail("tool predicate set is incomplete")
|
|
1672
|
+
if spec.name == "tool_allowed":
|
|
1673
|
+
value: object = {"actual_tool_name": receipt.tool_name}
|
|
1674
|
+
elif spec.name == "quota_remaining":
|
|
1675
|
+
value = {
|
|
1676
|
+
"grant_id": grant.grant_id,
|
|
1677
|
+
"metric": "tool_calls",
|
|
1678
|
+
"amount": 1,
|
|
1679
|
+
"grant_remaining_before": state.remaining_tool_calls,
|
|
1680
|
+
"ledger_prefix_digest": receipt.previous_receipt_digest,
|
|
1681
|
+
}
|
|
1682
|
+
elif spec.name == "path_allowed":
|
|
1683
|
+
path_input = result.input
|
|
1684
|
+
if isinstance(receipt.request_arguments, ApplyPatchArguments):
|
|
1685
|
+
requested = receipt.request_arguments.target_paths
|
|
1686
|
+
child_roots = grant.allowed_write_roots
|
|
1687
|
+
elif isinstance(receipt.request_arguments, RepoReadArguments):
|
|
1688
|
+
requested = (receipt.request_arguments.path,)
|
|
1689
|
+
child_roots = grant.allowed_read_roots
|
|
1690
|
+
else:
|
|
1691
|
+
raise _fail("path predicate request type is unavailable")
|
|
1692
|
+
resolved = tuple(
|
|
1693
|
+
item.resolved_relative_path
|
|
1694
|
+
for item in path_input.resolved_entries
|
|
1695
|
+
)
|
|
1696
|
+
child_scope_allowed = (
|
|
1697
|
+
path_input.requested_paths == tuple(requested)
|
|
1698
|
+
and tuple(
|
|
1699
|
+
item.requested_path
|
|
1700
|
+
for item in path_input.resolved_entries
|
|
1701
|
+
)
|
|
1702
|
+
== tuple(requested)
|
|
1703
|
+
and path_input.resolution_manifest_digest is not None
|
|
1704
|
+
and all(
|
|
1705
|
+
any(_root_contains(root, path) for root in child_roots)
|
|
1706
|
+
for path in requested
|
|
1707
|
+
)
|
|
1708
|
+
and all(
|
|
1709
|
+
path is not None
|
|
1710
|
+
and any(
|
|
1711
|
+
_root_contains(root, path)
|
|
1712
|
+
for root in child_roots
|
|
1713
|
+
)
|
|
1714
|
+
for path in resolved
|
|
1715
|
+
)
|
|
1716
|
+
)
|
|
1717
|
+
value = {
|
|
1718
|
+
"requested_paths": list(requested),
|
|
1719
|
+
"resolved_entries": [
|
|
1720
|
+
item.model_dump(mode="json")
|
|
1721
|
+
for item in path_input.resolved_entries
|
|
1722
|
+
],
|
|
1723
|
+
"resolution_manifest_digest": (
|
|
1724
|
+
path_input.resolution_manifest_digest
|
|
1725
|
+
),
|
|
1726
|
+
}
|
|
1727
|
+
elif spec.name == "tests_passed":
|
|
1728
|
+
arguments = receipt.request_arguments
|
|
1729
|
+
profile = next(
|
|
1730
|
+
(
|
|
1731
|
+
item
|
|
1732
|
+
for item in work_order.test_profiles
|
|
1733
|
+
if item.test_mode == "verifier"
|
|
1734
|
+
),
|
|
1735
|
+
None,
|
|
1736
|
+
)
|
|
1737
|
+
if (
|
|
1738
|
+
not isinstance(arguments, RunTestsArguments)
|
|
1739
|
+
or profile is None
|
|
1740
|
+
):
|
|
1741
|
+
raise _fail(
|
|
1742
|
+
"Verifier predicate authority is unavailable"
|
|
1743
|
+
)
|
|
1744
|
+
value = {
|
|
1745
|
+
**result.input.model_dump(mode="json"),
|
|
1746
|
+
"command_digest": profile.command_digest,
|
|
1747
|
+
"expected_exit_code": profile.expected_exit_code,
|
|
1748
|
+
"source_commit": arguments.source_commit,
|
|
1749
|
+
"candidate_commit": arguments.candidate_commit,
|
|
1750
|
+
"workspace_manifest_digest": (
|
|
1751
|
+
arguments.workspace_manifest_digest
|
|
1752
|
+
),
|
|
1753
|
+
"container_image_digest": profile.container_image_digest,
|
|
1754
|
+
"fixed_test_source_digest": (
|
|
1755
|
+
profile.fixed_test_source_digest
|
|
1756
|
+
),
|
|
1757
|
+
}
|
|
1758
|
+
else:
|
|
1759
|
+
raise _fail("tool predicate has no offline authority rule")
|
|
1760
|
+
authoritative[spec.predicate_id] = value
|
|
1761
|
+
evaluated = evaluate_required_predicates(
|
|
1762
|
+
selected,
|
|
1763
|
+
EvaluationContext(
|
|
1764
|
+
inputs={
|
|
1765
|
+
result.predicate_id: result.input
|
|
1766
|
+
for result in receipt.predicate_results
|
|
1767
|
+
},
|
|
1768
|
+
authoritative_inputs=authoritative,
|
|
1769
|
+
authoritative_ledger_prefix_digests={
|
|
1770
|
+
grant.grant_id: receipt.previous_receipt_digest,
|
|
1771
|
+
},
|
|
1772
|
+
),
|
|
1773
|
+
)
|
|
1774
|
+
if evaluated != receipt.predicate_results:
|
|
1775
|
+
raise _fail(
|
|
1776
|
+
"tool PredicateResults failed exact historical replay"
|
|
1777
|
+
)
|
|
1778
|
+
return evaluated, child_scope_allowed
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
def _approval_allowed(
|
|
1782
|
+
receipt: ToolCallReceipt,
|
|
1783
|
+
prior: Mapping[str, ActionReceiptEnvelope],
|
|
1784
|
+
decisions: Mapping[str, str],
|
|
1785
|
+
) -> bool:
|
|
1786
|
+
if receipt.tool_name != "owp.create_pr_proposal":
|
|
1787
|
+
return True
|
|
1788
|
+
decision = prior.get(receipt.approval_receipt_id)
|
|
1789
|
+
request = (
|
|
1790
|
+
prior.get(decision.request_receipt_id)
|
|
1791
|
+
if isinstance(decision, ApprovalDecisionReceipt)
|
|
1792
|
+
else None
|
|
1793
|
+
)
|
|
1794
|
+
requested_at = receipt.nested_claim.requested_at
|
|
1795
|
+
return (
|
|
1796
|
+
isinstance(decision, ApprovalDecisionReceipt)
|
|
1797
|
+
and isinstance(request, ApprovalRequestedReceipt)
|
|
1798
|
+
and decisions.get(request.receipt_id) == decision.receipt_id
|
|
1799
|
+
and receipt.approval_receipt_digest == decision.digest
|
|
1800
|
+
and decision.request_receipt_digest == request.digest
|
|
1801
|
+
and decision.decision == "approved"
|
|
1802
|
+
and decision.policy_decision == "allow"
|
|
1803
|
+
and decision.execution_status == "succeeded"
|
|
1804
|
+
and request.request_kind == "high_risk_action"
|
|
1805
|
+
and request.requested_scope["work_order_digest"]
|
|
1806
|
+
== receipt.work_order_digest
|
|
1807
|
+
and request.requested_scope["target_patch_digest"]
|
|
1808
|
+
== receipt.request_arguments.target_patch_digest
|
|
1809
|
+
and decision.approved_scope == request.requested_scope
|
|
1810
|
+
and requested_at <= decision.expires_at
|
|
1811
|
+
and requested_at <= request.expires_at
|
|
1812
|
+
and receipt.occurred_at <= decision.expires_at
|
|
1813
|
+
and receipt.occurred_at <= request.expires_at
|
|
1814
|
+
)
|
|
1815
|
+
|
|
1816
|
+
|
|
1817
|
+
def _tool_state_allowed(
|
|
1818
|
+
work_order: WorkOrder,
|
|
1819
|
+
receipt: ToolCallReceipt,
|
|
1820
|
+
causal_state: AuthorizationCausalSnapshot,
|
|
1821
|
+
) -> bool:
|
|
1822
|
+
state_before = receipt.state_before
|
|
1823
|
+
if receipt.tool_name == "owp.compose_proof":
|
|
1824
|
+
if receipt.state_after != state_before:
|
|
1825
|
+
return False
|
|
1826
|
+
if state_before == "locally_verified":
|
|
1827
|
+
return (
|
|
1828
|
+
causal_state.active_patch_receipt_id
|
|
1829
|
+
in receipt.parent_receipt_ids
|
|
1830
|
+
)
|
|
1831
|
+
if state_before != "evidence_incomplete":
|
|
1832
|
+
return False
|
|
1833
|
+
return (
|
|
1834
|
+
causal_state.latest_composition_trigger_id
|
|
1835
|
+
in receipt.parent_receipt_ids
|
|
1836
|
+
and causal_state.independent_result_receipt_id
|
|
1837
|
+
in receipt.parent_receipt_ids
|
|
1838
|
+
)
|
|
1839
|
+
if (
|
|
1840
|
+
receipt.tool_name == "owp.run_tests"
|
|
1841
|
+
and state_before == "evidence_incomplete"
|
|
1842
|
+
):
|
|
1843
|
+
if receipt.state_after != state_before:
|
|
1844
|
+
return False
|
|
1845
|
+
role = next(
|
|
1846
|
+
(
|
|
1847
|
+
binding.role
|
|
1848
|
+
for binding in work_order.key_bindings
|
|
1849
|
+
if (
|
|
1850
|
+
binding.subject_id == receipt.actor_id
|
|
1851
|
+
and binding.key_id == receipt.actor_key_id
|
|
1852
|
+
)
|
|
1853
|
+
),
|
|
1854
|
+
None,
|
|
1855
|
+
)
|
|
1856
|
+
test_results = tuple(
|
|
1857
|
+
result
|
|
1858
|
+
for result in receipt.predicate_results
|
|
1859
|
+
if result.name == "tests_passed"
|
|
1860
|
+
)
|
|
1861
|
+
tests_passed = bool(test_results) and all(
|
|
1862
|
+
result.passed for result in test_results
|
|
1863
|
+
)
|
|
1864
|
+
result_binding_valid = (
|
|
1865
|
+
causal_state.independent_result_receipt_id
|
|
1866
|
+
== receipt.receipt_id
|
|
1867
|
+
if tests_passed
|
|
1868
|
+
else causal_state.independent_result_receipt_id is None
|
|
1869
|
+
)
|
|
1870
|
+
return (
|
|
1871
|
+
role == "Verifier"
|
|
1872
|
+
and causal_state.active_patch_receipt_id
|
|
1873
|
+
in receipt.parent_receipt_ids
|
|
1874
|
+
and causal_state.latest_composition_trigger_id
|
|
1875
|
+
in receipt.parent_receipt_ids
|
|
1876
|
+
and result_binding_valid
|
|
1877
|
+
)
|
|
1878
|
+
return state_before in {"running", "retrying"}
|
|
1879
|
+
|
|
1880
|
+
|
|
1881
|
+
def _tool_state_context_exception(
|
|
1882
|
+
work_order: WorkOrder,
|
|
1883
|
+
receipt: ToolCallReceipt,
|
|
1884
|
+
causal_state: AuthorizationCausalSnapshot,
|
|
1885
|
+
) -> bool:
|
|
1886
|
+
is_context_dependent = (
|
|
1887
|
+
receipt.tool_name == "owp.compose_proof"
|
|
1888
|
+
and receipt.state_before
|
|
1889
|
+
in {"locally_verified", "evidence_incomplete"}
|
|
1890
|
+
) or (
|
|
1891
|
+
receipt.tool_name == "owp.run_tests"
|
|
1892
|
+
and receipt.state_before == "evidence_incomplete"
|
|
1893
|
+
)
|
|
1894
|
+
return (
|
|
1895
|
+
is_context_dependent
|
|
1896
|
+
and receipt.state_after == receipt.state_before
|
|
1897
|
+
and _tool_state_allowed(work_order, receipt, causal_state)
|
|
1898
|
+
)
|
|
1899
|
+
|
|
1900
|
+
|
|
1901
|
+
def _validate_policy_history(
|
|
1902
|
+
work_order: WorkOrder,
|
|
1903
|
+
receipts: tuple[ActionReceiptEnvelope, ...],
|
|
1904
|
+
grants: Mapping[str, CapabilityGrant],
|
|
1905
|
+
replay_context: _GrantReplayContext,
|
|
1906
|
+
causal_state: AuthorizationCausalState,
|
|
1907
|
+
) -> None:
|
|
1908
|
+
prior: dict[str, ActionReceiptEnvelope] = {}
|
|
1909
|
+
|
|
1910
|
+
for receipt in receipts:
|
|
1911
|
+
receipt_causal_state = causal_state.snapshot_for(
|
|
1912
|
+
receipt.receipt_id
|
|
1913
|
+
)
|
|
1914
|
+
decisions = dict(
|
|
1915
|
+
receipt_causal_state.approval_decision_by_request
|
|
1916
|
+
)
|
|
1917
|
+
if isinstance(receipt, GrantIssuedReceipt):
|
|
1918
|
+
prior[receipt.receipt_id] = receipt
|
|
1919
|
+
continue
|
|
1920
|
+
if isinstance(receipt, GrantRevokedReceipt):
|
|
1921
|
+
grant = grants.get(receipt.authorizing_grant_id)
|
|
1922
|
+
state = replay_context.state_before(
|
|
1923
|
+
receipt.receipt_id,
|
|
1924
|
+
receipt.authorizing_grant_id,
|
|
1925
|
+
)
|
|
1926
|
+
target = replay_context.state_before(
|
|
1927
|
+
receipt.receipt_id,
|
|
1928
|
+
receipt.revoked_grant_id,
|
|
1929
|
+
)
|
|
1930
|
+
request = receipt.nested_claim
|
|
1931
|
+
expected = _denial_code(
|
|
1932
|
+
state_allowed=receipt.state_before
|
|
1933
|
+
in {"running", "retrying", "needs_rework"},
|
|
1934
|
+
role_allowed=(
|
|
1935
|
+
grant is not None
|
|
1936
|
+
and state is not None
|
|
1937
|
+
and receipt.actor_id == grant.subject_agent_id
|
|
1938
|
+
and receipt.actor_key_id == grant.subject_key_id
|
|
1939
|
+
and _agent_direct_call_is_authorized(
|
|
1940
|
+
receipt,
|
|
1941
|
+
work_order,
|
|
1942
|
+
)
|
|
1943
|
+
),
|
|
1944
|
+
capability_allowed=(
|
|
1945
|
+
grant is not None
|
|
1946
|
+
and state is not None
|
|
1947
|
+
and target is not None
|
|
1948
|
+
and state.revoked is False
|
|
1949
|
+
and request.tool_name in grant.allowed_tools
|
|
1950
|
+
and grant.valid_from
|
|
1951
|
+
<= request.requested_at
|
|
1952
|
+
<= receipt.occurred_at
|
|
1953
|
+
<= grant.expires_at
|
|
1954
|
+
),
|
|
1955
|
+
)
|
|
1956
|
+
if receipt.policy_decision == "deny":
|
|
1957
|
+
if receipt.policy_error_code != expected:
|
|
1958
|
+
raise _fail(
|
|
1959
|
+
"revocation denial failed historical policy replay"
|
|
1960
|
+
)
|
|
1961
|
+
else:
|
|
1962
|
+
if expected is not None:
|
|
1963
|
+
raise _fail(
|
|
1964
|
+
"allowed revocation failed historical policy replay"
|
|
1965
|
+
)
|
|
1966
|
+
prior[receipt.receipt_id] = receipt
|
|
1967
|
+
continue
|
|
1968
|
+
if isinstance(
|
|
1969
|
+
receipt,
|
|
1970
|
+
(GrantConsumedReceipt, ApprovalRequestedReceipt, RollbackReceipt),
|
|
1971
|
+
):
|
|
1972
|
+
grant = grants.get(receipt.grant_id)
|
|
1973
|
+
state = replay_context.state_before(
|
|
1974
|
+
receipt.receipt_id,
|
|
1975
|
+
receipt.grant_id,
|
|
1976
|
+
)
|
|
1977
|
+
request = receipt.nested_claim
|
|
1978
|
+
if (
|
|
1979
|
+
isinstance(receipt, ApprovalRequestedReceipt)
|
|
1980
|
+
and receipt.request_kind == "high_risk_action"
|
|
1981
|
+
and grant is not None
|
|
1982
|
+
):
|
|
1983
|
+
gate = work_order.approval_gates[0]
|
|
1984
|
+
maximum_expiry = min(
|
|
1985
|
+
receipt.occurred_at
|
|
1986
|
+
+ timedelta(seconds=gate.max_validity_seconds),
|
|
1987
|
+
grant.expires_at,
|
|
1988
|
+
work_order.deadline,
|
|
1989
|
+
)
|
|
1990
|
+
if receipt.expires_at > maximum_expiry:
|
|
1991
|
+
raise _fail(
|
|
1992
|
+
"approval request validity exceeds protocol cap"
|
|
1993
|
+
)
|
|
1994
|
+
if isinstance(receipt, GrantConsumedReceipt):
|
|
1995
|
+
state_allowed = receipt.state_before == "needs_rework"
|
|
1996
|
+
quota_metric = "repair_rounds"
|
|
1997
|
+
elif isinstance(receipt, ApprovalRequestedReceipt):
|
|
1998
|
+
state_allowed = (
|
|
1999
|
+
receipt.state_before in {"running", "retrying"}
|
|
2000
|
+
if receipt.request_kind == "high_risk_action"
|
|
2001
|
+
else receipt.state_before == "proof_ready"
|
|
2002
|
+
)
|
|
2003
|
+
quota_metric = "tool_calls"
|
|
2004
|
+
else:
|
|
2005
|
+
state_allowed = receipt.state_before == "needs_rework"
|
|
2006
|
+
quota_metric = "tool_calls"
|
|
2007
|
+
expected = _denial_code(
|
|
2008
|
+
state_allowed=state_allowed,
|
|
2009
|
+
role_allowed=(
|
|
2010
|
+
grant is not None
|
|
2011
|
+
and state is not None
|
|
2012
|
+
and receipt.actor_id == grant.subject_agent_id
|
|
2013
|
+
and receipt.actor_key_id == grant.subject_key_id
|
|
2014
|
+
and _agent_direct_call_is_authorized(
|
|
2015
|
+
receipt,
|
|
2016
|
+
work_order,
|
|
2017
|
+
)
|
|
2018
|
+
),
|
|
2019
|
+
capability_allowed=(
|
|
2020
|
+
grant is not None
|
|
2021
|
+
and state is not None
|
|
2022
|
+
and state.revoked is False
|
|
2023
|
+
and request.tool_name in grant.allowed_tools
|
|
2024
|
+
and grant.valid_from
|
|
2025
|
+
<= request.requested_at
|
|
2026
|
+
<= receipt.occurred_at
|
|
2027
|
+
<= grant.expires_at
|
|
2028
|
+
),
|
|
2029
|
+
quota_allowed=(
|
|
2030
|
+
state is not None
|
|
2031
|
+
and (
|
|
2032
|
+
state.remaining_repair_rounds
|
|
2033
|
+
if quota_metric == "repair_rounds"
|
|
2034
|
+
else state.remaining_tool_calls
|
|
2035
|
+
)
|
|
2036
|
+
> 0
|
|
2037
|
+
),
|
|
2038
|
+
)
|
|
2039
|
+
if receipt.policy_decision == "deny":
|
|
2040
|
+
if receipt.policy_error_code != expected:
|
|
2041
|
+
raise _fail(
|
|
2042
|
+
"non-tool denial failed historical policy replay"
|
|
2043
|
+
)
|
|
2044
|
+
else:
|
|
2045
|
+
if expected is not None:
|
|
2046
|
+
raise _fail(
|
|
2047
|
+
"allowed non-tool action failed policy replay"
|
|
2048
|
+
)
|
|
2049
|
+
prior[receipt.receipt_id] = receipt
|
|
2050
|
+
continue
|
|
2051
|
+
if isinstance(receipt, ToolCallReceipt):
|
|
2052
|
+
grant = grants.get(receipt.grant_id)
|
|
2053
|
+
state = replay_context.state_before(
|
|
2054
|
+
receipt.receipt_id,
|
|
2055
|
+
receipt.grant_id,
|
|
2056
|
+
)
|
|
2057
|
+
if grant is None or state is None:
|
|
2058
|
+
raise _fail(
|
|
2059
|
+
"tool receipt references a never-effective Grant"
|
|
2060
|
+
)
|
|
2061
|
+
evaluated, child_scope_allowed = _tool_predicates(
|
|
2062
|
+
work_order,
|
|
2063
|
+
receipt,
|
|
2064
|
+
grant,
|
|
2065
|
+
state,
|
|
2066
|
+
)
|
|
2067
|
+
request = receipt.nested_claim
|
|
2068
|
+
role_allowed = (
|
|
2069
|
+
receipt.actor_id == grant.subject_agent_id
|
|
2070
|
+
and receipt.actor_key_id == grant.subject_key_id
|
|
2071
|
+
and _agent_direct_call_is_authorized(
|
|
2072
|
+
receipt,
|
|
2073
|
+
work_order,
|
|
2074
|
+
)
|
|
2075
|
+
)
|
|
2076
|
+
capability_allowed = (
|
|
2077
|
+
state.revoked is False
|
|
2078
|
+
and receipt.tool_name in grant.allowed_tools
|
|
2079
|
+
and child_scope_allowed
|
|
2080
|
+
and grant.valid_from
|
|
2081
|
+
<= request.requested_at
|
|
2082
|
+
<= receipt.occurred_at
|
|
2083
|
+
<= grant.expires_at
|
|
2084
|
+
)
|
|
2085
|
+
expected = _denial_code(
|
|
2086
|
+
state_allowed=_tool_state_allowed(
|
|
2087
|
+
work_order,
|
|
2088
|
+
receipt,
|
|
2089
|
+
receipt_causal_state,
|
|
2090
|
+
),
|
|
2091
|
+
role_allowed=role_allowed,
|
|
2092
|
+
capability_allowed=capability_allowed,
|
|
2093
|
+
approval_allowed=_approval_allowed(
|
|
2094
|
+
receipt,
|
|
2095
|
+
prior,
|
|
2096
|
+
decisions,
|
|
2097
|
+
),
|
|
2098
|
+
predicate_allowed=all(
|
|
2099
|
+
item.passed
|
|
2100
|
+
for item in evaluated
|
|
2101
|
+
if item.name != "tests_passed"
|
|
2102
|
+
),
|
|
2103
|
+
quota_allowed=state.remaining_tool_calls > 0,
|
|
2104
|
+
)
|
|
2105
|
+
if receipt.policy_decision == "deny":
|
|
2106
|
+
if receipt.policy_error_code != expected:
|
|
2107
|
+
raise _fail(
|
|
2108
|
+
"tool denial failed historical policy replay"
|
|
2109
|
+
)
|
|
2110
|
+
else:
|
|
2111
|
+
if expected is not None:
|
|
2112
|
+
raise _fail(
|
|
2113
|
+
"allowed tool failed historical policy replay"
|
|
2114
|
+
)
|
|
2115
|
+
prior[receipt.receipt_id] = receipt
|
|
2116
|
+
continue
|
|
2117
|
+
if isinstance(receipt, ApprovalDecisionReceipt):
|
|
2118
|
+
request = prior.get(receipt.request_receipt_id)
|
|
2119
|
+
approved = receipt.decision == "approved"
|
|
2120
|
+
expected_decision_id = (
|
|
2121
|
+
decisions.get(request.receipt_id)
|
|
2122
|
+
if isinstance(request, ApprovalRequestedReceipt)
|
|
2123
|
+
else None
|
|
2124
|
+
)
|
|
2125
|
+
if (
|
|
2126
|
+
not isinstance(request, ApprovalRequestedReceipt)
|
|
2127
|
+
or expected_decision_id != receipt.receipt_id
|
|
2128
|
+
or request.digest != receipt.request_receipt_digest
|
|
2129
|
+
or request.requested_scope != receipt.approved_scope
|
|
2130
|
+
or request.expires_at != receipt.expires_at
|
|
2131
|
+
or approved
|
|
2132
|
+
!= (
|
|
2133
|
+
receipt.policy_decision == "allow"
|
|
2134
|
+
and receipt.execution_status == "succeeded"
|
|
2135
|
+
)
|
|
2136
|
+
or (
|
|
2137
|
+
not approved
|
|
2138
|
+
and (
|
|
2139
|
+
receipt.policy_decision != "deny"
|
|
2140
|
+
or receipt.execution_status != "denied"
|
|
2141
|
+
)
|
|
2142
|
+
)
|
|
2143
|
+
or receipt.decided_at < request.occurred_at
|
|
2144
|
+
or receipt.decided_at > request.expires_at
|
|
2145
|
+
or receipt.occurred_at < receipt.decided_at
|
|
2146
|
+
or (
|
|
2147
|
+
receipt.occurred_at - receipt.decided_at
|
|
2148
|
+
).total_seconds()
|
|
2149
|
+
> 300
|
|
2150
|
+
):
|
|
2151
|
+
raise _fail("Human approval failed historical replay")
|
|
2152
|
+
elif isinstance(receipt, TerminationDecisionReceipt):
|
|
2153
|
+
if (
|
|
2154
|
+
receipt.target_work_order_digest != work_order.digest
|
|
2155
|
+
or receipt.decided_at < work_order.issued_at
|
|
2156
|
+
or receipt.occurred_at < receipt.decided_at
|
|
2157
|
+
or (
|
|
2158
|
+
receipt.occurred_at - receipt.decided_at
|
|
2159
|
+
).total_seconds()
|
|
2160
|
+
> 300
|
|
2161
|
+
):
|
|
2162
|
+
raise _fail("Human termination failed historical replay")
|
|
2163
|
+
prior[receipt.receipt_id] = receipt
|
|
2164
|
+
|
|
2165
|
+
|
|
2166
|
+
def replay_authorization_policy(
|
|
2167
|
+
work_order: WorkOrder,
|
|
2168
|
+
grants: Mapping[str, CapabilityGrant],
|
|
2169
|
+
attempts: Mapping[str, CapabilityGrant],
|
|
2170
|
+
receipts: Iterable[ActionReceiptEnvelope],
|
|
2171
|
+
causal_state: AuthorizationCausalState,
|
|
2172
|
+
) -> AuthorizationReplayState:
|
|
2173
|
+
"""Replay the bounded authorization policy without external state."""
|
|
2174
|
+
|
|
2175
|
+
if (
|
|
2176
|
+
not isinstance(work_order, WorkOrder)
|
|
2177
|
+
or not isinstance(causal_state, AuthorizationCausalState)
|
|
2178
|
+
):
|
|
2179
|
+
raise _fail("authorization policy inputs are unavailable")
|
|
2180
|
+
effective = _bounded_mapping(
|
|
2181
|
+
grants,
|
|
2182
|
+
cap=MAX_EFFECTIVE_GRANTS,
|
|
2183
|
+
label="effective Grants",
|
|
2184
|
+
)
|
|
2185
|
+
denied = _bounded_mapping(
|
|
2186
|
+
attempts,
|
|
2187
|
+
cap=MAX_GRANT_ATTEMPTS,
|
|
2188
|
+
label="Grant attempts",
|
|
2189
|
+
)
|
|
2190
|
+
bounded = _bounded_receipts(receipts)
|
|
2191
|
+
_validate_grant_signatures(work_order, effective, denied)
|
|
2192
|
+
try:
|
|
2193
|
+
validate_authorization_causal_bindings(
|
|
2194
|
+
work_order,
|
|
2195
|
+
bounded,
|
|
2196
|
+
causal_state,
|
|
2197
|
+
)
|
|
2198
|
+
except AuthorizationCausalityError as error:
|
|
2199
|
+
raise _fail(str(error)) from error
|
|
2200
|
+
decision_pairs = causal_state.approval_decision_by_request
|
|
2201
|
+
replay_context = _grant_history_replay_context(
|
|
2202
|
+
work_order,
|
|
2203
|
+
bounded,
|
|
2204
|
+
effective,
|
|
2205
|
+
denied,
|
|
2206
|
+
)
|
|
2207
|
+
quota_states = replay_context.final_by_grant()
|
|
2208
|
+
_validate_policy_history(
|
|
2209
|
+
work_order,
|
|
2210
|
+
bounded,
|
|
2211
|
+
effective,
|
|
2212
|
+
replay_context,
|
|
2213
|
+
causal_state,
|
|
2214
|
+
)
|
|
2215
|
+
_validate_state_history(work_order, bounded, causal_state)
|
|
2216
|
+
balances = tuple(
|
|
2217
|
+
entry
|
|
2218
|
+
for grant_id, state in sorted(quota_states.items())
|
|
2219
|
+
for entry in (
|
|
2220
|
+
(grant_id, "repair_rounds", state.remaining_repair_rounds),
|
|
2221
|
+
(grant_id, "tool_calls", state.remaining_tool_calls),
|
|
2222
|
+
)
|
|
2223
|
+
)
|
|
2224
|
+
return AuthorizationReplayState(
|
|
2225
|
+
active_patch_receipt_id=causal_state.active_patch_receipt_id,
|
|
2226
|
+
balances=balances,
|
|
2227
|
+
revoked_grant_ids=tuple(
|
|
2228
|
+
sorted(
|
|
2229
|
+
grant_id
|
|
2230
|
+
for grant_id, state in quota_states.items()
|
|
2231
|
+
if state.revoked
|
|
2232
|
+
)
|
|
2233
|
+
),
|
|
2234
|
+
used_single_use_grant_ids=tuple(
|
|
2235
|
+
sorted(
|
|
2236
|
+
grant_id
|
|
2237
|
+
for grant_id, state in quota_states.items()
|
|
2238
|
+
if effective[grant_id].usage_mode == "single_use"
|
|
2239
|
+
and state.use_count > 0
|
|
2240
|
+
)
|
|
2241
|
+
),
|
|
2242
|
+
approval_decision_by_request=decision_pairs,
|
|
2243
|
+
)
|
|
2244
|
+
|
|
2245
|
+
|
|
2246
|
+
def _validate_committed_evidence(
|
|
2247
|
+
work_order: WorkOrder,
|
|
2248
|
+
ledger_prefix: AuthorizationLedgerPrefix,
|
|
2249
|
+
committed_evidence: tuple[CommittedEvidence, ...],
|
|
2250
|
+
) -> dict[str, CommittedEvidence]:
|
|
2251
|
+
committed_paths = tuple(
|
|
2252
|
+
item.reference.path for item in committed_evidence
|
|
2253
|
+
)
|
|
2254
|
+
if (
|
|
2255
|
+
committed_paths
|
|
2256
|
+
!= tuple(sorted(committed_paths, key=lambda value: value.encode()))
|
|
2257
|
+
or len(set(committed_paths)) != len(committed_paths)
|
|
2258
|
+
):
|
|
2259
|
+
raise _fail("committed evidence is not canonical and unique")
|
|
2260
|
+
|
|
2261
|
+
expected_by_path: dict[str, EvidenceRef] = {}
|
|
2262
|
+
for receipt in ledger_prefix.receipts:
|
|
2263
|
+
for reference in receipt.evidence_refs:
|
|
2264
|
+
if reference.path in expected_by_path:
|
|
2265
|
+
raise _fail("receipt evidence coverage is not one-to-one")
|
|
2266
|
+
expected_by_path[reference.path] = reference
|
|
2267
|
+
committed_by_path = {
|
|
2268
|
+
item.reference.path: item for item in committed_evidence
|
|
2269
|
+
}
|
|
2270
|
+
if set(committed_by_path) != set(expected_by_path):
|
|
2271
|
+
raise _fail("committed evidence coverage is not exact")
|
|
2272
|
+
|
|
2273
|
+
artifacts = {
|
|
2274
|
+
f"{work_order.evidence_policy.evidence_root}/{artifact.path}": artifact
|
|
2275
|
+
for artifact in work_order.evidence_policy.artifacts
|
|
2276
|
+
}
|
|
2277
|
+
for path, expected_reference in expected_by_path.items():
|
|
2278
|
+
item = committed_by_path[path]
|
|
2279
|
+
artifact = artifacts.get(path)
|
|
2280
|
+
if artifact is None:
|
|
2281
|
+
raise _fail("committed evidence path is outside the allowlist")
|
|
2282
|
+
if item.reference != expected_reference:
|
|
2283
|
+
raise _fail("committed evidence reference does not match receipt")
|
|
2284
|
+
if item.reference.media_type != artifact.media_type:
|
|
2285
|
+
raise _fail("committed evidence media type is invalid")
|
|
2286
|
+
if item.reference.size_bytes > artifact.max_size_bytes:
|
|
2287
|
+
raise _fail("committed evidence exceeds its artifact limit")
|
|
2288
|
+
if len(item.payload) != item.reference.size_bytes:
|
|
2289
|
+
raise _fail("committed evidence size does not match reference")
|
|
2290
|
+
if (
|
|
2291
|
+
hashlib.sha256(item.payload).hexdigest()
|
|
2292
|
+
!= item.reference.sha256
|
|
2293
|
+
):
|
|
2294
|
+
raise _fail("committed evidence digest does not match bytes")
|
|
2295
|
+
if item.reference.media_type == "application/json":
|
|
2296
|
+
try:
|
|
2297
|
+
decoded = json.loads(item.payload)
|
|
2298
|
+
canonical = rfc8785.dumps(decoded)
|
|
2299
|
+
except (TypeError, ValueError, UnicodeDecodeError) as error:
|
|
2300
|
+
raise _fail("committed JSON evidence is invalid") from error
|
|
2301
|
+
if canonical != item.payload:
|
|
2302
|
+
raise _fail("committed JSON evidence is not RFC 8785 canonical")
|
|
2303
|
+
return committed_by_path
|
|
2304
|
+
|
|
2305
|
+
|
|
2306
|
+
def _validate_replay_checkpoint(
|
|
2307
|
+
work_order: WorkOrder,
|
|
2308
|
+
ledger_prefix: AuthorizationLedgerPrefix,
|
|
2309
|
+
committed_by_path: Mapping[str, CommittedEvidence],
|
|
2310
|
+
replay_checkpoint: ReplayCheckpoint,
|
|
2311
|
+
active_patch_receipt_id: str | None,
|
|
2312
|
+
) -> None:
|
|
2313
|
+
try:
|
|
2314
|
+
actual_manifest_digest = workspace_manifest_digest(
|
|
2315
|
+
replay_checkpoint.workspace_manifest
|
|
2316
|
+
)
|
|
2317
|
+
except Exception as error:
|
|
2318
|
+
raise _fail("replay checkpoint manifest is invalid") from error
|
|
2319
|
+
if (
|
|
2320
|
+
replay_checkpoint.workspace_manifest.head_commit
|
|
2321
|
+
!= replay_checkpoint.head_commit
|
|
2322
|
+
or replay_checkpoint.workspace_manifest_digest
|
|
2323
|
+
!= actual_manifest_digest
|
|
2324
|
+
):
|
|
2325
|
+
raise _fail("replay checkpoint manifest binding is invalid")
|
|
2326
|
+
if active_patch_receipt_id is None:
|
|
2327
|
+
if replay_checkpoint.head_commit != work_order.source_commit:
|
|
2328
|
+
raise _fail("source replay checkpoint head is invalid")
|
|
2329
|
+
return
|
|
2330
|
+
|
|
2331
|
+
active_patch = next(
|
|
2332
|
+
(
|
|
2333
|
+
receipt
|
|
2334
|
+
for receipt in ledger_prefix.receipts
|
|
2335
|
+
if receipt.receipt_id == active_patch_receipt_id
|
|
2336
|
+
),
|
|
2337
|
+
None,
|
|
2338
|
+
)
|
|
2339
|
+
if (
|
|
2340
|
+
not isinstance(active_patch, ToolCallReceipt)
|
|
2341
|
+
or active_patch.tool_name != "owp.apply_patch"
|
|
2342
|
+
or active_patch.policy_decision != "allow"
|
|
2343
|
+
or active_patch.execution_status != "succeeded"
|
|
2344
|
+
or not isinstance(active_patch.request_arguments, ApplyPatchArguments)
|
|
2345
|
+
):
|
|
2346
|
+
raise _fail("active patch checkpoint origin is invalid")
|
|
2347
|
+
artifacts = {
|
|
2348
|
+
f"{work_order.evidence_policy.evidence_root}/{artifact.path}": artifact
|
|
2349
|
+
for artifact in work_order.evidence_policy.artifacts
|
|
2350
|
+
}
|
|
2351
|
+
result_refs = tuple(
|
|
2352
|
+
reference
|
|
2353
|
+
for reference in active_patch.evidence_refs
|
|
2354
|
+
if artifacts.get(reference.path) is not None
|
|
2355
|
+
and artifacts[reference.path].purpose == "patch_result"
|
|
2356
|
+
)
|
|
2357
|
+
if len(result_refs) != 1:
|
|
2358
|
+
raise _fail("active patch evidence has no unique result")
|
|
2359
|
+
result_ref = result_refs[0]
|
|
2360
|
+
committed_result = committed_by_path.get(result_ref.path)
|
|
2361
|
+
if committed_result is None:
|
|
2362
|
+
raise _fail("active patch result evidence is unavailable")
|
|
2363
|
+
try:
|
|
2364
|
+
patch_result = PatchResultEvidence.model_validate_json(
|
|
2365
|
+
committed_result.payload
|
|
2366
|
+
)
|
|
2367
|
+
except (TypeError, ValueError) as error:
|
|
2368
|
+
raise _fail("active patch result evidence is invalid") from error
|
|
2369
|
+
if (
|
|
2370
|
+
active_patch.output_digest != result_ref.sha256
|
|
2371
|
+
or patch_result.patch_digest
|
|
2372
|
+
!= active_patch.request_arguments.patch_digest
|
|
2373
|
+
or patch_result.patch_size_bytes
|
|
2374
|
+
!= active_patch.request_arguments.patch_size_bytes
|
|
2375
|
+
or patch_result.replay_profile_digest
|
|
2376
|
+
!= work_order.replay_profile_digest
|
|
2377
|
+
or patch_result.candidate_commit != replay_checkpoint.head_commit
|
|
2378
|
+
or patch_result.workspace_manifest_digest
|
|
2379
|
+
!= replay_checkpoint.workspace_manifest_digest
|
|
2380
|
+
):
|
|
2381
|
+
raise _fail("active patch checkpoint binding is invalid")
|
|
2382
|
+
|
|
2383
|
+
|
|
2384
|
+
def derive_authorization_context(
|
|
2385
|
+
work_order: WorkOrder,
|
|
2386
|
+
ledger_prefix: AuthorizationLedgerPrefix,
|
|
2387
|
+
committed_evidence: tuple[CommittedEvidence, ...],
|
|
2388
|
+
replay_checkpoint: ReplayCheckpoint,
|
|
2389
|
+
transaction_time: datetime,
|
|
2390
|
+
) -> AuthorizationContext:
|
|
2391
|
+
"""Derive live policy facts from one closed, signed ledger snapshot."""
|
|
2392
|
+
|
|
2393
|
+
if (
|
|
2394
|
+
not isinstance(work_order, WorkOrder)
|
|
2395
|
+
or not isinstance(ledger_prefix, AuthorizationLedgerPrefix)
|
|
2396
|
+
or type(committed_evidence) is not tuple
|
|
2397
|
+
or any(
|
|
2398
|
+
not isinstance(item, CommittedEvidence)
|
|
2399
|
+
for item in committed_evidence
|
|
2400
|
+
)
|
|
2401
|
+
or not isinstance(replay_checkpoint, ReplayCheckpoint)
|
|
2402
|
+
):
|
|
2403
|
+
raise _fail("live authorization context inputs are unavailable")
|
|
2404
|
+
if (
|
|
2405
|
+
not isinstance(transaction_time, datetime)
|
|
2406
|
+
or transaction_time.tzinfo is None
|
|
2407
|
+
or transaction_time.utcoffset() != timedelta(0)
|
|
2408
|
+
or transaction_time.microsecond != 0
|
|
2409
|
+
):
|
|
2410
|
+
raise _fail("transaction time must be a trusted UTC second")
|
|
2411
|
+
canonical_time = transaction_time.astimezone(timezone.utc)
|
|
2412
|
+
earliest_time = (
|
|
2413
|
+
ledger_prefix.receipts[-1].occurred_at
|
|
2414
|
+
if ledger_prefix.receipts
|
|
2415
|
+
else work_order.issued_at
|
|
2416
|
+
)
|
|
2417
|
+
if canonical_time < earliest_time:
|
|
2418
|
+
raise _fail("transaction time precedes the signed ledger prefix")
|
|
2419
|
+
|
|
2420
|
+
effective_grants = {
|
|
2421
|
+
grant.grant_id: grant
|
|
2422
|
+
for grant in ledger_prefix.effective_grants
|
|
2423
|
+
}
|
|
2424
|
+
grant_attempts = {
|
|
2425
|
+
grant.digest: grant
|
|
2426
|
+
for grant in ledger_prefix.grant_attempts
|
|
2427
|
+
}
|
|
2428
|
+
try:
|
|
2429
|
+
causal_state = replay_authorization_causality(
|
|
2430
|
+
work_order,
|
|
2431
|
+
ledger_prefix.receipts,
|
|
2432
|
+
)
|
|
2433
|
+
except AuthorizationCausalityError as error:
|
|
2434
|
+
raise _fail(str(error)) from error
|
|
2435
|
+
replay_state = replay_authorization_policy(
|
|
2436
|
+
work_order,
|
|
2437
|
+
effective_grants,
|
|
2438
|
+
grant_attempts,
|
|
2439
|
+
ledger_prefix.receipts,
|
|
2440
|
+
causal_state,
|
|
2441
|
+
)
|
|
2442
|
+
committed_by_path = _validate_committed_evidence(
|
|
2443
|
+
work_order,
|
|
2444
|
+
ledger_prefix,
|
|
2445
|
+
committed_evidence,
|
|
2446
|
+
)
|
|
2447
|
+
_validate_replay_checkpoint(
|
|
2448
|
+
work_order,
|
|
2449
|
+
ledger_prefix,
|
|
2450
|
+
committed_by_path,
|
|
2451
|
+
replay_checkpoint,
|
|
2452
|
+
causal_state.active_patch_receipt_id,
|
|
2453
|
+
)
|
|
2454
|
+
current_state = (
|
|
2455
|
+
ledger_prefix.receipts[-1].state_after
|
|
2456
|
+
if ledger_prefix.receipts
|
|
2457
|
+
else "issued"
|
|
2458
|
+
)
|
|
2459
|
+
return AuthorizationContext(
|
|
2460
|
+
work_order=work_order,
|
|
2461
|
+
ledger_prefix=ledger_prefix,
|
|
2462
|
+
committed_evidence=committed_evidence,
|
|
2463
|
+
replay_checkpoint=replay_checkpoint,
|
|
2464
|
+
transaction_time=canonical_time,
|
|
2465
|
+
causal_state=causal_state,
|
|
2466
|
+
replay_state=replay_state,
|
|
2467
|
+
current_state=current_state,
|
|
2468
|
+
routine_capacity_remaining=max(
|
|
2469
|
+
0,
|
|
2470
|
+
61 - len(ledger_prefix.receipts),
|
|
2471
|
+
),
|
|
2472
|
+
denial_count=sum(
|
|
2473
|
+
receipt.policy_decision == "deny"
|
|
2474
|
+
for receipt in ledger_prefix.receipts
|
|
2475
|
+
),
|
|
2476
|
+
independent_failure_terminal=(
|
|
2477
|
+
causal_state.independent_failure_terminal
|
|
2478
|
+
),
|
|
2479
|
+
)
|
|
2480
|
+
|
|
2481
|
+
|
|
2482
|
+
__all__ = [
|
|
2483
|
+
"AuthorizationContext",
|
|
2484
|
+
"AuthorizationLedgerPrefix",
|
|
2485
|
+
"AuthorizationPolicyError",
|
|
2486
|
+
"AuthorizationReplayState",
|
|
2487
|
+
"CommittedEvidence",
|
|
2488
|
+
"ProspectiveExecutionFacts",
|
|
2489
|
+
"authorize_tool_call",
|
|
2490
|
+
"derive_authorization_context",
|
|
2491
|
+
"replay_authorization_policy",
|
|
2492
|
+
"validate_human_decision",
|
|
2493
|
+
"validate_rollback",
|
|
2494
|
+
]
|