@cello-protocol/daemon 0.0.23 → 0.0.25

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 (34) hide show
  1. package/dist/bin/cello-daemon.js +16 -65
  2. package/dist/bin/cello-daemon.js.map +1 -1
  3. package/dist/bundled-consortium-manifest.d.ts +45 -0
  4. package/dist/bundled-consortium-manifest.d.ts.map +1 -0
  5. package/dist/bundled-consortium-manifest.js +79 -0
  6. package/dist/bundled-consortium-manifest.js.map +1 -0
  7. package/dist/daemon.d.ts.map +1 -1
  8. package/dist/daemon.js +176 -12
  9. package/dist/daemon.js.map +1 -1
  10. package/dist/directory-bootstrap.d.ts +65 -0
  11. package/dist/directory-bootstrap.d.ts.map +1 -1
  12. package/dist/directory-bootstrap.js +105 -2
  13. package/dist/directory-bootstrap.js.map +1 -1
  14. package/dist/file-manifest-provider.d.ts +19 -0
  15. package/dist/file-manifest-provider.d.ts.map +1 -1
  16. package/dist/file-manifest-provider.js +32 -0
  17. package/dist/file-manifest-provider.js.map +1 -1
  18. package/dist/manifest-deps.d.ts +36 -0
  19. package/dist/manifest-deps.d.ts.map +1 -0
  20. package/dist/manifest-deps.js +111 -0
  21. package/dist/manifest-deps.js.map +1 -0
  22. package/dist/seal-frontier-verify.d.ts +45 -0
  23. package/dist/seal-frontier-verify.d.ts.map +1 -1
  24. package/dist/seal-frontier-verify.js +49 -0
  25. package/dist/seal-frontier-verify.js.map +1 -1
  26. package/dist/seal-receipt-upgrade.d.ts +30 -0
  27. package/dist/seal-receipt-upgrade.d.ts.map +1 -0
  28. package/dist/seal-receipt-upgrade.js +49 -0
  29. package/dist/seal-receipt-upgrade.js.map +1 -0
  30. package/dist/session-node-manager.d.ts +11 -0
  31. package/dist/session-node-manager.d.ts.map +1 -1
  32. package/dist/session-node-manager.js +21 -0
  33. package/dist/session-node-manager.js.map +1 -1
  34. package/package.json +3 -3
@@ -13,9 +13,7 @@ import { homedir } from "node:os";
13
13
  import { join } from "node:path";
14
14
  import { startDaemon } from "../daemon.js";
15
15
  import { createDirectoryEndpointResolver } from "../directory-bootstrap.js";
16
- import { FileManifestProvider } from "../file-manifest-provider.js";
17
- import { RandomizedPollScheduler } from "../manifest-poll-scheduler.js";
18
- import { ManifestDirectoryChallengeVerifier } from "@cello-protocol/transport";
16
+ import { buildManifestDeps } from "../manifest-deps.js";
19
17
  const MAX_CONNECTIONS = 16;
20
18
  // Composition root: stdout JSON logger
