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/state.py
ADDED
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
"""Evidence-gated task state transitions for protocol v0.1."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
|
|
9
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
10
|
+
from pydantic import ValidationError
|
|
11
|
+
|
|
12
|
+
from openworkproof.models import (
|
|
13
|
+
ACTION_RECEIPT_ADAPTER,
|
|
14
|
+
AcceptanceReceipt,
|
|
15
|
+
ActionReceipt,
|
|
16
|
+
ActionReceiptEnvelope,
|
|
17
|
+
ApprovalDecisionReceipt,
|
|
18
|
+
ApprovalRequestedReceipt,
|
|
19
|
+
GrantConsumedReceipt,
|
|
20
|
+
GrantIssuedReceipt,
|
|
21
|
+
GrantRevokedReceipt,
|
|
22
|
+
RollbackReceipt,
|
|
23
|
+
RunTestsArguments,
|
|
24
|
+
SystemEventReceipt,
|
|
25
|
+
TerminationDecisionReceipt,
|
|
26
|
+
TestsPassedPredicateInput,
|
|
27
|
+
ToolCallReceipt,
|
|
28
|
+
TransitionDecision,
|
|
29
|
+
WorkOrder,
|
|
30
|
+
)
|
|
31
|
+
from openworkproof.signing import (
|
|
32
|
+
verify_nested_claim,
|
|
33
|
+
verify_payload,
|
|
34
|
+
verify_work_order_identity_bindings,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TaskState(StrEnum):
|
|
39
|
+
ISSUED = "issued"
|
|
40
|
+
RUNNING = "running"
|
|
41
|
+
NEEDS_REWORK = "needs_rework"
|
|
42
|
+
RETRYING = "retrying"
|
|
43
|
+
LOCALLY_VERIFIED = "locally_verified"
|
|
44
|
+
EVIDENCE_INCOMPLETE = "evidence_incomplete"
|
|
45
|
+
PROOF_READY = "proof_ready"
|
|
46
|
+
AWAITING_HUMAN = "awaiting_human"
|
|
47
|
+
ACCEPTED = "accepted"
|
|
48
|
+
REJECTED = "rejected"
|
|
49
|
+
FROZEN = "frozen"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
ALLOWED_TRANSITIONS: dict[TaskState, frozenset[TaskState]] = {
|
|
53
|
+
TaskState.ISSUED: frozenset(
|
|
54
|
+
{TaskState.RUNNING, TaskState.FROZEN, TaskState.REJECTED}
|
|
55
|
+
),
|
|
56
|
+
TaskState.RUNNING: frozenset(
|
|
57
|
+
{
|
|
58
|
+
TaskState.NEEDS_REWORK,
|
|
59
|
+
TaskState.LOCALLY_VERIFIED,
|
|
60
|
+
TaskState.FROZEN,
|
|
61
|
+
TaskState.REJECTED,
|
|
62
|
+
}
|
|
63
|
+
),
|
|
64
|
+
TaskState.NEEDS_REWORK: frozenset(
|
|
65
|
+
{TaskState.RETRYING, TaskState.FROZEN, TaskState.REJECTED}
|
|
66
|
+
),
|
|
67
|
+
TaskState.RETRYING: frozenset(
|
|
68
|
+
{
|
|
69
|
+
TaskState.NEEDS_REWORK,
|
|
70
|
+
TaskState.LOCALLY_VERIFIED,
|
|
71
|
+
TaskState.FROZEN,
|
|
72
|
+
TaskState.REJECTED,
|
|
73
|
+
}
|
|
74
|
+
),
|
|
75
|
+
TaskState.LOCALLY_VERIFIED: frozenset(
|
|
76
|
+
{
|
|
77
|
+
TaskState.EVIDENCE_INCOMPLETE,
|
|
78
|
+
TaskState.PROOF_READY,
|
|
79
|
+
TaskState.FROZEN,
|
|
80
|
+
TaskState.REJECTED,
|
|
81
|
+
}
|
|
82
|
+
),
|
|
83
|
+
TaskState.EVIDENCE_INCOMPLETE: frozenset(
|
|
84
|
+
{TaskState.PROOF_READY, TaskState.FROZEN, TaskState.REJECTED}
|
|
85
|
+
),
|
|
86
|
+
TaskState.PROOF_READY: frozenset(
|
|
87
|
+
{TaskState.AWAITING_HUMAN, TaskState.FROZEN, TaskState.REJECTED}
|
|
88
|
+
),
|
|
89
|
+
TaskState.AWAITING_HUMAN: frozenset(
|
|
90
|
+
{TaskState.ACCEPTED, TaskState.FROZEN, TaskState.REJECTED}
|
|
91
|
+
),
|
|
92
|
+
TaskState.ACCEPTED: frozenset(),
|
|
93
|
+
TaskState.REJECTED: frozenset(),
|
|
94
|
+
TaskState.FROZEN: frozenset({TaskState.REJECTED}),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_EXPIRABLE_STATES = frozenset(
|
|
98
|
+
{
|
|
99
|
+
TaskState.ISSUED,
|
|
100
|
+
TaskState.RUNNING,
|
|
101
|
+
TaskState.NEEDS_REWORK,
|
|
102
|
+
TaskState.RETRYING,
|
|
103
|
+
TaskState.LOCALLY_VERIFIED,
|
|
104
|
+
TaskState.EVIDENCE_INCOMPLETE,
|
|
105
|
+
TaskState.PROOF_READY,
|
|
106
|
+
TaskState.AWAITING_HUMAN,
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _allowed(reason: str) -> TransitionDecision:
|
|
112
|
+
return TransitionDecision(allowed=True, error_code=None, reason=reason)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _denied(code: str, reason: str) -> TransitionDecision:
|
|
116
|
+
return TransitionDecision(allowed=False, error_code=code, reason=reason)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _public_key(
|
|
120
|
+
public_keys: Mapping[str, Ed25519PublicKey],
|
|
121
|
+
key_id: str,
|
|
122
|
+
) -> Ed25519PublicKey | None:
|
|
123
|
+
value = public_keys.get(key_id)
|
|
124
|
+
return value if isinstance(value, Ed25519PublicKey) else None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _work_order_is_trusted(
|
|
128
|
+
work_order: WorkOrder,
|
|
129
|
+
public_keys: Mapping[str, Ed25519PublicKey],
|
|
130
|
+
) -> bool:
|
|
131
|
+
if not verify_work_order_identity_bindings(work_order):
|
|
132
|
+
return False
|
|
133
|
+
maintainer = work_order.key_bindings[0]
|
|
134
|
+
public_key = _public_key(public_keys, maintainer.key_id)
|
|
135
|
+
return public_key is not None and verify_payload(
|
|
136
|
+
"work-order",
|
|
137
|
+
work_order.model_dump(mode="json"),
|
|
138
|
+
public_key,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _validate_action_receipt(
|
|
143
|
+
receipt: ActionReceipt,
|
|
144
|
+
work_order: WorkOrder,
|
|
145
|
+
public_keys: Mapping[str, Ed25519PublicKey],
|
|
146
|
+
now: datetime,
|
|
147
|
+
) -> ActionReceipt | None:
|
|
148
|
+
try:
|
|
149
|
+
parsed = ACTION_RECEIPT_ADAPTER.validate_python(
|
|
150
|
+
receipt.model_dump(mode="json")
|
|
151
|
+
)
|
|
152
|
+
parsed.validate_against_work_order(work_order)
|
|
153
|
+
if isinstance(parsed, ToolCallReceipt):
|
|
154
|
+
parsed.validate_predicates_against(work_order)
|
|
155
|
+
|
|
156
|
+
gateway_key = _public_key(public_keys, parsed.gateway_signer_key_id)
|
|
157
|
+
if gateway_key is None or not verify_payload(
|
|
158
|
+
"action-receipt",
|
|
159
|
+
parsed.model_dump(mode="json"),
|
|
160
|
+
gateway_key,
|
|
161
|
+
):
|
|
162
|
+
return None
|
|
163
|
+
if parsed.actor_type in {"agent", "human"}:
|
|
164
|
+
actor_key = _public_key(public_keys, parsed.actor_key_id)
|
|
165
|
+
domain = (
|
|
166
|
+
"agent-request"
|
|
167
|
+
if parsed.actor_type == "agent"
|
|
168
|
+
else "human-decision"
|
|
169
|
+
)
|
|
170
|
+
if (
|
|
171
|
+
actor_key is None
|
|
172
|
+
or not verify_nested_claim(parsed.nested_claim, work_order)
|
|
173
|
+
or not verify_payload(
|
|
174
|
+
domain,
|
|
175
|
+
parsed.nested_claim.model_dump(mode="json"),
|
|
176
|
+
actor_key,
|
|
177
|
+
)
|
|
178
|
+
):
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
if now.tzinfo is None or now.utcoffset() is None:
|
|
182
|
+
return None
|
|
183
|
+
normalized_now = now.astimezone(timezone.utc)
|
|
184
|
+
if parsed.occurred_at > normalized_now:
|
|
185
|
+
return None
|
|
186
|
+
if parsed.sequence == 1 and (
|
|
187
|
+
parsed.previous_receipt_digest is not None
|
|
188
|
+
or parsed.parent_receipt_ids
|
|
189
|
+
):
|
|
190
|
+
return None
|
|
191
|
+
if parsed.sequence > 1 and parsed.previous_receipt_digest is None:
|
|
192
|
+
return None
|
|
193
|
+
return parsed
|
|
194
|
+
except (AttributeError, TypeError, ValidationError, ValueError):
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _actor_role(receipt: ActionReceiptEnvelope, work_order: WorkOrder) -> str | None:
|
|
199
|
+
for binding in work_order.key_bindings:
|
|
200
|
+
if (
|
|
201
|
+
binding.subject_id == receipt.actor_id
|
|
202
|
+
and binding.key_id == receipt.actor_key_id
|
|
203
|
+
):
|
|
204
|
+
return binding.role
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _agent_direct_call_is_authorized(
|
|
209
|
+
receipt: ActionReceipt,
|
|
210
|
+
work_order: WorkOrder,
|
|
211
|
+
) -> bool:
|
|
212
|
+
role = _actor_role(receipt, work_order)
|
|
213
|
+
tool_name = receipt.nested_claim.tool_name
|
|
214
|
+
if role == "Manager":
|
|
215
|
+
if isinstance(receipt, GrantIssuedReceipt):
|
|
216
|
+
expected = (
|
|
217
|
+
"owp.activate_root_grant"
|
|
218
|
+
if receipt.parent_grant_id is None
|
|
219
|
+
else "owp.delegate_grant"
|
|
220
|
+
)
|
|
221
|
+
return tool_name == expected
|
|
222
|
+
if isinstance(receipt, GrantRevokedReceipt):
|
|
223
|
+
return tool_name == "owp.revoke_grant"
|
|
224
|
+
if isinstance(receipt, GrantConsumedReceipt):
|
|
225
|
+
return tool_name == "owp.start_retry"
|
|
226
|
+
if isinstance(receipt, ApprovalRequestedReceipt):
|
|
227
|
+
expected = (
|
|
228
|
+
"owp.request_acceptance"
|
|
229
|
+
if receipt.request_kind == "final_acceptance"
|
|
230
|
+
else "owp.request_pr_proposal"
|
|
231
|
+
)
|
|
232
|
+
return tool_name == expected
|
|
233
|
+
return isinstance(receipt, ToolCallReceipt) and tool_name in {
|
|
234
|
+
"owp.create_pr_proposal",
|
|
235
|
+
"owp.compose_proof",
|
|
236
|
+
}
|
|
237
|
+
if role == "Developer":
|
|
238
|
+
if isinstance(receipt, RollbackReceipt):
|
|
239
|
+
return tool_name == "owp.rollback_patch"
|
|
240
|
+
if not isinstance(receipt, ToolCallReceipt):
|
|
241
|
+
return False
|
|
242
|
+
if tool_name in {"owp.repo_read", "owp.apply_patch"}:
|
|
243
|
+
return True
|
|
244
|
+
return (
|
|
245
|
+
tool_name == "owp.run_tests"
|
|
246
|
+
and isinstance(receipt.request_arguments, RunTestsArguments)
|
|
247
|
+
and receipt.request_arguments.test_mode == "developer"
|
|
248
|
+
)
|
|
249
|
+
if role == "Verifier":
|
|
250
|
+
return (
|
|
251
|
+
isinstance(receipt, ToolCallReceipt)
|
|
252
|
+
and tool_name == "owp.run_tests"
|
|
253
|
+
and isinstance(receipt.request_arguments, RunTestsArguments)
|
|
254
|
+
and receipt.request_arguments.test_mode == "verifier"
|
|
255
|
+
)
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _validate_acceptance(
|
|
260
|
+
acceptance: AcceptanceReceipt,
|
|
261
|
+
work_order: WorkOrder,
|
|
262
|
+
public_keys: Mapping[str, Ed25519PublicKey],
|
|
263
|
+
now: datetime,
|
|
264
|
+
) -> bool:
|
|
265
|
+
try:
|
|
266
|
+
parsed = AcceptanceReceipt.model_validate(
|
|
267
|
+
acceptance.model_dump(mode="json")
|
|
268
|
+
)
|
|
269
|
+
parsed.validate_against_work_order(work_order)
|
|
270
|
+
acceptor = work_order.key_bindings[5]
|
|
271
|
+
public_key = _public_key(public_keys, acceptor.key_id)
|
|
272
|
+
return (
|
|
273
|
+
public_key is not None
|
|
274
|
+
and verify_payload(
|
|
275
|
+
"acceptance-receipt",
|
|
276
|
+
parsed.model_dump(mode="json"),
|
|
277
|
+
public_key,
|
|
278
|
+
)
|
|
279
|
+
and parsed.accepted_at <= now.astimezone(timezone.utc)
|
|
280
|
+
and parsed.accepted_at <= work_order.deadline
|
|
281
|
+
)
|
|
282
|
+
except (AttributeError, TypeError, ValidationError, ValueError):
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _validate_verifier_transition(
|
|
287
|
+
receipt: ToolCallReceipt,
|
|
288
|
+
work_order: WorkOrder,
|
|
289
|
+
state_after: TaskState,
|
|
290
|
+
) -> bool:
|
|
291
|
+
if (
|
|
292
|
+
not _agent_direct_call_is_authorized(receipt, work_order)
|
|
293
|
+
or receipt.policy_decision != "allow"
|
|
294
|
+
or receipt.execution_status != "succeeded"
|
|
295
|
+
):
|
|
296
|
+
return False
|
|
297
|
+
arguments = receipt.request_arguments
|
|
298
|
+
profile = next(
|
|
299
|
+
(
|
|
300
|
+
profile
|
|
301
|
+
for profile in work_order.test_profiles
|
|
302
|
+
if profile.test_mode == "verifier"
|
|
303
|
+
),
|
|
304
|
+
None,
|
|
305
|
+
)
|
|
306
|
+
if profile is None or (
|
|
307
|
+
getattr(arguments, "test_mode", None),
|
|
308
|
+
getattr(arguments, "command_digest", None),
|
|
309
|
+
getattr(arguments, "source_commit", None),
|
|
310
|
+
getattr(arguments, "container_image_digest", None),
|
|
311
|
+
getattr(arguments, "fixed_test_source_digest", None),
|
|
312
|
+
) != (
|
|
313
|
+
"verifier",
|
|
314
|
+
profile.command_digest,
|
|
315
|
+
work_order.source_commit,
|
|
316
|
+
profile.container_image_digest,
|
|
317
|
+
profile.fixed_test_source_digest,
|
|
318
|
+
):
|
|
319
|
+
return False
|
|
320
|
+
|
|
321
|
+
post_ids = {spec.predicate_id for spec in work_order.postconditions}
|
|
322
|
+
post_results = [
|
|
323
|
+
result
|
|
324
|
+
for result in receipt.predicate_results
|
|
325
|
+
if result.predicate_id in post_ids
|
|
326
|
+
]
|
|
327
|
+
if len(post_results) != len(work_order.postconditions):
|
|
328
|
+
return False
|
|
329
|
+
tests_results = [
|
|
330
|
+
result for result in post_results if result.name == "tests_passed"
|
|
331
|
+
]
|
|
332
|
+
if len(tests_results) != 1 or not isinstance(
|
|
333
|
+
tests_results[0].input,
|
|
334
|
+
TestsPassedPredicateInput,
|
|
335
|
+
):
|
|
336
|
+
return False
|
|
337
|
+
test_result = tests_results[0]
|
|
338
|
+
if state_after is TaskState.LOCALLY_VERIFIED:
|
|
339
|
+
return all(result.passed for result in post_results) and (
|
|
340
|
+
test_result.input.actual_exit_code
|
|
341
|
+
== test_result.input.expected_exit_code
|
|
342
|
+
)
|
|
343
|
+
if state_after is TaskState.NEEDS_REWORK:
|
|
344
|
+
return any(
|
|
345
|
+
not result.passed and result.error_code == "PREDICATE_FALSE"
|
|
346
|
+
for result in post_results
|
|
347
|
+
)
|
|
348
|
+
return False
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _validate_final_request(
|
|
352
|
+
receipt: ApprovalRequestedReceipt,
|
|
353
|
+
work_order: WorkOrder,
|
|
354
|
+
now: datetime,
|
|
355
|
+
) -> bool:
|
|
356
|
+
validity_seconds = (receipt.expires_at - receipt.occurred_at).total_seconds()
|
|
357
|
+
return (
|
|
358
|
+
_agent_direct_call_is_authorized(receipt, work_order)
|
|
359
|
+
and receipt.request_kind == "final_acceptance"
|
|
360
|
+
and receipt.nested_claim.tool_name == "owp.request_acceptance"
|
|
361
|
+
and receipt.policy_decision == "allow"
|
|
362
|
+
and receipt.execution_status == "succeeded"
|
|
363
|
+
and 0 <= validity_seconds <= 3600
|
|
364
|
+
and receipt.occurred_at <= now.astimezone(timezone.utc)
|
|
365
|
+
and now.astimezone(timezone.utc) <= receipt.expires_at
|
|
366
|
+
and receipt.expires_at <= work_order.deadline
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _validate_termination(
|
|
371
|
+
receipt: TerminationDecisionReceipt,
|
|
372
|
+
work_order: WorkOrder,
|
|
373
|
+
state_before: TaskState,
|
|
374
|
+
now: datetime,
|
|
375
|
+
) -> bool:
|
|
376
|
+
if now.tzinfo is None or now.utcoffset() is None:
|
|
377
|
+
return False
|
|
378
|
+
skew = abs((receipt.occurred_at - receipt.decided_at).total_seconds())
|
|
379
|
+
deadline_valid = state_before is TaskState.FROZEN or (
|
|
380
|
+
state_before in _EXPIRABLE_STATES
|
|
381
|
+
and receipt.decided_at <= work_order.deadline
|
|
382
|
+
and receipt.occurred_at <= work_order.deadline
|
|
383
|
+
)
|
|
384
|
+
return (
|
|
385
|
+
_actor_role(receipt, work_order) == "Maintainer"
|
|
386
|
+
and receipt.policy_decision == "allow"
|
|
387
|
+
and receipt.execution_status == "succeeded"
|
|
388
|
+
and receipt.target_work_order_digest == work_order.digest
|
|
389
|
+
and receipt.decision == "rejected"
|
|
390
|
+
and receipt.termination_reason == "MAINTAINER_REJECTED"
|
|
391
|
+
and receipt.decided_at >= work_order.issued_at
|
|
392
|
+
and receipt.occurred_at <= now.astimezone(timezone.utc)
|
|
393
|
+
and skew <= 300
|
|
394
|
+
and deadline_valid
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _validate_contract_expiry(
|
|
399
|
+
receipt: SystemEventReceipt,
|
|
400
|
+
work_order: WorkOrder,
|
|
401
|
+
now: datetime,
|
|
402
|
+
) -> bool:
|
|
403
|
+
cause = receipt.cause
|
|
404
|
+
return (
|
|
405
|
+
receipt.system_event_name == "contract_expired"
|
|
406
|
+
and receipt.error_code == "CONTRACT_EXPIRED"
|
|
407
|
+
and getattr(cause, "deadline", None) == work_order.deadline
|
|
408
|
+
and getattr(cause, "observed_at", None) == receipt.occurred_at
|
|
409
|
+
and getattr(cause, "tip_receipt_digest", None)
|
|
410
|
+
== receipt.previous_receipt_digest
|
|
411
|
+
and receipt.occurred_at > work_order.deadline
|
|
412
|
+
and now.astimezone(timezone.utc) > work_order.deadline
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def validate_transition_evidence(
|
|
417
|
+
work_order: WorkOrder,
|
|
418
|
+
state_before: TaskState,
|
|
419
|
+
state_after: TaskState,
|
|
420
|
+
trigger_receipt: ActionReceipt | None,
|
|
421
|
+
acceptance_receipt: AcceptanceReceipt | None,
|
|
422
|
+
public_keys: Mapping[str, Ed25519PublicKey],
|
|
423
|
+
now: datetime,
|
|
424
|
+
) -> TransitionDecision:
|
|
425
|
+
if (
|
|
426
|
+
not isinstance(work_order, WorkOrder)
|
|
427
|
+
or not isinstance(state_before, TaskState)
|
|
428
|
+
or not isinstance(state_after, TaskState)
|
|
429
|
+
or state_after not in ALLOWED_TRANSITIONS[state_before]
|
|
430
|
+
):
|
|
431
|
+
return _denied("INVALID_TRANSITION", "transition is not adjacent")
|
|
432
|
+
if (
|
|
433
|
+
not isinstance(now, datetime)
|
|
434
|
+
or now.tzinfo is None
|
|
435
|
+
or now.utcoffset() is None
|
|
436
|
+
):
|
|
437
|
+
return _denied("INVALID_EVIDENCE", "now must be timezone-aware")
|
|
438
|
+
if not _work_order_is_trusted(work_order, public_keys):
|
|
439
|
+
return _denied("INVALID_EVIDENCE", "WorkOrder trust failed")
|
|
440
|
+
|
|
441
|
+
if state_after is TaskState.ACCEPTED:
|
|
442
|
+
if (
|
|
443
|
+
trigger_receipt is not None
|
|
444
|
+
or not isinstance(acceptance_receipt, AcceptanceReceipt)
|
|
445
|
+
or not _validate_acceptance(
|
|
446
|
+
acceptance_receipt,
|
|
447
|
+
work_order,
|
|
448
|
+
public_keys,
|
|
449
|
+
now,
|
|
450
|
+
)
|
|
451
|
+
):
|
|
452
|
+
return _denied("INVALID_EVIDENCE", "acceptance evidence is invalid")
|
|
453
|
+
return _allowed("Acceptor accepted the WorkOrder")
|
|
454
|
+
|
|
455
|
+
if acceptance_receipt is not None or trigger_receipt is None:
|
|
456
|
+
return _denied("INVALID_EVIDENCE", "action transition evidence is missing")
|
|
457
|
+
receipt = _validate_action_receipt(
|
|
458
|
+
trigger_receipt,
|
|
459
|
+
work_order,
|
|
460
|
+
public_keys,
|
|
461
|
+
now,
|
|
462
|
+
)
|
|
463
|
+
if receipt is None or (
|
|
464
|
+
receipt.state_before != state_before.value
|
|
465
|
+
or receipt.state_after != state_after.value
|
|
466
|
+
):
|
|
467
|
+
return _denied("INVALID_EVIDENCE", "receipt binding is invalid")
|
|
468
|
+
|
|
469
|
+
if isinstance(receipt, SystemEventReceipt) and receipt.system_event_name in {
|
|
470
|
+
"proof_composed",
|
|
471
|
+
"security_violation",
|
|
472
|
+
}:
|
|
473
|
+
# Receipt-level validation is authoritative here; the full report
|
|
474
|
+
# recomputation is enforced by the compose/commit transactions.
|
|
475
|
+
return _allowed("composition event authorized the transition")
|
|
476
|
+
|
|
477
|
+
if (
|
|
478
|
+
isinstance(receipt, TerminationDecisionReceipt)
|
|
479
|
+
and (
|
|
480
|
+
state_before is TaskState.FROZEN
|
|
481
|
+
or receipt.sequence > 1
|
|
482
|
+
)
|
|
483
|
+
) or (
|
|
484
|
+
receipt.sequence > 1
|
|
485
|
+
and (
|
|
486
|
+
isinstance(receipt, SystemEventReceipt)
|
|
487
|
+
and receipt.system_event_name == "contract_expired"
|
|
488
|
+
)
|
|
489
|
+
):
|
|
490
|
+
return _denied(
|
|
491
|
+
"TRANSITION_CONTEXT_UNAVAILABLE",
|
|
492
|
+
"trusted chain tip is not available in Task 5",
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
if (
|
|
496
|
+
state_before is TaskState.NEEDS_REWORK
|
|
497
|
+
and state_after is TaskState.RETRYING
|
|
498
|
+
):
|
|
499
|
+
if (
|
|
500
|
+
not isinstance(receipt, GrantConsumedReceipt)
|
|
501
|
+
or not _agent_direct_call_is_authorized(receipt, work_order)
|
|
502
|
+
or receipt.metric != "repair_rounds"
|
|
503
|
+
or receipt.amount != 1
|
|
504
|
+
):
|
|
505
|
+
return _denied("INVALID_EVIDENCE", "retry charge is invalid")
|
|
506
|
+
return _denied(
|
|
507
|
+
"TRANSITION_CONTEXT_UNAVAILABLE",
|
|
508
|
+
"failure episode and rollback history are unavailable in Task 5",
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
if state_before is TaskState.ISSUED and state_after is TaskState.RUNNING:
|
|
512
|
+
root = work_order.root_grant_template
|
|
513
|
+
if (
|
|
514
|
+
isinstance(receipt, GrantIssuedReceipt)
|
|
515
|
+
and _agent_direct_call_is_authorized(receipt, work_order)
|
|
516
|
+
and receipt.parent_grant_id is None
|
|
517
|
+
and receipt.authorizing_grant_id == root.grant_id
|
|
518
|
+
and receipt.issued_grant_id == root.grant_id
|
|
519
|
+
and receipt.nested_claim.tool_name == "owp.activate_root_grant"
|
|
520
|
+
):
|
|
521
|
+
return _allowed("root Grant activated")
|
|
522
|
+
return _denied("INVALID_EVIDENCE", "root activation evidence is invalid")
|
|
523
|
+
|
|
524
|
+
if state_before in {TaskState.RUNNING, TaskState.RETRYING} and state_after in {
|
|
525
|
+
TaskState.NEEDS_REWORK,
|
|
526
|
+
TaskState.LOCALLY_VERIFIED,
|
|
527
|
+
}:
|
|
528
|
+
if isinstance(receipt, ToolCallReceipt) and _validate_verifier_transition(
|
|
529
|
+
receipt,
|
|
530
|
+
work_order,
|
|
531
|
+
state_after,
|
|
532
|
+
):
|
|
533
|
+
return _allowed("Verifier outcome authorizes transition")
|
|
534
|
+
return _denied("INVALID_EVIDENCE", "Verifier evidence is invalid")
|
|
535
|
+
|
|
536
|
+
if (
|
|
537
|
+
state_before is TaskState.PROOF_READY
|
|
538
|
+
and state_after is TaskState.AWAITING_HUMAN
|
|
539
|
+
):
|
|
540
|
+
if isinstance(receipt, ApprovalRequestedReceipt) and _validate_final_request(
|
|
541
|
+
receipt,
|
|
542
|
+
work_order,
|
|
543
|
+
now,
|
|
544
|
+
):
|
|
545
|
+
return _allowed("Manager requested final acceptance")
|
|
546
|
+
return _denied("INVALID_EVIDENCE", "final request evidence is invalid")
|
|
547
|
+
|
|
548
|
+
if state_after is TaskState.REJECTED:
|
|
549
|
+
if isinstance(receipt, TerminationDecisionReceipt) and _validate_termination(
|
|
550
|
+
receipt,
|
|
551
|
+
work_order,
|
|
552
|
+
state_before,
|
|
553
|
+
now,
|
|
554
|
+
):
|
|
555
|
+
return _allowed("Maintainer terminated WorkOrder")
|
|
556
|
+
return _denied("INVALID_EVIDENCE", "termination evidence is invalid")
|
|
557
|
+
|
|
558
|
+
if state_after is TaskState.FROZEN:
|
|
559
|
+
if isinstance(receipt, SystemEventReceipt) and _validate_contract_expiry(
|
|
560
|
+
receipt,
|
|
561
|
+
work_order,
|
|
562
|
+
now,
|
|
563
|
+
):
|
|
564
|
+
return _allowed("contract expiry observed")
|
|
565
|
+
return _denied("INVALID_EVIDENCE", "freeze evidence is invalid")
|
|
566
|
+
|
|
567
|
+
return _denied("INVALID_EVIDENCE", "transition evidence is unsupported")
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def apply_state_transition(
|
|
571
|
+
*,
|
|
572
|
+
work_order: WorkOrder,
|
|
573
|
+
state_before: TaskState,
|
|
574
|
+
state_after: TaskState,
|
|
575
|
+
trigger_receipt: ActionReceipt | None,
|
|
576
|
+
acceptance_receipt: AcceptanceReceipt | None,
|
|
577
|
+
public_keys: Mapping[str, Ed25519PublicKey],
|
|
578
|
+
now: datetime,
|
|
579
|
+
) -> TransitionDecision:
|
|
580
|
+
if state_before is state_after:
|
|
581
|
+
return _denied(
|
|
582
|
+
"STATE_CHANGE_REQUIRED",
|
|
583
|
+
"same-state receipts use append_receipt",
|
|
584
|
+
)
|
|
585
|
+
return validate_transition_evidence(
|
|
586
|
+
work_order,
|
|
587
|
+
state_before,
|
|
588
|
+
state_after,
|
|
589
|
+
trigger_receipt,
|
|
590
|
+
acceptance_receipt,
|
|
591
|
+
public_keys,
|
|
592
|
+
now,
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def append_receipt(
|
|
597
|
+
*,
|
|
598
|
+
work_order: WorkOrder,
|
|
599
|
+
state: TaskState,
|
|
600
|
+
receipt: ActionReceipt,
|
|
601
|
+
public_keys: Mapping[str, Ed25519PublicKey],
|
|
602
|
+
now: datetime,
|
|
603
|
+
allow_independent_verifier_rerun: bool = False,
|
|
604
|
+
) -> TransitionDecision:
|
|
605
|
+
if not _work_order_is_trusted(work_order, public_keys):
|
|
606
|
+
return _denied("INVALID_EVIDENCE", "WorkOrder trust failed")
|
|
607
|
+
parsed = _validate_action_receipt(receipt, work_order, public_keys, now)
|
|
608
|
+
if parsed is None:
|
|
609
|
+
return _denied("INVALID_EVIDENCE", "receipt integrity failed")
|
|
610
|
+
if parsed.state_before != state.value or parsed.state_after != state.value:
|
|
611
|
+
return _denied(
|
|
612
|
+
"TRANSITION_REQUIRED",
|
|
613
|
+
"state-changing receipts use apply_state_transition",
|
|
614
|
+
)
|
|
615
|
+
if state in {TaskState.ACCEPTED, TaskState.REJECTED, TaskState.FROZEN}:
|
|
616
|
+
return _denied("STATE_DENIED", "state does not accept same-state receipts")
|
|
617
|
+
if parsed.policy_decision == "deny":
|
|
618
|
+
return _allowed("authenticated same-state denial appended")
|
|
619
|
+
role = _actor_role(parsed, work_order)
|
|
620
|
+
if isinstance(
|
|
621
|
+
parsed,
|
|
622
|
+
(
|
|
623
|
+
GrantIssuedReceipt,
|
|
624
|
+
GrantConsumedReceipt,
|
|
625
|
+
GrantRevokedReceipt,
|
|
626
|
+
ToolCallReceipt,
|
|
627
|
+
ApprovalRequestedReceipt,
|
|
628
|
+
RollbackReceipt,
|
|
629
|
+
),
|
|
630
|
+
) and not _agent_direct_call_is_authorized(parsed, work_order):
|
|
631
|
+
return _denied(
|
|
632
|
+
"STATE_DENIED",
|
|
633
|
+
"Agent direct call is outside the frozen role matrix",
|
|
634
|
+
)
|
|
635
|
+
if (
|
|
636
|
+
isinstance(parsed, ToolCallReceipt)
|
|
637
|
+
and parsed.tool_name == "owp.compose_proof"
|
|
638
|
+
and role == "Manager"
|
|
639
|
+
and state
|
|
640
|
+
in {TaskState.LOCALLY_VERIFIED, TaskState.EVIDENCE_INCOMPLETE}
|
|
641
|
+
):
|
|
642
|
+
return _denied(
|
|
643
|
+
"COMPOSITION_VALIDATOR_UNAVAILABLE",
|
|
644
|
+
"same-state composition requires canonical receipt history",
|
|
645
|
+
)
|
|
646
|
+
if state is TaskState.EVIDENCE_INCOMPLETE:
|
|
647
|
+
if (
|
|
648
|
+
isinstance(parsed, ToolCallReceipt)
|
|
649
|
+
and parsed.tool_name == "owp.run_tests"
|
|
650
|
+
and role == "Verifier"
|
|
651
|
+
):
|
|
652
|
+
# The single-receipt API fails closed because it has no canonical
|
|
653
|
+
# episode history. Full-history replay paths pass the explicit
|
|
654
|
+
# allowance flag after their own causal/policy verification.
|
|
655
|
+
if allow_independent_verifier_rerun:
|
|
656
|
+
return _allowed(
|
|
657
|
+
"independent Verifier rerun in evidence-incomplete"
|
|
658
|
+
)
|
|
659
|
+
return _denied(
|
|
660
|
+
"TRANSITION_CONTEXT_UNAVAILABLE",
|
|
661
|
+
"same-state evidence requires unavailable episode history",
|
|
662
|
+
)
|
|
663
|
+
return _denied(
|
|
664
|
+
"STATE_DENIED",
|
|
665
|
+
"evidence-incomplete only accepts an independent Verifier rerun",
|
|
666
|
+
)
|
|
667
|
+
if state is TaskState.NEEDS_REWORK and isinstance(parsed, RollbackReceipt):
|
|
668
|
+
return _denied(
|
|
669
|
+
"TRANSITION_CONTEXT_UNAVAILABLE",
|
|
670
|
+
"same-state evidence requires unavailable episode history",
|
|
671
|
+
)
|
|
672
|
+
if state in {TaskState.PROOF_READY, TaskState.AWAITING_HUMAN}:
|
|
673
|
+
return _denied("STATE_DENIED", "human-gate tail forbids successful append")
|
|
674
|
+
if isinstance(parsed, GrantConsumedReceipt):
|
|
675
|
+
if state is TaskState.NEEDS_REWORK and role == "Manager":
|
|
676
|
+
return _denied(
|
|
677
|
+
"TRANSITION_REQUIRED",
|
|
678
|
+
"retry charge must select the retrying state transition",
|
|
679
|
+
)
|
|
680
|
+
return _denied(
|
|
681
|
+
"STATE_DENIED",
|
|
682
|
+
"retry charge is not a same-state audit event",
|
|
683
|
+
)
|
|
684
|
+
grant_audit_states = {
|
|
685
|
+
TaskState.RUNNING,
|
|
686
|
+
TaskState.RETRYING,
|
|
687
|
+
TaskState.NEEDS_REWORK,
|
|
688
|
+
}
|
|
689
|
+
if isinstance(parsed, GrantIssuedReceipt):
|
|
690
|
+
if (
|
|
691
|
+
parsed.parent_grant_id is not None
|
|
692
|
+
and role == "Manager"
|
|
693
|
+
and state in grant_audit_states
|
|
694
|
+
):
|
|
695
|
+
return _allowed("child Grant issuance appended")
|
|
696
|
+
return _denied(
|
|
697
|
+
"STATE_DENIED",
|
|
698
|
+
"child Grant issuance is outside its role or state window",
|
|
699
|
+
)
|
|
700
|
+
if isinstance(parsed, GrantRevokedReceipt):
|
|
701
|
+
if role == "Manager" and state in grant_audit_states:
|
|
702
|
+
return _allowed("Grant revocation appended")
|
|
703
|
+
return _denied(
|
|
704
|
+
"STATE_DENIED",
|
|
705
|
+
"Grant revocation is outside its role or state window",
|
|
706
|
+
)
|
|
707
|
+
if isinstance(parsed, ToolCallReceipt):
|
|
708
|
+
if state not in {TaskState.RUNNING, TaskState.RETRYING}:
|
|
709
|
+
return _denied(
|
|
710
|
+
"STATE_DENIED",
|
|
711
|
+
"tool results are outside their state window",
|
|
712
|
+
)
|
|
713
|
+
if (
|
|
714
|
+
parsed.tool_name == "owp.run_tests"
|
|
715
|
+
and role == "Verifier"
|
|
716
|
+
and parsed.execution_status == "succeeded"
|
|
717
|
+
):
|
|
718
|
+
return _denied(
|
|
719
|
+
"TRANSITION_REQUIRED",
|
|
720
|
+
"normal Verifier result must select a state transition",
|
|
721
|
+
)
|
|
722
|
+
if parsed.tool_name in {
|
|
723
|
+
"owp.repo_read",
|
|
724
|
+
"owp.apply_patch",
|
|
725
|
+
"owp.run_tests",
|
|
726
|
+
"owp.create_pr_proposal",
|
|
727
|
+
}:
|
|
728
|
+
return _allowed("ordinary tool result appended")
|
|
729
|
+
return _denied(
|
|
730
|
+
"STATE_DENIED",
|
|
731
|
+
"tool result is outside its role or tool window",
|
|
732
|
+
)
|
|
733
|
+
if (
|
|
734
|
+
isinstance(parsed, ApprovalRequestedReceipt)
|
|
735
|
+
and parsed.request_kind == "high_risk_action"
|
|
736
|
+
):
|
|
737
|
+
if (
|
|
738
|
+
state in {TaskState.RUNNING, TaskState.RETRYING}
|
|
739
|
+
and role == "Manager"
|
|
740
|
+
):
|
|
741
|
+
return _allowed("high-risk approval request appended")
|
|
742
|
+
return _denied(
|
|
743
|
+
"STATE_DENIED",
|
|
744
|
+
"approval request is outside its role or state window",
|
|
745
|
+
)
|
|
746
|
+
if isinstance(parsed, ApprovalDecisionReceipt):
|
|
747
|
+
if (
|
|
748
|
+
state in {TaskState.RUNNING, TaskState.RETRYING}
|
|
749
|
+
and role == "Maintainer"
|
|
750
|
+
):
|
|
751
|
+
return _allowed("approval decision appended")
|
|
752
|
+
return _denied(
|
|
753
|
+
"STATE_DENIED",
|
|
754
|
+
"approval decision is outside its role or state window",
|
|
755
|
+
)
|
|
756
|
+
return _denied("STATE_DENIED", "event is not a valid same-state append")
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
__all__ = [
|
|
760
|
+
"ALLOWED_TRANSITIONS",
|
|
761
|
+
"TaskState",
|
|
762
|
+
"append_receipt",
|
|
763
|
+
"apply_state_transition",
|
|
764
|
+
"validate_transition_evidence",
|
|
765
|
+
]
|