dos-kernel 0.22.0__py3-none-win_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. dos/__init__.py +261 -0
  2. dos/_bin/dos-hook.exe +0 -0
  3. dos/_filelock.py +255 -0
  4. dos/_job_policy.py +97 -0
  5. dos/_tree.py +145 -0
  6. dos/admission.py +433 -0
  7. dos/answer_shape.py +299 -0
  8. dos/arbiter.py +859 -0
  9. dos/archive_lock.py +266 -0
  10. dos/arg_provenance.py +814 -0
  11. dos/attest.py +472 -0
  12. dos/breaker.py +311 -0
  13. dos/churn.py +226 -0
  14. dos/claim_extract.py +229 -0
  15. dos/claim_ttl.py +150 -0
  16. dos/cli.py +8721 -0
  17. dos/commit_audit.py +666 -0
  18. dos/completion.py +466 -0
  19. dos/concurrency_class.py +154 -0
  20. dos/config.py +1380 -0
  21. dos/config_lint.py +464 -0
  22. dos/cooldown.py +390 -0
  23. dos/coverage.py +387 -0
  24. dos/dangling_intent.py +287 -0
  25. dos/data_class.py +397 -0
  26. dos/decisions.py +1274 -0
  27. dos/decisions_tui.py +251 -0
  28. dos/dispatch_top.py +740 -0
  29. dos/dispatch_top_tui.py +116 -0
  30. dos/drivers/__init__.py +40 -0
  31. dos/drivers/ci_status.py +630 -0
  32. dos/drivers/citation_resolve.py +703 -0
  33. dos/drivers/decision_stop.py +98 -0
  34. dos/drivers/export_file.py +173 -0
  35. dos/drivers/export_otlp.py +275 -0
  36. dos/drivers/export_statsd.py +242 -0
  37. dos/drivers/hook_dialects.py +391 -0
  38. dos/drivers/job.py +47 -0
  39. dos/drivers/llm_judge.py +360 -0
  40. dos/drivers/memory_recall.py +1231 -0
  41. dos/drivers/notify_slack.py +373 -0
  42. dos/drivers/notify_webhook.py +251 -0
  43. dos/drivers/operator_judge.py +114 -0
  44. dos/drivers/os_acceptance.py +228 -0
  45. dos/drivers/paste_log.py +132 -0
  46. dos/drivers/plan_scope.py +133 -0
  47. dos/drivers/self_improve.py +375 -0
  48. dos/drivers/similarity_judge.py +249 -0
  49. dos/drivers/state_diff.py +274 -0
  50. dos/drivers/supervisor.py +347 -0
  51. dos/drivers/watchdog.py +363 -0
  52. dos/drivers/workshop.py +160 -0
  53. dos/durable_schema.py +344 -0
  54. dos/effect_witness.py +393 -0
  55. dos/efficiency.py +318 -0
  56. dos/enforce.py +414 -0
  57. dos/enumerate.py +776 -0
  58. dos/env_print.py +378 -0
  59. dos/event_severity.py +258 -0
  60. dos/evidence.py +692 -0
  61. dos/exec_capability.py +256 -0
  62. dos/export_cursor.py +143 -0
  63. dos/exporter.py +320 -0
  64. dos/firing_label.py +353 -0
  65. dos/fleet_roll.py +226 -0
  66. dos/gate_classify.py +827 -0
  67. dos/gh4_coverage.py +179 -0
  68. dos/git_delta.py +122 -0
  69. dos/guard.py +215 -0
  70. dos/health.py +552 -0
  71. dos/help_summary.py +519 -0
  72. dos/home.py +934 -0
  73. dos/hook_binary.py +194 -0
  74. dos/hook_dialect.py +271 -0
  75. dos/hook_exit.py +191 -0
  76. dos/hook_install.py +437 -0
  77. dos/id_alloc.py +304 -0
  78. dos/improve.py +499 -0
  79. dos/intent_ledger.py +635 -0
  80. dos/interpret.py +176 -0
  81. dos/intervention.py +769 -0
  82. dos/intervention_eval.py +371 -0
  83. dos/journal_delta.py +308 -0
  84. dos/judge_eval.py +328 -0
  85. dos/judges.py +366 -0
  86. dos/lane_infer.py +127 -0
  87. dos/lane_journal.py +1001 -0
  88. dos/lane_lease.py +952 -0
  89. dos/lane_overlap.py +228 -0
  90. dos/lease_health.py +282 -0
  91. dos/lifecycle.py +211 -0
  92. dos/liveness.py +352 -0
  93. dos/lock_modes.py +185 -0
  94. dos/log_source.py +395 -0
  95. dos/loop_decide.py +1746 -0
  96. dos/marker_gate.py +254 -0
  97. dos/marker_sensor.py +396 -0
  98. dos/noop_streak.py +280 -0
  99. dos/notify.py +479 -0
  100. dos/observe.py +175 -0
  101. dos/oracle.py +1661 -0
  102. dos/overlap_eval.py +214 -0
  103. dos/overlap_policy.py +342 -0
  104. dos/packet_sidecar.py +267 -0
  105. dos/phase_shipped.py +1985 -0
  106. dos/pick_priority.py +225 -0
  107. dos/pickable.py +369 -0
  108. dos/picker_oracle.py +1037 -0
  109. dos/plan_board.py +513 -0
  110. dos/plan_board_tui.py +113 -0
  111. dos/plan_source.py +455 -0
  112. dos/posttool_sensor.py +528 -0
  113. dos/precursor_gate.py +499 -0
  114. dos/precursor_gate_eval.py +239 -0
  115. dos/preflight.py +825 -0
  116. dos/pretool_sensor.py +490 -0
  117. dos/proc_delta.py +181 -0
  118. dos/productivity.py +296 -0
  119. dos/provider_limit.py +242 -0
  120. dos/py.typed +4 -0
  121. dos/reason_morphology.py +299 -0
  122. dos/reasons.py +449 -0
  123. dos/reconcile.py +173 -0
  124. dos/recurring_wedge.py +206 -0
  125. dos/render.py +393 -0
  126. dos/result_state.py +468 -0
  127. dos/resume.py +578 -0
  128. dos/resume_evidence.py +293 -0
  129. dos/retention.py +344 -0
  130. dos/reward.py +372 -0
  131. dos/rewind.py +587 -0
  132. dos/rewind_evidence.py +168 -0
  133. dos/rewind_tokens.py +252 -0
  134. dos/run_id.py +342 -0
  135. dos/scope.py +520 -0
  136. dos/scope_source.py +382 -0
  137. dos/scout.py +982 -0
  138. dos/self_modify.py +209 -0
  139. dos/sibling_scan.py +569 -0
  140. dos/skills/EXAMPLES.md +584 -0
  141. dos/skills/dos-class-cycle/SKILL.md +107 -0
  142. dos/skills/dos-dispatch/SKILL.md +177 -0
  143. dos/skills/dos-dispatch-loop/SKILL.md +254 -0
  144. dos/skills/dos-goal-gate/SKILL.md +269 -0
  145. dos/skills/dos-next-up/SKILL.md +231 -0
  146. dos/skills/dos-promote/SKILL.md +114 -0
  147. dos/skills/dos-replan/SKILL.md +159 -0
  148. dos/skills/dos-replan-loop/SKILL.md +114 -0
  149. dos/skills/dos-self-improve/SKILL.md +213 -0
  150. dos/skills/dos-supervise-loop/SKILL.md +180 -0
  151. dos/skills/dos-unstick/SKILL.md +108 -0
  152. dos/skills/dos-witness-claim/SKILL.md +251 -0
  153. dos/stamp.py +1002 -0
  154. dos/state_health.py +387 -0
  155. dos/status.py +114 -0
  156. dos/stop_policy.py +334 -0
  157. dos/supervise.py +1014 -0
  158. dos/testwitness.py +392 -0
  159. dos/timeline.py +1027 -0
  160. dos/tokens.py +485 -0
  161. dos/tool_stream.py +393 -0
  162. dos/tool_stream_eval.py +226 -0
  163. dos/trace.py +524 -0
  164. dos/verdict.py +140 -0
  165. dos/verdict_cli.py +189 -0
  166. dos/verdict_journal.py +497 -0
  167. dos/verdict_rollup.py +217 -0
  168. dos/verdicts.py +181 -0
  169. dos/wedge_reason.py +282 -0
  170. dos_kernel-0.22.0.dist-info/METADATA +859 -0
  171. dos_kernel-0.22.0.dist-info/RECORD +178 -0
  172. dos_kernel-0.22.0.dist-info/WHEEL +5 -0
  173. dos_kernel-0.22.0.dist-info/entry_points.txt +39 -0
  174. dos_kernel-0.22.0.dist-info/licenses/LICENSE +21 -0
  175. dos_kernel-0.22.0.dist-info/top_level.txt +2 -0
  176. dos_mcp/__init__.py +52 -0
  177. dos_mcp/py.typed +2 -0
  178. dos_mcp/server.py +779 -0
