@cello-protocol/daemon 0.0.8 → 0.0.10
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 +153 -96
- 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 +10 -10
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +45 -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/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/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) => ({
|
|
@@ -335,7 +348,11 @@ export async function startDaemon(config) {
|
|
|
335
348
|
// L4: sort by name so the "primary" agent is STABLE across restarts — readdir
|
|
336
349
|
// order (agent-loader) is platform-dependent and unsorted, which would otherwise
|
|
337
350
|
// let the authenticating identity change between daemon restarts.
|
|
338
|
-
|
|
351
|
+
// CELLO-M7-ONBOARD-001: `primaryAgent` is mutable so the FIRST agent created at runtime
|
|
352
|
+
// (cello_create_agent on a fresh install) can be elected as the keystone identity — otherwise the
|
|
353
|
+
// directory door stays `reconnecting` forever until a restart. getAuthIdentity reads it on every
|
|
354
|
+
// reconnect attempt, so electing it makes the already-running reconnect loop authenticate + connect.
|
|
355
|
+
let primaryAgent = [...loadedAgents].sort((a, b) => a.name.localeCompare(b.name))[0];
|
|
339
356
|
const getAuthIdentity = () => {
|
|
340
357
|
if (!primaryAgent)
|
|
341
358
|
return null;
|
|
@@ -446,7 +463,7 @@ export async function startDaemon(config) {
|
|
|
446
463
|
// registration routing). Unregistered implicitly when the manager is stopped.
|
|
447
464
|
wireSessionCeremonyHandler({
|
|
448
465
|
agentName,
|
|
449
|
-
|
|
466
|
+
persistence: getPersistence(agentName),
|
|
450
467
|
agentPubkeyHex,
|
|
451
468
|
getNode: entry.getNode,
|
|
452
469
|
getDirectoryEndpoint: async () => (directoryEndpointResolver ? (await directoryEndpointResolver()) ?? null : null),
|
|
@@ -456,7 +473,7 @@ export async function startDaemon(config) {
|
|
|
456
473
|
// DOD-SPINE-7: coordinate the SEAL FROST ceremony on this agent's stream too.
|
|
457
474
|
wireSealCeremonyHandler({
|
|
458
475
|
agentName,
|
|
459
|
-
|
|
476
|
+
persistence: getPersistence(agentName),
|
|
460
477
|
agentPubkeyHex,
|
|
461
478
|
getNode: entry.getNode,
|
|
462
479
|
getDirectoryEndpoint: async () => (directoryEndpointResolver ? (await directoryEndpointResolver()) ?? null : null),
|
|
@@ -505,57 +522,56 @@ export async function startDaemon(config) {
|
|
|
505
522
|
await entry.signaling.stop();
|
|
506
523
|
logger.info("agent.signaling.dropped", { agentName });
|
|
507
524
|
}
|
|
508
|
-
// DOD-SPINE-5: the PRIMARY agent registers + initiates
|
|
509
|
-
// stream (not a per-agent one), so
|
|
510
|
-
// manager
|
|
511
|
-
|
|
525
|
+
// DOD-SPINE-5/7: the PRIMARY agent registers + initiates + coordinates the session/seal FROST
|
|
526
|
+
// ceremonies over the keystone signaling stream (not a per-agent one), so its ceremony/seal/offer
|
|
527
|
+
// handlers must be wired on the keystone manager — otherwise a primary-agent initiator's session
|
|
528
|
+
// ceremony would time out. CELLO-M7-ONBOARD-001: extracted into a function so it can also be wired
|
|
529
|
+
// when the first agent is elected at runtime (cello_create_agent), not only at startup.
|
|
530
|
+
function wireKeystonePrimary(agent) {
|
|
512
531
|
wireSessionCeremonyHandler({
|
|
513
|
-
agentName:
|
|
514
|
-
|
|
515
|
-
agentPubkeyHex:
|
|
532
|
+
agentName: agent.name,
|
|
533
|
+
persistence: getPersistence(agent.name),
|
|
534
|
+
agentPubkeyHex: agent.pubkey,
|
|
516
535
|
getNode: getDirectoryNode,
|
|
517
536
|
getDirectoryEndpoint: async () => (directoryEndpointResolver ? (await directoryEndpointResolver()) ?? null : null),
|
|
518
537
|
signaling: signalingManager,
|
|
519
538
|
logger,
|
|
520
539
|
});
|
|
521
|
-
// DOD-SPINE-7: the primary agent also coordinates the SEAL FROST ceremony on the keystone.
|
|
522
540
|
wireSealCeremonyHandler({
|
|
523
|
-
agentName:
|
|
524
|
-
|
|
525
|
-
agentPubkeyHex:
|
|
541
|
+
agentName: agent.name,
|
|
542
|
+
persistence: getPersistence(agent.name),
|
|
543
|
+
agentPubkeyHex: agent.pubkey,
|
|
526
544
|
getNode: getDirectoryNode,
|
|
527
545
|
getDirectoryEndpoint: async () => (directoryEndpointResolver ? (await directoryEndpointResolver()) ?? null : null),
|
|
528
546
|
signaling: signalingManager,
|
|
529
547
|
logger,
|
|
530
548
|
});
|
|
531
549
|
wireSessionOfferHandler({
|
|
532
|
-
agentName:
|
|
533
|
-
getStandingReceiverEndpoint: () => sessionNodeManager.getStandingReceiverInfo(
|
|
550
|
+
agentName: agent.name,
|
|
551
|
+
getStandingReceiverEndpoint: () => sessionNodeManager.getStandingReceiverInfo(agent.name),
|
|
534
552
|
signaling: signalingManager,
|
|
535
553
|
logger,
|
|
536
554
|
});
|
|
555
|
+
// The primary closes its OWN sessions over the keystone stream, so the directory routes its
|
|
556
|
+
// session_sealed / seal_unilateral_confirmed / seal_unilateral_notification frames there. These
|
|
557
|
+
// seal-completion listeners MUST be registered for the keystone primary (startup OR runtime
|
|
558
|
+
// election) — otherwise the close waiter never resolves and the seal silently never finalizes
|
|
559
|
+
// (review HIGH: previously these were a separate startup-only block, skipped on a fresh install).
|
|
560
|
+
// The register* functions are hoisted declarations within startDaemon, so callable here.
|
|
561
|
+
registerSessionSealedListener(signalingManager, agent.name, agent.pubkey);
|
|
562
|
+
registerUnilateralConfirmedListener(signalingManager, agent.name, agent.pubkey);
|
|
563
|
+
registerUnilateralUpgradeListener(signalingManager, agent.name, agent.pubkey);
|
|
537
564
|
}
|
|
565
|
+
if (primaryAgent)
|
|
566
|
+
wireKeystonePrimary(primaryAgent);
|
|
538
567
|
// Per-connection state: tracks which agent is "current" for each IPC connection.
|
|
539
568
|
// Key = connectionId (assigned by IPC server), Value = current agent name or null.
|
|
540
569
|
const perConnectionState = new Map();
|
|
541
570
|
// Set of agents currently in "online" state (transitioned via cello_start_agent)
|
|
542
571
|
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();
|
|
572
|
+
// SessionNodeManager was constructed + initialized at the top of startDaemon (PERSIST-002 — the
|
|
573
|
+
// encrypted store must open before agents load from the `agents` table). Its standing receiver +
|
|
574
|
+
// interrupted-session detection are already ready here, before the IPC socket opens.
|
|
559
575
|
// CELLO-M7-TRANSPORT-001: the daemon's runtime AutoNAT service is the one
|
|
560
576
|
// wrapping the standing receiver node (it emits transport.autonat.result /
|
|
561
577
|
// transport.autonat.unavailable and its dialability drives the SessionAssignment
|
|
@@ -687,7 +703,7 @@ export async function startDaemon(config) {
|
|
|
687
703
|
// DAEMON-003: Initialize RetryQueue and NonceDedupStore (AC-008).
|
|
688
704
|
// Both use the same SQLite DB as the SessionNodeManager (daemon.db equivalent).
|
|
689
705
|
// loadFromDb() must complete BEFORE IPC socket opens (AC-007).
|
|
690
|
-
const retryQueue = new RetryQueue(sessionNodeManager.getDb(), logger
|
|
706
|
+
const retryQueue = new RetryQueue(sessionNodeManager.getDb(), logger);
|
|
691
707
|
retryQueue.loadFromDb();
|
|
692
708
|
// CELLO-M7-MSG-001 (AC-001/AC-003/AC-019): wire the awaiting-ACK lifecycle's durable
|
|
693
709
|
// side effects to the retry_queue. A `persisted` delivery ACK clears the durable
|
|
@@ -925,6 +941,57 @@ export async function startDaemon(config) {
|
|
|
925
941
|
notificationDispatcher.dispatchAgentStateChanged(name, "online", "started");
|
|
926
942
|
return { ok: true };
|
|
927
943
|
});
|
|
944
|
+
// ─── PERSIST-002 (AC-004): cello_create_agent handler ───
|
|
945
|
+
// The explicit agent-creation path: generate a fresh K_local seed, write it as an `agents` row in
|
|
946
|
+
// the encrypted DB (NO key file), and wire the agent into the live daemon so it can be registered
|
|
947
|
+
// and used WITHOUT a restart. Creation is explicit — cello_start_agent never auto-creates on a typo.
|
|
948
|
+
handlers.set("cello_create_agent", async (params, _connectionId) => {
|
|
949
|
+
const name = params?.name;
|
|
950
|
+
if (!name || typeof name !== "string" || !/^[a-zA-Z0-9_-]{1,64}$/.test(name)) {
|
|
951
|
+
return { ok: false, reason: "invalid_agent_name", guidance: "Provide a 'name' (1-64 chars: letters, digits, '-' or '_') for the new agent." };
|
|
952
|
+
}
|
|
953
|
+
const store = new DbIdentityStore(sessionNodeManager.getDb(), logger);
|
|
954
|
+
if (store.hasAgent(name) || agents.some((a) => a.name === name)) {
|
|
955
|
+
return { ok: false, reason: "agent_already_exists", guidance: `Agent '${name}' already exists. Choose a different name, or see cello_list_agents.` };
|
|
956
|
+
}
|
|
957
|
+
let pubkeyHex;
|
|
958
|
+
try {
|
|
959
|
+
const seed = generateKLocalSeed();
|
|
960
|
+
const keyProvider = new InMemoryKeyProvider(seed);
|
|
961
|
+
pubkeyHex = Buffer.from(await keyProvider.getPublicKey()).toString("hex");
|
|
962
|
+
// SI-001: createAgent stores the seed in the encrypted DB and logs only the pubkey.
|
|
963
|
+
store.createAgent(name, seed, pubkeyHex);
|
|
964
|
+
// Runtime-add: make the agent immediately registrable/usable (the register handler resolves
|
|
965
|
+
// identity from keyProviders/loadedAgents; per-agent signaling is created lazily on register).
|
|
966
|
+
keyProviders.set(name, keyProvider);
|
|
967
|
+
const loaded = { name, pubkey: pubkeyHex, keyProvider };
|
|
968
|
+
loadedAgents.push(loaded);
|
|
969
|
+
agents.push({ name, state: "registered", pubkey: pubkeyHex });
|
|
970
|
+
// CELLO-M7-ONBOARD-001: on a FRESH install the daemon started with no agents, so there was no
|
|
971
|
+
// keystone identity and the directory door is stuck `reconnecting`. Elect this first agent as the
|
|
972
|
+
// keystone primary + wire its ceremony/seal handlers; the running reconnect loop then
|
|
973
|
+
// authenticates with it (getAuthIdentity now returns it) and connects on its next attempt — no
|
|
974
|
+
// restart needed. Only the FIRST agent is elected (loadedAgents is empty here, so it is also the
|
|
975
|
+
// name-sort min). NOTE (accepted LOW, review): if the operator then creates a lexicographically
|
|
976
|
+
// SMALLER second agent before any restart, the keystone primary stays this one until the next
|
|
977
|
+
// restart, where it re-derives to sort[0] — benign (each agent authenticates its own stream; the
|
|
978
|
+
// demoted agent gets fully wired via the per-agent path on restart), and not worth runtime
|
|
979
|
+
// re-election (un-wire/re-wire) churn.
|
|
980
|
+
if (!primaryAgent) {
|
|
981
|
+
primaryAgent = loaded;
|
|
982
|
+
wireKeystonePrimary(loaded);
|
|
983
|
+
logger.info("directory.keystone.elected", { agentName: name, agentPubkey: pubkeyHex });
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
catch (err) {
|
|
987
|
+
logger.error("persist.identity.persist.failed", { agentName: name, error: err instanceof Error ? err.message : String(err) });
|
|
988
|
+
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." };
|
|
989
|
+
}
|
|
990
|
+
// Creation is not an online/offline transition — the agent appears (cello_list_agents) but is
|
|
991
|
+
// not online until cello_start_agent. Just record it.
|
|
992
|
+
logger.info("agent.created", { agentName: name, agentPubkey: pubkeyHex });
|
|
993
|
+
return { ok: true, name, pubkey: pubkeyHex };
|
|
994
|
+
});
|
|
928
995
|
// ─── MCP-001: cello_stop_agent handler ───
|
|
929
996
|
handlers.set("cello_stop_agent", async (params, _connectionId) => {
|
|
930
997
|
const name = params?.name;
|
|
@@ -1028,7 +1095,7 @@ export async function startDaemon(config) {
|
|
|
1028
1095
|
}
|
|
1029
1096
|
const keyProvider = keyProviders.get(name);
|
|
1030
1097
|
if (!keyProvider) {
|
|
1031
|
-
return { ok: false, reason: "agent_not_found", guidance: `Agent '${name}'
|
|
1098
|
+
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
1099
|
}
|
|
1033
1100
|
if (!directoryEndpointResolver) {
|
|
1034
1101
|
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 +1141,7 @@ export async function startDaemon(config) {
|
|
|
1074
1141
|
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
1142
|
};
|
|
1076
1143
|
}
|
|
1077
|
-
const persistence =
|
|
1144
|
+
const persistence = getPersistence(name);
|
|
1078
1145
|
const ctx = new DaemonRegistrationContext({
|
|
1079
1146
|
signaling: agentSignaling,
|
|
1080
1147
|
getDirectoryNode: agentGetNode,
|
|
@@ -1092,6 +1159,10 @@ export async function startDaemon(config) {
|
|
|
1092
1159
|
await dropAgentSignaling(name);
|
|
1093
1160
|
return { ok: false, reason: result.error, guidance: registrationGuidance(result.error) };
|
|
1094
1161
|
}
|
|
1162
|
+
// PERSIST-002 (AC-013): the identity row (K_local + share + ML-DSA + registration) is durably
|
|
1163
|
+
// committed at this point (RegistrationManager awaits the persist before returning success).
|
|
1164
|
+
// SI-001: never log a secret — only the agent name + PUBLIC key.
|
|
1165
|
+
logger.info("persist.identity.persisted", { agentName: name, agentPubkey: agentPubkeyHex });
|
|
1095
1166
|
// Capture-now-or-lose-it: persist the agent→user link (using it is future
|
|
1096
1167
|
// trust-layer work). L1: the agent is already registered at this point —
|
|
1097
1168
|
// a link-write failure must NOT be reported as a registration failure.
|
|
@@ -1168,7 +1239,7 @@ export async function startDaemon(config) {
|
|
|
1168
1239
|
return;
|
|
1169
1240
|
}
|
|
1170
1241
|
const record = sessionNodeManager.getSessionRecord(agentName, sidHex);
|
|
1171
|
-
const verdict = await verifyBilateralSealCertificate({
|
|
1242
|
+
const verdict = await verifyBilateralSealCertificate({ persistence: getPersistence(agentName), agentPubkeyHex, logger, counterpartyPrimaryHex: record?.counterparty_primary_pubkey ?? null }, {
|
|
1172
1243
|
sessionId: sessionIdBytes,
|
|
1173
1244
|
sealedRoot: sealedRootBytes,
|
|
1174
1245
|
leafCount,
|
|
@@ -1318,7 +1389,7 @@ export async function startDaemon(config) {
|
|
|
1318
1389
|
waiter({ ok: false, reason: "malformed_certificate" });
|
|
1319
1390
|
return;
|
|
1320
1391
|
}
|
|
1321
|
-
const result = await verifyUnilateralCertificate({
|
|
1392
|
+
const result = await verifyUnilateralCertificate({ persistence: getPersistence(agentName), agentPubkeyHex, logger }, { sessionId, sealedRoot, leafCount, closeTimestamp: closeTs, frostSignature: frostSig, signatureType: sigType });
|
|
1322
1393
|
pendingUnilateralWaiters.delete(sidHex);
|
|
1323
1394
|
if (!result.ok) {
|
|
1324
1395
|
// SI-003: do NOT mark sealed when the certificate signature does not verify.
|
|
@@ -1374,7 +1445,7 @@ export async function startDaemon(config) {
|
|
|
1374
1445
|
// ONLY on ok. Never trust the directory's "bilateral" claim.
|
|
1375
1446
|
async function verifyAndApplyUpgradeConfirmed(agentName, agentPubkeyHex, sidHex, frame) {
|
|
1376
1447
|
const result = await verifyUpgradeConfirmedCert({
|
|
1377
|
-
logger, agentName, agentPubkeyHex,
|
|
1448
|
+
logger, agentName, agentPubkeyHex, persistence: getPersistence(agentName),
|
|
1378
1449
|
getCounterpartyHex: (a, s) => sessionNodeManager.getSessionRecord(a, s)?.counterparty_pubkey ?? null,
|
|
1379
1450
|
}, sidHex, frame);
|
|
1380
1451
|
if (!result.ok)
|
|
@@ -2564,25 +2635,11 @@ export async function startDaemon(config) {
|
|
|
2564
2635
|
handleInboundSessionAssignment(frame);
|
|
2565
2636
|
});
|
|
2566
2637
|
// M7 DOD-SPINE-7: session_sealed listener. The directory delivers this over the SESSION-OWNING
|
|
2567
|
-
//
|
|
2568
|
-
//
|
|
2569
|
-
//
|
|
2570
|
-
//
|
|
2571
|
-
//
|
|
2572
|
-
// needs the primary agent's name/pubkey to verify the seal signature (legibility-TBS-binding), and
|
|
2573
|
-
// with no agents there are no keystone sessions to seal anyway.
|
|
2574
|
-
if (primaryAgent) {
|
|
2575
|
-
registerSessionSealedListener(signalingManager, primaryAgent.name, primaryAgent.pubkey);
|
|
2576
|
-
// SESSION-002: the keystone counterpart for the unilateral certificate listener — the
|
|
2577
|
-
// primary agent closes over the keystone stream, so the directory routes its
|
|
2578
|
-
// seal_unilateral_confirmed there (mirrors the session_sealed keystone listener above).
|
|
2579
|
-
registerUnilateralConfirmedListener(signalingManager, primaryAgent.name, primaryAgent.pubkey);
|
|
2580
|
-
// DOD-UP-1: the keystone counterpart for the absent-party upgrade listener. The directory
|
|
2581
|
-
// PUSHES the queued seal_unilateral_notification during the keystone's auth/reconnect drain —
|
|
2582
|
-
// BEFORE any cello_start_agent runs — so the handler MUST be registered here at startup, not only
|
|
2583
|
-
// in startAgent, or B (the returning absent party) would miss its own ratification trigger.
|
|
2584
|
-
registerUnilateralUpgradeListener(signalingManager, primaryAgent.name, primaryAgent.pubkey);
|
|
2585
|
-
}
|
|
2638
|
+
// CELLO-M7-ONBOARD-001: the keystone primary's seal-completion listeners (session_sealed /
|
|
2639
|
+
// seal_unilateral_confirmed / seal_unilateral_notification) are now registered inside
|
|
2640
|
+
// wireKeystonePrimary — at startup for a loaded primary AND at runtime when the first agent is
|
|
2641
|
+
// elected (cello_create_agent on a fresh install). They must NOT be a separate startup-only block,
|
|
2642
|
+
// or a fresh-install primary's seals would silently never finalize (review HIGH).
|
|
2586
2643
|
// cello_await_session — the counterparty's blocking pull for the next inbound session.
|
|
2587
2644
|
// Returns immediately if one is already queued for the current agent (FIFO), otherwise
|
|
2588
2645
|
// blocks until one arrives or timeout_ms elapses. Response shape matches the established
|