@cello-protocol/daemon 0.0.181 → 0.0.182

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 (164) hide show
  1. package/dist/agent-id-migration.d.ts +9 -0
  2. package/dist/agent-id-migration.d.ts.map +1 -1
  3. package/dist/agent-id-migration.js +73 -1
  4. package/dist/agent-id-migration.js.map +1 -1
  5. package/dist/agent-selection.d.ts +73 -0
  6. package/dist/agent-selection.d.ts.map +1 -0
  7. package/dist/agent-selection.js +80 -0
  8. package/dist/agent-selection.js.map +1 -0
  9. package/dist/assignment-verify.d.ts +62 -0
  10. package/dist/assignment-verify.d.ts.map +1 -0
  11. package/dist/assignment-verify.js +185 -0
  12. package/dist/assignment-verify.js.map +1 -0
  13. package/dist/backup-restore.d.ts +102 -0
  14. package/dist/backup-restore.d.ts.map +1 -0
  15. package/dist/backup-restore.js +340 -0
  16. package/dist/backup-restore.js.map +1 -0
  17. package/dist/bin/cello-daemon.js +91 -49
  18. package/dist/bin/cello-daemon.js.map +1 -1
  19. package/dist/close-commitment.d.ts +67 -0
  20. package/dist/close-commitment.d.ts.map +1 -0
  21. package/dist/close-commitment.js +78 -0
  22. package/dist/close-commitment.js.map +1 -0
  23. package/dist/close-session-handler.d.ts +20 -0
  24. package/dist/close-session-handler.d.ts.map +1 -1
  25. package/dist/close-session-handler.js +351 -58
  26. package/dist/close-session-handler.js.map +1 -1
  27. package/dist/consortium-bootstrap.d.ts +2 -0
  28. package/dist/consortium-bootstrap.d.ts.map +1 -1
  29. package/dist/consortium-bootstrap.js +65 -7
  30. package/dist/consortium-bootstrap.js.map +1 -1
  31. package/dist/content-park.d.ts.map +1 -1
  32. package/dist/content-park.js +141 -10
  33. package/dist/content-park.js.map +1 -1
  34. package/dist/daemon.d.ts +13 -1
  35. package/dist/daemon.d.ts.map +1 -1
  36. package/dist/daemon.js +866 -122
  37. package/dist/daemon.js.map +1 -1
  38. package/dist/delivery-session-suspects.d.ts.map +1 -1
  39. package/dist/delivery-session-suspects.js +5 -0
  40. package/dist/delivery-session-suspects.js.map +1 -1
  41. package/dist/directory-auth-posture.d.ts +87 -0
  42. package/dist/directory-auth-posture.d.ts.map +1 -0
  43. package/dist/directory-auth-posture.js +134 -0
  44. package/dist/directory-auth-posture.js.map +1 -0
  45. package/dist/directory-bootstrap.d.ts +66 -3
  46. package/dist/directory-bootstrap.d.ts.map +1 -1
  47. package/dist/directory-bootstrap.js +133 -7
  48. package/dist/directory-bootstrap.js.map +1 -1
  49. package/dist/document-delivery-transport.d.ts +22 -2
  50. package/dist/document-delivery-transport.d.ts.map +1 -1
  51. package/dist/document-delivery-transport.js +11 -3
  52. package/dist/document-delivery-transport.js.map +1 -1
  53. package/dist/inbound-sessions.d.ts +31 -1
  54. package/dist/inbound-sessions.d.ts.map +1 -1
  55. package/dist/inbound-sessions.js +276 -16
  56. package/dist/inbound-sessions.js.map +1 -1
  57. package/dist/index.d.ts +2 -0
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +3 -0
  60. package/dist/index.js.map +1 -1
  61. package/dist/ipc-server.d.ts +5 -1
  62. package/dist/ipc-server.d.ts.map +1 -1
  63. package/dist/ipc-server.js +19 -3
  64. package/dist/ipc-server.js.map +1 -1
  65. package/dist/manifest-validity.d.ts +151 -0
  66. package/dist/manifest-validity.d.ts.map +1 -0
  67. package/dist/manifest-validity.js +268 -0
  68. package/dist/manifest-validity.js.map +1 -0
  69. package/dist/notification-handlers.d.ts.map +1 -1
  70. package/dist/notification-handlers.js +15 -0
  71. package/dist/notification-handlers.js.map +1 -1
  72. package/dist/outbound-sessions.d.ts +31 -0
  73. package/dist/outbound-sessions.d.ts.map +1 -1
  74. package/dist/outbound-sessions.js +198 -17
  75. package/dist/outbound-sessions.js.map +1 -1
  76. package/dist/park-envelope.d.ts +72 -1
  77. package/dist/park-envelope.d.ts.map +1 -1
  78. package/dist/park-envelope.js +119 -4
  79. package/dist/park-envelope.js.map +1 -1
  80. package/dist/refusal-reasons.d.ts +72 -0
  81. package/dist/refusal-reasons.d.ts.map +1 -0
  82. package/dist/refusal-reasons.js +69 -0
  83. package/dist/refusal-reasons.js.map +1 -0
  84. package/dist/registration-manager.d.ts.map +1 -1
  85. package/dist/registration-manager.js +20 -1
  86. package/dist/registration-manager.js.map +1 -1
  87. package/dist/restart-seal-resolver.d.ts.map +1 -1
  88. package/dist/restart-seal-resolver.js +6 -0
  89. package/dist/restart-seal-resolver.js.map +1 -1
  90. package/dist/retry-queue.d.ts +23 -1
  91. package/dist/retry-queue.d.ts.map +1 -1
  92. package/dist/retry-queue.js +37 -9
  93. package/dist/retry-queue.js.map +1 -1
  94. package/dist/roster-freshness.d.ts +169 -0
  95. package/dist/roster-freshness.d.ts.map +1 -0
  96. package/dist/roster-freshness.js +249 -0
  97. package/dist/roster-freshness.js.map +1 -0
  98. package/dist/seal-certificate-pull.d.ts +17 -1
  99. package/dist/seal-certificate-pull.d.ts.map +1 -1
  100. package/dist/seal-certificate-pull.js +37 -3
  101. package/dist/seal-certificate-pull.js.map +1 -1
  102. package/dist/seal-coordinator.d.ts +17 -1
  103. package/dist/seal-coordinator.d.ts.map +1 -1
  104. package/dist/seal-coordinator.js +71 -0
  105. package/dist/seal-coordinator.js.map +1 -1
  106. package/dist/seal-failure-store.d.ts +104 -0
  107. package/dist/seal-failure-store.d.ts.map +1 -0
  108. package/dist/seal-failure-store.js +126 -0
  109. package/dist/seal-failure-store.js.map +1 -0
  110. package/dist/seal-flows.d.ts.map +1 -1
  111. package/dist/seal-flows.js +35 -6
  112. package/dist/seal-flows.js.map +1 -1
  113. package/dist/seal-upgrade.d.ts +20 -5
  114. package/dist/seal-upgrade.d.ts.map +1 -1
  115. package/dist/seal-upgrade.js +28 -5
  116. package/dist/seal-upgrade.js.map +1 -1
  117. package/dist/session-assignment-parser.d.ts +41 -5
  118. package/dist/session-assignment-parser.d.ts.map +1 -1
  119. package/dist/session-assignment-parser.js +77 -14
  120. package/dist/session-assignment-parser.js.map +1 -1
  121. package/dist/session-ceremony.d.ts +29 -0
  122. package/dist/session-ceremony.d.ts.map +1 -1
  123. package/dist/session-ceremony.js +71 -0
  124. package/dist/session-ceremony.js.map +1 -1
  125. package/dist/session-connection-gater.d.ts +46 -7
  126. package/dist/session-connection-gater.d.ts.map +1 -1
  127. package/dist/session-connection-gater.js +163 -12
  128. package/dist/session-connection-gater.js.map +1 -1
  129. package/dist/session-content-handlers.d.ts.map +1 -1
  130. package/dist/session-content-handlers.js +110 -4
  131. package/dist/session-content-handlers.js.map +1 -1
  132. package/dist/session-node-manager.d.ts +406 -36
  133. package/dist/session-node-manager.d.ts.map +1 -1
  134. package/dist/session-node-manager.js +3025 -245
  135. package/dist/session-node-manager.js.map +1 -1
  136. package/dist/session-read-handlers.d.ts +27 -0
  137. package/dist/session-read-handlers.d.ts.map +1 -1
  138. package/dist/session-read-handlers.js +112 -6
  139. package/dist/session-read-handlers.js.map +1 -1
  140. package/dist/session-relay-client.d.ts +61 -2
  141. package/dist/session-relay-client.d.ts.map +1 -1
  142. package/dist/session-relay-client.js +170 -11
  143. package/dist/session-relay-client.js.map +1 -1
  144. package/dist/session-salt-agreement.d.ts +284 -0
  145. package/dist/session-salt-agreement.d.ts.map +1 -0
  146. package/dist/session-salt-agreement.js +371 -0
  147. package/dist/session-salt-agreement.js.map +1 -0
  148. package/dist/signal-submission.d.ts.map +1 -1
  149. package/dist/signal-submission.js +10 -1
  150. package/dist/signal-submission.js.map +1 -1
  151. package/dist/signaling-connect.d.ts.map +1 -1
  152. package/dist/signaling-connect.js +42 -5
  153. package/dist/signaling-connect.js.map +1 -1
  154. package/dist/types.d.ts +54 -0
  155. package/dist/types.d.ts.map +1 -1
  156. package/dist/types.js.map +1 -1
  157. package/dist/vocabulary.d.ts.map +1 -1
  158. package/dist/vocabulary.js +10 -0
  159. package/dist/vocabulary.js.map +1 -1
  160. package/dist/wire-content-hash.d.ts +56 -0
  161. package/dist/wire-content-hash.d.ts.map +1 -1
  162. package/dist/wire-content-hash.js +71 -0
  163. package/dist/wire-content-hash.js.map +1 -1
  164. package/package.json +5 -5