dos/attest.py ADDED
@@ -0,0 +1,472 @@
1
+ """attest — the portable, signed receipt over an effect-witness verdict (docs/246).
2
+
3
+ The missing **non-participant** surface over the already-shipped `effect_witness`
4
+ engine. `effect_witness.witness_effect(claim, readbacks)` already does the hard,
5
+ soundness-load-bearing part — it joins two independently-authored facts (the agent's
6
+ forgeable claim + an independent, accountable read-back of world state) and returns a
7
+ four-valued verdict whose trust is *capped by the read-back's accountability*. But it
8
+ returns that verdict **to the caller** — to the loop, or to an operator running the
9
+ witness drivers over their own fleet. Every surface that consumes it today presumes
10
+ the caller is the agent or its operator: a party *inside* the loop.
11
+
12
+ This module mints the surface for the party who was **not present** — an auditor at
13
+ quarter-end, an inspector general, a counterparty in an agent-to-agent transaction, an
14
+ allied partner verifying a shared system. It wraps the verdict in a portable
15
+ **`Receipt`**, signs the verdict *together with which witness authored the read-back
16
+ and at what accountability tier*, and emits a record a skeptic verifies with the
17
+ public/shared half of the signing key **alone** — without access to the agent, the
18
+ operator, or the original loop. The DocuSign step (a private check → a record a
19
+ non-participant can verify) applied to the kernel's existing notary engine.
20
+
21
+ The economy: this writes NO new decision logic
22
+ ===============================================
23
+
24
+ Identical in spirit to docs/126 §2 (*"the same verdicts, made binding — not new
25
+ policy"*): the verdict the receipt carries is `witness_effect`'s, untouched. Three
26
+ facts make the receipt a packaging layer, not a subsystem:
27
+
28
+ 1. The verdict already exists, four-valued and frozen — an `EffectWitnessVerdict`
29
+ with exactly the fields a certificate needs (`verdict`, `believe`, `refuted`,
30
+ `claim_key`, `narrated`, `witness`, `accountability`). The receipt is that data,
31
+ plus a timestamp and a signature.
32
+ 2. The floor discipline is already structural and lives in ONE place
33
+ (`evidence.believe_under_floor`, which `witness_effect` delegates to). A receipt
34
+ signed over a forgeable-floor read-back is, by construction, an `UNWITNESSED`
35
+ (never a `CONFIRMED`). The notary cannot be one of the signers, and that is
36
+ enforced *upstream of the signature*, not by it — so this module never re-asserts
37
+ it; it inherits it by carrying the verdict the engine already gated.
38
+ 3. The read-back authors already ship as drivers (`os_acceptance` → the OS authors
39
+ the exit code; `state_diff` → the store authors the delta). This module does not
40
+ invent a witness; it *records the witness's name and tier into the signed
41
+ payload* so a third party sees the rung the verdict rests on.
42
+
43
+ What the receipt is — and is NOT (the honest scope, docs/246 §2/§3.3)
44
+ =====================================================================
45
+
46
+ A `Receipt` is a notarized statement of *presence at an accountability tier and a
47
+ time* — exactly the scope a notary has always had (*this party signed this document on
48
+ this date*; it says nothing about whether the deal was good). It is the Wall §3
49
+ ceiling (docs/204), inherited unchanged from the engine: `witness_effect` verifies
50
+ **claim ⊆ witnessed-delta** (was the specific change the agent took credit for actually
51
+ made?), NOT *"is the end-state globally correct / wise / intended?"*. Correctness-of-
52
+ intent routes where it always has: ORACLE → JUDGE → HUMAN (docs/86). And a receipt
53
+ mints **evidentiary** weight (a tamper-evident, independently-authored record), not
54
+ **legal** weight — the honest starting position DocuSign itself started from.
55
+
56
+ Three load-bearing constraints on the Receipt shape:
57
+
58
+ * **The signed payload includes the witness's author and tier, not just the verdict
59
+ token.** A bare signed `CONFIRMED` tells a skeptic nothing about *what was checked*.
60
+ The receipt signs `witness_author` and `accountability_tier` INTO the payload so the
61
+ verdict is read together with the rung it stands on — the chain-of-custody field.
62
+ * **`REFUTED` is the load-bearing receipt** — the adverse certificate: a confidently-
63
+ narrated success the world does not corroborate, made portable. The receipt a
64
+ dispute / audit / after-action review turns on. It must be as easy to mint and as
65
+ cryptographically solid as `CONFIRMED`.
66
+ * **`UNWITNESSED` must stay LOUD and distinct from `REFUTED`** — the single most
67
+ important honesty rule. `UNWITNESSED` = *could-not-tell*; `REFUTED` = *checked-and-
68
+ absent*. Collapsing them would let a notary that merely failed to reach a witness
69
+ emit a false adverse finding. They stay separate verdict tokens.
70
+
71
+ The ONE place that fails LOUD
72
+ =============================
73
+
74
+ Every other DOS verdict degrades *quietly* toward abstain (fail-safe, never fail-open).
75
+ The **signature path is the one place that must fail LOUD**: an invalid signature, a
76
+ tampered field, or a canonical-serialization mismatch makes the receipt **invalid**,
77
+ surfaced as such — never downgraded to "unsigned but probably fine," never silently
78
+ accepted. A notary whose stamp is forgeable-without-detection is not a notary.
79
+
80
+ Purity & layering
81
+ =================
82
+
83
+ Pure stdlib — a frozen `Receipt` value type, the canonical serialization (the one
84
+ place a serialization bug would be a security bug, so it is pinned, not left to a
85
+ library default), and the HMAC sign/verify (`hmac` + `hashlib`, already in the kernel's
86
+ dependency set — `hashlib.sha256` is used in `home.py`/`posttool_sensor.py`/`rewind.py`
87
+ today, so this adds NO new dependency and keeps the PyYAML-only core intact). It names
88
+ no host and no vendor in code. The *which-algorithm* is policy chosen at the boundary
89
+ (the `--sign` flag); the *what-is-signed* (the canonical Receipt payload) is fixed
90
+ mechanism here. The asymmetric (public-key) signer is Phase 2, behind the `[attest]`
91
+ extra — it arrives as a by-name `Signer` the boundary resolves, the same kernel/driver
92
+ split as `judges`/`overlap_policy`, so the kernel core stays dependency-free. The
93
+ read-back is gathered at the CLI boundary (`evidence.gather_evidence` over a witness
94
+ driver); this module only PACKAGES an already-computed verdict + SIGNS it.
95
+ """
96
+
97
+ from __future__ import annotations
98
+
99
+ import enum
100
+ import hashlib
101
+ import hmac
102
+ from dataclasses import dataclass
103
+
104
+ from dos.effect_witness import EffectWitnessVerdict
105
+ from dos.evidence import Accountability
106
+
107
+ __all__ = [
108
+ "Receipt",
109
+ "SignatureAlgorithm",
110
+ "canonical_bytes",
111
+ "sign_hmac",
112
+ "verify_hmac",
113
+ "receipt_from_verdict",
114
+ "VerifyResult",
115
+ "ATTEST_KEY_ENV",
116
+ ]
117
+
118
+ # The env var the HMAC key is read from when no --key-file is given. Named here (not a
119
+ # bare literal at the CLI) so the module and the CLI read the SAME name.
120
+ ATTEST_KEY_ENV = "DOS_ATTEST_KEY"
121
+
122
+
123
+ class SignatureAlgorithm(str, enum.Enum):
124
+ """Which signing primitive minted/checks a receipt's signature.
125
+
126
+ A DATA field on the receipt, carried INTO the canonical payload (so the algorithm
127
+ is itself signed — a verifier cannot be fooled into checking an Ed25519 receipt as
128
+ if it were HMAC, or vice-versa; the alg is part of what the signature commits to).
129
+ `str`-valued so it round-trips through a CLI token / JSON without a lookup table
130
+ (the `Accountability` / `EffectStance` idiom).
131
+
132
+ HMAC_SHA256 — shared-secret. Cheap, stdlib-only, NO new dependency. The right
133
+ tool when the verifier SHARES a secret with the issuer (an internal
134
+ auditor, a same-org oversight function, a CI gate). Its hard limit:
135
+ anyone who can VERIFY an HMAC receipt can also FORGE one (the secret
136
+ is symmetric), so it cannot serve the non-participant notary case.
137
+ Phase 1 (docs/246 §3.1).
138
+ ED25519 — asymmetric/public-key. For a third party who does NOT share a
139
+ secret (the counterparty, the regulator, the allied partner):
140
+ verification uses the PUBLIC half while only the issuer holds the
141
+ private half — the DocuSign property (verify, cannot forge). Needs a
142
+ signing primitive the near-stdlib kernel lacks (`cryptography`), so
143
+ it arrives behind the `[attest]` extra. Phase 2 — the token is
144
+ reserved here so a Phase-1 receipt's `algorithm` field is already
145
+ drawn from the closed set the Phase-2 verifier will dispatch on.
146
+ """
147
+
148
+ HMAC_SHA256 = "HMAC-SHA256"
149
+ ED25519 = "ED25519"
150
+
151
+ def __str__(self) -> str: # pragma: no cover - trivial
152
+ return self.value
153
+
154
+
155
+ # The field order the canonical serialization commits to. FIXED and explicit (not the
156
+ # dataclass field order, not a dict's insertion order, not sorted-at-write) so the
157
+ # issuer and a verifier reconstruct byte-identical input independent of language /
158
+ # library / dataclass evolution. Adding a field here is a signature-contract change
159
+ # (old receipts would no longer round-trip) — deliberate, never incidental.
160
+ _CANONICAL_FIELDS: tuple[str, ...] = (
161
+ "schema",
162
+ "claim",
163
+ "narrated",
164
+ "witness_surface",
165
+ "witness_author",
166
+ "accountability_tier",
167
+ "verdict",
168
+ "believe",
169
+ "refuted",
170
+ "timestamp",
171
+ "algorithm",
172
+ )
173
+
174
+ # The receipt schema tag — versioned so a future field addition is a NEW schema a
175
+ # verifier can branch on, never a silent reinterpretation of v1 bytes.
176
+ _SCHEMA = "dos.attest/receipt@1"
177
+
178
+
179
+ @dataclass(frozen=True)
180
+ class Receipt:
181
+ """A portable, signed certificate over one effect-witness verdict (docs/246 §2).
182
+
183
+ Every field is either echoed from the `EffectWitnessVerdict` or added by the act of
184
+ signing. It is verifiable by a third party holding the public/shared half of the
185
+ signing key, WITHOUT access to the agent, the operator, or the original loop.
186
+
187
+ claim — the opaque effect key the agent asserted (`EffectClaim.key`)
188
+ narrated — the agent's original phrasing — SHOWN, never parsed for truth
189
+ witness_surface — the read-back subject (the command run / the state-key probed)
190
+ witness_author — the witness `source_name` (e.g. "os_acceptance", "state_diff")
191
+ accountability_tier — the read-back's rung: OS_RECORDED / THIRD_PARTY /
192
+ AGENT_AUTHORED. The chain-of-custody field — *who/what
193
+ witnessed this, provably*. `None` only for NO_CLAIM /
194
+ UNWITNESSED, where no accountable witness stood behind a
195
+ verdict; serialized as the empty string so the canonical
196
+ bytes are still well-defined.
197
+ verdict — CONFIRMED | REFUTED | UNWITNESSED | NO_CLAIM
198
+ believe — the positive bit, True ONLY on CONFIRMED (echoed so a
199
+ verifier need not re-derive it from the token)
200
+ refuted — surfaced separately so a consumer may red-flag the adverse
201
+ certificate even though `believe` is also False
202
+ timestamp — when the attestation was minted (RFC 3339 / ISO-8601 UTC).
203
+ Supplied by the CALLER (the clock is boundary I/O — this
204
+ module is pure and never reads the wall clock itself).
205
+ algorithm — which `SignatureAlgorithm` minted `signature`
206
+ signature — hex over the canonical serialization of ALL the above
207
+ (the `_CANONICAL_FIELDS`, which INCLUDES `algorithm` and the
208
+ schema tag — so the alg and version are themselves signed).
209
+ Empty string on an UNSIGNED receipt (the pure payload before
210
+ the one boundary signing step).
211
+
212
+ The `verdict` / `believe` / `refuted` / `accountability_tier` are NOT recomputed
213
+ here — they are the engine's, carried verbatim. The floor discipline that makes
214
+ `believe=True ⟹ a non-forgeable witness attested` is enforced UPSTREAM, in
215
+ `witness_effect` / `believe_under_floor`; a receipt cannot manufacture a CONFIRMED
216
+ the engine did not grant, because it only ever copies the engine's fields.
217
+ """
218
+
219
+ claim: str
220
+ narrated: str
221
+ witness_surface: str
222
+ witness_author: str
223
+ accountability_tier: Accountability | None
224
+ verdict: str
225
+ believe: bool
226
+ refuted: bool
227
+ timestamp: str
228
+ algorithm: SignatureAlgorithm = SignatureAlgorithm.HMAC_SHA256
229
+ signature: str = ""
230
+ schema: str = _SCHEMA
231
+
232
+ # -- the canonical, signature-committed view -----------------------------
233
+ def _canonical_view(self) -> dict[str, object]:
234
+ """The exact, ordered field→value mapping the signature commits to.
235
+
236
+ Deliberately EXCLUDES `signature` itself (you cannot sign your own signature)
237
+ and normalizes the two non-string fields the canonical bytes must pin: the
238
+ accountability tier (its `.value`, or "" when absent) and the algorithm (its
239
+ `.value`). Bools are emitted as the lowercase JSON literals `true`/`false` in
240
+ `canonical_bytes`, never Python's `True`/`False` repr."""
241
+ return {
242
+ "schema": self.schema,
243
+ "claim": self.claim,
244
+ "narrated": self.narrated,
245
+ "witness_surface": self.witness_surface,
246
+ "witness_author": self.witness_author,
247
+ "accountability_tier": (
248
+ self.accountability_tier.value if self.accountability_tier else ""
249
+ ),
250
+ "verdict": self.verdict,
251
+ "believe": self.believe,
252
+ "refuted": self.refuted,
253
+ "timestamp": self.timestamp,
254
+ "algorithm": self.algorithm.value,
255
+ }
256
+
257
+ def canonical_bytes(self) -> bytes:
258
+ """The bytes the signature is computed over — see `canonical_bytes`."""
259
+ return canonical_bytes(self)
260
+
261
+ # -- serialization for the wire / operator surface -----------------------
262
+ def to_dict(self) -> dict:
263
+ """The full JSON shape, INCLUDING the signature, for `--json` / MCP / a file.
264
+
265
+ This is the receipt as a third party receives it. `from_dict` reconstructs an
266
+ identical `Receipt` (and therefore identical `canonical_bytes`) from it — the
267
+ round-trip the verify path depends on.
268
+ """
269
+ d = dict(self._canonical_view())
270
+ d["signature"] = self.signature
271
+ return d
272
+
273
+ @classmethod
274
+ def from_dict(cls, d: dict) -> "Receipt":
275
+ """Reconstruct a `Receipt` from its `to_dict()` form (or a hand-written one).
276
+
277
+ Tolerant of the tier/algorithm being given as their string tokens (the wire
278
+ form) — it maps them back to the enums. An empty/missing `accountability_tier`
279
+ becomes `None`. An unknown algorithm/tier token raises (loud, never a silent
280
+ default): a verifier handed a receipt naming an algorithm it does not know must
281
+ not pretend to have checked it.
282
+ """
283
+ tier_tok = (d.get("accountability_tier") or "").strip()
284
+ tier = Accountability(tier_tok) if tier_tok else None
285
+ alg_tok = d.get("algorithm") or SignatureAlgorithm.HMAC_SHA256.value
286
+ algorithm = SignatureAlgorithm(alg_tok)
287
+ return cls(
288
+ claim=d.get("claim", ""),
289
+ narrated=d.get("narrated", ""),
290
+ witness_surface=d.get("witness_surface", ""),
291
+ witness_author=d.get("witness_author", ""),
292
+ accountability_tier=tier,
293
+ verdict=d.get("verdict", ""),
294
+ believe=bool(d.get("believe", False)),
295
+ refuted=bool(d.get("refuted", False)),
296
+ timestamp=d.get("timestamp", ""),
297
+ algorithm=algorithm,
298
+ signature=d.get("signature", ""),
299
+ schema=d.get("schema", _SCHEMA),
300
+ )
301
+
302
+ def with_signature(self, signature: str) -> "Receipt":
303
+ """A copy carrying `signature` — the one mutation, applied at the boundary
304
+ signing step (the dataclass is frozen, so this returns a new instance)."""
305
+ return Receipt(
306
+ claim=self.claim,
307
+ narrated=self.narrated,
308
+ witness_surface=self.witness_surface,
309
+ witness_author=self.witness_author,
310
+ accountability_tier=self.accountability_tier,
311
+ verdict=self.verdict,
312
+ believe=self.believe,
313
+ refuted=self.refuted,
314
+ timestamp=self.timestamp,
315
+ algorithm=self.algorithm,
316
+ signature=signature,
317
+ schema=self.schema,
318
+ )
319
+
320
+
321
+ def canonical_bytes(receipt: Receipt) -> bytes:
322
+ """The canonical serialization the signature is computed/checked over (docs/246 §3.2).
323
+
324
+ A signature is only checkable if the issuer and the verifier serialize the payload
325
+ BYTE-IDENTICALLY. A naive `json.dumps` is NOT canonical (key order, whitespace,
326
+ unicode escaping, and the bool/null spelling all vary across libraries and
327
+ languages). So this fixes the form once and signs over THAT:
328
+
329
+ * an explicit, FIXED field order (`_CANONICAL_FIELDS`) — never the dataclass
330
+ order, never a dict's insertion order, never sorted-at-write;
331
+ * each field rendered as ``key=value`` on its own line, the lines joined by ``\\n``;
332
+ * values are the raw UTF-8 text (the tier/algorithm already reduced to their
333
+ tokens, bools to the lowercase literals `true`/`false`); a missing value is the
334
+ empty string;
335
+ * the whole encoded UTF-8, with NO insignificant whitespace.
336
+
337
+ `signature` is excluded (you cannot sign the signature) but the schema tag and the
338
+ `algorithm` ARE included — so the version and the signing primitive are themselves
339
+ committed to. A verifier reconstructs these exact bytes from the receipt's fields
340
+ and checks the signature against them; a receipt whose canonical re-serialization
341
+ does not match its signature is INVALID, loudly (never silently downgraded). This
342
+ is line-oriented and `=`-delimited on purpose: the keys are a fixed closed set with
343
+ no `=`/newline in them, so the encoding is unambiguous without a JSON parser, and a
344
+ non-Python verifier can reproduce it trivially.
345
+
346
+ PURE — no I/O, no clock, no randomness.
347
+ """
348
+ view = receipt._canonical_view()
349
+ lines: list[str] = []
350
+ for key in _CANONICAL_FIELDS:
351
+ value = view[key]
352
+ if isinstance(value, bool):
353
+ rendered = "true" if value else "false"
354
+ else:
355
+ rendered = str(value)
356
+ lines.append(f"{key}={rendered}")
357
+ return "\n".join(lines).encode("utf-8")
358
+
359
+
360
+ def sign_hmac(receipt: Receipt, key: bytes) -> str:
361
+ """Sign a receipt's canonical bytes with HMAC-SHA256, returning the hex digest.
362
+
363
+ The Phase-1 signer: stdlib `hmac` + `hashlib.sha256`, NO new dependency. The
364
+ receipt's `algorithm` must be `HMAC_SHA256` (it is, by default) — the alg is part of
365
+ the canonical bytes, so signing commits to it. Returns the hex MAC; the caller wraps
366
+ it back in with `receipt.with_signature(...)`.
367
+
368
+ PURE given the key (the key was read at the boundary from `--key-file`/`$DOS_ATTEST_KEY`).
369
+ """
370
+ mac = hmac.new(key, canonical_bytes(receipt), hashlib.sha256)
371
+ return mac.hexdigest()
372
+
373
+
374
+ def verify_hmac(receipt: Receipt, key: bytes) -> "VerifyResult":
375
+ """Check an HMAC receipt's signature against its canonical bytes (constant-time).
376
+
377
+ The one place that must fail LOUD (docs/246 §5): a signature that does not match the
378
+ re-derived canonical bytes (a tampered field, a wrong key, a forged stamp) yields an
379
+ INVALID result — never a silent downgrade to "unsigned but probably fine." Uses
380
+ `hmac.compare_digest` so the check is constant-time (a non-constant-time `==` on a
381
+ MAC leaks a timing side-channel a forger can walk).
382
+
383
+ Returns a `VerifyResult` (valid + reason); the caller maps it to an exit code /
384
+ a rendered line. PURE given the key.
385
+ """
386
+ if receipt.algorithm is not SignatureAlgorithm.HMAC_SHA256:
387
+ return VerifyResult(
388
+ valid=False,
389
+ reason=(
390
+ f"INVALID — receipt names algorithm {receipt.algorithm.value!r}, but "
391
+ f"this is the HMAC verifier; verify it with the matching algorithm"
392
+ ),
393
+ )
394
+ if not receipt.signature:
395
+ return VerifyResult(
396
+ valid=False,
397
+ reason="INVALID — receipt carries no signature (unsigned payload, not a certificate)",
398
+ )
399
+ expected = sign_hmac(receipt, key)
400
+ # compare_digest over the hex strings — constant-time, and equal length for a
401
+ # well-formed pair (a malformed signature simply fails to match, never raises).
402
+ if hmac.compare_digest(expected, receipt.signature):
403
+ return VerifyResult(
404
+ valid=True,
405
+ reason=(
406
+ f"VALID — HMAC signature matches the canonical payload "
407
+ f"(verdict {receipt.verdict}, tier "
408
+ f"{receipt.accountability_tier.value if receipt.accountability_tier else '-'})"
409
+ ),
410
+ )
411
+ return VerifyResult(
412
+ valid=False,
413
+ reason=(
414
+ "INVALID — HMAC signature does NOT match the canonical payload: a field was "
415
+ "tampered, the wrong key was used, or the signature was forged"
416
+ ),
417
+ )
418
+
419
+
420
+ @dataclass(frozen=True)
421
+ class VerifyResult:
422
+ """The result of checking a receipt's signature — valid + a legible reason.
423
+
424
+ `valid` is the binary judgment a verifier acts on; `reason` is the one-line
425
+ legible-distrust string (rendered to the operator / `--json`). Deliberately tiny:
426
+ signature verification answers one question (does the stamp hold?), distinct from
427
+ the verdict the receipt CARRIES (CONFIRMED/REFUTED/…). A VALID receipt still has a
428
+ verdict the verifier reads; an INVALID one is not to be trusted at all.
429
+ """
430
+
431
+ valid: bool
432
+ reason: str
433
+
434
+ def to_dict(self) -> dict:
435
+ return {"valid": self.valid, "reason": self.reason}
436
+
437
+
438
+ def receipt_from_verdict(
439
+ verdict: EffectWitnessVerdict,
440
+ *,
441
+ timestamp: str,
442
+ witness_surface: str = "",
443
+ algorithm: SignatureAlgorithm = SignatureAlgorithm.HMAC_SHA256,
444
+ ) -> Receipt:
445
+ """Build the UNSIGNED `Receipt` for an already-computed effect-witness verdict.
446
+
447
+ The packaging step, pure: it copies the verdict's fields into the receipt shape and
448
+ stamps the caller-supplied `timestamp` (the clock is boundary I/O — this module
449
+ never reads the wall clock). `witness_surface` is the read-back subject (the command
450
+ run / the state-key probed); it is carried for the operator surface and defaults to
451
+ the verdict's `claim_key` when the caller does not distinguish them. The result has
452
+ an EMPTY signature — the one boundary signing step (`sign_hmac` + `with_signature`)
453
+ happens at the CLI, where the key lives.
454
+
455
+ The verdict's four-valued token, its `believe`/`refuted` bits, and its witness +
456
+ accountability tier are carried VERBATIM — so a receipt can never assert a CONFIRMED
457
+ the engine did not grant, nor collapse UNWITNESSED into REFUTED (they remain the
458
+ distinct tokens `witness_effect` produced; docs/246 §2.3).
459
+ """
460
+ return Receipt(
461
+ claim=verdict.claim_key,
462
+ narrated=verdict.narrated,
463
+ witness_surface=witness_surface or verdict.claim_key,
464
+ witness_author=verdict.witness,
465
+ accountability_tier=verdict.accountability,
466
+ verdict=verdict.verdict.value,
467
+ believe=verdict.believe,
468
+ refuted=verdict.refuted,
469
+ timestamp=timestamp,
470
+ algorithm=algorithm,
471
+ signature="",
472
+ )