problem-frame-gate 0.3.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.
- problem_frame_gate/__init__.py +104 -0
- problem_frame_gate/_version.py +1 -0
- problem_frame_gate/certificates.py +116 -0
- problem_frame_gate/cli.py +183 -0
- problem_frame_gate/digest.py +69 -0
- problem_frame_gate/errors.py +17 -0
- problem_frame_gate/fold.py +425 -0
- problem_frame_gate/formation.py +155 -0
- problem_frame_gate/gate.py +365 -0
- problem_frame_gate/join.py +150 -0
- problem_frame_gate/model.py +441 -0
- problem_frame_gate/patch.py +191 -0
- problem_frame_gate/py.typed +1 -0
- problem_frame_gate/records.py +210 -0
- problem_frame_gate/result.py +148 -0
- problem_frame_gate/risk.py +203 -0
- problem_frame_gate/security.py +88 -0
- problem_frame_gate/verifier.py +594 -0
- problem_frame_gate-0.3.0.dist-info/METADATA +153 -0
- problem_frame_gate-0.3.0.dist-info/RECORD +24 -0
- problem_frame_gate-0.3.0.dist-info/WHEEL +4 -0
- problem_frame_gate-0.3.0.dist-info/entry_points.txt +2 -0
- problem_frame_gate-0.3.0.dist-info/licenses/LICENSE +175 -0
- problem_frame_gate-0.3.0.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
"""Deterministic component replay over a legal envelope log."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from fractions import Fraction
|
|
9
|
+
from typing import Any, Protocol
|
|
10
|
+
|
|
11
|
+
from .errors import FoldError, LogVerificationError
|
|
12
|
+
from .model import Envelope, Frame, Horizon, Status
|
|
13
|
+
from .result import CheckBuilder, CheckResult
|
|
14
|
+
from .verifier import EnvelopeVerifier, digest_log
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class StateComponent(Protocol):
|
|
18
|
+
name: str
|
|
19
|
+
|
|
20
|
+
def initial_state(self) -> Any: ...
|
|
21
|
+
|
|
22
|
+
def apply(self, state: Any, envelope: Envelope) -> Any: ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class FoldState:
|
|
27
|
+
components: Mapping[str, Any]
|
|
28
|
+
ordered_eids: tuple[str, ...]
|
|
29
|
+
log_digest: str
|
|
30
|
+
|
|
31
|
+
def component(self, name: str) -> Any:
|
|
32
|
+
return self.components.get(name, {})
|
|
33
|
+
|
|
34
|
+
def to_json(self) -> dict[str, Any]:
|
|
35
|
+
return {
|
|
36
|
+
"components": self.components,
|
|
37
|
+
"ordered_eids": list(self.ordered_eids),
|
|
38
|
+
"log_digest": self.log_digest,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FoldKernel:
|
|
43
|
+
"""Replay kernel with manifest-supplied components."""
|
|
44
|
+
|
|
45
|
+
footprint = frozenset({"FoldKernel", "EnvelopeVerifier"})
|
|
46
|
+
|
|
47
|
+
def __init__(self, components: Sequence[StateComponent] | None = None) -> None:
|
|
48
|
+
self.components = tuple(components or default_components())
|
|
49
|
+
names = [component.name for component in self.components]
|
|
50
|
+
if len(names) != len(set(names)):
|
|
51
|
+
raise ValueError("component names must be unique")
|
|
52
|
+
self.verifier = EnvelopeVerifier()
|
|
53
|
+
|
|
54
|
+
def fold(self, horizon: Horizon, envelopes: Sequence[Envelope]) -> FoldState:
|
|
55
|
+
legal = self.verifier.verify(horizon, envelopes)
|
|
56
|
+
if not legal.ok:
|
|
57
|
+
raise LogVerificationError("log is not legal")
|
|
58
|
+
ordered = self.verifier.canonical_order(horizon, envelopes)
|
|
59
|
+
states: dict[str, Any] = {component.name: component.initial_state() for component in self.components}
|
|
60
|
+
for env in ordered:
|
|
61
|
+
for component in self.components:
|
|
62
|
+
try:
|
|
63
|
+
states[component.name] = component.apply(states[component.name], env)
|
|
64
|
+
except FoldError:
|
|
65
|
+
raise
|
|
66
|
+
except Exception as exc: # pragma: no cover - defensive wrapper
|
|
67
|
+
raise FoldError(f"{component.name} rejected envelope {env.eid}: {exc}") from exc
|
|
68
|
+
return FoldState(states, tuple(env.eid for env in ordered), digest_log(envelopes))
|
|
69
|
+
|
|
70
|
+
def check_fold(self, horizon: Horizon, envelopes: Sequence[Envelope]) -> CheckResult:
|
|
71
|
+
builder = CheckBuilder(footprint=set(self.footprint))
|
|
72
|
+
legal = self.verifier.verify(horizon, envelopes)
|
|
73
|
+
if not legal.ok:
|
|
74
|
+
return legal.merge(builder.result())
|
|
75
|
+
try:
|
|
76
|
+
state = self.fold(horizon, envelopes)
|
|
77
|
+
except FoldError as exc:
|
|
78
|
+
builder.error("fold-rejected", str(exc))
|
|
79
|
+
return builder.result(digest=legal.digest)
|
|
80
|
+
return builder.result(digest=state.log_digest)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class FrameComponent:
|
|
84
|
+
name = "frames"
|
|
85
|
+
|
|
86
|
+
def initial_state(self) -> dict[str, dict[str, Any]]:
|
|
87
|
+
return {}
|
|
88
|
+
|
|
89
|
+
def apply(self, state: dict[str, dict[str, Any]], envelope: Envelope) -> dict[str, dict[str, Any]]:
|
|
90
|
+
next_state = deepcopy(state)
|
|
91
|
+
kind = envelope.kind
|
|
92
|
+
payload = envelope.payload
|
|
93
|
+
frame_id = str(payload.get("frame_id", payload.get("object", "")))
|
|
94
|
+
|
|
95
|
+
if kind in {"Frame", "ProblemFrame"}:
|
|
96
|
+
frame = Frame.from_payload(payload)
|
|
97
|
+
if not frame.frame_id:
|
|
98
|
+
raise FoldError(f"frame payload in {envelope.eid} has no frame_id")
|
|
99
|
+
record = next_state.setdefault(frame.frame_id, {})
|
|
100
|
+
record["frame"] = frame.to_json()
|
|
101
|
+
record.setdefault("status", Status.INACTIVE.value)
|
|
102
|
+
elif kind == "Proposed":
|
|
103
|
+
_require(frame_id, "frame_id", envelope)
|
|
104
|
+
record = next_state.setdefault(frame_id, {})
|
|
105
|
+
record["status"] = Status.INACTIVE.value
|
|
106
|
+
elif kind == "Activated":
|
|
107
|
+
_require(frame_id, "frame_id", envelope)
|
|
108
|
+
record = next_state.setdefault(frame_id, {})
|
|
109
|
+
record["status"] = Status.ACTIVE.value
|
|
110
|
+
record["activated_by"] = envelope.eid
|
|
111
|
+
elif kind == "DiagnosticActivated":
|
|
112
|
+
_require(frame_id, "frame_id", envelope)
|
|
113
|
+
record = next_state.setdefault(frame_id, {})
|
|
114
|
+
record["status"] = Status.DIAGNOSTIC_ACTIVE.value
|
|
115
|
+
elif kind == "Suspended":
|
|
116
|
+
_require(frame_id, "frame_id", envelope)
|
|
117
|
+
record = next_state.setdefault(frame_id, {})
|
|
118
|
+
record["status"] = Status.SUSPENDED.value
|
|
119
|
+
elif kind == "Invalidated":
|
|
120
|
+
_require(frame_id, "frame_id", envelope)
|
|
121
|
+
record = next_state.setdefault(frame_id, {})
|
|
122
|
+
record["status"] = Status.INVALID.value
|
|
123
|
+
elif kind == "Withdrawn":
|
|
124
|
+
_require(frame_id, "frame_id", envelope)
|
|
125
|
+
record = next_state.setdefault(frame_id, {})
|
|
126
|
+
record["status"] = Status.WITHDRAWN.value
|
|
127
|
+
return next_state
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class CertificateComponent:
|
|
131
|
+
name = "certificates"
|
|
132
|
+
|
|
133
|
+
def initial_state(self) -> dict[str, dict[str, Any]]:
|
|
134
|
+
return {}
|
|
135
|
+
|
|
136
|
+
def apply(self, state: dict[str, dict[str, Any]], envelope: Envelope) -> dict[str, dict[str, Any]]:
|
|
137
|
+
next_state = deepcopy(state)
|
|
138
|
+
payload = envelope.payload
|
|
139
|
+
kind = envelope.kind
|
|
140
|
+
if kind == "Issue":
|
|
141
|
+
cert_id = str(payload.get("cert_id", payload.get("object", "")))
|
|
142
|
+
_require(cert_id, "cert_id", envelope)
|
|
143
|
+
if cert_id in next_state and next_state[cert_id].get("issued"):
|
|
144
|
+
raise FoldError(f"certificate {cert_id} is issued twice")
|
|
145
|
+
next_state[cert_id] = {
|
|
146
|
+
"issued": True,
|
|
147
|
+
"issued_at": envelope.commit_time,
|
|
148
|
+
"issuer": payload.get("issuer", envelope.writer),
|
|
149
|
+
"family": payload.get("family", ""),
|
|
150
|
+
"subject": payload.get("subject", payload.get("object", "")),
|
|
151
|
+
"expires_at": payload.get("expires_at"),
|
|
152
|
+
"dependencies": list(payload.get("dependencies", ())),
|
|
153
|
+
"source_ids": list(payload.get("source_ids", ())),
|
|
154
|
+
"dependency_digest": payload.get("dependency_digest"),
|
|
155
|
+
"family_check": payload.get("family_check"),
|
|
156
|
+
"assumption": payload.get("assumption"),
|
|
157
|
+
"revoked_at": None,
|
|
158
|
+
"payload_eid": envelope.eid,
|
|
159
|
+
}
|
|
160
|
+
elif kind in {"Revoke", "CapRevoked"}:
|
|
161
|
+
cert_id = str(payload.get("cert_id", ""))
|
|
162
|
+
if cert_id:
|
|
163
|
+
record = next_state.get(cert_id)
|
|
164
|
+
if record is None:
|
|
165
|
+
raise FoldError(f"cannot revoke unknown certificate {cert_id}")
|
|
166
|
+
if record.get("revoked_at") is not None:
|
|
167
|
+
raise FoldError(f"certificate {cert_id} is revoked twice")
|
|
168
|
+
record["revoked_at"] = envelope.commit_time
|
|
169
|
+
return next_state
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class CapabilityComponent:
|
|
173
|
+
name = "capabilities"
|
|
174
|
+
|
|
175
|
+
def initial_state(self) -> dict[str, dict[str, Any]]:
|
|
176
|
+
return {}
|
|
177
|
+
|
|
178
|
+
def apply(self, state: dict[str, dict[str, Any]], envelope: Envelope) -> dict[str, dict[str, Any]]:
|
|
179
|
+
next_state = deepcopy(state)
|
|
180
|
+
payload = envelope.payload
|
|
181
|
+
kind = envelope.kind
|
|
182
|
+
if kind in {"MintCap", "CapMinted"}:
|
|
183
|
+
cap_id = str(payload.get("capability_id", payload.get("cap_id", payload.get("object", ""))))
|
|
184
|
+
_require(cap_id, "capability_id", envelope)
|
|
185
|
+
if cap_id in next_state:
|
|
186
|
+
raise FoldError(f"capability {cap_id} is minted twice")
|
|
187
|
+
next_state[cap_id] = {
|
|
188
|
+
"status": "unused",
|
|
189
|
+
"frame_id": payload.get("frame_id"),
|
|
190
|
+
"action": payload.get("action"),
|
|
191
|
+
"source_digest": payload.get("source_digest"),
|
|
192
|
+
"minted_at": envelope.commit_time,
|
|
193
|
+
"expires_at": payload.get("expires_at"),
|
|
194
|
+
}
|
|
195
|
+
elif kind in {"UseCap", "CapUsed"}:
|
|
196
|
+
cap_id = str(payload.get("capability_id", payload.get("cap_id", "")))
|
|
197
|
+
record = _get(next_state, cap_id, "capability", envelope)
|
|
198
|
+
if record.get("status") != "unused":
|
|
199
|
+
raise FoldError(f"capability {cap_id} is not live")
|
|
200
|
+
record["status"] = "used"
|
|
201
|
+
record["outbox_id"] = payload.get("outbox_id")
|
|
202
|
+
record["used_at"] = envelope.commit_time
|
|
203
|
+
elif kind in {"RevokeCap", "CapRevoked"}:
|
|
204
|
+
cap_id = str(payload.get("capability_id", payload.get("cap_id", "")))
|
|
205
|
+
if cap_id:
|
|
206
|
+
record = _get(next_state, cap_id, "capability", envelope)
|
|
207
|
+
if record.get("status") == "used":
|
|
208
|
+
raise FoldError(f"capability {cap_id} is already used")
|
|
209
|
+
record["status"] = "revoked"
|
|
210
|
+
elif kind in {"ExpireCap", "CapExpired"}:
|
|
211
|
+
cap_id = str(payload.get("capability_id", payload.get("cap_id", "")))
|
|
212
|
+
record = _get(next_state, cap_id, "capability", envelope)
|
|
213
|
+
if record.get("status") == "used":
|
|
214
|
+
raise FoldError(f"capability {cap_id} is already used")
|
|
215
|
+
record["status"] = "expired"
|
|
216
|
+
return next_state
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class ResourceComponent:
|
|
220
|
+
name = "resources"
|
|
221
|
+
|
|
222
|
+
def initial_state(self) -> dict[str, dict[str, Any]]:
|
|
223
|
+
return {}
|
|
224
|
+
|
|
225
|
+
def apply(self, state: dict[str, dict[str, Any]], envelope: Envelope) -> dict[str, dict[str, Any]]:
|
|
226
|
+
next_state = deepcopy(state)
|
|
227
|
+
payload = envelope.payload
|
|
228
|
+
kind = envelope.kind
|
|
229
|
+
if kind == "ReserveResource":
|
|
230
|
+
lease_id = str(payload.get("lease_id", payload.get("object", "")))
|
|
231
|
+
_require(lease_id, "lease_id", envelope)
|
|
232
|
+
if lease_id in next_state:
|
|
233
|
+
raise FoldError(f"resource lease {lease_id} is reserved twice")
|
|
234
|
+
next_state[lease_id] = {
|
|
235
|
+
"status": "leased",
|
|
236
|
+
"token_id": payload.get("token_id"),
|
|
237
|
+
"frame_id": payload.get("frame_id"),
|
|
238
|
+
"amount": payload.get("amount", 1),
|
|
239
|
+
"reserved_at": envelope.commit_time,
|
|
240
|
+
}
|
|
241
|
+
elif kind == "ConsumeResource":
|
|
242
|
+
lease_id = str(payload.get("lease_id", payload.get("object", "")))
|
|
243
|
+
record = _get(next_state, lease_id, "resource lease", envelope)
|
|
244
|
+
if record.get("status") != "leased":
|
|
245
|
+
raise FoldError(f"resource lease {lease_id} is not leased")
|
|
246
|
+
record["status"] = "consumed"
|
|
247
|
+
record["consumed_at"] = envelope.commit_time
|
|
248
|
+
record["consumer"] = payload.get("consumer")
|
|
249
|
+
elif kind == "ReleaseResource":
|
|
250
|
+
lease_id = str(payload.get("lease_id", payload.get("object", "")))
|
|
251
|
+
record = _get(next_state, lease_id, "resource lease", envelope)
|
|
252
|
+
if record.get("status") != "leased":
|
|
253
|
+
raise FoldError(f"resource lease {lease_id} is not leased")
|
|
254
|
+
record["status"] = "released"
|
|
255
|
+
return next_state
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class OutboxComponent:
|
|
259
|
+
name = "outboxes"
|
|
260
|
+
|
|
261
|
+
def initial_state(self) -> dict[str, dict[str, Any]]:
|
|
262
|
+
return {}
|
|
263
|
+
|
|
264
|
+
def apply(self, state: dict[str, dict[str, Any]], envelope: Envelope) -> dict[str, dict[str, Any]]:
|
|
265
|
+
next_state = deepcopy(state)
|
|
266
|
+
payload = envelope.payload
|
|
267
|
+
kind = envelope.kind
|
|
268
|
+
outbox_id = str(payload.get("outbox_id", payload.get("object", "")))
|
|
269
|
+
if kind == "AuthorizeOutbox":
|
|
270
|
+
_require(outbox_id, "outbox_id", envelope)
|
|
271
|
+
if outbox_id in next_state:
|
|
272
|
+
raise FoldError(f"outbox {outbox_id} is authorized twice")
|
|
273
|
+
next_state[outbox_id] = {
|
|
274
|
+
"status": "authorized",
|
|
275
|
+
"frame_id": payload.get("frame_id"),
|
|
276
|
+
"action": payload.get("action"),
|
|
277
|
+
"authorized_at": envelope.commit_time,
|
|
278
|
+
}
|
|
279
|
+
elif kind == "OutboxClaim":
|
|
280
|
+
record = _get(next_state, outbox_id, "outbox", envelope)
|
|
281
|
+
if record.get("status") != "authorized":
|
|
282
|
+
raise FoldError(f"outbox {outbox_id} is not authorized")
|
|
283
|
+
record["status"] = "claimed"
|
|
284
|
+
record["gate_id"] = payload.get("gate_id")
|
|
285
|
+
record["claimed_at"] = envelope.commit_time
|
|
286
|
+
elif kind == "RevokeOutbox":
|
|
287
|
+
record = _get(next_state, outbox_id, "outbox", envelope)
|
|
288
|
+
if record.get("status") != "authorized":
|
|
289
|
+
raise FoldError(f"outbox {outbox_id} is not authorized")
|
|
290
|
+
record["status"] = "revoked"
|
|
291
|
+
elif kind in {
|
|
292
|
+
"DispatchStarted",
|
|
293
|
+
"ActuatorAccepted",
|
|
294
|
+
"ActuatorRejected",
|
|
295
|
+
"ReceiptCommitted",
|
|
296
|
+
"ReceiptMissing",
|
|
297
|
+
"ReceiptConflict",
|
|
298
|
+
}:
|
|
299
|
+
record = _get(next_state, outbox_id, "outbox", envelope)
|
|
300
|
+
transitions = {
|
|
301
|
+
"DispatchStarted": {"claimed"},
|
|
302
|
+
"ActuatorAccepted": {"dispatchStarted"},
|
|
303
|
+
"ActuatorRejected": {"dispatchStarted"},
|
|
304
|
+
"ReceiptCommitted": {"actuatorAccepted"},
|
|
305
|
+
"ReceiptMissing": {"actuatorAccepted"},
|
|
306
|
+
"ReceiptConflict": {"actuatorAccepted"},
|
|
307
|
+
}
|
|
308
|
+
if record.get("status") not in transitions[kind]:
|
|
309
|
+
raise FoldError(f"outbox {outbox_id} cannot transition to {kind}")
|
|
310
|
+
record["status"] = {
|
|
311
|
+
"DispatchStarted": "dispatchStarted",
|
|
312
|
+
"ActuatorAccepted": "actuatorAccepted",
|
|
313
|
+
"ActuatorRejected": "actuatorRejected",
|
|
314
|
+
"ReceiptCommitted": "receiptCommitted",
|
|
315
|
+
"ReceiptMissing": "receiptMissing",
|
|
316
|
+
"ReceiptConflict": "receiptConflict",
|
|
317
|
+
}[kind]
|
|
318
|
+
return next_state
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class RiskComponent:
|
|
322
|
+
name = "risk"
|
|
323
|
+
|
|
324
|
+
def initial_state(self) -> dict[str, Any]:
|
|
325
|
+
return {"hypotheses": {}, "reserves": {}, "spends": {}}
|
|
326
|
+
|
|
327
|
+
def apply(self, state: dict[str, Any], envelope: Envelope) -> dict[str, Any]:
|
|
328
|
+
next_state = deepcopy(state)
|
|
329
|
+
payload = envelope.payload
|
|
330
|
+
kind = envelope.kind
|
|
331
|
+
if kind == "RiskReg":
|
|
332
|
+
hyp = str(payload.get("hypothesis_id", payload.get("hypothesis", payload.get("object", ""))))
|
|
333
|
+
_require(hyp, "hypothesis_id", envelope)
|
|
334
|
+
if hyp in next_state["hypotheses"]:
|
|
335
|
+
raise FoldError(f"hypothesis {hyp} is registered twice")
|
|
336
|
+
next_state["hypotheses"][hyp] = {
|
|
337
|
+
"family": payload.get("family"),
|
|
338
|
+
"registered_at": envelope.commit_time,
|
|
339
|
+
}
|
|
340
|
+
elif kind == "RiskReserve":
|
|
341
|
+
risk_id = str(payload.get("risk_id", payload.get("object", "")))
|
|
342
|
+
_require(risk_id, "risk_id", envelope)
|
|
343
|
+
if risk_id in next_state["reserves"]:
|
|
344
|
+
raise FoldError(f"risk id {risk_id} is reserved twice")
|
|
345
|
+
next_state["reserves"][risk_id] = {
|
|
346
|
+
"hypothesis_id": payload.get("hypothesis_id"),
|
|
347
|
+
"frame_id": payload.get("frame_id"),
|
|
348
|
+
"eta": _fraction_text(payload.get("eta", "0")),
|
|
349
|
+
"reserved_at": envelope.commit_time,
|
|
350
|
+
}
|
|
351
|
+
elif kind == "RiskSpend":
|
|
352
|
+
risk_id = str(payload.get("risk_id", payload.get("object", "")))
|
|
353
|
+
reserve = _get(next_state["reserves"], risk_id, "risk reserve", envelope)
|
|
354
|
+
if risk_id in next_state["spends"]:
|
|
355
|
+
raise FoldError(f"risk id {risk_id} is spent twice")
|
|
356
|
+
next_state["spends"][risk_id] = {
|
|
357
|
+
"hypothesis_id": payload.get("hypothesis_id", reserve.get("hypothesis_id")),
|
|
358
|
+
"frame_id": payload.get("frame_id", reserve.get("frame_id")),
|
|
359
|
+
"eta": _fraction_text(payload.get("eta", reserve.get("eta", "0"))),
|
|
360
|
+
"mode": payload.get("mode"),
|
|
361
|
+
"cert_id": payload.get("cert_id"),
|
|
362
|
+
"ledger_digest": payload.get("ledger_digest"),
|
|
363
|
+
"spent_at": envelope.commit_time,
|
|
364
|
+
"closed_at": None,
|
|
365
|
+
}
|
|
366
|
+
elif kind == "RiskClose":
|
|
367
|
+
risk_id = str(payload.get("risk_id", payload.get("object", "")))
|
|
368
|
+
spend = _get(next_state["spends"], risk_id, "risk spend", envelope)
|
|
369
|
+
if spend.get("closed_at") is not None:
|
|
370
|
+
raise FoldError(f"risk id {risk_id} is closed twice")
|
|
371
|
+
spend["closed_at"] = envelope.commit_time
|
|
372
|
+
return next_state
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class EvidenceComponent:
|
|
376
|
+
name = "evidence"
|
|
377
|
+
|
|
378
|
+
def initial_state(self) -> dict[str, dict[str, Any]]:
|
|
379
|
+
return {}
|
|
380
|
+
|
|
381
|
+
def apply(self, state: dict[str, dict[str, Any]], envelope: Envelope) -> dict[str, dict[str, Any]]:
|
|
382
|
+
next_state = deepcopy(state)
|
|
383
|
+
payload = envelope.payload
|
|
384
|
+
kind = envelope.kind
|
|
385
|
+
if kind in {"Record", "Evidence", "Source"}:
|
|
386
|
+
evidence_id = str(payload.get("evidence_id", payload.get("source_id", payload.get("object", ""))))
|
|
387
|
+
_require(evidence_id, "evidence_id", envelope)
|
|
388
|
+
next_state[evidence_id] = {
|
|
389
|
+
"kind": kind,
|
|
390
|
+
"record_id": payload.get("record_id"),
|
|
391
|
+
"digest": payload.get("digest"),
|
|
392
|
+
"committed_at": envelope.commit_time,
|
|
393
|
+
"eid": envelope.eid,
|
|
394
|
+
}
|
|
395
|
+
return next_state
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def default_components() -> tuple[StateComponent, ...]:
|
|
399
|
+
return (
|
|
400
|
+
FrameComponent(),
|
|
401
|
+
CertificateComponent(),
|
|
402
|
+
EvidenceComponent(),
|
|
403
|
+
CapabilityComponent(),
|
|
404
|
+
ResourceComponent(),
|
|
405
|
+
OutboxComponent(),
|
|
406
|
+
RiskComponent(),
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _require(value: str, field_name: str, envelope: Envelope) -> None:
|
|
411
|
+
if not value:
|
|
412
|
+
raise FoldError(f"{field_name} is required in envelope {envelope.eid}")
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _get(state: dict[str, Any], key: str, label: str, envelope: Envelope) -> dict[str, Any]:
|
|
416
|
+
if not key or key not in state:
|
|
417
|
+
raise FoldError(f"unknown {label} {key!r} in envelope {envelope.eid}")
|
|
418
|
+
record = state[key]
|
|
419
|
+
if not isinstance(record, dict):
|
|
420
|
+
raise FoldError(f"{label} {key!r} has invalid state")
|
|
421
|
+
return record
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _fraction_text(value: Any) -> str:
|
|
425
|
+
return str(Fraction(str(value)))
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Finite problem-frame formation checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .certificates import all_certificates_live
|
|
10
|
+
from .fold import FoldState
|
|
11
|
+
from .model import Frame, Horizon
|
|
12
|
+
from .result import CheckBuilder, CheckResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class FormationProof:
|
|
17
|
+
"""Finite proof object for activating a bounded decision frame."""
|
|
18
|
+
|
|
19
|
+
frame_id: str
|
|
20
|
+
source_evidence: tuple[str, ...]
|
|
21
|
+
goal_witnesses: tuple[str, ...]
|
|
22
|
+
action_witnesses: tuple[str, ...]
|
|
23
|
+
acceptance_witnesses: tuple[str, ...]
|
|
24
|
+
risk_witnesses: tuple[str, ...] = ()
|
|
25
|
+
obligation_witnesses: tuple[str, ...] = ()
|
|
26
|
+
seed: str = "standard"
|
|
27
|
+
certificate_ids: tuple[str, ...] = ()
|
|
28
|
+
transcript_digest: str | None = None
|
|
29
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
30
|
+
|
|
31
|
+
def to_json(self) -> dict[str, Any]:
|
|
32
|
+
return {
|
|
33
|
+
"frame_id": self.frame_id,
|
|
34
|
+
"source_evidence": list(self.source_evidence),
|
|
35
|
+
"goal_witnesses": list(self.goal_witnesses),
|
|
36
|
+
"action_witnesses": list(self.action_witnesses),
|
|
37
|
+
"acceptance_witnesses": list(self.acceptance_witnesses),
|
|
38
|
+
"risk_witnesses": list(self.risk_witnesses),
|
|
39
|
+
"obligation_witnesses": list(self.obligation_witnesses),
|
|
40
|
+
"seed": self.seed,
|
|
41
|
+
"certificate_ids": list(self.certificate_ids),
|
|
42
|
+
"transcript_digest": self.transcript_digest,
|
|
43
|
+
"metadata": dict(self.metadata),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def check_formation(
|
|
48
|
+
state: FoldState, proof: FormationProof, *, at_time: int, horizon: Horizon | None = None
|
|
49
|
+
) -> CheckResult:
|
|
50
|
+
"""Check the semantic fields required before a frame carries authority."""
|
|
51
|
+
|
|
52
|
+
builder = CheckBuilder(
|
|
53
|
+
footprint={
|
|
54
|
+
"FoldKernel",
|
|
55
|
+
"ClauseKernel",
|
|
56
|
+
"IssuerAuthentication",
|
|
57
|
+
"RevocationOracle",
|
|
58
|
+
"ClockWatermark",
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
frames: dict[str, dict[str, Any]] = state.component("frames")
|
|
62
|
+
record = frames.get(proof.frame_id)
|
|
63
|
+
if record is None:
|
|
64
|
+
builder.error("frame-missing", "frame is not present in folded state", location=proof.frame_id)
|
|
65
|
+
return builder.result()
|
|
66
|
+
|
|
67
|
+
frame_data = record.get("frame")
|
|
68
|
+
if not isinstance(frame_data, Mapping):
|
|
69
|
+
builder.error(
|
|
70
|
+
"frame-definition-missing",
|
|
71
|
+
"frame definition payload is missing",
|
|
72
|
+
location=proof.frame_id,
|
|
73
|
+
)
|
|
74
|
+
return builder.result()
|
|
75
|
+
frame = Frame.from_payload(frame_data)
|
|
76
|
+
|
|
77
|
+
evidence = state.component("evidence")
|
|
78
|
+
_require_nonempty(proof.source_evidence, "formation-source", "source evidence is required", builder)
|
|
79
|
+
for evidence_id in proof.source_evidence:
|
|
80
|
+
if evidence_id not in evidence:
|
|
81
|
+
builder.error(
|
|
82
|
+
"evidence-missing",
|
|
83
|
+
"source evidence is absent from the folded state",
|
|
84
|
+
location=evidence_id,
|
|
85
|
+
)
|
|
86
|
+
for evidence_id in frame.evidence_ids:
|
|
87
|
+
if evidence_id not in proof.source_evidence:
|
|
88
|
+
builder.error(
|
|
89
|
+
"frame-evidence-unwitnessed",
|
|
90
|
+
"frame evidence id is not cited by proof",
|
|
91
|
+
location=evidence_id,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if not frame.goal.strip() or not proof.goal_witnesses:
|
|
95
|
+
builder.error("formation-goal", "frame goal must be finite and witnessed", location=proof.frame_id)
|
|
96
|
+
if not frame.actions or not proof.action_witnesses:
|
|
97
|
+
builder.error(
|
|
98
|
+
"formation-action",
|
|
99
|
+
"frame action set must be finite and witnessed",
|
|
100
|
+
location=proof.frame_id,
|
|
101
|
+
)
|
|
102
|
+
if not frame.acceptance or not proof.acceptance_witnesses:
|
|
103
|
+
builder.error(
|
|
104
|
+
"formation-acceptance",
|
|
105
|
+
"acceptance criteria must be finite and witnessed",
|
|
106
|
+
location=proof.frame_id,
|
|
107
|
+
)
|
|
108
|
+
for risk_id in frame.risk_ids:
|
|
109
|
+
if risk_id not in proof.risk_witnesses:
|
|
110
|
+
builder.error("formation-risk", "frame risk id is not witnessed", location=risk_id)
|
|
111
|
+
for obligation in frame.obligations:
|
|
112
|
+
if obligation not in proof.obligation_witnesses:
|
|
113
|
+
builder.error("formation-obligation", "frame obligation is not witnessed", location=obligation)
|
|
114
|
+
|
|
115
|
+
if proof.seed == "residue" and not (proof.risk_witnesses or proof.obligation_witnesses):
|
|
116
|
+
builder.error(
|
|
117
|
+
"formation-residue-route",
|
|
118
|
+
"residue seed requires either risk evidence or diagnostic obligations",
|
|
119
|
+
location=proof.frame_id,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return builder.result().merge(all_certificates_live(state, proof.certificate_ids, at_time, horizon=horizon))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def check_well_audited(state: FoldState) -> CheckResult:
|
|
126
|
+
"""Built-in safety checks for the default components."""
|
|
127
|
+
|
|
128
|
+
builder = CheckBuilder(footprint={"ClauseKernel", "FoldKernel"})
|
|
129
|
+
for cap_id, cap in state.component("capabilities").items():
|
|
130
|
+
if cap.get("status") == "used" and not cap.get("outbox_id"):
|
|
131
|
+
builder.error(
|
|
132
|
+
"capability-use-without-outbox",
|
|
133
|
+
"used capability must cite an outbox",
|
|
134
|
+
location=cap_id,
|
|
135
|
+
)
|
|
136
|
+
for lease_id, lease in state.component("resources").items():
|
|
137
|
+
if lease.get("status") == "consumed" and lease.get("consumed_at") is None:
|
|
138
|
+
builder.error(
|
|
139
|
+
"resource-consume-time-missing",
|
|
140
|
+
"consumed resource must record a time",
|
|
141
|
+
location=lease_id,
|
|
142
|
+
)
|
|
143
|
+
for outbox_id, outbox in state.component("outboxes").items():
|
|
144
|
+
if outbox.get("status") in {
|
|
145
|
+
"claimed",
|
|
146
|
+
"dispatchStarted",
|
|
147
|
+
"actuatorAccepted",
|
|
148
|
+
} and not outbox.get("gate_id"):
|
|
149
|
+
builder.error("outbox-claim-without-gate", "claimed outbox must cite a gate", location=outbox_id)
|
|
150
|
+
return builder.result()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _require_nonempty(values: tuple[str, ...], code: str, message: str, builder: CheckBuilder) -> None:
|
|
154
|
+
if not values:
|
|
155
|
+
builder.error(code, message)
|