@@ -14,6 +14,8 @@
14
14
  * Interrupted-session detection runs BEFORE the IPC socket opens, so a client cannot observe a
15
15
  * stale 'active' row from a previous process.
16
16
  */
17
+ import { type ContentHashAlg } from "./wire-content-hash.js";
18
+ import { type CapacityReason } from "./refusal-reasons.js";
17
19
  import { type DaemonDatabase } from "./sqlcipher-db.js";
18
20
  import type { Stream } from "@libp2p/interface";
19
21
  import type { Logger, SessionRecord, SealReadinessView } from "./types.js";
@@ -25,6 +27,7 @@ import { type ParkEnvelope } from "./park-envelope.js";
25
27
  import { AgentRelayClient, type RelayAssignmentCarry } from "./session-relay-client.js";
26
28
  import { RelayReceiptStore, type RelayReceipt } from "./relay-receipt-store.js";
27
29
  import { SessionSealLeafStore, type SealCarryLeaf } from "./session-seal-leaf-store.js";
30
+ import type { SealUpgradeReadiness } from "./seal-upgrade.js";
28
31
  import { type SecurityGatewayClient } from "@cello-protocol/gateway";
29
32
  /**
30
33
  * DOD-CAP-SELF-HEAL-1 — how long an interrupted session keeps consuming a cap slot.
@@ -260,6 +263,15 @@ export declare class SessionNodeManager {
260
263
  receiptStore?: RelayReceiptStore;
261
264
  sealLeafStore?: SessionSealLeafStore;
262
265
  }) => AgentRelayClient | undefined): void;
266
+ /**
267
+ * Record that this session's tree and the relay's counter have provably parted.
268
+ *
269
+ * Idempotent, and deliberately does NOT touch `updated_at`: that column drives the inbox's
270
+ * last-spoke ordering, and divergence is not activity.
271
+ */
272
+ markSessionDiverged(agentName: string, sessionId: string): void;
273
+ /** Whether this session has provably parted from the relay's ordering. */
274
+ isSessionDiverged(agentName: string, sessionId: string): boolean;
263
275
  /** Arm the park-deposit fault. Returns the count now armed. */
