dos-kernel 0.22.0__py3-none-win_amd64.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.
- dos/__init__.py +261 -0
- dos/_bin/dos-hook.exe +0 -0
- dos/_filelock.py +255 -0
- dos/_job_policy.py +97 -0
- dos/_tree.py +145 -0
- dos/admission.py +433 -0
- dos/answer_shape.py +299 -0
- dos/arbiter.py +859 -0
- dos/archive_lock.py +266 -0
- dos/arg_provenance.py +814 -0
- dos/attest.py +472 -0
- dos/breaker.py +311 -0
- dos/churn.py +226 -0
- dos/claim_extract.py +229 -0
- dos/claim_ttl.py +150 -0
- dos/cli.py +8721 -0
- dos/commit_audit.py +666 -0
- dos/completion.py +466 -0
- dos/concurrency_class.py +154 -0
- dos/config.py +1380 -0
- dos/config_lint.py +464 -0
- dos/cooldown.py +390 -0
- dos/coverage.py +387 -0
- dos/dangling_intent.py +287 -0
- dos/data_class.py +397 -0
- dos/decisions.py +1274 -0
- dos/decisions_tui.py +251 -0
- dos/dispatch_top.py +740 -0
- dos/dispatch_top_tui.py +116 -0
- dos/drivers/__init__.py +40 -0
- dos/drivers/ci_status.py +630 -0
- dos/drivers/citation_resolve.py +703 -0
- dos/drivers/decision_stop.py +98 -0
- dos/drivers/export_file.py +173 -0
- dos/drivers/export_otlp.py +275 -0
- dos/drivers/export_statsd.py +242 -0
- dos/drivers/hook_dialects.py +391 -0
- dos/drivers/job.py +47 -0
- dos/drivers/llm_judge.py +360 -0
- dos/drivers/memory_recall.py +1231 -0
- dos/drivers/notify_slack.py +373 -0
- dos/drivers/notify_webhook.py +251 -0
- dos/drivers/operator_judge.py +114 -0
- dos/drivers/os_acceptance.py +228 -0
- dos/drivers/paste_log.py +132 -0
- dos/drivers/plan_scope.py +133 -0
- dos/drivers/self_improve.py +375 -0
- dos/drivers/similarity_judge.py +249 -0
- dos/drivers/state_diff.py +274 -0
- dos/drivers/supervisor.py +347 -0
- dos/drivers/watchdog.py +363 -0
- dos/drivers/workshop.py +160 -0
- dos/durable_schema.py +344 -0
- dos/effect_witness.py +393 -0
- dos/efficiency.py +318 -0
- dos/enforce.py +414 -0
- dos/enumerate.py +776 -0
- dos/env_print.py +378 -0
- dos/event_severity.py +258 -0
- dos/evidence.py +692 -0
- dos/exec_capability.py +256 -0
- dos/export_cursor.py +143 -0
- dos/exporter.py +320 -0
- dos/firing_label.py +353 -0
- dos/fleet_roll.py +226 -0
- dos/gate_classify.py +827 -0
- dos/gh4_coverage.py +179 -0
- dos/git_delta.py +122 -0
- dos/guard.py +215 -0
- dos/health.py +552 -0
- dos/help_summary.py +519 -0
- dos/home.py +934 -0
- dos/hook_binary.py +194 -0
- dos/hook_dialect.py +271 -0
- dos/hook_exit.py +191 -0
- dos/hook_install.py +437 -0
- dos/id_alloc.py +304 -0
- dos/improve.py +499 -0
- dos/intent_ledger.py +635 -0
- dos/interpret.py +176 -0
- dos/intervention.py +769 -0
- dos/intervention_eval.py +371 -0
- dos/journal_delta.py +308 -0
- dos/judge_eval.py +328 -0
- dos/judges.py +366 -0
- dos/lane_infer.py +127 -0
- dos/lane_journal.py +1001 -0
- dos/lane_lease.py +952 -0
- dos/lane_overlap.py +228 -0
- dos/lease_health.py +282 -0
- dos/lifecycle.py +211 -0
- dos/liveness.py +352 -0
- dos/lock_modes.py +185 -0
- dos/log_source.py +395 -0
- dos/loop_decide.py +1746 -0
- dos/marker_gate.py +254 -0
- dos/marker_sensor.py +396 -0
- dos/noop_streak.py +280 -0
- dos/notify.py +479 -0
- dos/observe.py +175 -0
- dos/oracle.py +1661 -0
- dos/overlap_eval.py +214 -0
- dos/overlap_policy.py +342 -0
- dos/packet_sidecar.py +267 -0
- dos/phase_shipped.py +1985 -0
- dos/pick_priority.py +225 -0
- dos/pickable.py +369 -0
- dos/picker_oracle.py +1037 -0
- dos/plan_board.py +513 -0
- dos/plan_board_tui.py +113 -0
- dos/plan_source.py +455 -0
- dos/posttool_sensor.py +528 -0
- dos/precursor_gate.py +499 -0
- dos/precursor_gate_eval.py +239 -0
- dos/preflight.py +825 -0
- dos/pretool_sensor.py +490 -0
- dos/proc_delta.py +181 -0
- dos/productivity.py +296 -0
- dos/provider_limit.py +242 -0
- dos/py.typed +4 -0
- dos/reason_morphology.py +299 -0
- dos/reasons.py +449 -0
- dos/reconcile.py +173 -0
- dos/recurring_wedge.py +206 -0
- dos/render.py +393 -0
- dos/result_state.py +468 -0
- dos/resume.py +578 -0
- dos/resume_evidence.py +293 -0
- dos/retention.py +344 -0
- dos/reward.py +372 -0
- dos/rewind.py +587 -0
- dos/rewind_evidence.py +168 -0
- dos/rewind_tokens.py +252 -0
- dos/run_id.py +342 -0
- dos/scope.py +520 -0
- dos/scope_source.py +382 -0
- dos/scout.py +982 -0
- dos/self_modify.py +209 -0
- dos/sibling_scan.py +569 -0
- dos/skills/EXAMPLES.md +584 -0
- dos/skills/dos-class-cycle/SKILL.md +107 -0
- dos/skills/dos-dispatch/SKILL.md +177 -0
- dos/skills/dos-dispatch-loop/SKILL.md +254 -0
- dos/skills/dos-goal-gate/SKILL.md +269 -0
- dos/skills/dos-next-up/SKILL.md +231 -0
- dos/skills/dos-promote/SKILL.md +114 -0
- dos/skills/dos-replan/SKILL.md +159 -0
- dos/skills/dos-replan-loop/SKILL.md +114 -0
- dos/skills/dos-self-improve/SKILL.md +213 -0
- dos/skills/dos-supervise-loop/SKILL.md +180 -0
- dos/skills/dos-unstick/SKILL.md +108 -0
- dos/skills/dos-witness-claim/SKILL.md +251 -0
- dos/stamp.py +1002 -0
- dos/state_health.py +387 -0
- dos/status.py +114 -0
- dos/stop_policy.py +334 -0
- dos/supervise.py +1014 -0
- dos/testwitness.py +392 -0
- dos/timeline.py +1027 -0
- dos/tokens.py +485 -0
- dos/tool_stream.py +393 -0
- dos/tool_stream_eval.py +226 -0
- dos/trace.py +524 -0
- dos/verdict.py +140 -0
- dos/verdict_cli.py +189 -0
- dos/verdict_journal.py +497 -0
- dos/verdict_rollup.py +217 -0
- dos/verdicts.py +181 -0
- dos/wedge_reason.py +282 -0
- dos_kernel-0.22.0.dist-info/METADATA +859 -0
- dos_kernel-0.22.0.dist-info/RECORD +178 -0
- dos_kernel-0.22.0.dist-info/WHEEL +5 -0
- dos_kernel-0.22.0.dist-info/entry_points.txt +39 -0
- dos_kernel-0.22.0.dist-info/licenses/LICENSE +21 -0
- dos_kernel-0.22.0.dist-info/top_level.txt +2 -0
- dos_mcp/__init__.py +52 -0
- dos_mcp/py.typed +2 -0
- 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
|
+
)
|