@sentropic/h2a 0.96.1 → 0.97.0
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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +98 -29
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/drive/cluster-mesh-outer.d.ts +44 -0
- package/dist/runtime/drive/cluster-mesh-outer.d.ts.map +1 -0
- package/dist/runtime/drive/cluster-mesh-outer.js +70 -0
- package/dist/runtime/drive/cluster-mesh-outer.js.map +1 -0
- package/dist/runtime/drive/inbox-wake.d.ts +5 -0
- package/dist/runtime/drive/inbox-wake.d.ts.map +1 -1
- package/dist/runtime/drive/inbox-wake.js +4 -1
- package/dist/runtime/drive/inbox-wake.js.map +1 -1
- package/dist/runtime/drive/index.d.ts +4 -0
- package/dist/runtime/drive/index.d.ts.map +1 -1
- package/dist/runtime/drive/index.js +12 -0
- package/dist/runtime/drive/index.js.map +1 -1
- package/dist/runtime/drive/pty-actuator.d.ts +54 -0
- package/dist/runtime/drive/pty-actuator.d.ts.map +1 -0
- package/dist/runtime/drive/pty-actuator.js +371 -0
- package/dist/runtime/drive/pty-actuator.js.map +1 -0
- package/dist/runtime/local-files/paths.js +1 -1
- package/dist/runtime/local-files/paths.js.map +1 -1
- package/dist/runtime/local-files/presence.d.ts.map +1 -1
- package/dist/runtime/local-files/presence.js +10 -1
- package/dist/runtime/local-files/presence.js.map +1 -1
- package/dist/runtime/loop/engine/adapters.d.ts +7 -1
- package/dist/runtime/loop/engine/adapters.d.ts.map +1 -1
- package/dist/runtime/loop/engine/adapters.js +43 -11
- package/dist/runtime/loop/engine/adapters.js.map +1 -1
- package/dist/runtime/loop/engine/decision.d.ts +5 -1
- package/dist/runtime/loop/engine/decision.d.ts.map +1 -1
- package/dist/runtime/loop/engine/decision.js +9 -3
- package/dist/runtime/loop/engine/decision.js.map +1 -1
- package/dist/runtime/loop/index.d.ts +2 -0
- package/dist/runtime/loop/index.d.ts.map +1 -1
- package/dist/runtime/loop/index.js +11 -3
- package/dist/runtime/loop/index.js.map +1 -1
- package/dist/runtime/mcp/agent-launch.d.ts +2 -1
- package/dist/runtime/mcp/agent-launch.d.ts.map +1 -1
- package/dist/runtime/mcp/agent-launch.js +51 -8
- package/dist/runtime/mcp/agent-launch.js.map +1 -1
- package/dist/runtime/mcp/handlers.d.ts.map +1 -1
- package/dist/runtime/mcp/handlers.js +17 -0
- package/dist/runtime/mcp/handlers.js.map +1 -1
- package/dist/runtime/mcp/stdio.d.ts +2 -0
- package/dist/runtime/mcp/stdio.d.ts.map +1 -1
- package/dist/runtime/mcp/stdio.js +8 -5
- package/dist/runtime/mcp/stdio.js.map +1 -1
- package/dist/runtime/mcp/tools.d.ts.map +1 -1
- package/dist/runtime/mcp/tools.js +19 -6
- package/dist/runtime/mcp/tools.js.map +1 -1
- package/dist/runtime/memory/admit-client.d.ts +30 -0
- package/dist/runtime/memory/admit-client.d.ts.map +1 -0
- package/dist/runtime/memory/admit-client.js +44 -0
- package/dist/runtime/memory/admit-client.js.map +1 -0
- package/dist/runtime/memory/d11-ceremony.d.ts +731 -0
- package/dist/runtime/memory/d11-ceremony.d.ts.map +1 -0
- package/dist/runtime/memory/d11-ceremony.js +1282 -0
- package/dist/runtime/memory/d11-ceremony.js.map +1 -0
- package/dist/runtime/memory/graphify-contracts-v2.vendored.d.ts +922 -0
- package/dist/runtime/memory/graphify-contracts-v2.vendored.d.ts.map +1 -0
- package/dist/runtime/memory/graphify-contracts-v2.vendored.js +2 -0
- package/dist/runtime/memory/graphify-contracts-v2.vendored.js.map +1 -0
- package/dist/runtime/memory/note-builder.d.ts +83 -0
- package/dist/runtime/memory/note-builder.d.ts.map +1 -0
- package/dist/runtime/memory/note-builder.js +89 -0
- package/dist/runtime/memory/note-builder.js.map +1 -0
- package/dist/runtime/memory/port-v1.d.ts +235 -0
- package/dist/runtime/memory/port-v1.d.ts.map +1 -0
- package/dist/runtime/memory/port-v1.js +129 -0
- package/dist/runtime/memory/port-v1.js.map +1 -0
- package/dist/runtime/memory/port-v2-adapter.d.ts +117 -0
- package/dist/runtime/memory/port-v2-adapter.d.ts.map +1 -0
- package/dist/runtime/memory/port-v2-adapter.js +140 -0
- package/dist/runtime/memory/port-v2-adapter.js.map +1 -0
- package/dist/runtime/memory/port-v2-compat.d.ts +50 -0
- package/dist/runtime/memory/port-v2-compat.d.ts.map +1 -0
- package/dist/runtime/memory/port-v2-compat.js +102 -0
- package/dist/runtime/memory/port-v2-compat.js.map +1 -0
- package/dist/runtime/memory/preflight.d.ts +17 -0
- package/dist/runtime/memory/preflight.d.ts.map +1 -0
- package/dist/runtime/memory/preflight.js +18 -0
- package/dist/runtime/memory/preflight.js.map +1 -0
- package/dist/runtime/memory/promote-client.d.ts +172 -0
- package/dist/runtime/memory/promote-client.d.ts.map +1 -0
- package/dist/runtime/memory/promote-client.js +168 -0
- package/dist/runtime/memory/promote-client.js.map +1 -0
- package/dist/runtime/memory/recall-client.d.ts +74 -0
- package/dist/runtime/memory/recall-client.d.ts.map +1 -0
- package/dist/runtime/memory/recall-client.js +91 -0
- package/dist/runtime/memory/recall-client.js.map +1 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +7 -0
- package/dist/session.js.map +1 -1
- package/package.json +7 -4
- package/skills/h2a-run/SKILL.md +39 -6
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D11 ceremony orchestrator — WP11 slice 5 (build brief). Anti-fabrication
|
|
3
|
+
* FIXED in two rounds, each closing a hole an independent review NO-GO'd.
|
|
4
|
+
* See "D11 FIX — ROUND 1" and "D11 FIX — ROUND 2" below for what changed, in
|
|
5
|
+
* order, and why; the rest of this doc describes the ceremony's unchanged
|
|
6
|
+
* shape.
|
|
7
|
+
*
|
|
8
|
+
* Composes the double-consensus END TO END: launch the two independent legs,
|
|
9
|
+
* collect their INLINE verdicts, gate on slice 3's `checkDoubleConsensusPreconditions`
|
|
10
|
+
* (the SAME structural check, not a reimplementation) as a cheap pre-write
|
|
11
|
+
* reject, write the verdict + attestation artifacts, READ THEM BACK from a
|
|
12
|
+
* REAL, construction-time-bound durable store and cryptographically verify
|
|
13
|
+
* them (ROUND 1 + ROUND 2 together), and only then dispatch through
|
|
14
|
+
* `promoteNoteWithDoubleConsensus` — now fed the READ, VERIFIED verdicts,
|
|
15
|
+
* never the caller-controlled inline ones.
|
|
16
|
+
*
|
|
17
|
+
* SCOPE (bounded), same shape as the rest of `runtime/memory/`: the actual
|
|
18
|
+
* model-leg launching and the verdict/attestation FILE WRITES are INJECTED
|
|
19
|
+
* (`deps.launchLeg`, `deps.writeVerdict`, `deps.writeAttestation`,
|
|
20
|
+
* `deps.port`) and stubbed in tests — never implemented for real here. This
|
|
21
|
+
* module is the ORCHESTRATION only: what gets called, in what order, and
|
|
22
|
+
* what refuses the ceremony before the next step ever runs. Real Ed25519
|
|
23
|
+
* signature verification (`verifySignature`, the default), the concrete
|
|
24
|
+
* `trustedKeystore` backing store, AND (ROUND 2) the default verdict-artifact
|
|
25
|
+
* READER (`readVerdict`, a real path-bound filesystem reader) ARE
|
|
26
|
+
* implemented for real — crypto, and "read back exactly what is actually
|
|
27
|
+
* persisted", are not "out of scope, inject a stub" concerns the way
|
|
28
|
+
* launching a model or writing a file are. The keystore's actual key
|
|
29
|
+
* material/distribution and the durable store's actual deployment location
|
|
30
|
+
* remain a WP5 concern (see ROUND 1 §1 and ROUND 2 below).
|
|
31
|
+
*
|
|
32
|
+
* ===========================================================================
|
|
33
|
+
* D11 FIX — ROUND 1 — the anti-fabrication anchor (signature verification).
|
|
34
|
+
* ===========================================================================
|
|
35
|
+
*
|
|
36
|
+
* THE HOLE (proven by an independent review): before this fix, `launchLeg`
|
|
37
|
+
* returned verdicts INLINE (caller-controlled), `writeVerdict` returned ref
|
|
38
|
+
* strings that were NEVER READ BACK, and the consensus gate checked only the
|
|
39
|
+
* caller-supplied inline verdicts. A `writeVerdict` that returns invented ref
|
|
40
|
+
* strings without ever creating a file still reached `promoteNote` with
|
|
41
|
+
* `promoted: true` — a caller fabricating "2 GO" verdicts as plain JavaScript
|
|
42
|
+
* objects could promote a note with no real evidence anywhere. This is the
|
|
43
|
+
* classic fabricated-verdict hole: an assertion was trusted where a document
|
|
44
|
+
* should have been demanded.
|
|
45
|
+
*
|
|
46
|
+
* THE FIX, in three parts:
|
|
47
|
+
*
|
|
48
|
+
* 1. `createD11Ceremony({ trustedKeystore, verifySignature? })` — a FACTORY
|
|
49
|
+
* that CLOSES OVER the trusted keystore (and, optionally, the signature
|
|
50
|
+
* verifier). It returns the actual `runD11Ceremony(input, deps)` function.
|
|
51
|
+
* This is the load-bearing design choice: `trustedKeystore` is a
|
|
52
|
+
* CONSTRUCTION-TIME input, never part of the per-call `RunD11CeremonyDeps`.
|
|
53
|
+
* At the time of THIS round, `readVerdict` and `launchLeg` were BOTH still
|
|
54
|
+
* per-call (caller-injected, untrusted); if signature verification used a
|
|
55
|
+
* per-call-supplied keystore too, a caller could simply hand the ceremony
|
|
56
|
+
* ITS OWN keystore (mapping its own throwaway key to whatever leg it
|
|
57
|
+
* likes) and sign its own fabrication — the hole would just move one layer
|
|
58
|
+
* down. The keystore must come from a trusted CONSTRUCTION site (a
|
|
59
|
+
* conductor/bootstrap that wires `createD11Ceremony` once, not a value an
|
|
60
|
+
* arbitrary per-call caller of the returned `runD11Ceremony` can swap).
|
|
61
|
+
* `trustedKeystore` is documented here as the LOCAL anchor for today
|
|
62
|
+
* (an in-process map/interface from leg identity to an Ed25519 public
|
|
63
|
+
* key); it is designed to be swapped later for the WP5 system-wide
|
|
64
|
+
* identity keystore WITHOUT a rewrite of this module — `D11TrustedKeystore`
|
|
65
|
+
* is a one-method lookup interface (`getPublicKey(leg)`) precisely so a
|
|
66
|
+
* different backing store (network-fetched, rotated, cross-repo) can
|
|
67
|
+
* implement it later; the ceremony's own logic never changes.
|
|
68
|
+
*
|
|
69
|
+
* 2. `readVerdict(ref) => Promise<VerdictArtifact | null>` — at THIS round, a
|
|
70
|
+
* per-call dep. It OPENS+READS the verdict artifact actually persisted at
|
|
71
|
+
* `ref` (or returns `null` if absent/unreadable — the exact shape of the
|
|
72
|
+
* proven counter-example: a `writeVerdict` that never created a file).
|
|
73
|
+
* Distinct from `MemoryVerdict` (slice 3's in-memory, caller-asserted
|
|
74
|
+
* shape): `VerdictArtifact` additionally carries a `signature` — proof the
|
|
75
|
+
* artifact was actually produced by whoever holds the claimed leg's
|
|
76
|
+
* private key, not merely narrated by whoever called this ceremony.
|
|
77
|
+
* ROUND 2 (below) moves this dep to construction-time too — read that
|
|
78
|
+
* section for why leaving it per-call was itself still a hole.
|
|
79
|
+
*
|
|
80
|
+
* 3. The gate (fail-closed at every step, `promoteNote` NEVER reached on any
|
|
81
|
+
* failure): after `launchLeg` → `writeVerdict` produce the two refs, for
|
|
82
|
+
* EACH ref —
|
|
83
|
+
* a. `readVerdict(ref)` → REFUSE if `null` (closes the proven
|
|
84
|
+
* counter-example structurally: an invented ref with no real file).
|
|
85
|
+
* b. Look up the trusted public key for the artifact's OWN CLAIMED
|
|
86
|
+
* `leg` (`trustedKeystore.getPublicKey(artifact.leg)`) — REFUSE if
|
|
87
|
+
* the keystore has no entry (unknown leg OR an empty keystore; a
|
|
88
|
+
* missing key is never treated as "skip verification", it is always
|
|
89
|
+
* a refusal — I5 fail-closed applies to the keystore lookup too).
|
|
90
|
+
* Verify the artifact's `signature` against THAT key over the exact
|
|
91
|
+
* payload `{noteId, verdict, leg, at}` — REFUSE if invalid. Because
|
|
92
|
+
* the key is looked up BY THE CLAIMED LEG, a verdict that declares
|
|
93
|
+
* leg A but was actually signed with leg B's key (even though B's
|
|
94
|
+
* key IS in the keystore) fails here: leg A's public key cannot
|
|
95
|
+
* verify a signature produced by leg B's private key.
|
|
96
|
+
* c. REFUSE unless the READ `verdict === "GO"`.
|
|
97
|
+
* d. REFUSE unless the READ `noteId` equals the note actually being
|
|
98
|
+
* promoted (`input.note.noteId`) — ANTI-REPLAY. The signed payload
|
|
99
|
+
* in (b) already binds `noteId` into what the signature covers, so a
|
|
100
|
+
* genuinely-signed verdict for note X cannot be replayed to promote
|
|
101
|
+
* note Y: either its `noteId` fails this pin (this check), or an
|
|
102
|
+
* attacker who edits the `noteId` field to match Y invalidates the
|
|
103
|
+
* signature (check (b) fires instead). Either way: REFUSE.
|
|
104
|
+
* Then, across the two READ artifacts (off the READ content only, never
|
|
105
|
+
* the inline verdicts or the legSpecs):
|
|
106
|
+
* e. REFUSE if the two artifacts' `leg`s are not structurally distinct.
|
|
107
|
+
* f. REFUSE if either artifact's `leg.session === authorId` (separation
|
|
108
|
+
* of powers, re-checked off what was actually read — a `readVerdict`
|
|
109
|
+
* that returns a different leg than the one that was launched is
|
|
110
|
+
* still caught here).
|
|
111
|
+
* Then, per artifact again:
|
|
112
|
+
* g. REFUSE unless the READ content is COHERENT with the INLINE verdict
|
|
113
|
+
* `launchLeg` returned for that same position (`noteId`, `verdict`,
|
|
114
|
+
* `leg` must all match) — a mismatch means what got signed/persisted
|
|
115
|
+
* disagrees with what the leg claimed inline, which is refused
|
|
116
|
+
* rather than silently preferring one over the other.
|
|
117
|
+
* Only if ALL of (a)-(g) pass for both legs: the FINAL verdicts and
|
|
118
|
+
* attestation fed to `promoteNoteWithDoubleConsensus` are built from the
|
|
119
|
+
* READ, VERIFIED artifacts (never the inline ones) — so even a bug in this
|
|
120
|
+
* module's own gate logic cannot cause a fabricated inline verdict to
|
|
121
|
+
* reach the port, because the inline verdicts are never what gets
|
|
122
|
+
* promoted. `promoteNoteWithDoubleConsensus` re-runs
|
|
123
|
+
* `checkDoubleConsensusPreconditions` on that verified data as an
|
|
124
|
+
* unconditional second layer (unchanged from slice 3) before ever
|
|
125
|
+
* touching the port.
|
|
126
|
+
*
|
|
127
|
+
* ===========================================================================
|
|
128
|
+
* D11 FIX — ROUND 2 (this build) — `readVerdict` moves to construction time.
|
|
129
|
+
* ===========================================================================
|
|
130
|
+
*
|
|
131
|
+
* THE HOLE ROUND 1 LEFT (proven by an independent review, a SECOND, separate
|
|
132
|
+
* leg from the one that forced ROUND 1): ROUND 1 moved `trustedKeystore` and
|
|
133
|
+
* `verifySignature` to construction-time, but left `readVerdict` as a
|
|
134
|
+
* PER-CALL, caller-injected dep. An adversarial per-call caller can inject a
|
|
135
|
+
* `readVerdict` that returns a NON-NULL, TRUSTED-SIGNED artifact that exists
|
|
136
|
+
* ONLY in memory — a ref string pointing at NO durable file anywhere — and
|
|
137
|
+
* the gate above (§3a) only refuses `null`; a fabricated-but-signed in-memory
|
|
138
|
+
* object sails through every check in ROUND 1 §3. ROUND 1's signature
|
|
139
|
+
* verification only actually defends anything IF the per-call caller lacks
|
|
140
|
+
* the trusted legs' private keys — but nothing in this module, or in what it
|
|
141
|
+
* composes, ESTABLISHES that. Key custody is a WP5 wiring concern that is not
|
|
142
|
+
* yet specified; assuming the caller can't sign is exactly the kind of
|
|
143
|
+
* unverified assumption D11 exists to eliminate. We do not rely on it here.
|
|
144
|
+
* The deeper problem: a trust-critical READER that the untrusted per-call
|
|
145
|
+
* caller can inject is an EMPTY ANCHOR — no matter how strong the
|
|
146
|
+
* verification logic downstream of it is, the caller controls whether that
|
|
147
|
+
* logic ever sees real data at all. This is the same injected-deps lesson
|
|
148
|
+
* ROUND 1 only half-applied (it anchored the VERIFIER but not the READER).
|
|
149
|
+
*
|
|
150
|
+
* THE FIX:
|
|
151
|
+
*
|
|
152
|
+
* 1. `readVerdict` moves from `RunD11CeremonyDeps` (per-call) to
|
|
153
|
+
* `CreateD11CeremonyOptions` (construction-time) — EXACTLY like
|
|
154
|
+
* `trustedKeystore` and `verifySignature`, and for the identical reason:
|
|
155
|
+
* the per-call caller of the returned `runD11Ceremony(input, deps)` is the
|
|
156
|
+
* UNTRUSTED adversary this whole ceremony exists to defend against. A
|
|
157
|
+
* trust-critical dependency that adversary can inject or override is not
|
|
158
|
+
* a defense, it is a hook for the exact fabrication being defended
|
|
159
|
+
* against. `RunD11CeremonyDeps` no longer has a `readVerdict` field at
|
|
160
|
+
* all (a type-level guarantee, not merely a runtime check) — and even if
|
|
161
|
+
* a caller attaches an extra `readVerdict` property to the `deps` object
|
|
162
|
+
* at runtime anyway (JS does not enforce the TS shape), `runD11Ceremony`
|
|
163
|
+
* never reads `deps.readVerdict` — it always uses the reader CLOSED OVER
|
|
164
|
+
* at construction. Proven in tests: an attached-but-unused
|
|
165
|
+
* `deps.readVerdict` records zero calls.
|
|
166
|
+
*
|
|
167
|
+
* 2. The DEFAULT construction-time reader, `defaultReadVerdict`, is a REAL
|
|
168
|
+
* in-module durable-store reader with PATH-BINDING: `ref` IS a filesystem
|
|
169
|
+
* path, and the reader does a genuine `readFile` at exactly that path,
|
|
170
|
+
* then JSON-parses and shape-validates the result. It returns `null` on
|
|
171
|
+
* ANY failure — missing file, unreadable, malformed JSON, wrong shape —
|
|
172
|
+
* which is exactly ROUND 1's proven counter-example (an invented ref with
|
|
173
|
+
* no real file behind it) and stays refused for the identical reason.
|
|
174
|
+
* Because the reader always dereferences the REAL filesystem at the ref's
|
|
175
|
+
* own path, what it returns is BOUND to that ref — it is literally what
|
|
176
|
+
* is stored there — never a caller-fabricated in-memory object: there is
|
|
177
|
+
* no code path by which an in-memory `VerdictArtifact`, however validly
|
|
178
|
+
* signed, can reach `promoteNote` without first being real bytes on disk
|
|
179
|
+
* at the exact path the ceremony itself derived from `writeVerdict`'s
|
|
180
|
+
* return value. `readVerdict` remains overridable AT CONSTRUCTION for
|
|
181
|
+
* tests only — a test may supply a fake store-reader (e.g. an in-memory
|
|
182
|
+
* `Map<ref, VerdictArtifact>`) to `createD11Ceremony({ readVerdict: ... })`
|
|
183
|
+
* so tests don't need real disk I/O for every scenario — but that
|
|
184
|
+
* override is wired ONCE, at the same trusted construction site as
|
|
185
|
+
* `trustedKeystore`, never by the per-call caller under test.
|
|
186
|
+
*
|
|
187
|
+
* 3. Everything from ROUND 1 §3 is UNCHANGED: signature-by-claimed-leg,
|
|
188
|
+
* GO, anti-replay (noteId pin), cross-artifact distinctness, separation
|
|
189
|
+
* of powers off the READ content, and coherence with the inline verdict.
|
|
190
|
+
* ROUND 2 only changes WHERE `readVerdict` comes from and WHAT its
|
|
191
|
+
* default implementation actually does; it adds no new gate steps because
|
|
192
|
+
* none were missing — the missing piece was that the existing gate could
|
|
193
|
+
* be handed fabricated input before it ever ran.
|
|
194
|
+
*
|
|
195
|
+
* WHAT THIS DOES NOT CLAIM (residual, stated honestly): the CONSTRUCTION
|
|
196
|
+
* SITE — whoever calls `createD11Ceremony({ trustedKeystore, readVerdict })`
|
|
197
|
+
* — remains a trusted wiring point (WP5 domain, unchanged from ROUND 1 §1):
|
|
198
|
+
* its choice of keystore, its choice of durable-store reader (or acceptance
|
|
199
|
+
* of the real filesystem default), and the actual key material/store
|
|
200
|
+
* deployment behind both are not re-verified by this module. That is ONE
|
|
201
|
+
* trusted wiring point exercised once at bootstrap, not any of the
|
|
202
|
+
* potentially many per-call callers of the returned `runD11Ceremony` — a
|
|
203
|
+
* much smaller, much more auditable surface than ROUND 1 left. This module
|
|
204
|
+
* defends against a per-call caller fabricating a plausible-looking verdict,
|
|
205
|
+
* signed or not, durable or not; it does not defend against a compromised
|
|
206
|
+
* private key for a leg genuinely registered in the trusted keystore, a
|
|
207
|
+
* compromised/malicious construction site, or a durable store an attacker
|
|
208
|
+
* can write to directly (bypassing `writeVerdict`) — those remain
|
|
209
|
+
* key-management, wiring, and storage-access concerns, not gaps in this
|
|
210
|
+
* gate.
|
|
211
|
+
*
|
|
212
|
+
* ===========================================================================
|
|
213
|
+
* D11 FIX — ROUND 3 (this build) — `defaultReadVerdict` is now PATH-CONFINED.
|
|
214
|
+
* ===========================================================================
|
|
215
|
+
*
|
|
216
|
+
* THE HOLE ROUND 2 LEFT (proven by a THIRD, independent review, a separate
|
|
217
|
+
* leg from the two that forced ROUNDS 1 and 2): ROUND 2 moved `readVerdict`
|
|
218
|
+
* to construction time, closing the "fabricated in-memory artifact" hole.
|
|
219
|
+
* But the DEFAULT construction-time reader, `defaultReadVerdict`, still did
|
|
220
|
+
* a plain `readFile(ref)` — and `ref` is `writeVerdict`'s PER-CALL,
|
|
221
|
+
* caller-controlled return value. Moving the READER to construction time
|
|
222
|
+
* defends against a caller substituting a DIFFERENT reader; it does nothing
|
|
223
|
+
* to confine WHERE the real, trusted, construction-time reader looks, when
|
|
224
|
+
* that location is still a bare string the untrusted per-call caller
|
|
225
|
+
* chose. Three attacks reached `promoteNote` with `promoted: true` at
|
|
226
|
+
* ROUND 2 (@824b633e):
|
|
227
|
+
* 1. TRAVERSAL — `writeVerdict` returns a `../../…` ref pointing at some
|
|
228
|
+
* OTHER, genuinely signed verdict file outside the intended zone (e.g.
|
|
229
|
+
* one written by a real leg for a DIFFERENT ceremony run entirely).
|
|
230
|
+
* 2. SYMLINK — `writeVerdict` "writes" its ref location as a symlink to
|
|
231
|
+
* an attacker-controlled file (or makes an ancestor directory a
|
|
232
|
+
* symlink); `readFile` follows it transparently.
|
|
233
|
+
* 3. TOCTOU — `writeVerdict`'s ref looks confined and could even pass a
|
|
234
|
+
* naive check, but the file at that path is swapped between the
|
|
235
|
+
* moment any confinement check runs and the moment bytes are actually
|
|
236
|
+
* read — a real risk IF that check and that read are two separate
|
|
237
|
+
* pathname-based filesystem operations ("realpath(ref), then
|
|
238
|
+
* separately re-open(ref)" is itself racy this way).
|
|
239
|
+
*
|
|
240
|
+
* THE FIX, REUSING the path-confinement technique already proven and MERGED
|
|
241
|
+
* in this repo — `packages/h2a-runtime/src/identity-cull/cull.ts` (PR
|
|
242
|
+
* #160): `realpathSync` canonicalization of a TRUSTED root,
|
|
243
|
+
* `openSync(..., O_RDONLY | O_DIRECTORY | O_NOFOLLOW)` descriptor-relative
|
|
244
|
+
* directory walking that refuses a symlink at ANY path component, and — the
|
|
245
|
+
* TOCTOU-closing move — a SINGLE held file descriptor carried from open
|
|
246
|
+
* through the `fstatSync` regular-file check to the actual read, never a
|
|
247
|
+
* second, pathname-based open. This module reimplements the same
|
|
248
|
+
* primitives locally (`h2a` does not depend on `h2a-runtime` at build time;
|
|
249
|
+
* the peer dependency is optional) — the TECHNIQUE is reused, not
|
|
250
|
+
* reinvented, per that module's own comments on the same primitives.
|
|
251
|
+
*
|
|
252
|
+
* 1. `createD11Ceremony({ ..., authorizedRoot })` — a new construction-time
|
|
253
|
+
* option, REQUIRED whenever the REAL default reader is in use (no custom
|
|
254
|
+
* `readVerdict` override): constructing without it throws synchronously,
|
|
255
|
+
* the same fail-loud pattern as a missing `trustedKeystore`. A real
|
|
256
|
+
* filesystem reader with no confinement root IS the hole this round
|
|
257
|
+
* closes; there is no safe default for it.
|
|
258
|
+
*
|
|
259
|
+
* 2. `deriveVerdictRef(authorizedRoot, noteId, leg)` — a new, exported,
|
|
260
|
+
* PURE function (no filesystem access): the ONE location a leg's verdict
|
|
261
|
+
* for a note may ever be read from — `beneath(authorizedRoot,
|
|
262
|
+
* sanitized(noteId), sanitized(legId))`. Both `noteId` and the leg's
|
|
263
|
+
* `{model, session}` are sanitized (reject `/`, `\`, NUL, `.`, `..`) —
|
|
264
|
+
* REFUSED outright (thrown, caught, turned into a ceremony refusal)
|
|
265
|
+
* rather than silently stripped, so a hostile value can never be
|
|
266
|
+
* "cleaned" into some OTHER, ambiguous, colliding path. This is derived
|
|
267
|
+
* from `legSpec1`/`legSpec2` — the ceremony's OWN dispatch decision,
|
|
268
|
+
* already checked structurally distinct and non-author BEFORE launch —
|
|
269
|
+
* never from anything `launchLeg` or `writeVerdict` claim back. (A
|
|
270
|
+
* deliberate, narrow exception to I1's "opaque, never parsed/derived
|
|
271
|
+
* from" stance for `noteId`/leg identity: here they are used ONLY to
|
|
272
|
+
* build a confined filesystem path, via sanitize-or-refuse, never
|
|
273
|
+
* split/parsed/compared piecewise.)
|
|
274
|
+
*
|
|
275
|
+
* 3. Per leg, BEFORE any filesystem access on `writeVerdict`'s ref: the
|
|
276
|
+
* ceremony computes the derived path and requires
|
|
277
|
+
* `resolve(writeVerdictRef) === derivedPath`, EXACTLY, string-for-string
|
|
278
|
+
* — REFUSED otherwise, before `readVerdict` is ever called with it. This
|
|
279
|
+
* alone closes TRAVERSAL structurally: a `../`-laden ref can never be
|
|
280
|
+
* textually identical to the clean, sanitized, ceremony-derived path, so
|
|
281
|
+
* it is refused without a single filesystem call ever touching it.
|
|
282
|
+
*
|
|
283
|
+
* 4. `defaultReadVerdict(ref, authorizedRoot)` (signature change from
|
|
284
|
+
* ROUND 2 — now confined) opens the verdict file via the cull.ts-style
|
|
285
|
+
* descriptor-relative O_NOFOLLOW walk from the realpath'd
|
|
286
|
+
* `authorizedRoot` down through `ref`'s path components — refusing a
|
|
287
|
+
* symlink at ANY component (closes SYMLINK) — then, from the ONE
|
|
288
|
+
* resulting file descriptor: `fstatSync`s it (must be a regular file),
|
|
289
|
+
* re-derives its canonical path via `realpath("/proc/self/fd/<fd>")` as
|
|
290
|
+
* defense-in-depth confirmation it is still beneath `authorizedRoot`,
|
|
291
|
+
* and reads its bytes from THAT SAME descriptor — never a second,
|
|
292
|
+
* pathname-based open (closes TOCTOU: there is no gap between "this is
|
|
293
|
+
* confirmed a regular file beneath the root" and "these are the bytes
|
|
294
|
+
* verified" — both statements are about the identical open file
|
|
295
|
+
* description).
|
|
296
|
+
*
|
|
297
|
+
* 5. When a construction site supplies a CUSTOM `readVerdict` (an
|
|
298
|
+
* alternate, non-default store — still construction-time-only,
|
|
299
|
+
* unchanged from ROUND 2), the ref-derivation VALIDATION in (3) still
|
|
300
|
+
* applies whenever `authorizedRoot` is ALSO configured (defense in
|
|
301
|
+
* depth, cheap, store-agnostic — pure string comparison); it is skipped
|
|
302
|
+
* only when a construction site both overrides the reader AND supplies
|
|
303
|
+
* no `authorizedRoot`, in which case that override remains, as in
|
|
304
|
+
* ROUND 2, a fully trusted construction-time decision this module does
|
|
305
|
+
* not second-guess.
|
|
306
|
+
*
|
|
307
|
+
* Everything from ROUND 1 §3 and ROUND 2 remains UNCHANGED: signature-by-
|
|
308
|
+
* claimed-leg, GO, anti-replay (noteId pin), cross-artifact distinctness,
|
|
309
|
+
* separation of powers off the READ content, and coherence with the inline
|
|
310
|
+
* verdict. ROUND 3 adds a confinement gate BEFORE that pipeline runs; it
|
|
311
|
+
* removes no existing check.
|
|
312
|
+
*
|
|
313
|
+
* ===========================================================================
|
|
314
|
+
* D11 FIX — ROUND 3 §B (this build) — `authorId` is now BOUND to a verified
|
|
315
|
+
* author signature; `input.authorId` is no longer trusted for anything.
|
|
316
|
+
* ===========================================================================
|
|
317
|
+
*
|
|
318
|
+
* THE HOLE: `RunD11CeremonyInput.authorId` was a bare, per-call,
|
|
319
|
+
* caller-supplied string. Every separation-of-powers check compared a leg
|
|
320
|
+
* against WHATEVER `authorId` the caller happened to supply — a caller could
|
|
321
|
+
* simply lie (supply an `authorId` distinct from every leg) even when the
|
|
322
|
+
* TRUE author was genuinely one of the two legs reviewing its own note, and
|
|
323
|
+
* the ceremony had no way to detect it.
|
|
324
|
+
*
|
|
325
|
+
* WHY NOT a trusted-author field on the note itself: graphify verified
|
|
326
|
+
* (@67bf73c7) that `MemoryNote` carries NO trusted author field — graphify
|
|
327
|
+
* authors nothing at admission (its anti-cycle boundary, §8, forbids it from
|
|
328
|
+
* verifying signatures there). So this cannot be closed by reading a field
|
|
329
|
+
* graphify stamped; it must be closed INSIDE the ceremony, the same way the
|
|
330
|
+
* verdict-fabrication hole was: bind the claim to an unforgeable signature,
|
|
331
|
+
* verified against a trust anchor this module already owns.
|
|
332
|
+
*
|
|
333
|
+
* THE FIX — reuses the EXACT SAME trust anchor as verdict signatures, no
|
|
334
|
+
* second trust root:
|
|
335
|
+
*
|
|
336
|
+
* 1. The note carries an AUTHOR SIGNATURE in its open extension field
|
|
337
|
+
* (`note["h2a.author_signature"]`, `D11CeremonyNote`'s `[key: string]:
|
|
338
|
+
* unknown` slot — `MemoryNoteInput`/`note-builder.ts` already carry such
|
|
339
|
+
* an open slot; graphify stores it OPAQUE/unverified, it never inspects
|
|
340
|
+
* or verifies it). The shape, `AuthorSignature`, mirrors `VerdictArtifact`:
|
|
341
|
+
* `{authorLeg: LegIdentity, noteId, signature}` — a base64 Ed25519
|
|
342
|
+
* signature over the canonicalized `{authorLeg, noteId}` payload, binding
|
|
343
|
+
* the identity claim to BOTH who claims it and which note it is for
|
|
344
|
+
* (anti-replay, identical shape to how verdict signatures bind `noteId`).
|
|
345
|
+
* `authorLeg` reuses `LegIdentity` (not a new taxonomy, I4) precisely so
|
|
346
|
+
* it can be looked up in the SAME `trustedKeystore` a leg's key is.
|
|
347
|
+
*
|
|
348
|
+
* 2. `runD11Ceremony` RE-VERIFIES this signature — FIRST, before any other
|
|
349
|
+
* check — against the SAME construction-time `trustedKeystore` +
|
|
350
|
+
* `verifySignature` already closed over for verdicts (`verifyNoteAuthor`,
|
|
351
|
+
* below). No per-call/caller-injected dependency is consulted for this
|
|
352
|
+
* (injected_deps_hide_fabrication_hole discipline, same as ROUND 1/2/3's
|
|
353
|
+
* other anchors): an attacker who controls `deps` cannot make their own
|
|
354
|
+
* verifier say yes.
|
|
355
|
+
*
|
|
356
|
+
* 3. On success, the VERIFIED signer's identity — `authorLeg.session` — is
|
|
357
|
+
* what EVERY separation-of-powers check in this module now compares
|
|
358
|
+
* against, replacing every prior use of `input.authorId`.
|
|
359
|
+
* `RunD11CeremonyInput.authorId` remains in the type (callers may still
|
|
360
|
+
* pass it) but is STRUCTURALLY NEVER READ for a trust decision anywhere
|
|
361
|
+
* in this module — at most an unverified, decorative hint. Lying about it
|
|
362
|
+
* has zero effect: it is not consulted.
|
|
363
|
+
*
|
|
364
|
+
* 4. FAIL-CLOSED: a note with no author signature, a malformed one, one
|
|
365
|
+
* whose `noteId` does not match the note it is attached to, or one that
|
|
366
|
+
* does not verify against the trusted keystore, REFUSES the entire
|
|
367
|
+
* ceremony immediately — `launchLeg` is never called, exactly like the
|
|
368
|
+
* other pre-launch fail-closed checks (I5).
|
|
369
|
+
*
|
|
370
|
+
* PARKED (owner-gated, not this round): having graphify itself stamp a
|
|
371
|
+
* trusted `authored_by` field at admission (via `AdmissionDeps`) — a
|
|
372
|
+
* heavier, graphify-contract change. This round stays entirely h2a-side,
|
|
373
|
+
* zero graphify change, closing the hole with a mechanism this module
|
|
374
|
+
* already had (signature verification against its own trust anchor).
|
|
375
|
+
*
|
|
376
|
+
* ===========================================================================
|
|
377
|
+
* D11 FIX — ROUND 4 (this build) — same-note verdict REPLAY is now refused
|
|
378
|
+
* (a fresh, gate-issued ceremony nonce), and separation-of-powers is now
|
|
379
|
+
* enforced on CANONICAL CRYPTO PRINCIPALS (public-key fingerprints), not
|
|
380
|
+
* session strings.
|
|
381
|
+
* ===========================================================================
|
|
382
|
+
*
|
|
383
|
+
* THE TWO HOLES (proven by an independent review, a FOURTH, separate leg from
|
|
384
|
+
* the three that forced ROUNDS 1-3): at ROUND 3 (@64e3f8a4), the signed
|
|
385
|
+
* verdict payload was `{noteId, verdict, leg, at}` — signed, noteId-pinned
|
|
386
|
+
* against cross-note replay, but with NO freshness, NO nonce, NO one-shot
|
|
387
|
+
* consumption. An attacker holding NO private key at all could REPLAY the
|
|
388
|
+
* already-signed BYTES of two OLD, genuinely-produced GO verdicts for the
|
|
389
|
+
* SAME noteId (from a prior, real ceremony) straight into a FRESH ceremony's
|
|
390
|
+
* confined artifact locations and mint a fresh promotion — BLOCKER 1,
|
|
391
|
+
* SAME-NOTE REPLAY. Separately, separation-of-powers derived and compared
|
|
392
|
+
* `authorLeg.session` / `leg.session` STRINGS only, never the verified
|
|
393
|
+
* PUBLIC KEY those sessions claim to speak for; `D11TrustedKeystore` never
|
|
394
|
+
* enforced that a public key maps to at most one identity. If the same
|
|
395
|
+
* keystore mapped ONE Ed25519 key to TWO (or three) different
|
|
396
|
+
* `{model, session}` identities, the SAME cryptographic principal could
|
|
397
|
+
* author a note under one alias and review it — genuinely signing, no forged
|
|
398
|
+
* signature anywhere — under another, or "independently" review it TWICE
|
|
399
|
+
* under two reviewer aliases — BLOCKER 2, CRYPTO AUTHOR/REVIEWER (and
|
|
400
|
+
* REVIEWER/REVIEWER) ALIAS. Both reach `promoteNote({promoted:true})` at
|
|
401
|
+
* @64e3f8a4 without ever forging a signature.
|
|
402
|
+
*
|
|
403
|
+
* FIX 1 — REPLAY: a fresh, gate-issued, unpredictable ONE-SHOT ceremony
|
|
404
|
+
* nonce.
|
|
405
|
+
*
|
|
406
|
+
* 1. `runD11Ceremony` generates `ceremonyNonce = randomBytes(32).toString(
|
|
407
|
+
* "base64")` (`node:crypto`, NOT `Math.random`/`Date.now` — unpredictable)
|
|
408
|
+
* as the FIRST thing it does, every call. Never persisted, never reused
|
|
409
|
+
* across calls — a fresh value exists ONLY for the lifetime of this one
|
|
410
|
+
* ceremony invocation ("stateless one-shot": no server-side nonce store
|
|
411
|
+
* is needed because the GATE regenerates its own value every run and
|
|
412
|
+
* trusts nothing else).
|
|
413
|
+
* 2. The nonce is threaded to `launchLeg(note, legSpec, ceremonyNonce)` (new
|
|
414
|
+
* 3rd arg), so an honest leg's inline verdict — now `LegVerdict =
|
|
415
|
+
* MemoryVerdict & {ceremonyNonce}` (a LOCAL extension; slice 3's own
|
|
416
|
+
* `MemoryVerdict` is untouched, I4) — can carry it through `writeVerdict`
|
|
417
|
+
* into what actually gets signed. `VerdictArtifact` and
|
|
418
|
+
* `verdictSignedPayload` gain `ceremonyNonce`; the signature now covers
|
|
419
|
+
* `{noteId, verdict, leg, at, ceremonyNonce}`.
|
|
420
|
+
* 3. THE GATE (load-bearing): for each READ artifact, (a) REQUIRE
|
|
421
|
+
* `artifact.ceremonyNonce === ceremonyNonce` (THIS run's generated value)
|
|
422
|
+
* — reject a stale/replayed artifact immediately, before any signature
|
|
423
|
+
* math; AND (b), independently, verify the signature over a payload
|
|
424
|
+
* REBUILT from the GATE's OWN `ceremonyNonce` variable — never
|
|
425
|
+
* `artifact.ceremonyNonce` as the source of truth for what to verify. A
|
|
426
|
+
* replayed old artifact's real signed bytes covered a DIFFERENT (or
|
|
427
|
+
* absent) nonce, so (b) alone already fails to verify against a payload
|
|
428
|
+
* built with the CURRENT run's nonce, even hypothetically without (a).
|
|
429
|
+
* Both (a) and (b) independently refuse a same-note replay — proven by
|
|
430
|
+
* the ROUND 4 mutation-check in the test file (each neutralized alone,
|
|
431
|
+
* then together, then restored — see that file's comments for the
|
|
432
|
+
* ACTUAL, reproduced results).
|
|
433
|
+
* 4. `coherentWithInline` is extended to also require
|
|
434
|
+
* `artifact.ceremonyNonce === inline.ceremonyNonce`.
|
|
435
|
+
*
|
|
436
|
+
* FIX 2 — ALIAS: separation-of-powers on canonical CRYPTO PRINCIPALS.
|
|
437
|
+
*
|
|
438
|
+
* 1. `canonicalKeyFingerprint(pem)` — `createPublicKey` then re-export as
|
|
439
|
+
* SPKI DER, SHA-256 of that DER, base64. Two differently-formatted PEM
|
|
440
|
+
* encodings of the identical key canonicalize to the SAME fingerprint
|
|
441
|
+
* (robust to encoding, unlike a raw PEM string compare); an unparseable
|
|
442
|
+
* key throws (caught and turned into a ceremony refusal, I5).
|
|
443
|
+
* 2. `verifyNoteAuthor` resolves and returns the VERIFIED author's
|
|
444
|
+
* fingerprint alongside its `authorId` — the SAME public key it already
|
|
445
|
+
* looked up to verify the author signature, so no extra trust decision is
|
|
446
|
+
* introduced.
|
|
447
|
+
* 3. Separation-of-powers now requires THREE PAIRWISE-DISTINCT crypto
|
|
448
|
+
* principals (author, reviewer1, reviewer2), by fingerprint, fail-closed
|
|
449
|
+
* on any missing/unparseable key:
|
|
450
|
+
* - PRE-LAUNCH: `legSpec1`/`legSpec2`'s trusted keys are resolved and
|
|
451
|
+
* fingerprinted BEFORE `launchLeg` is ever called (symmetric with the
|
|
452
|
+
* existing pre-launch session-string checks) — refuses before a
|
|
453
|
+
* single model call is ever made.
|
|
454
|
+
* - POST-READ: re-checked off the ACTUAL READ artifacts' `leg`s'
|
|
455
|
+
* fingerprints (the SAME trusted keys that just verified each
|
|
456
|
+
* artifact's signature) — a `readVerdict` returning a different leg
|
|
457
|
+
* than the one launched is still caught, symmetric with ROUND 1's
|
|
458
|
+
* existing "off the READ content only" discipline.
|
|
459
|
+
* The existing session-string distinctness checks (`sameLegSpec`,
|
|
460
|
+
* `.session === verifiedAuthorId`) are KEPT, unchanged, as defense in
|
|
461
|
+
* depth; the fingerprint checks are the AUTHORITATIVE crypto layer added
|
|
462
|
+
* this round — a `.session` alias backed by a shared key now refuses even
|
|
463
|
+
* when every session string looks pairwise distinct (the
|
|
464
|
+
* reviewer/reviewer variant ROUND 3 could not catch, since `sameLegSpec`
|
|
465
|
+
* only compares `{model, session}`, never the key behind them).
|
|
466
|
+
*
|
|
467
|
+
* Everything from ROUNDS 1-3 remains UNCHANGED: path confinement,
|
|
468
|
+
* signature-by-claimed-leg, GO, cross-note noteId anti-replay, the two
|
|
469
|
+
* artifacts' structural (session) distinctness, coherence with the inline
|
|
470
|
+
* verdict, and author-signature verification. ROUND 4 adds a freshness gate
|
|
471
|
+
* and a crypto-principal gate; it removes no existing check.
|
|
472
|
+
*
|
|
473
|
+
* ===========================================================================
|
|
474
|
+
* (Unchanged) FLOW summary and invariants.
|
|
475
|
+
* ===========================================================================
|
|
476
|
+
*
|
|
477
|
+
* I5 — fail-closed at EVERY injected/lookup step: `deps` itself,
|
|
478
|
+
* `deps.legSpecs`, `deps.launchLeg`, `deps.writeVerdict`, `deps.writeAttestation`,
|
|
479
|
+
* `deps.port`, the CONSTRUCTION-TIME `readVerdict` (ROUND 2), and the
|
|
480
|
+
* `trustedKeystore` lookup are all treated as untrusted or fallible — absent,
|
|
481
|
+
* wrong shape, throwing, rejecting, or (for the keystore, and for
|
|
482
|
+
* `readVerdict`'s return) simply "nothing there" all REFUSE with a structured
|
|
483
|
+
* `{promoted:false, reason}`, never a silent success. A ceremony that fails
|
|
484
|
+
* partway NEVER reaches `promoteNote` (slice 3's raw dispatch) — proven in
|
|
485
|
+
* tests via counting stubs showing later steps are never invoked once an
|
|
486
|
+
* earlier one refuses.
|
|
487
|
+
*
|
|
488
|
+
* I1 — durable identity slot: `note.noteId`, `note.principal_owner`,
|
|
489
|
+
* `authorId`, and every `legSpec.{model,session}` / `leg.{model,session}` are
|
|
490
|
+
* OPAQUE strings here — compared with `===` only (via slice 3's own
|
|
491
|
+
* equality, or the local `sameLegSpec` mirror below), never parsed, split or
|
|
492
|
+
* derived-from. The `trustedKeystore` is looked up BY the opaque `LegIdentity`
|
|
493
|
+
* value, not by any string built from parsing it.
|
|
494
|
+
* I4 — no new capabilities vocabulary: this module reuses `MemoryVerdict`,
|
|
495
|
+
* `IndependenceAttestation`, `LegIdentity`, `checkDoubleConsensusPreconditions`
|
|
496
|
+
* and `promoteNoteWithDoubleConsensus` UNCHANGED from `./promote-client.ts`;
|
|
497
|
+
* `LegSpec` is a type alias of `LegIdentity`, not a second taxonomy, and
|
|
498
|
+
* `D11CeremonyResult` reuses slice 3's own `PromoteNoteResult` shape — a
|
|
499
|
+
* ceremony IS a (composed) promotion attempt, not a new outcome vocabulary.
|
|
500
|
+
* `VerdictArtifact` is the one genuinely NEW shape this fix introduces — the
|
|
501
|
+
* READ, signed record `MemoryVerdict` never was.
|
|
502
|
+
*/
|
|
503
|
+
import type { MemoryContext, MemoryProducerPort } from "./port-v1.js";
|
|
504
|
+
import { type IndependenceAttestation, type LegIdentity, type MemoryVerdict, type PromoteNoteResult } from "./promote-client.js";
|
|
505
|
+
/** A leg's launch spec — the same opaque `{model, session}` shape as `LegIdentity`. */
|
|
506
|
+
export type LegSpec = LegIdentity;
|
|
507
|
+
/**
|
|
508
|
+
* The note the ceremony reviews. Deliberately narrow (I1: `noteId` and
|
|
509
|
+
* `principal_owner` are opaque, carried, never derived); open beyond that
|
|
510
|
+
* for whatever else a caller's note representation happens to carry.
|
|
511
|
+
*/
|
|
512
|
+
export interface D11CeremonyNote {
|
|
513
|
+
readonly noteId: string;
|
|
514
|
+
readonly principal_owner: string;
|
|
515
|
+
readonly [key: string]: unknown;
|
|
516
|
+
}
|
|
517
|
+
export interface RunD11CeremonyInput {
|
|
518
|
+
readonly note: D11CeremonyNote;
|
|
519
|
+
/**
|
|
520
|
+
* D11 FIX ROUND 3 §B: this is an UNVERIFIED, per-call, caller-supplied
|
|
521
|
+
* hint — structurally NEVER READ for a trust decision anywhere in this
|
|
522
|
+
* module. Separation of powers ("no leg may equal the author") is
|
|
523
|
+
* enforced against the VERIFIED signer of `note["h2a.author_signature"]`
|
|
524
|
+
* (see `verifyNoteAuthor`, below), not against this field. Kept in the
|
|
525
|
+
* type only so a caller may still attach it for its own bookkeeping.
|
|
526
|
+
*/
|
|
527
|
+
readonly authorId: string;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* D11 FIX ROUND 3 §B — the note's AUTHOR SIGNATURE, carried in the note's
|
|
531
|
+
* open extension slot (`note[AUTHOR_SIGNATURE_KEY]`). Mirrors
|
|
532
|
+
* `VerdictArtifact`'s shape deliberately: `authorLeg` reuses `LegIdentity`
|
|
533
|
+
* (I4 — not a new taxonomy) precisely so it is looked up in the SAME
|
|
534
|
+
* `trustedKeystore` a leg's key is — no second trust root. The signature
|
|
535
|
+
* covers the canonicalized `{authorLeg, noteId}` payload, binding the
|
|
536
|
+
* identity claim to both who claims it and which note it is for.
|
|
537
|
+
*/
|
|
538
|
+
export interface AuthorSignature {
|
|
539
|
+
readonly authorLeg: LegIdentity;
|
|
540
|
+
readonly noteId: string;
|
|
541
|
+
/** Base64 Ed25519 signature over the canonicalized payload `{authorLeg, noteId}`. */
|
|
542
|
+
readonly signature: string;
|
|
543
|
+
}
|
|
544
|
+
/** The note extension key `AuthorSignature` is carried under (`D11CeremonyNote`'s open `[key: string]: unknown` slot). */
|
|
545
|
+
export declare const AUTHOR_SIGNATURE_KEY: "h2a.author_signature";
|
|
546
|
+
/**
|
|
547
|
+
* A verdict ARTIFACT — what `readVerdict` returns after actually opening and
|
|
548
|
+
* reading the file persisted at a ref (D11 FIX ROUND 1, path-bound for real
|
|
549
|
+
* by ROUND 2's default reader). Distinct from `MemoryVerdict`
|
|
550
|
+
* (`./promote-client.ts`), which is the in-memory shape a `launchLeg` call
|
|
551
|
+
* returns INLINE and is never, by itself, trusted for a promotion decision:
|
|
552
|
+
* `VerdictArtifact` additionally carries `signature`, proof of authorship
|
|
553
|
+
* the inline shape has no room for.
|
|
554
|
+
*/
|
|
555
|
+
export interface VerdictArtifact {
|
|
556
|
+
readonly noteId: string;
|
|
557
|
+
readonly verdict: "GO" | "NO-GO";
|
|
558
|
+
readonly leg: LegIdentity;
|
|
559
|
+
readonly at: number;
|
|
560
|
+
/**
|
|
561
|
+
* D11 FIX ROUND 4 FIX 1 — the FRESH, gate-issued, unpredictable nonce this
|
|
562
|
+
* verdict was signed under (`crypto.randomBytes(32)`, base64). The gate
|
|
563
|
+
* REQUIRES this equal THIS RUN's own generated nonce (never trusted from
|
|
564
|
+
* the artifact alone) and rebuilds the signed payload from ITS OWN nonce
|
|
565
|
+
* value when verifying — see `verdictSignedPayload` and the ROUND 4 module
|
|
566
|
+
* doc. Closes SAME-NOTE REPLAY: an old, genuinely-signed artifact for the
|
|
567
|
+
* SAME noteId carries a DIFFERENT (stale) nonce and fails both checks.
|
|
568
|
+
*/
|
|
569
|
+
readonly ceremonyNonce: string;
|
|
570
|
+
/**
|
|
571
|
+
* Base64 Ed25519 signature over the canonicalized payload
|
|
572
|
+
* `{noteId, verdict, leg, at, ceremonyNonce}` (exactly these five fields,
|
|
573
|
+
* nothing else) — binds the verdict to the note it reviews (anti-replay
|
|
574
|
+
* across notes), the leg that claims to have produced it
|
|
575
|
+
* (anti-impersonation), and the ceremony run it was produced for
|
|
576
|
+
* (anti-replay of the SAME note, ROUND 4).
|
|
577
|
+
*/
|
|
578
|
+
readonly signature: string;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* D11 FIX ROUND 4 FIX 1 — the inline verdict a leg returns, extended with the
|
|
582
|
+
* ceremony's own freshly-generated nonce so it can flow through
|
|
583
|
+
* `writeVerdict` into what actually gets signed. A LOCAL extension of slice
|
|
584
|
+
* 3's `MemoryVerdict` (I4 — not a new taxonomy: every `MemoryVerdict` field
|
|
585
|
+
* is untouched; `ceremonyNonce` is additive and never leaves this module —
|
|
586
|
+
* `toMemoryVerdict` strips it back out before anything reaches
|
|
587
|
+
* `promoteNoteWithDoubleConsensus`).
|
|
588
|
+
*/
|
|
589
|
+
export type LegVerdict = MemoryVerdict & {
|
|
590
|
+
readonly ceremonyNonce: string;
|
|
591
|
+
};
|
|
592
|
+
/**
|
|
593
|
+
* The trusted keystore — an opaque `LegIdentity` -> Ed25519 public key (PEM)
|
|
594
|
+
* lookup. D11 FIX ROUND 1 §1: NEVER supplied per-call (that would make it
|
|
595
|
+
* caller-swappable, reopening the fabrication hole) — it is closed over at
|
|
596
|
+
* `createD11Ceremony` construction time by a trusted wiring site. Today this
|
|
597
|
+
* is the LOCAL anchor (an in-process map/interface); it is designed to be
|
|
598
|
+
* swapped later for the WP5 system-wide identity keystore WITHOUT a rewrite
|
|
599
|
+
* of this module — only a different `D11TrustedKeystore` implementation is
|
|
600
|
+
* needed, same one-method shape.
|
|
601
|
+
*/
|
|
602
|
+
export interface D11TrustedKeystore {
|
|
603
|
+
/**
|
|
604
|
+
* The Ed25519 public key (PEM), trusted for this CLAIMED leg, or
|
|
605
|
+
* `undefined`/`null` if the leg is not known to the keystore (including an
|
|
606
|
+
* entirely empty keystore) — treated as fail-closed REFUSE by the
|
|
607
|
+
* ceremony, never as "no key, skip verification".
|
|
608
|
+
*/
|
|
609
|
+
getPublicKey(leg: LegIdentity): string | undefined | null;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* The signature-verification seam. Real Ed25519 (`defaultVerifySignature`,
|
|
613
|
+
* below) in production; stubbable at `createD11Ceremony` construction time
|
|
614
|
+
* for tests. Injected at CONSTRUCTION, like the keystore, and for the same
|
|
615
|
+
* reason: per-call injection would let a caller supply a verifier that
|
|
616
|
+
* always says yes.
|
|
617
|
+
*/
|
|
618
|
+
export type VerifySignatureFn = (payload: unknown, signature: string, publicKey: string) => boolean;
|
|
619
|
+
/**
|
|
620
|
+
* D11 FIX ROUND 2: the verdict-artifact reader seam. Real, CONFINED,
|
|
621
|
+
* path-bound filesystem I/O (`defaultReadVerdict`, below — ROUND 3 makes it
|
|
622
|
+
* confined) in production; stubbable at `createD11Ceremony` construction
|
|
623
|
+
* time for tests. Injected at CONSTRUCTION, like the keystore and the
|
|
624
|
+
* verifier, and for the identical reason: per-call injection is exactly
|
|
625
|
+
* what let a caller hand the ceremony a fabricated, durability-free "read"
|
|
626
|
+
* in the hole ROUND 2 closes. `ref` here is always a value THIS MODULE
|
|
627
|
+
* derived (ROUND 3 §3), never the raw, unvalidated string a per-call
|
|
628
|
+
* `writeVerdict` returned.
|
|
629
|
+
*/
|
|
630
|
+
export type ReadVerdictFn = (ref: string) => Promise<VerdictArtifact | null>;
|
|
631
|
+
export interface RunD11CeremonyDeps {
|
|
632
|
+
/**
|
|
633
|
+
* Launch one leg's review. INJECTED — real model-launching is out of scope
|
|
634
|
+
* here. D11 FIX ROUND 4 FIX 1: the 3rd arg is THIS ceremony run's freshly
|
|
635
|
+
* generated `ceremonyNonce` — an honest leg threads it into its returned
|
|
636
|
+
* `LegVerdict` so it flows through `writeVerdict` into what gets signed.
|
|
637
|
+
*/
|
|
638
|
+
readonly launchLeg?: ((note: D11CeremonyNote, legSpec: LegSpec, ceremonyNonce: string) => Promise<LegVerdict>) | undefined | null;
|
|
639
|
+
/** Persist one verdict, return its REF (locator). INJECTED — real file I/O is out of scope here. */
|
|
640
|
+
readonly writeVerdict?: ((verdict: LegVerdict) => Promise<string>) | undefined | null;
|
|
641
|
+
/** Persist the attestation, return its REF. INJECTED — real file I/O + signing are out of scope here. */
|
|
642
|
+
readonly writeAttestation?: ((attestation: IndependenceAttestation) => Promise<string>) | undefined | null;
|
|
643
|
+
/** The two legs to launch. Must be structurally distinct (checked BEFORE launch). */
|
|
644
|
+
readonly legSpecs?: readonly [LegSpec, LegSpec] | undefined | null;
|
|
645
|
+
readonly port?: MemoryProducerPort | undefined | null;
|
|
646
|
+
readonly ctx: MemoryContext;
|
|
647
|
+
}
|
|
648
|
+
/** Reuses slice 3's result shape unchanged — a ceremony IS a composed promotion attempt. */
|
|
649
|
+
export type D11CeremonyResult = PromoteNoteResult;
|
|
650
|
+
/** What `createD11Ceremony` closes over — construction-time only, never per-call. */
|
|
651
|
+
export interface CreateD11CeremonyOptions {
|
|
652
|
+
/** The trust anchor for verdict signatures. See `D11TrustedKeystore` above. */
|
|
653
|
+
readonly trustedKeystore: D11TrustedKeystore;
|
|
654
|
+
/** Defaults to real Ed25519 (`defaultVerifySignature`). Override only for tests. */
|
|
655
|
+
readonly verifySignature?: VerifySignatureFn | undefined;
|
|
656
|
+
/**
|
|
657
|
+
* D11 FIX ROUND 2: the verdict-artifact reader. Defaults to
|
|
658
|
+
* `defaultReadVerdict`, a REAL, CONFINED, path-bound filesystem reader
|
|
659
|
+
* (ROUND 3). Override ONLY for tests — e.g. a fake, construction-time
|
|
660
|
+
* `Map`-backed store-reader — wired at the SAME trusted construction site
|
|
661
|
+
* as `trustedKeystore`, never by a per-call caller of the returned
|
|
662
|
+
* `runD11Ceremony`.
|
|
663
|
+
*/
|
|
664
|
+
readonly readVerdict?: ReadVerdictFn | undefined;
|
|
665
|
+
/**
|
|
666
|
+
* D11 FIX ROUND 3: the confinement root for verdict reads. REQUIRED
|
|
667
|
+
* (construction throws otherwise) whenever the REAL default reader is in
|
|
668
|
+
* use (no `readVerdict` override) — a real filesystem reader with no
|
|
669
|
+
* confinement root is exactly the hole ROUND 3 closes. When supplied
|
|
670
|
+
* alongside a CUSTOM `readVerdict`, it still activates the ref-derivation
|
|
671
|
+
* validation (ROUND 3 §3/§5) as defense in depth, even though that
|
|
672
|
+
* override reader itself is not required to be filesystem-backed.
|
|
673
|
+
*/
|
|
674
|
+
readonly authorizedRoot?: string | undefined;
|
|
675
|
+
}
|
|
676
|
+
export type RunD11Ceremony = (input: RunD11CeremonyInput, deps: RunD11CeremonyDeps | undefined | null) => Promise<D11CeremonyResult>;
|
|
677
|
+
/**
|
|
678
|
+
* The default, real signature verifier: Ed25519 over the canonicalized
|
|
679
|
+
* payload, via `node:crypto` — the same primitive `./signature.ts` uses
|
|
680
|
+
* elsewhere in h2a, applied here to a raw base64 signature string (this
|
|
681
|
+
* seam's shape) rather than the `H2ASignature{by,alg,value}` envelope.
|
|
682
|
+
*/
|
|
683
|
+
export declare function defaultVerifySignature(payload: unknown, signature: string, publicKeyPem: string): boolean;
|
|
684
|
+
/**
|
|
685
|
+
* D11 FIX ROUND 4 FIX 2 — the canonical CRYPTO PRINCIPAL fingerprint for an
|
|
686
|
+
* Ed25519 public key: parse the PEM, re-export as SPKI DER (a canonical
|
|
687
|
+
* encoding independent of the ORIGINAL PEM's own formatting/line-wrapping),
|
|
688
|
+
* then SHA-256 that DER. Two differently-formatted PEM encodings of the
|
|
689
|
+
* IDENTICAL key canonicalize to the SAME fingerprint — robust to encoding,
|
|
690
|
+
* unlike a raw PEM string compare. Throws on an unparseable key; callers
|
|
691
|
+
* MUST catch this and turn it into a fail-closed ceremony refusal (I5) —
|
|
692
|
+
* there is no safe default fingerprint for a key that cannot even be parsed.
|
|
693
|
+
*/
|
|
694
|
+
export declare function canonicalKeyFingerprint(publicKeyPem: string): string;
|
|
695
|
+
/**
|
|
696
|
+
* D11 FIX ROUND 3: the ONE location a leg's verdict for a note may ever be
|
|
697
|
+
* read from — `beneath(authorizedRoot, sanitized(noteId), sanitized(legId))`.
|
|
698
|
+
* Pure (no filesystem access) so the ceremony can compute it and compare it
|
|
699
|
+
* against `writeVerdict`'s per-call return value BEFORE touching disk.
|
|
700
|
+
* Derived from the ceremony's OWN dispatch decision (the `legSpec` it
|
|
701
|
+
* actually launched, already checked distinct/non-author before launch) —
|
|
702
|
+
* never from anything `launchLeg`/`writeVerdict` claim back.
|
|
703
|
+
*/
|
|
704
|
+
export declare function deriveVerdictRef(authorizedRoot: string, noteId: string, leg: LegIdentity): string;
|
|
705
|
+
/**
|
|
706
|
+
* D11 FIX ROUND 2/3 — the default, REAL, CONFINED durable-store reader.
|
|
707
|
+
* `ref` IS a filesystem path, opened only via the confined,
|
|
708
|
+
* descriptor-relative, O_NOFOLLOW walk (`readConfinedFileBytes`, ROUND 3)
|
|
709
|
+
* beneath `authorizedRoot` — never a bare `readFile(ref)` — then
|
|
710
|
+
* JSON-parsed and shape-validated. Returns `null` on ANY failure — missing
|
|
711
|
+
* file, escaped root, a symlink anywhere in the chain, permission error,
|
|
712
|
+
* malformed JSON, or a well-formed-but-wrong-shape value — never throws.
|
|
713
|
+
* What it returns, when non-null, is PATH-BOUND: literally the bytes
|
|
714
|
+
* actually persisted at `ref`, confirmed beneath `authorizedRoot`, never a
|
|
715
|
+
* value the caller constructed in memory and never bytes read via a path
|
|
716
|
+
* that could have been re-resolved after any check.
|
|
717
|
+
*/
|
|
718
|
+
export declare function defaultReadVerdict(ref: string, authorizedRoot: string): Promise<VerdictArtifact | null>;
|
|
719
|
+
/**
|
|
720
|
+
* Factory: builds the actual `runD11Ceremony` function, CLOSED OVER
|
|
721
|
+
* `trustedKeystore` (and, optionally, `verifySignature` and `readVerdict`).
|
|
722
|
+
* This is the anti-fabrication anchor (D11 FIX ROUND 1 §1 + ROUND 2 §1):
|
|
723
|
+
* signature verification always uses THIS keystore, and the verdict
|
|
724
|
+
* read-back always uses THIS reader (real filesystem by default) — never
|
|
725
|
+
* one a caller of the returned function can inject per-call. Throws
|
|
726
|
+
* synchronously on a missing/malformed `trustedKeystore` — a
|
|
727
|
+
* construction-site wiring bug should fail loudly and immediately, not
|
|
728
|
+
* silently produce a ceremony that can never verify anything.
|
|
729
|
+
*/
|
|
730
|
+
export declare function createD11Ceremony(options: CreateD11CeremonyOptions): RunD11Ceremony;
|
|
731
|
+
//# sourceMappingURL=d11-ceremony.d.ts.map
|