@cello-protocol/daemon 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-loader.d.ts +15 -18
- package/dist/agent-loader.d.ts.map +1 -1
- package/dist/agent-loader.js +24 -81
- package/dist/agent-loader.js.map +1 -1
- package/dist/bin/cello-daemon.js +5 -7
- package/dist/bin/cello-daemon.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +133 -66
- package/dist/daemon.js.map +1 -1
- package/dist/db-identity-store.d.ts +97 -0
- package/dist/db-identity-store.d.ts.map +1 -0
- package/dist/db-identity-store.js +235 -0
- package/dist/db-identity-store.js.map +1 -0
- package/dist/identity-migration.d.ts +40 -0
- package/dist/identity-migration.d.ts.map +1 -0
- package/dist/identity-migration.js +455 -0
- package/dist/identity-migration.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/manifest-version-store-db.d.ts +24 -0
- package/dist/manifest-version-store-db.d.ts.map +1 -0
- package/dist/manifest-version-store-db.js +57 -0
- package/dist/manifest-version-store-db.js.map +1 -0
- package/dist/manifest-version-store.d.ts +3 -1
- package/dist/manifest-version-store.d.ts.map +1 -1
- package/dist/manifest-version-store.js +3 -1
- package/dist/manifest-version-store.js.map +1 -1
- package/dist/nonce-dedup.d.ts +2 -2
- package/dist/nonce-dedup.d.ts.map +1 -1
- package/dist/nonce-dedup.js.map +1 -1
- package/dist/registration-manager.d.ts.map +1 -1
- package/dist/registration-manager.js +78 -44
- package/dist/registration-manager.js.map +1 -1
- package/dist/retry-queue.d.ts +2 -3
- package/dist/retry-queue.d.ts.map +1 -1
- package/dist/retry-queue.js +8 -16
- package/dist/retry-queue.js.map +1 -1
- package/dist/seal-upgrade.d.ts +3 -1
- package/dist/seal-upgrade.d.ts.map +1 -1
- package/dist/seal-upgrade.js +1 -2
- package/dist/seal-upgrade.js.map +1 -1
- package/dist/session-ceremony.d.ts +8 -4
- package/dist/session-ceremony.d.ts.map +1 -1
- package/dist/session-ceremony.js +3 -7
- package/dist/session-ceremony.js.map +1 -1
- package/dist/session-node-manager.d.ts +19 -10
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +60 -45
- package/dist/session-node-manager.js.map +1 -1
- package/dist/sqlcipher-db.d.ts +87 -0
- package/dist/sqlcipher-db.d.ts.map +1 -0
- package/dist/sqlcipher-db.js +259 -0
- package/dist/sqlcipher-db.js.map +1 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +5 -4
- package/dist/manifest-version-store-file.d.ts +0 -18
- package/dist/manifest-version-store-file.d.ts.map +0 -1
- package/dist/manifest-version-store-file.js +0 -40
- package/dist/manifest-version-store-file.js.map +0 -1
- package/dist/transcript-cipher.d.ts +0 -31
- package/dist/transcript-cipher.d.ts.map +0 -1
- package/dist/transcript-cipher.js +0 -74
- package/dist/transcript-cipher.js.map +0 -1
package/dist/agent-loader.d.ts
CHANGED
|
@@ -1,31 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent identity loader for the CELLO daemon.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - If legacy key exists, load it as agent 'default'
|
|
9
|
-
* c. If agents/ exists, enumerate subdirectories
|
|
10
|
-
* - For each subdir, check if a 'key' file exists
|
|
11
|
-
* - If yes, attempt to load it via FileKeyProvider.load()
|
|
12
|
-
* - If load fails (corrupt), log agent.load.failed and skip
|
|
13
|
-
* - Subdirectories without a 'key' file are silently skipped
|
|
14
|
-
* d. Return array of {name, pubkey} for successfully loaded agents
|
|
4
|
+
* CELLO-M7-PERSIST-002 (AC-007): agents are enumerated from the encrypted `agents` table — ONE
|
|
5
|
+
* loading path. The legacy flat-file paths (`~/.cello/agents/<name>/key` and the single-file
|
|
6
|
+
* `~/.cello/key` "default" fallback) are gone; any pre-story key files are imported into the
|
|
7
|
+
* `agents` table by the one-time migration (identity-migration.ts) before this loader runs.
|
|
15
8
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
9
|
+
* Each agent's K_local Ed25519 seed is stored as a BLOB column; the loader builds a sign-only
|
|
10
|
+
* InMemoryKeyProvider from it (the private scalar never leaves the provider — only signatures and
|
|
11
|
+
* content-seal opens are emitted).
|
|
18
12
|
*/
|
|
19
13
|
import type { KeyProvider } from "@cello-protocol/crypto";
|
|
14
|
+
import type { DaemonDatabase } from "./sqlcipher-db.js";
|
|
20
15
|
import type { Logger } from "./types.js";
|
|
21
16
|
export interface LoadedAgent {
|
|
22
17
|
name: string;
|
|
23
18
|
pubkey: string;
|
|
24
19
|
/**
|
|
25
|
-
* The agent's K_local signing key, retained so the daemon can produce
|
|
26
|
-
*
|
|
27
|
-
* seal-interrupted bilateral flow). The private scalar never leaves this
|
|
28
|
-
* provider — only signatures are emitted.
|
|
20
|
+
* The agent's K_local signing key, retained so the daemon can produce K_local-signed control
|
|
21
|
+
* leaves (e.g. the SEAL-INTERRUPTED leaf). The private scalar never leaves this provider.
|
|
29
22
|
*/
|
|
30
23
|
keyProvider: KeyProvider;
|
|
31
24
|
}
|
|
@@ -37,5 +30,9 @@ export interface AgentLoadResult {
|
|
|
37
30
|
loaded: LoadedAgent[];
|
|
38
31
|
failed: FailedAgent[];
|
|
39
32
|
}
|
|
40
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Load every agent from the encrypted `agents` table. A row whose seed is unusable (corrupt/wrong
|
|
35
|
+
* length) is reported as a failed agent and skipped — never silently dropped.
|
|
36
|
+
*/
|
|
37
|
+
export declare function loadAgents(db: DaemonDatabase, logger: Logger): Promise<AgentLoadResult>;
|
|
41
38
|
//# sourceMappingURL=agent-loader.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-loader.d.ts","sourceRoot":"","sources":["../src/agent-loader.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"agent-loader.d.ts","sourceRoot":"","sources":["../src/agent-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,WAAW,EAAE,WAAW,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAkB7F"}
|
package/dist/agent-loader.js
CHANGED
|
@@ -1,94 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent identity loader for the CELLO daemon.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - If legacy key exists, load it as agent 'default'
|
|
9
|
-
* c. If agents/ exists, enumerate subdirectories
|
|
10
|
-
* - For each subdir, check if a 'key' file exists
|
|
11
|
-
* - If yes, attempt to load it via FileKeyProvider.load()
|
|
12
|
-
* - If load fails (corrupt), log agent.load.failed and skip
|
|
13
|
-
* - Subdirectories without a 'key' file are silently skipped
|
|
14
|
-
* d. Return array of {name, pubkey} for successfully loaded agents
|
|
4
|
+
* CELLO-M7-PERSIST-002 (AC-007): agents are enumerated from the encrypted `agents` table — ONE
|
|
5
|
+
* loading path. The legacy flat-file paths (`~/.cello/agents/<name>/key` and the single-file
|
|
6
|
+
* `~/.cello/key` "default" fallback) are gone; any pre-story key files are imported into the
|
|
7
|
+
* `agents` table by the one-time migration (identity-migration.ts) before this loader runs.
|
|
15
8
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
9
|
+
* Each agent's K_local Ed25519 seed is stored as a BLOB column; the loader builds a sign-only
|
|
10
|
+
* InMemoryKeyProvider from it (the private scalar never leaves the provider — only signatures and
|
|
11
|
+
* content-seal opens are emitted).
|
|
18
12
|
*/
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
import { InMemoryKeyProvider } from "@cello-protocol/crypto";
|
|
14
|
+
import { DbIdentityStore } from "./db-identity-store.js";
|
|
15
|
+
/**
|
|
16
|
+
* Load every agent from the encrypted `agents` table. A row whose seed is unusable (corrupt/wrong
|
|
17
|
+
* length) is reported as a failed agent and skipped — never silently dropped.
|
|
18
|
+
*/
|
|
19
|
+
export async function loadAgents(db, logger) {
|
|
20
|
+
const store = new DbIdentityStore(db, logger);
|
|
24
21
|
const loaded = [];
|
|
25
22
|
const failed = [];
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const result = await loadSingleAgent("default", legacyKeyPath, logger);
|
|
32
|
-
if (result.ok) {
|
|
33
|
-
loaded.push(result.agent);
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
failed.push({ name: "default", error: result.error });
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return { loaded, failed };
|
|
40
|
-
}
|
|
41
|
-
// Enumerate subdirectories of agents/
|
|
42
|
-
const entries = await readdir(agentsDir, { withFileTypes: true });
|
|
43
|
-
for (const entry of entries) {
|
|
44
|
-
if (!entry.isDirectory())
|
|
45
|
-
continue;
|
|
46
|
-
const keyPath = join(agentsDir, entry.name, "key");
|
|
47
|
-
if (!(await fileExists(keyPath))) {
|
|
48
|
-
continue;
|
|
23
|
+
for (const row of store.listAgents()) {
|
|
24
|
+
try {
|
|
25
|
+
const keyProvider = new InMemoryKeyProvider(row.kLocalSeed);
|
|
26
|
+
const pubkey = Buffer.from(await keyProvider.getPublicKey()).toString("hex");
|
|
27
|
+
loaded.push({ name: row.agentName, pubkey, keyProvider });
|
|
49
28
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
else {
|
|
55
|
-
failed.push({ name: entry.name, error: result.error });
|
|
29
|
+
catch (err) {
|
|
30
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
31
|
+
logger.error("agent.load.failed", { agentName: row.agentName, error });
|
|
32
|
+
failed.push({ name: row.agentName, error });
|
|
56
33
|
}
|
|
57
34
|
}
|
|
58
35
|
return { loaded, failed };
|
|
59
36
|
}
|
|
60
|
-
async function loadSingleAgent(name, keyPath, logger) {
|
|
61
|
-
try {
|
|
62
|
-
const keyProvider = await FileKeyProvider.load(keyPath);
|
|
63
|
-
const pubkeyBytes = await keyProvider.getPublicKey();
|
|
64
|
-
const pubkey = Buffer.from(pubkeyBytes).toString("hex");
|
|
65
|
-
return { ok: true, agent: { name, pubkey, keyProvider } };
|
|
66
|
-
}
|
|
67
|
-
catch (err) {
|
|
68
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
69
|
-
logger.error("agent.load.failed", {
|
|
70
|
-
agentName: name,
|
|
71
|
-
error: errorMsg,
|
|
72
|
-
});
|
|
73
|
-
return { ok: false, error: errorMsg };
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
async function directoryExists(path) {
|
|
77
|
-
try {
|
|
78
|
-
const s = await stat(path);
|
|
79
|
-
return s.isDirectory();
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
async function fileExists(path) {
|
|
86
|
-
try {
|
|
87
|
-
await access(path);
|
|
88
|
-
return true;
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
37
|
//# sourceMappingURL=agent-loader.js.map
|
package/dist/agent-loader.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-loader.js","sourceRoot":"","sources":["../src/agent-loader.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"agent-loader.js","sourceRoot":"","sources":["../src/agent-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE7D,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAwBzD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,EAAkB,EAAE,MAAc;IACjE,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IAC9C,MAAM,MAAM,GAAkB,EAAE,CAAC;IACjC,MAAM,MAAM,GAAkB,EAAE,CAAC;IAEjC,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,UAAU,EAAE,EAAE,CAAC;QACrC,IAAI,CAAC;YACH,MAAM,WAAW,GAAG,IAAI,mBAAmB,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YAC5D,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,WAAW,CAAC,YAAY,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;YAC7E,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;QAC5D,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,MAAM,KAAK,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC/D,MAAM,CAAC,KAAK,CAAC,mBAAmB,EAAE,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;YACvE,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC5B,CAAC"}
|
package/dist/bin/cello-daemon.js
CHANGED
|
@@ -14,7 +14,6 @@ import { join } from "node:path";
|
|
|
14
14
|
import { startDaemon } from "../daemon.js";
|
|
15
15
|
import { createDirectoryEndpointResolver } from "../directory-bootstrap.js";
|
|
16
16
|
import { FileManifestProvider } from "../file-manifest-provider.js";
|
|
17
|
-
import { FileManifestVersionStore } from "../manifest-version-store-file.js";
|
|
18
17
|
import { RandomizedPollScheduler } from "../manifest-poll-scheduler.js";
|
|
19
18
|
import { ManifestDirectoryChallengeVerifier } from "@cello-protocol/transport";
|
|
20
19
|
const MAX_CONNECTIONS = 16;
|
|
@@ -65,11 +64,10 @@ function buildManifestDeps(logger) {
|
|
|
65
64
|
}
|
|
66
65
|
const manifestProvider = new FileManifestProvider({ path: manifestPath });
|
|
67
66
|
const challengeVerifier = new ManifestDirectoryChallengeVerifier(manifestProvider);
|
|
68
|
-
// Anti-rollback (DOD-AUTH-2 / TUF):
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
const manifestVersionStore = new FileManifestVersionStore(join(celloDir, "manifest-version.json"));
|
|
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.
|
|
73
71
|
// DOD-AUTH-2: background manifest poll. The directory is re-polled on a randomized
|
|
74
72
|
// 6–12h interval (thundering-herd avoidance) and a newer signed manifest is adopted.
|
|
75
73
|
// The interval is env-injectable so the live binary test can poll sub-second instead
|
|
@@ -95,7 +93,7 @@ function buildManifestDeps(logger) {
|
|
|
95
93
|
pollMinMs: pollOpts?.minMs ?? null,
|
|
96
94
|
pollMaxMs: pollOpts?.maxMs ?? null,
|
|
97
95
|
});
|
|
98
|
-
return { manifestProvider, manifestRootKeys, manifestThreshold, challengeVerifier,
|
|
96
|
+
return { manifestProvider, manifestRootKeys, manifestThreshold, challengeVerifier, manifestPollScheduler };
|
|
99
97
|
}
|
|
100
98
|
async function main() {
|
|
101
99
|
// M7 Keystone (Part 1): give the daemon its door to the directory. The resolver
|
|
@@ -1 +1 @@
|
|
|
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,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EAAE,
|
|
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,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAExE,OAAO,EAAE,kCAAkC,EAAqH,MAAM,2BAA2B,CAAC;AAElM,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;;;;;;;;;;;GAWG;AACH,SAAS,iBAAiB,CAAC,MAAc;IAQvC,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC;IAC3D,IAAI,CAAC,YAAY;QAAE,OAAO,EAAE,CAAC;IAE7B,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,0BAA0B,IAAI,EAAE,CAAC;IACjE,MAAM,gBAAgB,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACjG,MAAM,iBAAiB,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IAC5F,IAAI,gBAAgB,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,KAAK,CACb,qHAAqH,CACtH,CAAC;IACJ,CAAC;IAED,MAAM,gBAAgB,GAAG,IAAI,oBAAoB,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;IAC1E,MAAM,iBAAiB,GAAG,IAAI,kCAAkC,CAAC,gBAAgB,CAAC,CAAC;IACnF,gGAAgG;IAChG,gGAAgG;IAChG,mGAAmG;IACnG,+FAA+F;IAC/F,mFAAmF;IACnF,qFAAqF;IACrF,qFAAqF;IACrF,8EAA8E;IAC9E,kFAAkF;IAClF,sFAAsF;IACtF,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC;IAC1D,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC;IAC1D,IAAI,QAAsD,CAAC;IAC3D,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QACzD,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,UAAU,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;QACpD,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,UAAU,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;QACpD,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,GAAG,KAAK,EAAE,CAAC;YAC9E,MAAM,IAAI,KAAK,CACb,4FAA4F,CAC7F,CAAC;QACJ,CAAC;QACD,QAAQ,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAC9B,CAAC;IACD,MAAM,qBAAqB,GAAG,IAAI,uBAAuB,CAAC,QAAQ,CAAC,CAAC;IACpE,MAAM,CAAC,IAAI,CAAC,4BAA4B,EAAE;QACxC,YAAY;QACZ,YAAY,EAAE,gBAAgB,CAAC,MAAM;QACrC,SAAS,EAAE,iBAAiB;QAC5B,SAAS,EAAE,QAAQ,EAAE,KAAK,IAAI,IAAI;QAClC,SAAS,EAAE,QAAQ,EAAE,KAAK,IAAI,IAAI;KACnC,CAAC,CAAC;IACH,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,CAAC;AAC7G,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,gFAAgF;IAChF,iFAAiF;IACjF,8EAA8E;IAC9E,0BAA0B;IAC1B,MAAM,yBAAyB,GAAG,+BAA+B,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IAE9E,kFAAkF;IAClF,oFAAoF;IACpF,mFAAmF;IACnF,mFAAmF;IACnF,kFAAkF;IAClF,4EAA4E;IAC5E,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"}
|
package/dist/daemon.d.ts.map
CHANGED
|
@@ -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,
|
|
1
|
+
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EAKrB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAK/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAoB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAM/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAmInF,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,CAk5G7E"}
|
package/dist/daemon.js
CHANGED
|
@@ -40,8 +40,9 @@ import { createNode, SignalingManager } from "@cello-protocol/transport";
|
|
|
40
40
|
import { createSignalingConnect } from "./signaling-connect.js";
|
|
41
41
|
import { RegistrationManager } from "./registration-manager.js";
|
|
42
42
|
import { DaemonRegistrationContext } from "./registration-context.js";
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
43
|
+
import { DbRegistrationPersistence, DbIdentityStore } from "./db-identity-store.js";
|
|
44
|
+
import { DbManifestVersionStore } from "./manifest-version-store-db.js";
|
|
45
|
+
import { verify as ed25519Verify, sealToRecipient, generateKLocalSeed, InMemoryKeyProvider } from "@cello-protocol/crypto";
|
|
45
46
|
import { attemptSealUpgrade as attemptSealUpgradeImpl, verifyUpgradeConfirmedCert } from "./seal-upgrade.js";
|
|
46
47
|
// CELLO-M7-MSG-001 (AC-013/AC-018): the single application content-size cap, enforced
|
|
47
48
|
// at the send point here (the receive point lives in the transport content decode).
|
|
@@ -169,7 +170,7 @@ class ProductionSessionNodeFactory {
|
|
|
169
170
|
}
|
|
170
171
|
}
|
|
171
172
|
export async function startDaemon(config) {
|
|
172
|
-
const { celloDir, socketPath, lockFilePath, maxConnections, version, logger, manifestProvider, manifestRootKeys, manifestThreshold, manifestVersionStore, manifestPollScheduler, signalingConnect, challengeVerifier, directoryEndpointResolver, sessionNodeFactory, sessionNegotiator, getRelayCircuitAddress, } = config;
|
|
173
|
+
const { celloDir, socketPath, lockFilePath, maxConnections, version, logger, manifestProvider, manifestRootKeys, manifestThreshold, manifestVersionStore: injectedManifestVersionStore, manifestPollScheduler, signalingConnect, challengeVerifier, directoryEndpointResolver, sessionNodeFactory, sessionNegotiator, getRelayCircuitAddress, } = config;
|
|
173
174
|
// CELLO-M7-TRANSPORT-001: composition-root selection of the transport selector.
|
|
174
175
|
// Driven by CELLO_ENV; fails fast at startup (here, not at first session) when a
|
|
175
176
|
// production environment is missing the required transport dialer (AC-010).
|
|
@@ -183,6 +184,31 @@ export async function startDaemon(config) {
|
|
|
183
184
|
env: celloEnv,
|
|
184
185
|
selector: isProductionVariant(celloEnv) ? "real" : "stub",
|
|
185
186
|
});
|
|
187
|
+
// ADV-006 + ADV-008 (hoisted — code-review MED): pure config validation runs BEFORE any disk side
|
|
188
|
+
// effect (lock, the irreversible one-time migration, DB open). A misconfigured daemon must fail
|
|
189
|
+
// before mutating state. If manifestProvider is set, manifestRootKeys + a positive threshold are
|
|
190
|
+
// required.
|
|
191
|
+
if (manifestProvider && (!manifestRootKeys || !manifestThreshold || manifestThreshold <= 0)) {
|
|
192
|
+
throw new Error("DaemonConfig: manifestProvider requires manifestRootKeys (non-empty) and manifestThreshold (positive integer >= 1)");
|
|
193
|
+
}
|
|
194
|
+
// ── PERSIST-002: open the encrypted store FIRST (runs the one-time flat-file → SQLCipher migration
|
|
195
|
+
// (AC-006) + creates the agents/manifest_state schema), under the single-instance lock. This must
|
|
196
|
+
// precede the manifest verification below because the manifest version is now stored in the
|
|
197
|
+
// encrypted DB (AC-008), not a manifest-version.json file. ──
|
|
198
|
+
await mkdir(celloDir, { recursive: true });
|
|
199
|
+
await mkdir(dirname(socketPath), { recursive: true });
|
|
200
|
+
await acquireLock(lockFilePath, { pid: process.pid, socketPath, version });
|
|
201
|
+
const sessionNodeManager = new SessionNodeManager({
|
|
202
|
+
factory: sessionNodeFactory ?? new ProductionSessionNodeFactory(),
|
|
203
|
+
logger,
|
|
204
|
+
dbPath: join(celloDir, "sessions.db"),
|
|
205
|
+
contentTtfMs: config.contentTtfMs,
|
|
206
|
+
autoNatProbers: () => [],
|
|
207
|
+
});
|
|
208
|
+
await sessionNodeManager.initialize();
|
|
209
|
+
// AC-008: the manifest version store is DB-backed by default (encrypted manifest_state table). A
|
|
210
|
+
// test may inject an override (e.g. InMemoryManifestVersionStore) via config.
|
|
211
|
+
const manifestVersionStore = injectedManifestVersionStore ?? new DbManifestVersionStore(sessionNodeManager.getDb(), logger);
|
|
186
212
|
// M7-MANIFEST-002: Load and verify consortium manifest BEFORE any directory connection.
|
|
187
213
|
//
|
|
188
214
|
// Pseudocode for manifest loading:
|
|
@@ -197,12 +223,6 @@ export async function startDaemon(config) {
|
|
|
197
223
|
// M7 Keystone: the version of the verified manifest, surfaced in ConnectResult.
|
|
198
224
|
// Stays 0 when no manifestProvider is configured (the M6 backward-compat path).
|
|
199
225
|
let verifiedManifestVersion = 0;
|
|
200
|
-
// ADV-006 + ADV-008: If manifestProvider is set, manifestRootKeys and a positive
|
|
201
|
-
// manifestThreshold are required. Fail loudly on misconfiguration rather than
|
|
202
|
-
// silently proceeding unverified.
|
|
203
|
-
if (manifestProvider && (!manifestRootKeys || !manifestThreshold || manifestThreshold <= 0)) {
|
|
204
|
-
throw new Error("DaemonConfig: manifestProvider requires manifestRootKeys (non-empty) and manifestThreshold (positive integer >= 1)");
|
|
205
|
-
}
|
|
206
226
|
if (manifestProvider && manifestRootKeys && manifestThreshold !== undefined) {
|
|
207
227
|
try {
|
|
208
228
|
const manifest = await manifestProvider.loadAndVerify(manifestRootKeys, manifestThreshold);
|
|
@@ -223,26 +243,16 @@ export async function startDaemon(config) {
|
|
|
223
243
|
});
|
|
224
244
|
}
|
|
225
245
|
else {
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
else {
|
|
236
|
-
await manifestVersionStore.persistVersion(manifest.version);
|
|
237
|
-
manifestVerified = true;
|
|
238
|
-
verifiedManifestVersion = manifest.version;
|
|
239
|
-
logger.info("directory.auth.manifest.verified", {
|
|
240
|
-
manifestVersion: manifest.version,
|
|
241
|
-
signerCount: manifest.signatures.length,
|
|
242
|
-
});
|
|
243
|
-
}
|
|
246
|
+
// Anti-rollback monotonicity (manifestVersionStore is always present now — DB-backed default).
|
|
247
|
+
const lastSeen = await manifestVersionStore.getLastSeenVersion();
|
|
248
|
+
if (lastSeen !== null && manifest.version < lastSeen) {
|
|
249
|
+
logger.error("directory.auth.manifest.version.rollback", {
|
|
250
|
+
manifestVersion: manifest.version,
|
|
251
|
+
lastSeenVersion: lastSeen,
|
|
252
|
+
});
|
|
244
253
|
}
|
|
245
254
|
else {
|
|
255
|
+
await manifestVersionStore.persistVersion(manifest.version);
|
|
246
256
|
manifestVerified = true;
|
|
247
257
|
verifiedManifestVersion = manifest.version;
|
|
248
258
|
logger.info("directory.auth.manifest.verified", {
|
|
@@ -262,21 +272,24 @@ export async function startDaemon(config) {
|
|
|
262
272
|
// failed, the daemon must refuse to proceed. Operators who configure
|
|
263
273
|
// manifestProvider have opted into manifest enforcement.
|
|
264
274
|
if (manifestProvider && !manifestVerified) {
|
|
275
|
+
// code-review MED: this refuse now runs AFTER the DB is open + the lock is held (the DB had to
|
|
276
|
+
// open for the anti-rollback check). Release both before rethrowing so an in-process caller does
|
|
277
|
+
// not leak the DB handle / lock (in production the process exits, but be tidy).
|
|
278
|
+
try {
|
|
279
|
+
sessionNodeManager.getDb().close();
|
|
280
|
+
}
|
|
281
|
+
catch { /* ignore */ }
|
|
282
|
+
await removeLock(lockFilePath, logger).catch(() => { });
|
|
265
283
|
throw new Error("Manifest verification failed. The daemon cannot start with an unverified manifest when manifestProvider is configured. " +
|
|
266
284
|
"Check the logs for the specific failure reason (manifest_signature_invalid, manifest_expired, or manifest_version_rollback).");
|
|
267
285
|
}
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
await acquireLock(lockFilePath, {
|
|
276
|
-
pid: process.pid,
|
|
277
|
-
socketPath,
|
|
278
|
-
version,
|
|
279
|
-
});
|
|
286
|
+
// Load agent identities from the encrypted `agents` table (PERSIST-002 AC-007 — one path).
|
|
287
|
+
// (The encrypted store + lock were established above, before manifest verification.)
|
|
288
|
+
const { loaded: loadedAgents, failed: failedAgents } = await loadAgents(sessionNodeManager.getDb(), logger);
|
|
289
|
+
// PERSIST-002: per-agent DB-backed identity persistence. The registration handler and the
|
|
290
|
+
// ceremony/seal signer-reconstruction load the FROST share (and persist the identity) through this
|
|
291
|
+
// seam — the encrypted `agents` row, never a flat file.
|
|
292
|
+
const getPersistence = (agentName) => new DbRegistrationPersistence({ db: sessionNodeManager.getDb(), agentName, logger });
|
|
280
293
|
// Build agent state (all start in 'registered' state — no auto-start)
|
|
281
294
|
const agents = [
|
|
282
295
|
...loadedAgents.map((a) => ({
|
|
@@ -446,7 +459,7 @@ export async function startDaemon(config) {
|
|
|
446
459
|
// registration routing). Unregistered implicitly when the manager is stopped.
|
|
447
460
|
wireSessionCeremonyHandler({
|
|
448
461
|
agentName,
|
|
449
|
-
|
|
462
|
+
persistence: getPersistence(agentName),
|
|
450
463
|
agentPubkeyHex,
|
|
451
464
|
getNode: entry.getNode,
|
|
452
465
|
getDirectoryEndpoint: async () => (directoryEndpointResolver ? (await directoryEndpointResolver()) ?? null : null),
|
|
@@ -456,7 +469,7 @@ export async function startDaemon(config) {
|
|
|
456
469
|
// DOD-SPINE-7: coordinate the SEAL FROST ceremony on this agent's stream too.
|
|
457
470
|
wireSealCeremonyHandler({
|
|
458
471
|
agentName,
|
|
459
|
-
|
|
472
|
+
persistence: getPersistence(agentName),
|
|
460
473
|
agentPubkeyHex,
|
|
461
474
|
getNode: entry.getNode,
|
|
462
475
|
getDirectoryEndpoint: async () => (directoryEndpointResolver ? (await directoryEndpointResolver()) ?? null : null),
|
|
@@ -511,7 +524,7 @@ export async function startDaemon(config) {
|
|
|
511
524
|
if (primaryAgent) {
|
|
512
525
|
wireSessionCeremonyHandler({
|
|
513
526
|
agentName: primaryAgent.name,
|
|
514
|
-
|
|
527
|
+
persistence: getPersistence(primaryAgent.name),
|
|
515
528
|
agentPubkeyHex: primaryAgent.pubkey,
|
|
516
529
|
getNode: getDirectoryNode,
|
|
517
530
|
getDirectoryEndpoint: async () => (directoryEndpointResolver ? (await directoryEndpointResolver()) ?? null : null),
|
|
@@ -521,7 +534,7 @@ export async function startDaemon(config) {
|
|
|
521
534
|
// DOD-SPINE-7: the primary agent also coordinates the SEAL FROST ceremony on the keystone.
|
|
522
535
|
wireSealCeremonyHandler({
|
|
523
536
|
agentName: primaryAgent.name,
|
|
524
|
-
|
|
537
|
+
persistence: getPersistence(primaryAgent.name),
|
|
525
538
|
agentPubkeyHex: primaryAgent.pubkey,
|
|
526
539
|
getNode: getDirectoryNode,
|
|
527
540
|
getDirectoryEndpoint: async () => (directoryEndpointResolver ? (await directoryEndpointResolver()) ?? null : null),
|
|
@@ -540,22 +553,9 @@ export async function startDaemon(config) {
|
|
|
540
553
|
const perConnectionState = new Map();
|
|
541
554
|
// Set of agents currently in "online" state (transitioned via cello_start_agent)
|
|
542
555
|
const onlineAgents = new Set();
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
// 2. Interrupted session detection runs before any tool call can race.
|
|
547
|
-
const sessionNodeManager = new SessionNodeManager({
|
|
548
|
-
factory: sessionNodeFactory ?? new ProductionSessionNodeFactory(),
|
|
549
|
-
logger,
|
|
550
|
-
dbPath: join(celloDir, "sessions.db"),
|
|
551
|
-
contentTtfMs: config.contentTtfMs,
|
|
552
|
-
// CELLO-M7-TRANSPORT-001: directory-node AutoNAT probers (SI-002). The
|
|
553
|
-
// directory connection (SIGNAL-001) is not yet wired into the daemon, so the
|
|
554
|
-
// prober set is empty — AutoNAT cannot run and the standing receiver reports
|
|
555
|
-
// the conservative default + transport.autonat.unavailable (AC-004/DB-001).
|
|
556
|
-
autoNatProbers: () => [],
|
|
557
|
-
});
|
|
558
|
-
await sessionNodeManager.initialize();
|
|
556
|
+
// SessionNodeManager was constructed + initialized at the top of startDaemon (PERSIST-002 — the
|
|
557
|
+
// encrypted store must open before agents load from the `agents` table). Its standing receiver +
|
|
558
|
+
// interrupted-session detection are already ready here, before the IPC socket opens.
|
|
559
559
|
// CELLO-M7-TRANSPORT-001: the daemon's runtime AutoNAT service is the one
|
|
560
560
|
// wrapping the standing receiver node (it emits transport.autonat.result /
|
|
561
561
|
// transport.autonat.unavailable and its dialability drives the SessionAssignment
|
|
@@ -687,7 +687,7 @@ export async function startDaemon(config) {
|
|
|
687
687
|
// DAEMON-003: Initialize RetryQueue and NonceDedupStore (AC-008).
|
|
688
688
|
// Both use the same SQLite DB as the SessionNodeManager (daemon.db equivalent).
|
|
689
689
|
// loadFromDb() must complete BEFORE IPC socket opens (AC-007).
|
|
690
|
-
const retryQueue = new RetryQueue(sessionNodeManager.getDb(), logger
|
|
690
|
+
const retryQueue = new RetryQueue(sessionNodeManager.getDb(), logger);
|
|
691
691
|
retryQueue.loadFromDb();
|
|
692
692
|
// CELLO-M7-MSG-001 (AC-001/AC-003/AC-019): wire the awaiting-ACK lifecycle's durable
|
|
693
693
|
// side effects to the retry_queue. A `persisted` delivery ACK clears the durable
|
|
@@ -925,6 +925,41 @@ export async function startDaemon(config) {
|
|
|
925
925
|
notificationDispatcher.dispatchAgentStateChanged(name, "online", "started");
|
|
926
926
|
return { ok: true };
|
|
927
927
|
});
|
|
928
|
+
// ─── PERSIST-002 (AC-004): cello_create_agent handler ───
|
|
929
|
+
// The explicit agent-creation path: generate a fresh K_local seed, write it as an `agents` row in
|
|
930
|
+
// the encrypted DB (NO key file), and wire the agent into the live daemon so it can be registered
|
|
931
|
+
// and used WITHOUT a restart. Creation is explicit — cello_start_agent never auto-creates on a typo.
|
|
932
|
+
handlers.set("cello_create_agent", async (params, _connectionId) => {
|
|
933
|
+
const name = params?.name;
|
|
934
|
+
if (!name || typeof name !== "string" || !/^[a-zA-Z0-9_-]{1,64}$/.test(name)) {
|
|
935
|
+
return { ok: false, reason: "invalid_agent_name", guidance: "Provide a 'name' (1-64 chars: letters, digits, '-' or '_') for the new agent." };
|
|
936
|
+
}
|
|
937
|
+
const store = new DbIdentityStore(sessionNodeManager.getDb(), logger);
|
|
938
|
+
if (store.hasAgent(name) || agents.some((a) => a.name === name)) {
|
|
939
|
+
return { ok: false, reason: "agent_already_exists", guidance: `Agent '${name}' already exists. Choose a different name, or see cello_list_agents.` };
|
|
940
|
+
}
|
|
941
|
+
let pubkeyHex;
|
|
942
|
+
try {
|
|
943
|
+
const seed = generateKLocalSeed();
|
|
944
|
+
const keyProvider = new InMemoryKeyProvider(seed);
|
|
945
|
+
pubkeyHex = Buffer.from(await keyProvider.getPublicKey()).toString("hex");
|
|
946
|
+
// SI-001: createAgent stores the seed in the encrypted DB and logs only the pubkey.
|
|
947
|
+
store.createAgent(name, seed, pubkeyHex);
|
|
948
|
+
// Runtime-add: make the agent immediately registrable/usable (the register handler resolves
|
|
949
|
+
// identity from keyProviders/loadedAgents; per-agent signaling is created lazily on register).
|
|
950
|
+
keyProviders.set(name, keyProvider);
|
|
951
|
+
loadedAgents.push({ name, pubkey: pubkeyHex, keyProvider });
|
|
952
|
+
agents.push({ name, state: "registered", pubkey: pubkeyHex });
|
|
953
|
+
}
|
|
954
|
+
catch (err) {
|
|
955
|
+
logger.error("persist.identity.persist.failed", { agentName: name, error: err instanceof Error ? err.message : String(err) });
|
|
956
|
+
return { ok: false, reason: "agent_create_failed", guidance: "Could not create the agent. Check the daemon log and that the CELLO directory is writable, then retry." };
|
|
957
|
+
}
|
|
958
|
+
// Creation is not an online/offline transition — the agent appears (cello_list_agents) but is
|
|
959
|
+
// not online until cello_start_agent. Just record it.
|
|
960
|
+
logger.info("agent.created", { agentName: name, agentPubkey: pubkeyHex });
|
|
961
|
+
return { ok: true, name, pubkey: pubkeyHex };
|
|
962
|
+
});
|
|
928
963
|
// ─── MCP-001: cello_stop_agent handler ───
|
|
929
964
|
handlers.set("cello_stop_agent", async (params, _connectionId) => {
|
|
930
965
|
const name = params?.name;
|
|
@@ -1028,7 +1063,7 @@ export async function startDaemon(config) {
|
|
|
1028
1063
|
}
|
|
1029
1064
|
const keyProvider = keyProviders.get(name);
|
|
1030
1065
|
if (!keyProvider) {
|
|
1031
|
-
return { ok: false, reason: "agent_not_found", guidance: `Agent '${name}'
|
|
1066
|
+
return { ok: false, reason: "agent_not_found", guidance: `Agent '${name}' does not exist. Create it first with cello_create_agent('${name}') (or 'cello create-agent ${name}'), then retry cello_register.` };
|
|
1032
1067
|
}
|
|
1033
1068
|
if (!directoryEndpointResolver) {
|
|
1034
1069
|
return { ok: false, reason: "directory_unreachable", guidance: "The daemon has no directory endpoint resolver configured, so it cannot reach the directory to register." };
|
|
@@ -1074,7 +1109,7 @@ export async function startDaemon(config) {
|
|
|
1074
1109
|
guidance: `Agent '${name}' could not establish its directory signaling stream within 10s. Check CELLO_DIRECTORY_URL and that the directory is reachable, then retry cello_register.`,
|
|
1075
1110
|
};
|
|
1076
1111
|
}
|
|
1077
|
-
const persistence =
|
|
1112
|
+
const persistence = getPersistence(name);
|
|
1078
1113
|
const ctx = new DaemonRegistrationContext({
|
|
1079
1114
|
signaling: agentSignaling,
|
|
1080
1115
|
getDirectoryNode: agentGetNode,
|
|
@@ -1092,6 +1127,10 @@ export async function startDaemon(config) {
|
|
|
1092
1127
|
await dropAgentSignaling(name);
|
|
1093
1128
|
return { ok: false, reason: result.error, guidance: registrationGuidance(result.error) };
|
|
1094
1129
|
}
|
|
1130
|
+
// PERSIST-002 (AC-013): the identity row (K_local + share + ML-DSA + registration) is durably
|
|
1131
|
+
// committed at this point (RegistrationManager awaits the persist before returning success).
|
|
1132
|
+
// SI-001: never log a secret — only the agent name + PUBLIC key.
|
|
1133
|
+
logger.info("persist.identity.persisted", { agentName: name, agentPubkey: agentPubkeyHex });
|
|
1095
1134
|
// Capture-now-or-lose-it: persist the agent→user link (using it is future
|
|
1096
1135
|
// trust-layer work). L1: the agent is already registered at this point —
|
|
1097
1136
|
// a link-write failure must NOT be reported as a registration failure.
|
|
@@ -1168,7 +1207,7 @@ export async function startDaemon(config) {
|
|
|
1168
1207
|
return;
|
|
1169
1208
|
}
|
|
1170
1209
|
const record = sessionNodeManager.getSessionRecord(agentName, sidHex);
|
|
1171
|
-
const verdict = await verifyBilateralSealCertificate({
|
|
1210
|
+
const verdict = await verifyBilateralSealCertificate({ persistence: getPersistence(agentName), agentPubkeyHex, logger, counterpartyPrimaryHex: record?.counterparty_primary_pubkey ?? null }, {
|
|
1172
1211
|
sessionId: sessionIdBytes,
|
|
1173
1212
|
sealedRoot: sealedRootBytes,
|
|
1174
1213
|
leafCount,
|
|
@@ -1318,7 +1357,7 @@ export async function startDaemon(config) {
|
|
|
1318
1357
|
waiter({ ok: false, reason: "malformed_certificate" });
|
|
1319
1358
|
return;
|
|
1320
1359
|
}
|
|
1321
|
-
const result = await verifyUnilateralCertificate({
|
|
1360
|
+
const result = await verifyUnilateralCertificate({ persistence: getPersistence(agentName), agentPubkeyHex, logger }, { sessionId, sealedRoot, leafCount, closeTimestamp: closeTs, frostSignature: frostSig, signatureType: sigType });
|
|
1322
1361
|
pendingUnilateralWaiters.delete(sidHex);
|
|
1323
1362
|
if (!result.ok) {
|
|
1324
1363
|
// SI-003: do NOT mark sealed when the certificate signature does not verify.
|
|
@@ -1374,7 +1413,7 @@ export async function startDaemon(config) {
|
|
|
1374
1413
|
// ONLY on ok. Never trust the directory's "bilateral" claim.
|
|
1375
1414
|
async function verifyAndApplyUpgradeConfirmed(agentName, agentPubkeyHex, sidHex, frame) {
|
|
1376
1415
|
const result = await verifyUpgradeConfirmedCert({
|
|
1377
|
-
logger, agentName, agentPubkeyHex,
|
|
1416
|
+
logger, agentName, agentPubkeyHex, persistence: getPersistence(agentName),
|
|
1378
1417
|
getCounterpartyHex: (a, s) => sessionNodeManager.getSessionRecord(a, s)?.counterparty_pubkey ?? null,
|
|
1379
1418
|
}, sidHex, frame);
|
|
1380
1419
|
if (!result.ok)
|
|
@@ -1435,7 +1474,6 @@ export async function startDaemon(config) {
|
|
|
1435
1474
|
// no_current_agent guard.
|
|
1436
1475
|
const SESSION_TOOLS_REQUIRING_AGENT = [
|
|
1437
1476
|
"cello_receive_session",
|
|
1438
|
-
"cello_list_sessions",
|
|
1439
1477
|
];
|
|
1440
1478
|
const NO_CURRENT_AGENT_RESPONSE = {
|
|
1441
1479
|
ok: false,
|
|
@@ -1825,6 +1863,35 @@ export async function startDaemon(config) {
|
|
|
1825
1863
|
// so the reader can tell a real gap from an empty transcript.
|
|
1826
1864
|
return { ok: true, session_id: sessionId, messages, undecryptable };
|
|
1827
1865
|
});
|
|
1866
|
+
// cello_list_sessions: the discovery surface — every persisted session for the
|
|
1867
|
+
// current agent (active, interrupted, sealed, seal_interrupted_pending), newest
|
|
1868
|
+
// updated first. This is where cello_get_transcript / cello_get_sealed_receipt
|
|
1869
|
+
// get their session ids; without it those by-id reads have no starting point,
|
|
1870
|
+
// and the guidance strings that point here ("See cello_list_sessions") dead-end.
|
|
1871
|
+
// Read from the persisted SQLite store, so it works after a daemon restart and
|
|
1872
|
+
// from a fresh MCP connection (no in-memory session-node required).
|
|
1873
|
+
handlers.set("cello_list_sessions", async (_params, connectionId) => {
|
|
1874
|
+
const connState = perConnectionState.get(connectionId);
|
|
1875
|
+
if (!connState || !connState.currentAgent) {
|
|
1876
|
+
return NO_CURRENT_AGENT_RESPONSE;
|
|
1877
|
+
}
|
|
1878
|
+
const agentName = connState.currentAgent;
|
|
1879
|
+
const sessions = sessionNodeManager
|
|
1880
|
+
.getSessionsForAgent(agentName)
|
|
1881
|
+
.map((row) => ({
|
|
1882
|
+
sessionId: row.session_id,
|
|
1883
|
+
agentName: row.agent_name,
|
|
1884
|
+
counterpartyPubkey: row.counterparty_pubkey,
|
|
1885
|
+
status: row.status,
|
|
1886
|
+
messageCount: row.message_count ?? 0,
|
|
1887
|
+
createdAt: new Date(row.created_at).toISOString(),
|
|
1888
|
+
updatedAt: new Date(row.updated_at).toISOString(),
|
|
1889
|
+
// interrupted_at is the canonical ISO interruption stamp; null for any
|
|
1890
|
+
// session that was never interrupted (active/sealed).
|
|
1891
|
+
interruptedAt: row.interrupted_at ?? null,
|
|
1892
|
+
}));
|
|
1893
|
+
return { ok: true, sessions };
|
|
1894
|
+
});
|
|
1828
1895
|
// DAEMON-003 IPC handlers: queue_failed_send and check_nonce (AC-010)
|
|
1829
1896
|
handlers.set("queue_failed_send", async (params, _connectionId) => {
|
|
1830
1897
|
const sessionId = params?.sessionId;
|