264
276
  injectParkFault(count: number, cause?: string): number;
265
277
  /** Arm the direct-send fault — makes the next N sends take the dial-failure path. */
@@ -336,8 +348,8 @@ export declare class SessionNodeManager {
336
348
  */
337
349
  setAwaitingAckHooks(hooks: {
338
350
  onPersisted?: (agentName: string, sessionId: string, contentHashHex: string) => void;
339
- onTtf?: (agentName: string, sessionId: string, contentHashHex: string, content: Uint8Array, structure1Cbor?: Uint8Array, structure2Cbor?: Uint8Array) => void;
340
- onParkFailed?: (agentName: string, sessionId: string, contentHashHex: string, content: Uint8Array, structure1Cbor?: Uint8Array, structure2Cbor?: Uint8Array) => boolean;
351
+ onTtf?: (agentName: string, sessionId: string, contentHashHex: string, content: Uint8Array, structure1Cbor?: Uint8Array, structure2Cbor?: Uint8Array, contentHashAlg?: string) => void;
352
+ onParkFailed?: (agentName: string, sessionId: string, contentHashHex: string, content: Uint8Array, structure1Cbor?: Uint8Array, structure2Cbor?: Uint8Array, contentHashAlg?: string) => boolean;
341
353
  }): void;
342
354
  /**
343
355
  * DOD-RETRYQ-STRAND-1: wire the disposition of durable state a session can no longer drain.
@@ -368,6 +380,7 @@ export declare class SessionNodeManager {
368
380
  content: Uint8Array;
369
381
  structure1Cbor?: Uint8Array;
370
382
  structure2Cbor?: Uint8Array;
383
+ contentHashAlg: string | undefined;
371
384
  }) => Promise<{
372
385
  ok: true;
373
386
  } | {
@@ -417,6 +430,41 @@ export declare class SessionNodeManager {
417
430
  * directory for the OFFLINE tree rebuild. Empty when no leaves were logged (e.g. a direct-only session
418
431
  * with no relay witness) — the caller then has nothing to carry and the seal stays bilateral/pending.
419
432
  */
433
+ /**
434
+ * REBUILD THE CERTIFIED ROOT FROM THIS DAEMON'S OWN LEAVES — `DOD-M15-SEALWIRE-1` bullet 2.
435
+ *
436
+ * The receipt used to prove only that the directory signed SOMETHING: the client took the sealed
437
+ * root off the wire, confirmed the directory's signature over those bytes, stored it, and threw
438
+ * away the root it had computed a step earlier. At co-signing time that means **your key signs a
439
+ * root you never checked.**
440
+ *
441
+ * Bullet 1 moved the certified root into the content-hash domain, which is the domain this daemon
442
+ * can actually rebuild — each carry leaf's `content_hash` is the leaf hash (RFC 6962 §2.1 "hash"
443
+ * leaves are used as-is), and the carry is ordered by the relay's canonical `sequence_number`,
444
+ * which is the order the directory rebuilds in.
445
+ *
446
+ * ─── Why this returns "cannot judge" instead of always answering ───────────────────────────
447
+ *
448
+ * A root comparison that is WRONG makes every session unsealable, and force-abandon — no receipt —
449
+ * becomes the only exit. That failure is worse than the one being guarded, and this file already
450
+ * carries two comments saying so about other gates.
451
+ *
452
+ * The carry is this daemon's view, and it is not guaranteed complete at the instant a certificate
453
+ * arrives: the counterparty's SEAL ctrl leaf is what TRIGGERS the seal, so it may not have been
454
+ * witnessed here yet. So completeness is checked FIRST, against the certificate's own leaf count.
455
+ * A short carry means this daemon cannot judge — which is a different answer from "the roots
456
+ * disagree", and conflating them would turn a local timing gap into an accusation.
457
+ */
458
+ verifyCertifiedRoot(agentPubkeyHex: string, sessionIdHex: string, certifiedRoot: Uint8Array, certifiedLeafCount: number): {
459
+ verdict: "match";
460
+ } | {
461
+ verdict: "mismatch";
462
+ ownRootHex: string | null;
463
+ detail: string;
464
+ } | {
465
+ verdict: "cannot_judge";
466
+ reason: string;
467
+ };
420
468
  getSealCarry(agentPubkeyHex: string, sessionIdHex: string): SealCarryLeaf[];
421
469
  /**
422
470
  * DOD-LOG-1 / PERSIST-002 (AC-010): append one readable message to the durable transcript, keyed
@@ -429,7 +477,19 @@ export declare class SessionNodeManager {
429
477
  * lost row only cost the unread count. Delivery reads the transcript now, so a swallowed received
430
478
  * row is TOTAL content loss and the caller has to know.
431
479
  */
432
- recordTranscriptMessage(agentName: string, sessionId: string, sequence: number, direction: "sent" | "received", plaintext: Uint8Array, correlationId?: string): boolean;
480
+ recordTranscriptMessage(agentName: string, sessionId: string, sequence: number, direction: "sent" | "received", plaintext: Uint8Array, correlationId?: string,
481
+ /**
482
+ * DOD-M15-SEALWIRE-1 bullet 5: the VERIFIED authorship proof, when there is one.
483
+ *
484
+ * Optional because there legitimately is not always one — the ordering decode can fail SOFT and
485
+ * the message is still ingested via hash-dedup. Optional is NOT the same as unremarked: absence
486
+ * is written into the row as `attribution = 'local_session_state'`, so a reader can tell a row
487
+ * whose author was proven from one whose author was assumed. That distinction is the bullet.
488
+ */
489
+ authorship?: {
490
+ senderPubkey: Uint8Array;
491
+ senderSig: Uint8Array;
492
+ }): boolean;
433
493
  /**
434
494
  * DOD-LOG-1: read a session's durable transcript back (after a restart), decrypted and ordered by
435
495
  * canonical sequence then direction. A blob that fails to decrypt (tamper/wrong key) is skipped
@@ -621,6 +681,24 @@ export declare class SessionNodeManager {
621
681
  clearRenameNotice(agentName: string, pubkey: string): void;
622
682
  /** M8C-CONTACT-1: known stays known until explicitly removed. */
623
683
  removeContact(agentName: string, pubkey: string): boolean;
684
+ /**
685
+ * Forget the pinned threshold group key for a counterparty, so the next session re-pins.
686
+ *
687
+ * DOD-M15-OFFER-SIGNED-1 review F2 — WITHOUT THIS THE REFUSAL WAS PERMANENT. The identity-change
688
+ * check refuses a counterparty whose group key differs from the one recorded in an earlier
689
+ * session, and its guidance told the operator to confirm out of band and then remove the contact
690
+ * so the new identity is pinned afresh. `removeContact` deleted a row in `contacts`; the pin lives
691
+ * in `sessions.counterparty_primary_pubkey`, and nothing in the daemon ever cleared it.
692
+ *
693
+ * So an operator who did exactly as instructed — called their counterparty, confirmed the
694
+ * re-registration was genuine, removed the contact, retried — got the identical refusal, with no
695
+ * way out short of editing the database. A security control that cannot be reset by the person it
696
+ * protects is a lockout, and the printed remedy made it worse by reading as though it worked.
697
+ *
698
+ * Nulls the column rather than deleting the session rows: those rows are the transcript record,
699
+ * and a re-pin is not a reason to lose them.
700
+ */
701
+ clearPinnedCounterpartyPrimary(agentName: string, counterpartyPubkeyHex: string): number;
624
702
  /** MONIKER-4: the operator's pet name for a pubkey (whoLabel's top tier), or null. Read-only
625
703
  * and tolerant of a not-yet-open DB (a missing label degrades the doorbell, never blocks it). */
626
704
  getContactMoniker(agentName: string, pubkey: string): string | null;
@@ -666,7 +744,7 @@ export declare class SessionNodeManager {
666
744
  ok: true;
667
745
  } | {
668
746
  ok: false;
669
- reason: string;
747
+ reason: CapacityReason;
670
748
  };
671
749
  /** M8C-TGDOOR-1: the daemon-wide Telegram bot settings, or null if never configured. */
672
750
  getTelegramSettings(): {
@@ -737,10 +815,85 @@ export declare class SessionNodeManager {
737
815
  addrs: string[];
738
816
  } | null;
739
817
  /**
740
- * The standing receiver's libp2p node a general-purpose, OPEN-gater node usable for
741
- * OUTBOUND dials that are not session-scoped (e.g. the content-park deposit/pull to the
742
- * relay, MSG-001-3b). Session nodes have restrictive gaters; the standing receiver does not.
743
- * Returns null until the receiver is ready.
818
+ * DOD-M15-ASSIGN-1 name the one peer allowed to dial this agent's standing receiver, at the
819
+ * moment the directory's `session_offer` says who is coming.
820
+ *
821
+ * This is what makes the receiver's deny-by-default safe. The offer names
822
+ * `initiator_session_peer_id`, and the responder answers it by advertising its OWN address in
823
+ * `session_offer_accept`. Narrowing here — BEFORE that answer goes out — means the door opens to
824
+ * exactly one peer at the same instant the address that reaches them is published, and never
825
+ * before. The initiator cannot know where to dial until the accept it triggers has been sent.
826
+ *
827
+ * Returns WHICH failure it was, never a bare false (review F6). The caller reports a distinct
828
+ * reason per cause: "no receiver" and "the directory named nobody" are different subsystems, and
829
+ * collapsing them sent the operator to the directory for a local problem. This method never
830
+ * widens the gate to compensate.
831
+ *
832
+ * Narrows INBOUND ONLY. The receiver is still the daemon's general-purpose dialer at this point
833
+ * — no assignment exists yet — so revoking its outbound latitude here would break content
834
+ * parking and restart-seal submission (review F2).
835
+ */
836
+ admitOfferedDialer(agentName: string, initiatorSessionPeerId: string, sessionIdHex: string): "narrowed" | "no_receiver" | "no_peer_named";
837
+ /**
838
+ * What the UNSIGNED offer claimed, so the SIGNED assignment can be checked against it.
839
+ *
840
+ * DOD-M15-OFFER-SIGNED-1. Decision 2 rules that the listening socket is "gated on the
841
+ * assignment", and the gate is narrowed from `session_offer` — a frame carrying no signature —
842
+ * because that is the only thing that arrives early enough. Timing forced the offer; it does not
843
+ * excuse trusting it.
844
+ *
845
+ * Keeping what the offer said turns the two frames into a CHECK ON EACH OTHER. The assignment is
846
+ * FROST-signed by the initiator's own threshold group, which no single directory can produce, and
847
+ * it names the same peer id. A directory that says one peer in the offer and another in the
848
+ * assignment is naming two different dialers for one session — which a truthful directory never
849
+ * does, and which is exactly the move a compromised one would make to slip a peer past the gate
850
+ * before the signed document arrives.
851
+ */
852
+ getOfferedDialer(agentName: string, sessionIdHex: string): string | null;
853
+ /**
854
+ * Which peer this agent's standing receiver is currently admitting INBOUND — `null` for nobody.
855
+ *
856
+ * Read-only, and it answers a question the daemon otherwise cannot: *"whose dial would this
857
+ * receiver accept right now?"* The gate is narrowed and re-closed from several paths (an offer
858
+ * arrives, an assignment is refused, a session is promoted), and until now the only way to know
859
+ * where it had ended up was to reproduce the sequence in your head.
860
+ *
861
+ * Added for `DOD-M15-RESPONDER-VERIFY-1`, where a refusal for one session was closing the gate a
862
+ * DIFFERENT session had narrowed — a defect with no observable symptom short of the second
863
+ * session's initiator being refused with "nothing invited it".
864
+ */
865
+ getStandingReceiverAllowedPeer(agentName: string): string | null;
866
+ /** Forget the offered dialer for ONE session — called on BOTH the claim and the refusal paths. */
867
+ clearOfferedDialer(agentName: string, sessionIdHex: string): void;
868
+ /**
869
+ * RE-CLOSE the standing receiver — but ONLY if this session is still the one holding it.
870
+ *
871
+ * DOD-M15-OFFER-SIGNED-1 review F4, then N1. The first version closed the gate unconditionally,
872
+ * and that was worse than the defect it fixed: an agent has ONE standing receiver with ONE allowed
873
+ * peer, so a refusal for session P closed the gate that offer Q had narrowed. Q's initiator —
874
+ * invited, legitimate — was then refused with *"nothing invited it"*, which this daemon had.
875
+ *
876
+ * That is the same cross-session interference F1 was written to remove, moved one method along,
877
+ * and triggerable the same way: one bogus offer/assignment pair collapses a concurrent real
878
+ * session.
879
+ *
880
+ * So the gate is closed only when it still names the peer THIS session opened it to. If a later
881
+ * offer has already re-narrowed it, that offer owns the receiver and its narrowing stands.
882
+ *
883
+ * NO EVICTION SWEEP, deliberately (N4). The sweep evicts by "not the allowed peer", and
884
+ * `getConnections()` returns OUTBOUND connections too — including the content-park and
885
+ * restart-seal dials this node makes as the daemon's general-purpose dialer, whose targets are on
886
+ * no allowlist by construction. Sweeping here hung those up, and the failure surfaced as
887
+ * `relay_unavailable`: a transport label for a local decision, which is the exact substitution
888
+ * that comment was written to prevent. The load-bearing control is `DOD-M15-FRAME-1`'s frame gate,
889
+ * which refuses what an unauthorised peer sends; closing the door is enough here.
890
+ */
891
+ revokeOfferedDialer(agentName: string, sessionIdHex: string, offeredPeerId: string | null): void;
892
+ /**
893
+ * The standing receiver's libp2p node — a general-purpose node usable for OUTBOUND dials that
894
+ * are not session-scoped (e.g. the content-park deposit/pull to the relay, MSG-001-3b). Its
895
+ * gater admits nobody INBOUND until a session names them (DOD-M15-ASSIGN-1), but leaves these
896
+ * outbound errands open. Returns null until the receiver is ready.
744
897
  */
745
898
  getStandingReceiverNode(agentName?: string): CelloNode | null;
746
899
  /**
@@ -1040,14 +1193,6 @@ export declare class SessionNodeManager {
1040
1193
  * escalates to a unilateral seal. This column only withdraws the session from AUTOMATIC retries.
1041
1194
  */
1042
1195
  markRestartSealGaveUp(agentName: string, sessionId: string, reason: string): void;
1043
- /**
1044
- * cello_list_sessions: every persisted session for one agent, regardless of
1045
- * status (active, interrupted, sealed, seal_interrupted_pending). Ordered most
1046
- * recently updated first so the live session surfaces at the top. This is the
1047
- * discovery surface that the by-id reads (cello_get_transcript /
1048
- * cello_get_sealed_receipt) depend on — without it an agent has no way to learn
1049
- * its own session ids after a restart or from a fresh MCP connection.
1050
- */
1051
1196
  getSessionsForAgent(agentName: string): SessionRecord[];
1052
1197
  /**
1053
1198
  * Every persisted session across ALL agents, most-recently-updated first. Backs the daemon-wide
@@ -1102,6 +1247,26 @@ export declare class SessionNodeManager {
1102
1247
  * signature locally. Best-effort — a missing row (race) is a no-op; the seal then falls back to
1103
1248
  * accept-without-verify (still sound: the live frame arrives over the authenticated Noise channel).
1104
1249
  */
1250
+ /**
1251
+ * The counterparty's threshold group key as this agent has seen it BEFORE — trust on first use.
1252
+ *
1253
+ * DOD-M15-OFFER-SIGNED-1 / RESPONDER-VERIFY-1. The responder does not verify the assignment's
1254
+ * signature (deferred to SESSION-004), so every field in it is whatever the directory said. That
1255
+ * makes a same-frame check circular: a compromised directory just says the same thing twice.
1256
+ *
1257
+ * This is the one anchor the responder holds that a directory CANNOT retroactively change — its
1258
+ * own memory of previous sessions with this counterparty. A directory that names a different
1259
+ * threshold group key for someone you have already talked to is either substituting an identity
1260
+ * or has been compromised since; neither is a session to accept quietly.
1261
+ *
1262
+ * THE BOUND, stated rather than glossed: this is worth nothing on FIRST contact, which is the
1263
+ * definition of trust-on-first-use. It hardens every session after it, which is where a long-lived
1264
+ * counterparty relationship actually lives.
1265
+ *
1266
+ * Keyed on `counterparty_pubkey` — the K_local IDENTITY, which is the stable thing — not on a
1267
+ * session id or a display name.
1268
+ */
1269
+ getPinnedCounterpartyPrimary(agentName: string, counterpartyPubkeyHex: string): string | null;
1105
1270
  recordCounterpartyPrimary(agentName: string, sessionId: string, primaryPubkeyHex: string): void;
1106
1271
  /**
1107
1272
  * M7-SESSION-004 (AC-005/AC-006): read the persisted seal certificate for a session.
@@ -1276,14 +1441,45 @@ export declare class SessionNodeManager {
1276
1441
  * cross-process ordering (and thus AC-002 root agreement under concurrent
1277
1442
  * bidirectional traffic) requires the relay-assigned sequence from MSG-001.
1278
1443
  */
1279
- sendContent(agentName: string, sessionId: string, content: Uint8Array, contentHash: Uint8Array, correlationId?: string,
1444
+ sendContent(agentName: string, sessionId: string, content: Uint8Array, contentHash: Uint8Array,
1445
+ /**
1446
+ * Required alongside the two below — every production caller already passes one, and an optional
1447
+ * parameter in front of a required one is what TypeScript refuses. Making it explicit costs
1448
+ * nothing and removes the last place a positional argument can silently shift.
1449
+ */
1450
+ correlationId: string | undefined,
1280
1451
  /**
1281
1452
  * The DOMAIN this content belongs to, as the relay and the directory will see it. Defaults to
1282
1453
  * MESSAGE so `cello_send` is unchanged; the document path passes 0x04/0x05. Not cosmetic — the
1283
1454
  * directory computes `final_message` and `answered` from the witnessed kind, and both of its
1284
1455
  * document exclusions were dead while every document leaf arrived here as a message.
1456
+ *
1457
+ * ⚠️ ALSO REQUIRED NOW, and for the same reason as `contentHashAlg` below — this parameter is the
1458
+ * precedent, not a bystander. It defaulted to MESSAGE, the document adapter in `daemon.ts`
1459
+ * silently dropped it, and the wire was wrong for a whole release: *"0.0.145 shipped the fix
1460
+ * everywhere except here."* A default that matches the common case makes the omission invisible
1461
+ * at every call site and at typecheck. Every caller states its kind now.
1462
+ */
1463
+ leafKind: number,
1464
+ /**
1465
+ * `DOD-M15-SEALWIRE-1` part B2b — the algorithm `contentHash` was produced under, taken from
1466
+ * `contentHashForSession` by the caller that computed the hash.
1467
+ *
1468
+ * Passed rather than re-derived HERE, deliberately: re-deriving would ask "how would this
1469
+ * session hash something now?", and the answer can differ from how THIS message was actually
1470
+ * hashed. A hash and its label must travel together or the peer refuses a message nobody touched.
1471
+ *
1472
+ * ⚠️ REQUIRED, NOT DEFAULTED — review B2b-1 F4, and the default is what made four mutants
1473
+ * unfalsifiable. It was `= CONTENT_HASH_ALGS.SHA256`, which equals the only value in play today,
1474
+ * so DROPPING THE ARGUMENT AT ANY OF THE FIVE HOPS produced byte-identical output and the whole
1475
+ * 2,800-test daemon suite stayed green. Measured at all four send sites individually.
1476
+ *
1477
+ * A default that equals the current value makes every threading edit invisible until the value
1478
+ * changes — and the day it changes, the dropped argument mislabels the message and every peer
1479
+ * refuses it as a tamper. Required makes a dropped argument a TYPECHECK failure instead of a
1480
+ * test question nobody can answer.
1285
1481
  */
1286
- leafKind?: number): Promise<{
1482
+ contentHashAlg: string): Promise<{
1287
1483
  ok: true;
1288
1484
  delivered: true;
1289
1485
  sequenceNumber?: number;
@@ -1334,27 +1530,55 @@ export declare class SessionNodeManager {
1334
1530
  * completeness check (the `desynced` reason) requires the deferred MSG-001-3b canonical-sequence
1335
1531
  * reconciliation — same documented limitation as the UP-2 gate above.
1336
1532
  */
1337
- getSealUpgradeReadiness(agentName: string, sessionId: string): {
1338
- known: boolean;
1339
- tampered: boolean;
1340
- };
1533
+ getSealUpgradeReadiness(agentName: string, sessionId: string): SealUpgradeReadiness;
1534
+ /** Record an inbound refusal for the operator. First of its kind per session is the signal. */
1535
+ noteContentRefusal(agentName: string, sessionId: string, reason: string, detail?: {
1536
+ impact?: string;
1537
+ guidance?: string;
1538
+ }): void;
1341
1539
  /**
1342
- * DAEMON-004: cross-check received content against its hash, append the
1343
- * verified leaf to the daemon-owned tree, and buffer it for cello_receive.
1344
- * A hash MISMATCH is genuine tamper rejected without append or buffer.
1540
+ * Drain the refusals a GIVEN CONSUMER has not been shown yet, and remember what it was shown.
1541
+ *
1542
+ * ─── Why this is keyed by connection, and not by a single flag ─────────────────────────────────
1345
1543
  *
1346
- * SCOPE / finding #5 what this cross-check does and does NOT prove today:
1347
- * `contentHash` here is carried in the SAME content_frame as `content`, so this
1348
- * comparison only catches wire corruption of a single frame it does NOT prove
1349
- * the content matches what the sender independently committed. Full tamper-
1350
- * evidence (EARS behavior #2) requires cross-checking against the K_local-signed
1351
- * content_hash leaf the sender submits to the RELAY on a separate channel; that
1352
- * relay hash-submit path is MSG-001's scope and does not exist yet. Until MSG-001
1353
- * lands, a malicious sender that sends matching (content, hash) in one frame is
1354
- * not detected here — only the relay-relayed signed leaf closes that gap.
1544
+ * It used to set one `surfaced: boolean` on the notice. Two MCP windows attending the same agent
1545
+ * is the ordinary case, and under that flag whoever read FIRST consumed the notice — the second
1546
+ * window was told nothing, permanently. **That is the same defect `takeReceivedContent` had**, and
1547
+ * the comment above the delivery loop in `session-content-handlers.ts` spells out why it was
1548
+ * removed: *"reading is non-destructive by construction. Nothing one consumer does mutates state
1549
+ * another consumer reads."* The whole `taken_by_sibling` apparatus exists because this was paid
1550
+ * for once already; re-introducing it on a different surface makes it no less true.
1355
1551
  *
1356
- * @returns the appended leaf index (as sequenceNumber) on success.
1552
+ * ─── Why the count now has a reader ────────────────────────────────────────────────────────────
1553
+ *
1554
+ * The old docstring claimed *"count still grows underneath, so a later reader can ask how many
1555
+ * without being told again."* **There was no later reader.** After the first surfacing the count
1556
+ * incremented under a flag the drain skipped unconditionally, so 3 refusals became 903 and nothing
1557
+ * anywhere could say so — while the comment asserted the opposite.
1558
+ *
1559
+ * So a reason RE-ANNOUNCES to a consumer when its count has grown by an order of magnitude since
1560
+ * that consumer last saw it (1 → 10 → 100 → …), marked `repeat: true`. That keeps the first
1561
+ * refusal the signal and the ninetieth silent, which is the dedup's point, while still making a
1562
+ * skew that has swallowed hundreds of messages visible — at a handful of announcements per
1563
+ * session, not one per message.
1357
1564
  */
1565
+ takeContentRefusals(agentName: string, sessionId: string,
1566
+ /**
1567
+ * REQUIRED, deliberately — no default.
1568
+ *
1569
+ * It had one (`"default"`), and a default is the defect this method was rewritten to remove,
1570
+ * lying in wait: any future call site that omits the argument silently shares ONE bucket across
1571
+ * every window, the first reader consumes the notice for all the others, and nothing fails to
1572
+ * compile and no test goes red. The parameter existing is not the protection; being unable to
1573
+ * forget it is.
1574
+ */
1575
+ consumerId: string): Array<{
1576
+ reason: string;
1577
+ impact?: string;
1578
+ guidance?: string;
1579
+ count: number;
1580
+ repeat?: boolean;
1581
+ }>;
1358
1582
  ingestReceivedContent(agentName: string, sessionId: string, content: Uint8Array, contentHash: Uint8Array, correlationId?: string,
1359
1583
  /**
1360
1584
  * DOD-FRONTIER-STRAND-1 AC1: the relay-assigned canonical position for THIS message, taken from
@@ -1363,7 +1587,29 @@ export declare class SessionNodeManager {
1363
1587
  * collapse in it before dedup is ever consulted, which is the whole defect. Absent when the
1364
1588
  * session has no relay witness (relay-degraded): see the announced fallback below.
1365
1589
  */
1366
- canonicalSeqIn?: number): Promise<{
1590
+ canonicalSeqIn?: number,
1591
+ /**
1592
+ * DOD-M15-SEALWIRE-1 part B1 — the algorithm the SENDER named on the frame, verbatim.
1593
+ *
1594
+ * `undefined` means the frame carried no name, which is a peer that predates the field and is
1595
+ * the one case we may safely assume `sha256` for. It is threaded through rather than read off
1596
+ * the session, because whether a hash is salted is a fact about the FRAME and its sender, never
1597
+ * about what this side happens to hold.
1598
+ */
1599
+ contentHashAlgIn?: string | null,
1600
+ /**
1601
+ * DOD-M15-SEALWIRE-1 bullet 5: the VERIFIED authorship proof for this message, when the caller
1602
+ * has one. The caller is the only place that has it — `#recordFrameOrdering` verifies the
1603
+ * signature against the key inside the sender's own signed bytes and matches the signer to this
1604
+ * session's counterparty, and that result reaches here or nowhere.
1605
+ *
1606
+ * Optional, because the soft decode-failure path ingests without it. The row records which it
1607
+ * was, so absence is never silent.
1608
+ */
1609
+ verifiedAuthorship?: {
1610
+ senderPubkey: Uint8Array;
1611
+ senderSig: Uint8Array;
1612
+ }): Promise<{
1367
1613
  ok: true;
1368
1614
  leafIndex: number;
1369
1615
  sequenceNumber: number;
@@ -1435,6 +1681,16 @@ export declare class SessionNodeManager {
1435
1681
  * counterparty's. They block a seal identically and mean completely different things. */
1436
1682
  heldOwn: number;
1437
1683
  heldReceived: number;
1684
+ /**
1685
+ * DOD-M15-DIVERGE-1 — this tree and the relay's counter have PROVABLY parted.
1686
+ *
1687
+ * The other two counters both measure the same direction: positions the relay committed that
1688
+ * this tree has not appended. This is the OPPOSITE direction — this tree holds a leaf at a
1689
+ * position the relay assigned to something else — and it is the direction an injected or forged
1690
+ * leaf appears in. Without it `ready` was asymmetric, and a diverged session read as perfectly
1691
+ * sealable right up until the counterparty answered `leaf_count_mismatch`, which is terminal.
1692
+ */
1693
+ diverged: boolean;
1438
1694
  };
1439
1695
  /**
1440
1696
  * DOD-M12B-SEAL-STUCK-1 — the operator-facing answer to "can this session be closed?".
@@ -1626,6 +1882,120 @@ export declare class SessionNodeManager {
1626
1882
  * record (the recover handler lives in daemon.ts, which has no access to the private method).
1627
1883
  */
1628
1884
  recordOrderingRecord(agentName: string, sessionId: string, structure1Cbor: Uint8Array, structure2Cbor: Uint8Array, contentHash: Uint8Array, correlationId?: string): number | null;
1885
+ /**
1886
+ * Test seam: force this session's own salt half, so the LOCAL-defect path is reachable.
1887
+ *
1888
+ * `generateSaltContribution` cannot produce a degenerate half, which is the point of it — so the
1889
+ * only way to exercise "our own random source is broken" end-to-end is to stand in for the broken
1890
+ * source. Named `…ForTest` like every other seam in this file, and it writes the same map
1891
+ * production writes rather than a parallel one, so a test cannot pass against state the daemon
1892
+ * never reads.
1893
+ */
1894
+ /**
1895
+ * Test seam: run the auto-acknowledge gate, exactly as the counterparty's SEAL ctrl leaf does.
1896
+ *
1897
+ * `DOD-M15-SEALWIRE-1` part B1, review F-B. The gate has ONE production call site — inside the
1898
+ * relay leaf handler, behind `leaf_kind === CTRL && !authored_by_us` — so reaching it from a test
1899
+ * needs a live relay client delivering a real ctrl leaf. The consequence was measured: my
1900
+ * "tampered never downgrades" test wrapped its decisive assertion in
1901
+ * `if (skipped.length > 0)`, which was ALWAYS FALSE, so the whole `content_tamper` vs
1902
+ * `content_verification_unavailable` branch had no coverage anywhere in the repo and two mutants
1903
+ * on it survived the full gate.
1904
+ *
1905
+ * It calls the REAL private method rather than reproducing its logic, so a test cannot pass
1906
+ * against a decision production does not make.
1907
+ */
1908
+ runAutoAcknowledgeGateForTest(agentName: string, sessionId: string, correlationId?: string): void;
1909
+ setSaltContributionForTest(agentName: string, sessionId: string, contribution: Uint8Array): void;
1910
+ /**
1911
+ * Test seam: drop this session's own half while leaving the stored salt in place — the state every
1912
+ * teardown produces, because `#evictSessionCaches` clears the map and the row survives.
1913
+ *
1914
+ * It clears the SAME map the eviction clears rather than a stand-in, so a test cannot pass against
1915
+ * a state the daemon never reaches. Reproducing it through a real teardown/revive would also drag
1916
+ * in node rebuild and relay reconnection, none of which this is about.
1917
+ */
1918
+ forgetSaltContributionForTest(agentName: string, sessionId: string): void;
1919
+ /**
1920
+ * THE ONE PLACE THAT DECIDES HOW A SESSION'S OUTBOUND CONTENT IS HASHED —
1921
+ * `DOD-M15-SEALWIRE-1` part B2b.
1922
+ *
1923
+ * Returns the hash AND the algorithm that produced it, together, because the two must not be
1924
+ * decided separately. `wire-content-hash.ts` exists for exactly this reason and says so in its own
1925
+ * header: the expression was written out at five call sites, the two added last got it wrong, and
1926
+ * the failure was invisible — *"the send succeeds, `parked: false`, the sender's log says the frame
1927
+ * left, and the receiver discards it at the authenticity check."* It took two real daemons.
1928
+ *
1929
+ * There are FOUR outbound sites (`session-content-handlers.ts`, two in `daemon.ts`,
1930
+ * `document-delivery-transport.ts`). Once salting is switchable, each of them independently
1931
+ * deciding whether to salt is that defect again with a worse failure mode — a message hashed one
1932
+ * way and LABELLED another is refused by every peer, including a correct one.
1933
+ *
1934
+ * ⚠️ ASYNC, AND THAT IS THE POINT — B2b-2 constraint 2, not an implementation detail.
1935
+ *
1936
+ * The agreement is in flight while the operator composes their first message. Hash without waiting
1937
+ * and it comes out unsalted, and that first unsalted hash closes adoption for the LIFE of the
1938
+ * session (Decision #8, unit 1). Every session would fall back permanently while every log line
1939
+ * about it stayed true — the feature present, wired, tested, and never once reached.
1940
+ *
1941
+ * The wait lives HERE rather than at the four call sites for the same reason `contentHashAlg` is a
1942
+ * required parameter rather than a defaulted one: a site that forgets it must fail to compile. A
1943
+ * caller that drops the `await` gets a `Promise` where bytes belong, which is a typecheck error;
1944
+ * a caller that forgot to call a separate `awaitSaltSettled()` would silently send unsalted.
1945
+ */
1946
+ contentHashForSession(agentName: string, sessionId: string, content: Uint8Array): Promise<{
1947
+ hash: Uint8Array;
1948
+ alg: ContentHashAlg;
1949
+ }>;
1950
+ /**
1951
+ * THIS SESSION'S UNSALTED HASH NEVER BECAME A MESSAGE — release the permanent closure it caused.
1952
+ *
1953
+ * ⚠️ REVIEW FINDING 3, and it is the opposite of the direction the flag was written to defend.
1954
+ * `#hashedWithoutSalt` closes adoption at hash time, because for a session's first message the
1955
+ * leaf is a network round trip away and every frontier count reads zero in between. Correct — but
1956
+ * `cello_send` has three paths that compute the hash and then produce NOTHING: a sibling send
1957
+ * holding the in-flight claim, the frontier moving under the send, and a non-durable send failure
1958
+ * whose bytes go to a queue with no production consumer.
1959
+ *
1960
+ * In all three the session was permanently unsalted for a message that exists nowhere: no leaf, no
1961
+ * wire, no copy at the peer. And B2b-2 made two of them MORE likely on a first message, because
1962
+ * the five-second wait widens the very window the frontier re-check is watching.
1963
+ *
1964
+ * Only safe because it is called on paths that provably sent nothing. It deliberately does NOT
1965
+ * clear `#unsaltedAnnounced`: the announcement was true when it fired and re-announcing on the
1966
+ * retry would be the per-message flood Decision #15 forbids.
1967
+ *
1968
+ * ─── THREE OTHER SITES HASH AND MAY SEND NOTHING, AND ARE EXEMPT ON PURPOSE (pass 2, F8) ──────
1969
+ *
1970
+ * `daemon.ts`'s one-shot rejection and away reply, and `document-delivery-transport.ts`'s frame
1971
+ * send, can all fail after hashing. None of them needs to abandon, and the reason is the same in
1972
+ * each: every one is a REPLY. The inbound message that triggered it has already been leafed on
1973
+ * this side, so `#saltAdoptionClosed` is already closed by the leaf count and would stay closed
1974
+ * whatever this flag said. Calling abandon there would be a no-op that looks like a guarantee.
1975
+ *
1976
+ * Written down rather than left to be re-derived: the next reader's first question is why the
1977
+ * list is three and not six.
1978
+ */
1979
+ abandonUnsaltedHash(agentName: string, sessionId: string): void;
1980
+ /**
1981
+ * TEST SEAM — put a session into the state a real one is in between announcing and being answered.
1982
+ *
1983
+ * Reaching that state for real needs a live counterparty connection, which the daemon-level
1984
+ * fixtures do not have; without a seam the wait could only be tested by not testing it. It calls
1985
+ * the same private registration the announce path calls, so it cannot drift from it.
1986
+ */
1987
+ markSaltAgreementPendingForTest(agentName: string, sessionId: string, boundMs?: number): void;
1988
+ /**
1989
+ * PUBLIC read of a session's agreed salt — `DOD-M15-SEALWIRE-1` part B2a.
1990
+ *
1991
+ * `content-park.ts` runs a SECOND, independent content-hash verifier (the park signature does not
1992
+ * cover the envelope content, so it checks before `ingestReceivedContent` is ever reached), and it
1993
+ * hardcoded `sha256`. It needs the salt to verify a v3 envelope, and it is outside this class.
1994
+ *
1995
+ * Read-only and cache-backed, so exposing it adds no way to CHANGE the salt from outside — the
1996
+ * only writer remains `#persistSessionSalt`, behind the one-salt-per-session predicate.
1997
+ */
1998
+ getSessionContentSalt(agentName: string, sessionId: string): Uint8Array | null;
1629
1999
  /**
1630
2000
  * M7-SESSION-001 AC-004/AC-005: Register a relay stream for an active session.
1631
2001
  * Starts a background reader that watches for session_interrupted frames and