@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.
Files changed (102) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +98 -29
  5. package/dist/cli.js.map +1 -1
  6. package/dist/index.d.ts +12 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +6 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/runtime/drive/cluster-mesh-outer.d.ts +44 -0
  11. package/dist/runtime/drive/cluster-mesh-outer.d.ts.map +1 -0
  12. package/dist/runtime/drive/cluster-mesh-outer.js +70 -0
  13. package/dist/runtime/drive/cluster-mesh-outer.js.map +1 -0
  14. package/dist/runtime/drive/inbox-wake.d.ts +5 -0
  15. package/dist/runtime/drive/inbox-wake.d.ts.map +1 -1
  16. package/dist/runtime/drive/inbox-wake.js +4 -1
  17. package/dist/runtime/drive/inbox-wake.js.map +1 -1
  18. package/dist/runtime/drive/index.d.ts +4 -0
  19. package/dist/runtime/drive/index.d.ts.map +1 -1
  20. package/dist/runtime/drive/index.js +12 -0
  21. package/dist/runtime/drive/index.js.map +1 -1
  22. package/dist/runtime/drive/pty-actuator.d.ts +54 -0
  23. package/dist/runtime/drive/pty-actuator.d.ts.map +1 -0
  24. package/dist/runtime/drive/pty-actuator.js +371 -0
  25. package/dist/runtime/drive/pty-actuator.js.map +1 -0
  26. package/dist/runtime/local-files/paths.js +1 -1
  27. package/dist/runtime/local-files/paths.js.map +1 -1
  28. package/dist/runtime/local-files/presence.d.ts.map +1 -1
  29. package/dist/runtime/local-files/presence.js +10 -1
  30. package/dist/runtime/local-files/presence.js.map +1 -1
  31. package/dist/runtime/loop/engine/adapters.d.ts +7 -1
  32. package/dist/runtime/loop/engine/adapters.d.ts.map +1 -1
  33. package/dist/runtime/loop/engine/adapters.js +43 -11
  34. package/dist/runtime/loop/engine/adapters.js.map +1 -1
  35. package/dist/runtime/loop/engine/decision.d.ts +5 -1
  36. package/dist/runtime/loop/engine/decision.d.ts.map +1 -1
  37. package/dist/runtime/loop/engine/decision.js +9 -3
  38. package/dist/runtime/loop/engine/decision.js.map +1 -1
  39. package/dist/runtime/loop/index.d.ts +2 -0
  40. package/dist/runtime/loop/index.d.ts.map +1 -1
  41. package/dist/runtime/loop/index.js +11 -3
  42. package/dist/runtime/loop/index.js.map +1 -1
  43. package/dist/runtime/mcp/agent-launch.d.ts +2 -1
  44. package/dist/runtime/mcp/agent-launch.d.ts.map +1 -1
  45. package/dist/runtime/mcp/agent-launch.js +51 -8
  46. package/dist/runtime/mcp/agent-launch.js.map +1 -1
  47. package/dist/runtime/mcp/handlers.d.ts.map +1 -1
  48. package/dist/runtime/mcp/handlers.js +17 -0
  49. package/dist/runtime/mcp/handlers.js.map +1 -1
  50. package/dist/runtime/mcp/stdio.d.ts +2 -0
  51. package/dist/runtime/mcp/stdio.d.ts.map +1 -1
  52. package/dist/runtime/mcp/stdio.js +8 -5
  53. package/dist/runtime/mcp/stdio.js.map +1 -1
  54. package/dist/runtime/mcp/tools.d.ts.map +1 -1
  55. package/dist/runtime/mcp/tools.js +19 -6
  56. package/dist/runtime/mcp/tools.js.map +1 -1
  57. package/dist/runtime/memory/admit-client.d.ts +30 -0
  58. package/dist/runtime/memory/admit-client.d.ts.map +1 -0
  59. package/dist/runtime/memory/admit-client.js +44 -0
  60. package/dist/runtime/memory/admit-client.js.map +1 -0
  61. package/dist/runtime/memory/d11-ceremony.d.ts +731 -0
  62. package/dist/runtime/memory/d11-ceremony.d.ts.map +1 -0
  63. package/dist/runtime/memory/d11-ceremony.js +1282 -0
  64. package/dist/runtime/memory/d11-ceremony.js.map +1 -0
  65. package/dist/runtime/memory/graphify-contracts-v2.vendored.d.ts +922 -0
  66. package/dist/runtime/memory/graphify-contracts-v2.vendored.d.ts.map +1 -0
  67. package/dist/runtime/memory/graphify-contracts-v2.vendored.js +2 -0
  68. package/dist/runtime/memory/graphify-contracts-v2.vendored.js.map +1 -0
  69. package/dist/runtime/memory/note-builder.d.ts +83 -0
  70. package/dist/runtime/memory/note-builder.d.ts.map +1 -0
  71. package/dist/runtime/memory/note-builder.js +89 -0
  72. package/dist/runtime/memory/note-builder.js.map +1 -0
  73. package/dist/runtime/memory/port-v1.d.ts +235 -0
  74. package/dist/runtime/memory/port-v1.d.ts.map +1 -0
  75. package/dist/runtime/memory/port-v1.js +129 -0
  76. package/dist/runtime/memory/port-v1.js.map +1 -0
  77. package/dist/runtime/memory/port-v2-adapter.d.ts +117 -0
  78. package/dist/runtime/memory/port-v2-adapter.d.ts.map +1 -0
  79. package/dist/runtime/memory/port-v2-adapter.js +140 -0
  80. package/dist/runtime/memory/port-v2-adapter.js.map +1 -0
  81. package/dist/runtime/memory/port-v2-compat.d.ts +50 -0
  82. package/dist/runtime/memory/port-v2-compat.d.ts.map +1 -0
  83. package/dist/runtime/memory/port-v2-compat.js +102 -0
  84. package/dist/runtime/memory/port-v2-compat.js.map +1 -0
  85. package/dist/runtime/memory/preflight.d.ts +17 -0
  86. package/dist/runtime/memory/preflight.d.ts.map +1 -0
  87. package/dist/runtime/memory/preflight.js +18 -0
  88. package/dist/runtime/memory/preflight.js.map +1 -0
  89. package/dist/runtime/memory/promote-client.d.ts +172 -0
  90. package/dist/runtime/memory/promote-client.d.ts.map +1 -0
  91. package/dist/runtime/memory/promote-client.js +168 -0
  92. package/dist/runtime/memory/promote-client.js.map +1 -0
  93. package/dist/runtime/memory/recall-client.d.ts +74 -0
  94. package/dist/runtime/memory/recall-client.d.ts.map +1 -0
  95. package/dist/runtime/memory/recall-client.js +91 -0
  96. package/dist/runtime/memory/recall-client.js.map +1 -0
  97. package/dist/session.d.ts +4 -0
  98. package/dist/session.d.ts.map +1 -1
  99. package/dist/session.js +7 -0
  100. package/dist/session.js.map +1 -1
  101. package/package.json +7 -4
  102. 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