21
19
  const logger = {
@@ -40,73 +38,26 @@ const celloDir = process.env.CELLO_DIR || join(homedir(), ".cello");
40
38
  const socketPath = join(celloDir, "daemon.sock");
41
39
  const lockFilePath = join(celloDir, "daemon.lock");
42
40
  const version = process.env.CELLO_VERSION || "0.0.1";
43
- /**
44
- * Build the optional consortium-manifest deps from the environment (M7 J-AUTH).
45
- *
46
- * CELLO_CONSORTIUM_MANIFEST absolute path to the manifest JSON
47
- * CELLO_CONSORTIUM_ROOT_KEYS comma-separated officer root pubkeys (hex)
48
- * CELLO_CONSORTIUM_THRESHOLD minimum officer signatures (integer)
49
- *
50
- * When the manifest path is unset, returns {} — step-6 stays off (M6 compat).
51
- * One FileManifestProvider instance is shared between startDaemon's loadAndVerify
52
- * call and the ManifestDirectoryChallengeVerifier, so step-6 reads the same cached,
53
- * verified manifest.
54
- */
55
- function buildManifestDeps(logger) {
56
- const manifestPath = process.env.CELLO_CONSORTIUM_MANIFEST;
57
- if (!manifestPath)
58
- return {};
59
- const rootKeysRaw = process.env.CELLO_CONSORTIUM_ROOT_KEYS ?? "";
60
- const manifestRootKeys = rootKeysRaw.split(",").map((k) => k.trim()).filter((k) => k.length > 0);
61
- const manifestThreshold = Number.parseInt(process.env.CELLO_CONSORTIUM_THRESHOLD ?? "", 10);
62
- if (manifestRootKeys.length === 0 || Number.isNaN(manifestThreshold)) {
63
- throw new Error("CELLO_CONSORTIUM_MANIFEST is set but CELLO_CONSORTIUM_ROOT_KEYS / CELLO_CONSORTIUM_THRESHOLD are missing or invalid");
64
- }
65
- const manifestProvider = new FileManifestProvider({ path: manifestPath });
66
- const challengeVerifier = new ManifestDirectoryChallengeVerifier(manifestProvider);
67
- // Anti-rollback (DOD-AUTH-2 / TUF): the last-verified manifest version is persisted to refuse a
68
- // manifest whose version regressed across restarts. PERSIST-002 (AC-008): this now lives in the
69
- // encrypted manifest_state table (a manifest-version.json file is no longer written) — startDaemon
70
- // constructs the DB-backed store itself after opening the DB, so the bin injects nothing here.
71
- // DOD-AUTH-2: background manifest poll. The directory is re-polled on a randomized
72
- // 6–12h interval (thundering-herd avoidance) and a newer signed manifest is adopted.
73
- // The interval is env-injectable so the live binary test can poll sub-second instead
74
- // of waiting hours; production leaves these unset → the 6–12h default window.
75
- // Both-or-neither, positive, min <= max — a partial/invalid override fails LOUDLY
76
- // rather than silently reverting to 6–12h or producing a negative (tight-loop) delay.
77
- const rawPollMin = process.env.CELLO_MANIFEST_POLL_MIN_MS;
78
- const rawPollMax = process.env.CELLO_MANIFEST_POLL_MAX_MS;
79
- let pollOpts;
80
- if (rawPollMin !== undefined || rawPollMax !== undefined) {
81
- const minMs = Number.parseInt(rawPollMin ?? "", 10);
82
- const maxMs = Number.parseInt(rawPollMax ?? "", 10);
83
- if (Number.isNaN(minMs) || Number.isNaN(maxMs) || minMs <= 0 || maxMs < minMs) {
84
- throw new Error("CELLO_MANIFEST_POLL_MIN_MS / _MAX_MS must BOTH be set to positive integers with min <= max");
85
- }
86
- pollOpts = { minMs, maxMs };
87
- }
88
- const manifestPollScheduler = new RandomizedPollScheduler(pollOpts);
89
- logger.info("daemon.manifest.configured", {
90
- manifestPath,
91
- rootKeyCount: manifestRootKeys.length,
92
- threshold: manifestThreshold,
93
- pollMinMs: pollOpts?.minMs ?? null,
94
- pollMaxMs: pollOpts?.maxMs ?? null,
95
- });
96
- return { manifestProvider, manifestRootKeys, manifestThreshold, challengeVerifier, manifestPollScheduler };
97
- }
98
41
  async function main() {
99
42
  // M7 Keystone (Part 1): give the daemon its door to the directory. The resolver
100
43
  // discovers the directory endpoint via GET ${CELLO_DIRECTORY_URL}/bootstrap (the
101
44
  // proven M6 path); startDaemon builds the real signalingConnect from it + the
102
45
  // primary agent identity.
103
- const directoryEndpointResolver = createDirectoryEndpointResolver({ logger });
104
- // M7 J-AUTH (DOD-AUTH-1/2): the consortium-manifest hardening is OPT-IN. When the
105
- // operator (or the live harness) configures a manifest, the daemon loads + verifies
106
- // it (threshold officer signatures + validity window) and verifies the directory's
107
- // step-6 identity proof against the node pubkeys in that manifest. When unset, the
108
- // daemon runs the M6 backward-compat path: no challengeVerifier step-6 skipped.
109
- // Both dialers (keystone + per-agent) receive the verifier via startDaemon.
46
+ // FINDING-4: this resolver is wrapped by the daemon's roster-aware failover resolver, which owns
47
+ // ALL fallback semantics (roster + sticky). staleFallback:false makes it report a dead primary as
48
+ // null on a fresh /bootstrap failure WITHOUT it, the wrapper would keep receiving the stale dead
49
+ // endpoint and never fail over (the exact live kill-primary bug). The roster is the real fallback.
50
+ const directoryEndpointResolver = createDirectoryEndpointResolver({ logger, staleFallback: false });
51
+ // M7 J-AUTH (DOD-AUTH-1/2) + FINDING-4: consortium-manifest deps. buildManifestDeps chooses:
52
+ // - DEFAULT (no CELLO_CONSORTIUM_MANIFEST) load the COMPILED-IN production roster + step-6
53
+ // directory identity auth, so a cold-boot daemon knows every directory and can fail over to a
54
+ // reachable one (redundancy invariant). Gated on CELLO_DIRECTORY_URL actually being a bundled
55
+ // node — a daemon pointed at a local/non-bundled directory (local dev, e2e spine harness) gets
56
+ // the M6 backward-compat path (no roster, no step-6) instead of wrongly failing step-6.
57
+ // - OVERRIDE (CELLO_CONSORTIUM_MANIFEST set) — operator-supplied manifest FILE + env root keys /
58
+ // threshold + optional /manifest poll (the pre-FINDING-4 opt-in path).
59
+ // When a manifest is active, the daemon verifies the directory's step-6 identity proof against the
60
+ // node pubkeys in it. Both dialers (keystone + per-agent) receive the verifier via startDaemon.
110
61
  const manifest = buildManifestDeps(logger);
111
62
  const handle = await startDaemon({
112
63
  celloDir,
@@ -1 +1 @@
1
- {"version":3,"file":"cello-daemon.js","sourceRoot":"","sources":["../../src/bin/cello-daemon.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cello-daemon.js","sourceRoot":"","sources":["../../src/bin/cello-daemon.ts"],"names":[],"mappings":";AACA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,+BAA+B,EAAE,MAAM,2BAA2B,CAAC;AAC5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAGxD,MAAM,eAAe,GAAG,EAAE,CAAC;AAE3B,uCAAuC;AACvC,MAAM,MAAM,GAAW;IACrB,KAAK,CAAC,KAAa,EAAE,OAAgC;QACnD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QACjG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IACpC,CAAC;IACD,IAAI,CAAC,KAAa,EAAE,OAAgC;QAClD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QAChG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IACpC,CAAC;IACD,IAAI,CAAC,KAAa,EAAE,OAAgC;QAClD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QAChG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IACpC,CAAC;IACD,KAAK,CAAC,KAAa,EAAE,OAAgC;QACnD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QACjG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IACpC,CAAC;CACF,CAAC;AAEF,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,CAAC;AACpE,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;AACjD,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;AACnD,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,OAAO,CAAC;AAErD,KAAK,UAAU,IAAI;IACjB,gFAAgF;IAChF,iFAAiF;IACjF,8EAA8E;IAC9E,0BAA0B;IAC1B,iGAAiG;IACjG,kGAAkG;IAClG,mGAAmG;IACnG,mGAAmG;IACnG,MAAM,yBAAyB,GAAG,+BAA+B,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,CAAC;IAEpG,6FAA6F;IAC7F,+FAA+F;IAC/F,kGAAkG;IAClG,kGAAkG;IAClG,mGAAmG;IACnG,4FAA4F;IAC5F,mGAAmG;IACnG,2EAA2E;IAC3E,mGAAmG;IACnG,gGAAgG;IAChG,MAAM,QAAQ,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAE3C,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC;QAC/B,QAAQ;QACR,UAAU;QACV,YAAY;QACZ,cAAc,EAAE,eAAe;QAC/B,OAAO;QACP,MAAM;QACN,yBAAyB;QACzB,GAAG,QAAQ;KACZ,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,KAAK,EAAE,MAAc,EAAiB,EAAE;QACvD,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC1B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IAEF,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;QACzB,IAAI,CAAC;YACH,QAAQ,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;gBACzC,MAAM,CAAC,KAAK,CAAC,wBAAwB,EAAE;oBACrC,MAAM,EAAE,SAAS;oBACjB,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;iBACxD,CAAC,CAAC;gBACH,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAClB,CAAC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,MAAM,CAAC,KAAK,CAAC,wBAAwB,EAAE;gBACrC,MAAM,EAAE,SAAS;gBACjB,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;aACxD,CAAC,CAAC;YACH,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QACxB,IAAI,CAAC;YACH,QAAQ,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;gBACxC,MAAM,CAAC,KAAK,CAAC,wBAAwB,EAAE;oBACrC,MAAM,EAAE,QAAQ;oBAChB,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;iBACxD,CAAC,CAAC;gBACH,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAClB,CAAC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,MAAM,CAAC,KAAK,CAAC,wBAAwB,EAAE;gBACrC,MAAM,EAAE,QAAQ;gBAChB,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;aACxD,CAAC,CAAC;YACH,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC5B,MAAM,CAAC,KAAK,CAAC,uBAAuB,EAAE;QACpC,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;KACxD,CAAC,CAAC;IACH,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * FINDING-4 — the bundled consortium roster (sovereign-node REDUNDANCY invariant).
3
+ *
4
+ * This is the signed list of the sovereign directory nodes that make up the CELLO consortium,
5
+ * COMPILED INTO the client. It is loaded by default (when the operator does not override it via
6
+ * CELLO_CONSORTIUM_MANIFEST) so that a cold-boot daemon already knows every directory — and can
7
+ * fail over to a reachable one — even when its configured/primary directory is unreachable. Without
8
+ * a bundled roster the failover resolver has nothing to route to (the exact FINDING-4 gap: the
9
+ * roster-aware dialer was correct but the roster was empty), so a single directory being down
10
+ * stranded the client at startup, violating the redundancy invariant.
11
+ *
12
+ * Trust model:
13
+ * - The manifest is threshold-signed by the consortium officer key(s). At load the client
14
+ * RE-VERIFIES it against BUNDLED_CONSORTIUM_ROOT_KEYS (a self-consistency / anti-corruption
15
+ * gate — the manifest and root keys ship together, so this catches a bad regeneration, not a
16
+ * network adversary).
17
+ * - The node `pubkey`s are the directories' Ed25519 node keys. They are the trust anchor for
18
+ * step-6 directory identity auth (M7-MANIFEST-002): when the client connects to (or fails over
19
+ * to) a directory, the directory must sign the client's challenge with the matching node key,
20
+ * proving it is the real consortium node for that nodeId — defeating a MITM on the plaintext
21
+ * /bootstrap that would otherwise redirect the client to a rogue directory. THIS is the
22
+ * adversarial defense; it is enabled by default alongside the roster.
23
+ *
24
+ * nodeId MUST equal the directory's reported NODE_ID (its AWS region string); the step-6 verifier
25
+ * looks the node up by nodeId. pubkey MUST equal /cello/<env>/directory/manifest-signer-pubkey for
26
+ * that region (derived from the directory's node-private-key). endpoint is the HTTP /bootstrap base.
27
+ *
28
+ * Regeneration: on directory node-key or officer-key rotation, re-sign with
29
+ * infra/scripts/sign-consortium-manifest and update this constant + BUNDLED_CONSORTIUM_ROOT_KEYS.
30
+ * The officer signing key lives in Secrets Manager (cello/<env>/consortium/officer-key-0); only its
31
+ * PUBLIC key is embedded here.
32
+ *
33
+ * Crypto reference: RFC 8032 (Ed25519 threshold signatures, verified by verifyManifest).
34
+ */
35
+ import type { ConsortiumManifestInput } from "@cello-protocol/crypto";
36
+ /** The signed consortium manifest (roster of sovereign directory nodes). */
37
+ export declare const BUNDLED_CONSORTIUM_MANIFEST: ConsortiumManifestInput;
38
+ /**
39
+ * Pinned consortium officer root public key(s). Officer 0's Ed25519 public key (hex). The client
40
+ * verifies BUNDLED_CONSORTIUM_MANIFEST's threshold signatures against this set. Public data.
41
+ */
42
+ export declare const BUNDLED_CONSORTIUM_ROOT_KEYS: readonly string[];
43
+ /** Minimum number of distinct valid officer signatures required (dev: single officer, threshold 1). */
44
+ export declare const BUNDLED_CONSORTIUM_THRESHOLD = 1;
45
+ //# sourceMappingURL=bundled-consortium-manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundled-consortium-manifest.d.ts","sourceRoot":"","sources":["../src/bundled-consortium-manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEtE,4EAA4E;AAC5E,eAAO,MAAM,2BAA2B,EAAE,uBAkCzC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,EAAE,SAAS,MAAM,EAEzD,CAAC;AAEF,uGAAuG;AACvG,eAAO,MAAM,4BAA4B,IAAI,CAAC"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * FINDING-4 — the bundled consortium roster (sovereign-node REDUNDANCY invariant).
3
+ *
4
+ * This is the signed list of the sovereign directory nodes that make up the CELLO consortium,
5
+ * COMPILED INTO the client. It is loaded by default (when the operator does not override it via
6
+ * CELLO_CONSORTIUM_MANIFEST) so that a cold-boot daemon already knows every directory — and can
7
+ * fail over to a reachable one — even when its configured/primary directory is unreachable. Without
8
+ * a bundled roster the failover resolver has nothing to route to (the exact FINDING-4 gap: the
9
+ * roster-aware dialer was correct but the roster was empty), so a single directory being down
10
+ * stranded the client at startup, violating the redundancy invariant.
11
+ *
12
+ * Trust model:
13
+ * - The manifest is threshold-signed by the consortium officer key(s). At load the client
14
+ * RE-VERIFIES it against BUNDLED_CONSORTIUM_ROOT_KEYS (a self-consistency / anti-corruption
15
+ * gate — the manifest and root keys ship together, so this catches a bad regeneration, not a
16
+ * network adversary).
17
+ * - The node `pubkey`s are the directories' Ed25519 node keys. They are the trust anchor for
18
+ * step-6 directory identity auth (M7-MANIFEST-002): when the client connects to (or fails over
19
+ * to) a directory, the directory must sign the client's challenge with the matching node key,
20
+ * proving it is the real consortium node for that nodeId — defeating a MITM on the plaintext
21
+ * /bootstrap that would otherwise redirect the client to a rogue directory. THIS is the
22
+ * adversarial defense; it is enabled by default alongside the roster.
23
+ *
24
+ * nodeId MUST equal the directory's reported NODE_ID (its AWS region string); the step-6 verifier
25
+ * looks the node up by nodeId. pubkey MUST equal /cello/<env>/directory/manifest-signer-pubkey for
26
+ * that region (derived from the directory's node-private-key). endpoint is the HTTP /bootstrap base.
27
+ *
28
+ * Regeneration: on directory node-key or officer-key rotation, re-sign with
29
+ * infra/scripts/sign-consortium-manifest and update this constant + BUNDLED_CONSORTIUM_ROOT_KEYS.
30
+ * The officer signing key lives in Secrets Manager (cello/<env>/consortium/officer-key-0); only its
31
+ * PUBLIC key is embedded here.
32
+ *
33
+ * Crypto reference: RFC 8032 (Ed25519 threshold signatures, verified by verifyManifest).
34
+ */
35
+ /** The signed consortium manifest (roster of sovereign directory nodes). */
36
+ export const BUNDLED_CONSORTIUM_MANIFEST = {
37
+ version: 1,
38
+ not_before: "2026-01-01T00:00:00Z",
39
+ expires: "2030-01-01T00:00:00Z",
40
+ nodes: [
41
+ {
42
+ nodeId: "us-east-1",
43
+ pubkey: "167ca6b145bfdd3696af8f4befd883c3dc610f4a9c8d52a30f6a22f669dc27b5",
44
+ region: "us-east-1",
45
+ provider: "aws",
46
+ endpoint: "http://directory-us1.cello.mygentic.ai",
47
+ },
48
+ {
49
+ nodeId: "eu-central-1",
50
+ pubkey: "8105b180b753d97b50039a7e94433fd2b419f43d61f9ad7caf2ac15ad5cd1b45",
51
+ region: "eu-central-1",
52
+ provider: "aws",
53
+ endpoint: "http://directory-eu1.cello.mygentic.ai",
54
+ },
55
+ {
56
+ nodeId: "ap-northeast-1",
57
+ pubkey: "9b4b673a16487ba47363e3eaff844bf68f19736d82967918fb896b813e39b984",
58
+ region: "ap-northeast-1",
59
+ provider: "aws",
60
+ endpoint: "http://directory-ap1.cello.mygentic.ai",
61
+ },
62
+ ],
63
+ signatures: [
64
+ {
65
+ officerIndex: 0,
66
+ signature: "1a372676e83ae323be8dbc6f8b786b3424706999efcccd70027798cbca91fe0b60cb35ac1e1af21078f24618d1f740b61bdcf06da03c5f90acc48d954039a104",
67
+ },
68
+ ],
69
+ };
70
+ /**
71
+ * Pinned consortium officer root public key(s). Officer 0's Ed25519 public key (hex). The client
72
+ * verifies BUNDLED_CONSORTIUM_MANIFEST's threshold signatures against this set. Public data.
73
+ */
74
+ export const BUNDLED_CONSORTIUM_ROOT_KEYS = [
75
+ "8e9b99e5aa5505f2f50ec9f933f497a64956614575edce3427ec8a096c864199",
76
+ ];
77
+ /** Minimum number of distinct valid officer signatures required (dev: single officer, threshold 1). */
78
+ export const BUNDLED_CONSORTIUM_THRESHOLD = 1;
79
+ //# sourceMappingURL=bundled-consortium-manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundled-consortium-manifest.js","sourceRoot":"","sources":["../src/bundled-consortium-manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAIH,4EAA4E;AAC5E,MAAM,CAAC,MAAM,2BAA2B,GAA4B;IAClE,OAAO,EAAE,CAAC;IACV,UAAU,EAAE,sBAAsB;IAClC,OAAO,EAAE,sBAAsB;IAC/B,KAAK,EAAE;QACL;YACE,MAAM,EAAE,WAAW;YACnB,MAAM,EAAE,kEAAkE;YAC1E,MAAM,EAAE,WAAW;YACnB,QAAQ,EAAE,KAAK;YACf,QAAQ,EAAE,wCAAwC;SACnD;QACD;YACE,MAAM,EAAE,cAAc;YACtB,MAAM,EAAE,kEAAkE;YAC1E,MAAM,EAAE,cAAc;YACtB,QAAQ,EAAE,KAAK;YACf,QAAQ,EAAE,wCAAwC;SACnD;QACD;YACE,MAAM,EAAE,gBAAgB;YACxB,MAAM,EAAE,kEAAkE;YAC1E,MAAM,EAAE,gBAAgB;YACxB,QAAQ,EAAE,KAAK;YACf,QAAQ,EAAE,wCAAwC;SACnD;KACF;IACD,UAAU,EAAE;QACV;YACE,YAAY,EAAE,CAAC;YACf,SAAS,EACP,kIAAkI;SACrI;KACF;CACF,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAsB;IAC7D,kEAAkE;CACnE,CAAC;AAEF,uGAAuG;AACvG,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EASrB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAM/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAqB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAM/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAqInF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,IAAI,oBAAoB,CAAC;IAClC;;;;OAIG;IACH,qBAAqB,IAAI,kBAAkB,CAAC;IAC5C;;;;;;;;OAQG;IACH,gBAAgB,IAAI,SAAS,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;CACtC;AAyCD,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAqmI7E"}
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EASrB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAM/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAsB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAM/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA0InF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,IAAI,oBAAoB,CAAC;IAClC;;;;OAIG;IACH,qBAAqB,IAAI,kBAAkB,CAAC;IAC5C;;;;;;;;OAQG;IACH,gBAAgB,IAAI,SAAS,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;CACtC;AAyCD,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAwwI7E"}
package/dist/daemon.js CHANGED
@@ -45,6 +45,7 @@ import { DbRegistrationPersistence, DbIdentityStore } from "./db-identity-store.
45
45
  import { DbManifestVersionStore } from "./manifest-version-store-db.js";
46
46
  import { verify as ed25519Verify, sealToRecipient, generateKLocalSeed, InMemoryKeyProvider, hash as cryptoHash } from "@cello-protocol/crypto";
47
47
  import { attemptSealUpgrade as attemptSealUpgradeImpl, verifyUpgradeConfirmedCert } from "./seal-upgrade.js";
48
+ import { upgradeAbsentToRecovered, hasAbsentParticipant } from "./seal-receipt-upgrade.js";
48
49
  // CELLO-M7-MSG-001 (AC-013/AC-018): the single application content-size cap, enforced
49
50
  // at the send point here (the receive point lives in the transport content decode).
50
51
  import { MAX_CONTENT_BYTES, computeGenesisPrevRoot, buildAgentRevocationTbs } from "@cello-protocol/protocol-types";
@@ -52,10 +53,10 @@ import { resolveCelloEnv, createTransportSelector, isProductionVariant, } from "
52
53
  import { selectAdvertisedAddress } from "./transport-selector.js";
53
54
  import { parseSessionAssignment, sessionRequestErrorReason } from "./session-assignment-parser.js";
54
55
  import { wireSessionCeremonyHandler, wireSessionOfferHandler, wireSealCeremonyHandler, verifyUnilateralCertificate, verifyBilateralSealCertificate, runAgentRefresh } from "./session-ceremony.js";
55
- import { reDeriveFrontiers, findInflatedFrontier } from "./seal-frontier-verify.js";
56
+ import { reDeriveFrontiers, findInflatedFrontier, checkUnilateralFrontier } from "./seal-frontier-verify.js";
56
57
  import { LocalAutoNatStub } from "@cello-protocol/transport";
57
58
  import { startHttpManifestPoll } from "./http-manifest-poll.js";
58
- import { resolveDirectoryUrl, manifestNodesToEndpoints } from "./directory-bootstrap.js";
59
+ import { resolveDirectoryUrl, manifestNodesToEndpoints, createRosterAwareEndpointResolver, } from "./directory-bootstrap.js";
59
60
  /**
60
61
  * M7-SESSION-001 (H-1): canonical byte encoding of a SEAL-INTERRUPTED leaf for
61
62
  * Ed25519 signing/verification. Field order is fixed and deterministic. Both the
@@ -330,6 +331,25 @@ export async function startDaemon(config) {
330
331
  const m = manifestProvider?.getCurrentManifest();
331
332
  return m ? await manifestNodesToEndpoints(m.nodes, { logger }) : null;
332
333
  };
334
+ // FINDING-4: roster-aware directory failover (bootstrap SPOF fix). The injected
335
+ // directoryEndpointResolver only ever probes the single configured node (CELLO_DIRECTORY_URL),
336
+ // so a down primary stranded the client at startup even though it held the other nodes'
337
+ // addresses. Wrap it with the consortium roster so the signaling dialer — and the ceremony
338
+ // endpoint that shares this same instance — route AROUND a down primary (primary-first,
339
+ // sticky-until-fail, randomized fallback), honoring the sovereign-node REDUNDANCY invariant.
340
+ // ONE instance → shared sticky state so signaling + ceremonies stay on the SAME directory node
341
+ // and fail over together. Undefined on the in-process test path (no directoryEndpointResolver),
342
+ // where the shared manager uses an injected signalingConnect instead.
343
+ const failoverEndpointResolver = directoryEndpointResolver
344
+ ? createRosterAwareEndpointResolver({
345
+ primaryResolver: directoryEndpointResolver,
346
+ getConsortiumRoster: resolveConsortiumRoster,
347
+ logger,
348
+ })
349
+ : undefined;
350
+ // Null-safe accessor for the ceremony/handler getDirectoryEndpoint sites (mirrors the old
351
+ // `directoryEndpointResolver ? ... : null` wrapper, now routed through the failover resolver).
352
+ const getFailoverEndpoint = async () => failoverEndpointResolver ? await failoverEndpointResolver() : null;
333
353
  // CELLO-M7-CONN-001 (DOD-CONN-3): start the daemon-level HTTP manifest poll. It fetches
334
354
  // ${directoryHttpUrl}/manifest, verifies the threshold signature against the locally-pinned
335
355
  // root keys, applies anti-rollback + expiry, and adopts a newer manifest — independent of any
@@ -484,7 +504,10 @@ export async function startDaemon(config) {
484
504
  if (!directoryEndpointResolver) {
485
505
  throw new Error("getAgentSignaling: no directory endpoint resolver configured (and no shared manager)");
486
506
  }
487
- const resolver = directoryEndpointResolver;
507
+ // FINDING-4: dial through the roster-aware failover resolver so this agent's signaling
508
+ // stream routes around a down primary node. (failoverEndpointResolver is defined here
509
+ // because it is built iff directoryEndpointResolver is — guarded non-null just above.)
510
+ const resolver = failoverEndpointResolver ?? directoryEndpointResolver;
488
511
  let nodeRef = null;
489
512
  const connect = createSignalingConnect({
490
513
  getDirectoryEndpoint: resolver,
@@ -513,7 +536,7 @@ export async function startDaemon(config) {
513
536
  persistence: getPersistence(agentName),
514
537
  agentPubkeyHex,
515
538
  getNode: entry.getNode,
516
- getDirectoryEndpoint: async () => (directoryEndpointResolver ? (await directoryEndpointResolver()) ?? null : null),
539
+ getDirectoryEndpoint: getFailoverEndpoint,
517
540
  getConsortiumEndpoints: resolveConsortiumRoster,
518
541
  signaling: mgr,
519
542
  logger,
@@ -524,7 +547,7 @@ export async function startDaemon(config) {
524
547
  persistence: getPersistence(agentName),
525
548
  agentPubkeyHex,
526
549
  getNode: entry.getNode,
527
- getDirectoryEndpoint: async () => (directoryEndpointResolver ? (await directoryEndpointResolver()) ?? null : null),
550
+ getDirectoryEndpoint: getFailoverEndpoint,
528
551
  getConsortiumEndpoints: resolveConsortiumRoster,
529
552
  signaling: mgr,
530
553
  logger,
@@ -591,7 +614,7 @@ export async function startDaemon(config) {
591
614
  persistence: getPersistence(agent.name),
592
615
  agentPubkeyHex: agent.pubkey,
593
616
  getNode: getDirectoryNode,
594
- getDirectoryEndpoint: async () => (directoryEndpointResolver ? (await directoryEndpointResolver()) ?? null : null),
617
+ getDirectoryEndpoint: getFailoverEndpoint,
595
618
  getConsortiumEndpoints: resolveConsortiumRoster,
596
619
  signaling: mgr,
597
620
  logger,
@@ -601,7 +624,7 @@ export async function startDaemon(config) {
601
624
  persistence: getPersistence(agent.name),
602
625
  agentPubkeyHex: agent.pubkey,
603
626
  getNode: getDirectoryNode,
604
- getDirectoryEndpoint: async () => (directoryEndpointResolver ? (await directoryEndpointResolver()) ?? null : null),
627
+ getDirectoryEndpoint: getFailoverEndpoint,
605
628
  getConsortiumEndpoints: resolveConsortiumRoster,
606
629
  signaling: mgr,
607
630
  logger,
@@ -1444,9 +1467,16 @@ export async function startDaemon(config) {
1444
1467
  registrationInProgress = true;
1445
1468
  try {
1446
1469
  // Resolve the directory endpoint once for this registration (the context's
1447
- // getDirectoryEndpoint is synchronous; the daemon's resolver is async with a
1448
- // last-known-good fallback). The endpoint is stable for the duration of one
1449
- // registrationif it changed mid-flow the DKG streams would break anyway.
1470
+ // getDirectoryEndpoint is synchronous; the daemon's resolver is async).
1471
+ // FINDING-4 scope note: this uses the PRIMARY resolver directly (not the roster-aware
1472
+ // failover resolver registration is deliberately out of FINDING-4's "signaling +
1473
+ // ceremony" scope). Because that primary is now built with staleFallback:false (so the
1474
+ // failover wrapper sees a dead primary as null), a transient /bootstrap blip here resolves
1475
+ // to null → directory_unreachable rather than riding through on a stale last-known-good;
1476
+ // registration is a rare, manual, retryable op, so failing fast (retry) is acceptable. With
1477
+ // a manifest configured the DKG fans out over the independently-probed roster regardless of
1478
+ // this endpoint. The endpoint is stable for one registration — if it changed mid-flow the
1479
+ // DKG streams would break anyway.
1450
1480
  const ep = await directoryEndpointResolver();
1451
1481
  if (!ep || !ep.multiaddr) {
1452
1482
  // FROST DKG must dial the directory's /cello/frost/1.0.0 — a dialable
@@ -1769,6 +1799,84 @@ export async function startDaemon(config) {
1769
1799
  // seal still completes — surfaced loudly so this can never masquerade as a produced receipt.
1770
1800
  const rootHex = Buffer.from(sealedRoot).toString("hex");
1771
1801
  const legibility = normalizeLegibility(frame["legibility"]);
1802
+ // M8B FINDING-5 (SI-002): before persisting, INDEPENDENTLY re-derive each CLIENT-VERIFIABLE
1803
+ // ('live', the present party) frontier from the signed frontier_leaves and OVERRIDE an inflated
1804
+ // directory-published value DOWN to the provable one (the directory cannot forge signed leaves,
1805
+ // so the re-derived value is truth). We OVERRIDE, never reject — the unilateral seal has a
1806
+ // directory-side dedup guard that makes a client rejection unrecoverable (a retry close is
1807
+ // silently ignored → no receipt ever, worse than FINDING-3; cascade-2 reviewer Critical 2). The
1808
+ // absent party's frontier stays directory-attested (its remainder is not re-derivable here). No
1809
+ // frontier_leaves (pre-FINDING-5 directory) → the cert stays directory-attested (FINDING-3).
1810
+ // A receipt is ALWAYS persisted; the close never dead-ends here.
1811
+ if (legibility !== undefined) {
1812
+ const rawParticipants = legibility.participants ?? [];
1813
+ const guardParticipants = rawParticipants
1814
+ .filter((p) => typeof p.pubkey === "string" && typeof p.attestation_mode === "string")
1815
+ .map((p) => ({
1816
+ pubkey: p.pubkey,
1817
+ content_frontier_seq: typeof p.content_frontier_seq === "number" ? p.content_frontier_seq : 0,
1818
+ attestation_mode: p.attestation_mode,
1819
+ }));
1820
+ const frontierLeavesRaw = frame["frontier_leaves"];
1821
+ const toU8f = (v) => (v instanceof Uint8Array ? v : new Uint8Array(v));
1822
+ const parsedFrontierLeaves = Array.isArray(frontierLeavesRaw) && frontierLeavesRaw.length > 0
1823
+ ? frontierLeavesRaw.map((l) => {
1824
+ const o = l;
1825
+ return {
1826
+ structure1_cbor: toU8f(o["structure1_cbor"]),
1827
+ sender_pubkey: toU8f(o["sender_pubkey"]),
1828
+ sender_signature: toU8f(o["sender_signature"]),
1829
+ };
1830
+ })
1831
+ : undefined;
1832
+ const check = checkUnilateralFrontier(guardParticipants, parsedFrontierLeaves, sessionId);
1833
+ // Apply any frontier corrections to the object we persist — override inflated 'live'
1834
+ // frontier(s) DOWN to the provable value, so the stored receipt never claims more than the
1835
+ // signed leaves support. Runs for both 'corrected' (valid leaves) and 'leaves_invalid'
1836
+ // (forged leaves → corrected to 0). Never rejects → the close never dead-ends.
1837
+ if (check.corrections.size > 0) {
1838
+ for (const p of rawParticipants) {
1839
+ if (typeof p.pubkey === "string" && check.corrections.has(p.pubkey.toLowerCase())) {
1840
+ p.content_frontier_seq = check.corrections.get(p.pubkey.toLowerCase());
1841
+ }
1842
+ }
1843
+ }
1844
+ if (check.status === "corrected") {
1845
+ logger.error("seal.certificate.frontier.overridden", {
1846
+ sessionId: sidHex,
1847
+ corrections: [...check.corrections.entries()].map(([party, seq]) => ({ party, correctedTo: seq })),
1848
+ path: "unilateral",
1849
+ });
1850
+ }
1851
+ else if (check.status === "verified") {
1852
+ logger.info("seal.certificate.frontier.verified", {
1853
+ sessionId: sidHex,
1854
+ parties: guardParticipants.filter((p) => p.attestation_mode === "live").length,
1855
+ path: "unilateral",
1856
+ });
1857
+ }
1858
+ else if (check.status === "leaves_invalid") {
1859
+ // Forged / cross-session leaves — a tamper signal. The 'live' frontier(s) have been
1860
+ // corrected to 0 above (zero trustworthy evidence); persist that (never reject → never
1861
+ // dead-end). Surfaced loudly for audit.
1862
+ logger.error("seal.certificate.frontier.leaves_invalid", {
1863
+ sessionId: sidHex,
1864
+ reason: check.reason,
1865
+ corrections: [...check.corrections.entries()].map(([party, seq]) => ({ party, correctedTo: seq })),
1866
+ path: "unilateral",
1867
+ });
1868
+ }
1869
+ else {
1870
+ // directory_attested: no frontier_leaves shipped (pre-FINDING-5 directory). Frontiers stay
1871
+ // DIRECTORY-attested (already marked per-participant) — visible/auditable, never silently
1872
+ // presented as client-verified.
1873
+ logger.warn("seal.certificate.frontier.directory_attested", {
1874
+ sessionId: sidHex,
1875
+ reason: "no_frontier_leaves",
1876
+ path: "unilateral",
1877
+ });
1878
+ }
1879
+ }
1772
1880
  if (legibility !== undefined) {
1773
1881
  try {
1774
1882
  sessionNodeManager.recordSealCertificate(agentName, sidHex, rootHex, JSON.stringify(legibility));
@@ -1819,7 +1927,7 @@ export async function startDaemon(config) {
1819
1927
  return;
1820
1928
  sealUpgradeInFlight.add(key);
1821
1929
  try {
1822
- await attemptSealUpgradeImpl({
1930
+ const result = await attemptSealUpgradeImpl({
1823
1931
  logger, agentName, agentPubkeyHex,
1824
1932
  getReadiness: (a, s) => sessionNodeManager.getSealUpgradeReadiness(a, s),
1825
1933
  getContentLeafCount: (a, s) => sessionNodeManager.getSessionTree(a, s).size(),
@@ -1827,6 +1935,44 @@ export async function startDaemon(config) {
1827
1935
  getKeyProvider: (a) => keyProviders.get(a),
1828
1936
  sendRaw: (f) => signaling.sendRaw(f),
1829
1937
  }, sidHex, frame);
1938
+ // M8B FINDING-6 (3b): the ABSENT party (B) persists its UNILATERAL receipt from the
1939
+ // notification's legibility — but ONLY after the KERNEL content-recovery/verify gate passed.
1940
+ // `result.sent` is true iff attemptSealUpgradeImpl recovered + integrity-verified the content
1941
+ // behind R1 and sent the ratification request; a tampered/unrecoverable/incomplete case returns
1942
+ // sent:false → NO receipt (never a receipt for content B could not verify). B may hold no local
1943
+ // `sessions` row (recordSealCertificateEnsuringRow inserts a stub, counterparty = A's pubkey).
1944
+ // The notification carries no frontier_leaves (FINDING-5 ships those only on the present party's
1945
+ // confirm frame), so B's legibility is directory-attested — consistent with FINDING-5's
1946
+ // directory_attested path; B trusts its own KERNEL-verified content, not a re-derivation.
1947
+ if (result.sent) {
1948
+ try {
1949
+ // ONE-WAY RATCHET (cascade-2 FINDING-6 review): a re-delivered seal_unilateral_notification
1950
+ // (reconnect burst) re-runs this path and would re-persist the notification's ORIGINAL
1951
+ // legibility (counterparty 'absent'). If a prior upgrade already flipped the stored receipt
1952
+ // to 'recovered' (no 'absent' participant), re-persisting would REGRESS it — and no later
1953
+ // event restores 'recovered' (the directory dedups the duplicate as already_bilateral →
1954
+ // seal_upgrade_rejected, which only logs). So skip the re-persist once the cert is upgraded.
1955
+ const existing = sessionNodeManager.getSealCertificate(agentName, sidHex);
1956
+ if (existing && !hasAbsentParticipant(existing.legibility)) {
1957
+ logger.debug("session.unilateral.receipt.persist.skipped", { sessionId: sidHex, reason: "already_upgraded" });
1958
+ }
1959
+ else {
1960
+ const legibility = normalizeLegibility(frame["legibility"]);
1961
+ const rootHex = frameValueToHex(frame["sealed_root"]);
1962
+ const counterpartyHex = frameValueToHex(frame["present_pubkey"]); // A — the present party
1963
+ if (legibility !== undefined && rootHex && counterpartyHex) {
1964
+ sessionNodeManager.recordSealCertificateEnsuringRow(agentName, sidHex, counterpartyHex, rootHex, JSON.stringify(legibility));
1965
+ logger.info("session.unilateral.receipt.persisted", { sessionId: sidHex, sealedRoot: rootHex, party: "absent" });
1966
+ }
1967
+ else if (legibility === undefined) {
1968
+ logger.warn("session.unilateral.receipt.absent", { sessionId: sidHex, reason: "no_legibility_on_notification" });
1969
+ }
1970
+ }
1971
+ }
1972
+ catch (error) {
1973
+ logger.warn("seal.certificate.persist.failed", { sessionId: sidHex, reason: error instanceof Error ? error.message : String(error) });
1974
+ }
1975
+ }
1830
1976
  }
1831
1977
  finally {
1832
1978
  // Clear the guard so a later reconnect can retry if the request never reached the directory;
@@ -1844,6 +1990,24 @@ export async function startDaemon(config) {
1844
1990
  if (!result.ok)
1845
1991
  return; // cert.invalid already logged inside; do NOT accept as bilateral.
1846
1992
  logger.info("session.seal.upgraded", { sessionId: sidHex, agentName, party: result.party });
1993
+ // M8B FINDING-6 (3a): the seal is now BILATERAL (verified) — upgrade THIS party's own stored
1994
+ // receipt so the counterparty recorded 'absent' becomes 'recovered'. Client-side: the directory
1995
+ // ships no bilateral legibility at upgrade time (the seal leaves aren't persisted there), so each
1996
+ // party rebuilds from the cert it already holds — the present party's from its unilateral close
1997
+ // (FINDING-3), the returning party's from the notification it persisted after the KERNEL gate
1998
+ // (3b). The seal signatures do not bind the (unsigned) legibility, so mutating it is sound; only
1999
+ // the attestation flips (frontiers unchanged — never overstated). No-op if no cert is stored yet.
2000
+ const stored = sessionNodeManager.getSealCertificate(agentName, sidHex);
2001
+ if (stored) {
2002
+ try {
2003
+ const upgraded = upgradeAbsentToRecovered(stored.legibility);
2004
+ sessionNodeManager.recordSealCertificate(agentName, sidHex, stored.sealed_root, JSON.stringify(upgraded));
2005
+ logger.info("session.seal.receipt.upgraded", { sessionId: sidHex, agentName, party: result.party });
2006
+ }
2007
+ catch (error) {
2008
+ logger.warn("seal.certificate.persist.failed", { sessionId: sidHex, reason: error instanceof Error ? error.message : String(error) });
2009
+ }
2010
+ }
1847
2011
  void sessionNodeManager.destroySessionNode(agentName, sidHex, "sealed");
1848
2012
  }
1849
2013
  /**
@@ -1901,7 +2065,7 @@ export async function startDaemon(config) {
1901
2065
  persistence: getPersistence(agentName),
1902
2066
  agentPubkeyHex: loaded.pubkey,
1903
2067
  getNode: entry.getNode,
1904
- getDirectoryEndpoint: async () => (directoryEndpointResolver ? (await directoryEndpointResolver()) ?? null : null),
2068
+ getDirectoryEndpoint: getFailoverEndpoint,
1905
2069
  getConsortiumEndpoints: resolveConsortiumRoster,
1906
2070
  signaling: entry.signaling,
1907
2071
  logger,