@cello-protocol/daemon 0.0.10 → 0.0.11
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/daemon.d.ts.map +1 -1
- package/dist/daemon.js +200 -17
- package/dist/daemon.js.map +1 -1
- package/dist/db-identity-store.d.ts +37 -9
- package/dist/db-identity-store.d.ts.map +1 -1
- package/dist/db-identity-store.js +186 -54
- package/dist/db-identity-store.js.map +1 -1
- package/dist/identity-migration.js +14 -8
- package/dist/identity-migration.js.map +1 -1
- package/dist/session-assignment-parser.d.ts.map +1 -1
- package/dist/session-assignment-parser.js +1 -0
- package/dist/session-assignment-parser.js.map +1 -1
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +11 -2
- package/dist/session-node-manager.js.map +1 -1
- package/package.json +4 -4
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,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,
|
|
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,CAulH7E"}
|
package/dist/daemon.js
CHANGED
|
@@ -46,7 +46,7 @@ import { verify as ed25519Verify, sealToRecipient, generateKLocalSeed, InMemoryK
|
|
|
46
46
|
import { attemptSealUpgrade as attemptSealUpgradeImpl, verifyUpgradeConfirmedCert } from "./seal-upgrade.js";
|
|
47
47
|
// CELLO-M7-MSG-001 (AC-013/AC-018): the single application content-size cap, enforced
|
|
48
48
|
// at the send point here (the receive point lives in the transport content decode).
|
|
49
|
-
import { MAX_CONTENT_BYTES, computeGenesisPrevRoot } from "@cello-protocol/protocol-types";
|
|
49
|
+
import { MAX_CONTENT_BYTES, computeGenesisPrevRoot, buildAgentRevocationTbs } from "@cello-protocol/protocol-types";
|
|
50
50
|
import { resolveCelloEnv, createTransportSelector, isProductionVariant, } from "./transport-composition.js";
|
|
51
51
|
import { selectAdvertisedAddress } from "./transport-selector.js";
|
|
52
52
|
import { parseSessionAssignment, sessionRequestErrorReason } from "./session-assignment-parser.js";
|
|
@@ -527,8 +527,14 @@ export async function startDaemon(config) {
|
|
|
527
527
|
// handlers must be wired on the keystone manager — otherwise a primary-agent initiator's session
|
|
528
528
|
// ceremony would time out. CELLO-M7-ONBOARD-001: extracted into a function so it can also be wired
|
|
529
529
|
// when the first agent is elected at runtime (cello_create_agent), not only at startup.
|
|
530
|
+
// Returns a disposer that UNREGISTERS the six keystone listeners. Each wire*/register* helper returns
|
|
531
|
+
// its own unregister fn; the keystone manager is shared and never stopped, so (unlike the per-agent
|
|
532
|
+
// managers, which unregister implicitly on stop) these MUST be unregistered explicitly when the
|
|
533
|
+
// primary is cleared or re-elected — otherwise removing-then-recreating the primary STACKS a second
|
|
534
|
+
// full set of listeners on the shared stream, double-firing every ceremony/seal frame (review HIGH).
|
|
530
535
|
function wireKeystonePrimary(agent) {
|
|
531
|
-
|
|
536
|
+
const unregister = [];
|
|
537
|
+
unregister.push(wireSessionCeremonyHandler({
|
|
532
538
|
agentName: agent.name,
|
|
533
539
|
persistence: getPersistence(agent.name),
|
|
534
540
|
agentPubkeyHex: agent.pubkey,
|
|
@@ -536,8 +542,8 @@ export async function startDaemon(config) {
|
|
|
536
542
|
getDirectoryEndpoint: async () => (directoryEndpointResolver ? (await directoryEndpointResolver()) ?? null : null),
|
|
537
543
|
signaling: signalingManager,
|
|
538
544
|
logger,
|
|
539
|
-
});
|
|
540
|
-
wireSealCeremonyHandler({
|
|
545
|
+
}));
|
|
546
|
+
unregister.push(wireSealCeremonyHandler({
|
|
541
547
|
agentName: agent.name,
|
|
542
548
|
persistence: getPersistence(agent.name),
|
|
543
549
|
agentPubkeyHex: agent.pubkey,
|
|
@@ -545,25 +551,34 @@ export async function startDaemon(config) {
|
|
|
545
551
|
getDirectoryEndpoint: async () => (directoryEndpointResolver ? (await directoryEndpointResolver()) ?? null : null),
|
|
546
552
|
signaling: signalingManager,
|
|
547
553
|
logger,
|
|
548
|
-
});
|
|
549
|
-
wireSessionOfferHandler({
|
|
554
|
+
}));
|
|
555
|
+
unregister.push(wireSessionOfferHandler({
|
|
550
556
|
agentName: agent.name,
|
|
551
557
|
getStandingReceiverEndpoint: () => sessionNodeManager.getStandingReceiverInfo(agent.name),
|
|
552
558
|
signaling: signalingManager,
|
|
553
559
|
logger,
|
|
554
|
-
});
|
|
560
|
+
}));
|
|
555
561
|
// The primary closes its OWN sessions over the keystone stream, so the directory routes its
|
|
556
562
|
// session_sealed / seal_unilateral_confirmed / seal_unilateral_notification frames there. These
|
|
557
563
|
// seal-completion listeners MUST be registered for the keystone primary (startup OR runtime
|
|
558
564
|
// election) — otherwise the close waiter never resolves and the seal silently never finalizes
|
|
559
565
|
// (review HIGH: previously these were a separate startup-only block, skipped on a fresh install).
|
|
560
566
|
// 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);
|
|
567
|
+
unregister.push(registerSessionSealedListener(signalingManager, agent.name, agent.pubkey));
|
|
568
|
+
unregister.push(registerUnilateralConfirmedListener(signalingManager, agent.name, agent.pubkey));
|
|
569
|
+
unregister.push(registerUnilateralUpgradeListener(signalingManager, agent.name, agent.pubkey));
|
|
570
|
+
return () => { for (const u of unregister) {
|
|
571
|
+
try {
|
|
572
|
+
u();
|
|
573
|
+
}
|
|
574
|
+
catch { /* unregister is best-effort */ }
|
|
575
|
+
} };
|
|
564
576
|
}
|
|
577
|
+
// Disposer for the currently-wired keystone primary listeners (CELLO-M7-REMOVE-001): call it before
|
|
578
|
+
// clearing or re-electing the primary so the shared stream never carries a stale/duplicate set.
|
|
579
|
+
let keystoneDispose;
|
|
565
580
|
if (primaryAgent)
|
|
566
|
-
wireKeystonePrimary(primaryAgent);
|
|
581
|
+
keystoneDispose = wireKeystonePrimary(primaryAgent);
|
|
567
582
|
// Per-connection state: tracks which agent is "current" for each IPC connection.
|
|
568
583
|
// Key = connectionId (assigned by IPC server), Value = current agent name or null.
|
|
569
584
|
const perConnectionState = new Map();
|
|
@@ -951,16 +966,17 @@ export async function startDaemon(config) {
|
|
|
951
966
|
return { ok: false, reason: "invalid_agent_name", guidance: "Provide a 'name' (1-64 chars: letters, digits, '-' or '_') for the new agent." };
|
|
952
967
|
}
|
|
953
968
|
const store = new DbIdentityStore(sessionNodeManager.getDb(), logger);
|
|
954
|
-
if (store.
|
|
969
|
+
if (store.hasActiveAgent(name) || agents.some((a) => a.name === name)) {
|
|
955
970
|
return { ok: false, reason: "agent_already_exists", guidance: `Agent '${name}' already exists. Choose a different name, or see cello_list_agents.` };
|
|
956
971
|
}
|
|
957
972
|
let pubkeyHex;
|
|
973
|
+
let agentId;
|
|
958
974
|
try {
|
|
959
975
|
const seed = generateKLocalSeed();
|
|
960
976
|
const keyProvider = new InMemoryKeyProvider(seed);
|
|
961
977
|
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);
|
|
978
|
+
// SI-001: createAgent stores the seed in the encrypted DB and logs only the pubkey + agent_id.
|
|
979
|
+
agentId = store.createAgent(name, seed, pubkeyHex);
|
|
964
980
|
// Runtime-add: make the agent immediately registrable/usable (the register handler resolves
|
|
965
981
|
// identity from keyProviders/loadedAgents; per-agent signaling is created lazily on register).
|
|
966
982
|
keyProviders.set(name, keyProvider);
|
|
@@ -978,8 +994,10 @@ export async function startDaemon(config) {
|
|
|
978
994
|
// demoted agent gets fully wired via the per-agent path on restart), and not worth runtime
|
|
979
995
|
// re-election (un-wire/re-wire) churn.
|
|
980
996
|
if (!primaryAgent) {
|
|
997
|
+
// Dispose any prior keystone wiring before re-electing, so listeners never stack (REMOVE-001).
|
|
998
|
+
keystoneDispose?.();
|
|
981
999
|
primaryAgent = loaded;
|
|
982
|
-
wireKeystonePrimary(loaded);
|
|
1000
|
+
keystoneDispose = wireKeystonePrimary(loaded);
|
|
983
1001
|
logger.info("directory.keystone.elected", { agentName: name, agentPubkey: pubkeyHex });
|
|
984
1002
|
}
|
|
985
1003
|
}
|
|
@@ -989,8 +1007,173 @@ export async function startDaemon(config) {
|
|
|
989
1007
|
}
|
|
990
1008
|
// Creation is not an online/offline transition — the agent appears (cello_list_agents) but is
|
|
991
1009
|
// 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 };
|
|
1010
|
+
logger.info("agent.created", { agentName: name, agentId, agentPubkey: pubkeyHex });
|
|
1011
|
+
return { ok: true, name, pubkey: pubkeyHex, agentId };
|
|
1012
|
+
});
|
|
1013
|
+
// ─── CELLO-M7-REMOVE-001 (DOD-REMOVE-1): cello_remove_agent handler ───
|
|
1014
|
+
// RETIRE-AND-KEEP: flip the agent's local row to state='retired' (its row, keys, and history are
|
|
1015
|
+
// KEPT for accountability — never hard-deleted, SI-002) and FREE the human name for reuse. Purge the
|
|
1016
|
+
// retired identity from the live runtime so it stops operating and the name is immediately available
|
|
1017
|
+
// to a NEW `cello_create_agent`. One-way. (DEC-4: the signed DIRECTORY revocation is DOD-REMOVE-2 —
|
|
1018
|
+
// not built here; this unit is the local record shape only.)
|
|
1019
|
+
// CELLO-M7-REMOVE-001 (DOD-REMOVE-2): build + self-sign an agent revocation and submit it to the
|
|
1020
|
+
// directory on the agent's K_local-authenticated signaling stream. Self-authorized — the directory
|
|
1021
|
+
// verifies the signature against the agent's registered K_local before appending. Best-effort: returns
|
|
1022
|
+
// { recorded:false, reason } if the directory is unreachable / rejects (DB-001 — the caller still
|
|
1023
|
+
// applies the one-way local retire and surfaces a distinct status).
|
|
1024
|
+
async function submitAgentRevocation(opts) {
|
|
1025
|
+
const { agentName, signer, kLocalPubkeyHex, regAgentId } = opts;
|
|
1026
|
+
const epochId = "";
|
|
1027
|
+
const reason = "voluntary";
|
|
1028
|
+
const revokedAt = Date.now();
|
|
1029
|
+
const tbs = buildAgentRevocationTbs(regAgentId, kLocalPubkeyHex, epochId, reason, revokedAt);
|
|
1030
|
+
const sigHex = Buffer.from(await signer.sign(tbs)).toString("hex");
|
|
1031
|
+
// If the agent has no live signaling (e.g. a re-push of an already-retired agent), getAgentSignaling
|
|
1032
|
+
// lazily creates a dedicated manager — drop it again afterwards so we don't leak a reconnect loop.
|
|
1033
|
+
const hadSignaling = perAgentSignaling.has(agentName) || primaryAgent?.name === agentName;
|
|
1034
|
+
const { signaling } = getAgentSignaling(agentName, signer, kLocalPubkeyHex);
|
|
1035
|
+
const createdSignaling = !hadSignaling && primaryAgent?.name !== agentName;
|
|
1036
|
+
try {
|
|
1037
|
+
const connected = await waitForSignalingConnected(signaling, 10_000);
|
|
1038
|
+
if (!connected)
|
|
1039
|
+
return { recorded: false, reason: "directory_unreachable" };
|
|
1040
|
+
let resolveFrame;
|
|
1041
|
+
const pending = new Promise((r) => { resolveFrame = r; });
|
|
1042
|
+
// Match the reply by agent_id so a revocation on the shared keystone is never cross-wired.
|
|
1043
|
+
const unregister = signaling.registerInboundHandler((frame) => {
|
|
1044
|
+
const t = frame["type"];
|
|
1045
|
+
if ((t === "agent_revocation_ack" || t === "agent_revocation_error") && frame["agent_id"] === regAgentId)
|
|
1046
|
+
resolveFrame(frame);
|
|
1047
|
+
});
|
|
1048
|
+
try {
|
|
1049
|
+
const sent = await signaling.sendRaw({ type: "revoke_agent", agent_id: regAgentId, epoch_id: epochId, reason, revoked_at: revokedAt, signature: sigHex });
|
|
1050
|
+
if (!sent.ok)
|
|
1051
|
+
return { recorded: false, reason: sent.reason ?? "directory_unreachable" };
|
|
1052
|
+
let timer;
|
|
1053
|
+
const timeoutP = new Promise((r) => { timer = setTimeout(() => r({ type: "__timeout__" }), 15_000); });
|
|
1054
|
+
const frame = await Promise.race([pending, timeoutP]);
|
|
1055
|
+
clearTimeout(timer);
|
|
1056
|
+
if (frame["type"] === "__timeout__")
|
|
1057
|
+
return { recorded: false, reason: "timeout" };
|
|
1058
|
+
if (frame["type"] === "agent_revocation_error")
|
|
1059
|
+
return { recorded: false, reason: String(frame["reason"] ?? "rejected") };
|
|
1060
|
+
logger.info("agent.revocation.submitted", { agentName, agentId: regAgentId });
|
|
1061
|
+
return { recorded: true };
|
|
1062
|
+
}
|
|
1063
|
+
finally {
|
|
1064
|
+
unregister();
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
finally {
|
|
1068
|
+
if (createdSignaling) {
|
|
1069
|
+
await dropAgentSignaling(agentName).catch((err) => {
|
|
1070
|
+
logger.warn("agent.revocation.signaling_teardown_failed", { agentName, error: err instanceof Error ? err.message : String(err) });
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
handlers.set("cello_remove_agent", async (params, _connectionId) => {
|
|
1076
|
+
const name = params?.name;
|
|
1077
|
+
if (!name || typeof name !== "string" || !/^[a-zA-Z0-9_-]{1,64}$/.test(name)) {
|
|
1078
|
+
return { ok: false, reason: "invalid_agent_name", guidance: "Provide the 'name' of the agent to remove (1-64 chars: letters, digits, '-' or '_')." };
|
|
1079
|
+
}
|
|
1080
|
+
const store = new DbIdentityStore(sessionNodeManager.getDb(), logger);
|
|
1081
|
+
// The active row (a fresh removal) OR the most-recent retired row (a DB-001 re-push). Captured BEFORE
|
|
1082
|
+
// any retire — the active-only accessors filter retired rows out once state is flipped.
|
|
1083
|
+
const target = store.getAgentForRevocation(name);
|
|
1084
|
+
if (!target) {
|
|
1085
|
+
return { ok: false, reason: "agent_not_found", guidance: `No agent named '${name}'. Check cello_list_agents.` };
|
|
1086
|
+
}
|
|
1087
|
+
const wasActive = target.state !== "retired";
|
|
1088
|
+
const agentId = target.localAgentId;
|
|
1089
|
+
// An already-retired agent that was never registered has nothing to do — no local retire (one-way,
|
|
1090
|
+
// already done) and no directory revocation to push. Treat a repeat removal as agent_not_found (a
|
|
1091
|
+
// tombstone is never re-retired). A retired agent WITH a directory id falls through to the DB-001
|
|
1092
|
+
// re-push below.
|
|
1093
|
+
if (!wasActive && !target.regAgentId) {
|
|
1094
|
+
return { ok: false, reason: "agent_not_found", guidance: `No active agent named '${name}'. Removal is one-way; '${name}' is already retired.` };
|
|
1095
|
+
}
|
|
1096
|
+
// DOD-REMOVE-2: build + self-sign + submit the directory revocation. Done for a fresh removal AND a
|
|
1097
|
+
// re-push of an already-retired agent (DB-001). Skipped only if the agent was never registered (no
|
|
1098
|
+
// directory-known id to revoke). The signer is re-derived from the kept K_local seed, so it works
|
|
1099
|
+
// even for an already-retired agent whose runtime keyProvider was purged.
|
|
1100
|
+
let directoryRevocation = "skipped";
|
|
1101
|
+
let revocationReason;
|
|
1102
|
+
if (target.regAgentId) {
|
|
1103
|
+
const signer = new InMemoryKeyProvider(target.kLocalSeed);
|
|
1104
|
+
const kLocalPubkeyHex = Buffer.from(await signer.getPublicKey()).toString("hex");
|
|
1105
|
+
const res = await submitAgentRevocation({ agentName: name, signer, kLocalPubkeyHex, regAgentId: target.regAgentId });
|
|
1106
|
+
directoryRevocation = res.recorded ? "recorded" : "deferred";
|
|
1107
|
+
revocationReason = res.reason;
|
|
1108
|
+
if (!res.recorded) {
|
|
1109
|
+
logger.error("agent.removal.failed", { agentName: name, error: res.reason ?? "directory_unreachable" });
|
|
1110
|
+
}
|
|
1111
|
+
else {
|
|
1112
|
+
logger.info("agent.revocation.recorded", { agentId: target.regAgentId });
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
else if (target.state === "registered") {
|
|
1116
|
+
// Anomaly (fallback-finder MEDIUM): the agent is locally marked registered but has no
|
|
1117
|
+
// directory-known id, so the revocation cannot be pushed. Do NOT report the benign "never
|
|
1118
|
+
// registered" — surface it loudly so a registered-but-unrevocable agent is visible.
|
|
1119
|
+
revocationReason = "registered_without_directory_id";
|
|
1120
|
+
logger.error("agent.removal.failed", { agentName: name, error: "registered_without_directory_id" });
|
|
1121
|
+
}
|
|
1122
|
+
// Local retire (one-way) + runtime purge — only for a fresh removal. An already-retired re-push does
|
|
1123
|
+
// not re-retire (and has nothing loaded to purge).
|
|
1124
|
+
if (wasActive) {
|
|
1125
|
+
store.retireAgent(name);
|
|
1126
|
+
// Tear down the retired identity's live runtime so it can no longer receive or re-authenticate.
|
|
1127
|
+
// AWAITED + LOGGED on failure (review HIGH-1 / fallback-finder MEDIUM): a teardown that didn't
|
|
1128
|
+
// happen must be visible.
|
|
1129
|
+
if (onlineAgents.has(name)) {
|
|
1130
|
+
onlineAgents.delete(name);
|
|
1131
|
+
await sessionNodeManager.removeStandingReceiverForAgent(name).catch((err) => {
|
|
1132
|
+
logger.warn("agent.removal.receiver_teardown_failed", { agentName: name, agentId, error: err instanceof Error ? err.message : String(err) });
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
// Stop+forget the retired agent's dedicated per-agent signaling manager (review HIGH-1).
|
|
1136
|
+
await dropAgentSignaling(name).catch((err) => {
|
|
1137
|
+
logger.warn("agent.removal.signaling_teardown_failed", { agentName: name, agentId, error: err instanceof Error ? err.message : String(err) });
|
|
1138
|
+
});
|
|
1139
|
+
keyProviders.delete(name);
|
|
1140
|
+
const li = loadedAgents.findIndex((a) => a.name === name);
|
|
1141
|
+
if (li >= 0)
|
|
1142
|
+
loadedAgents.splice(li, 1);
|
|
1143
|
+
const ai = agents.findIndex((a) => a.name === name);
|
|
1144
|
+
if (ai >= 0)
|
|
1145
|
+
agents.splice(ai, 1);
|
|
1146
|
+
// If the retired agent was the directory keystone, UNWIRE its keystone listeners + clear it
|
|
1147
|
+
// (review HIGH-2: the listeners were never unregistered → double-wire on re-election).
|
|
1148
|
+
if (primaryAgent && primaryAgent.name === name) {
|
|
1149
|
+
keystoneDispose?.();
|
|
1150
|
+
keystoneDispose = undefined;
|
|
1151
|
+
primaryAgent = undefined;
|
|
1152
|
+
logger.info("directory.keystone.cleared", { agentName: name, agentId });
|
|
1153
|
+
}
|
|
1154
|
+
// Drop the retired agent as any connection's current agent.
|
|
1155
|
+
for (const [connId, state] of perConnectionState) {
|
|
1156
|
+
if (state.currentAgent === name) {
|
|
1157
|
+
state.currentAgent = null;
|
|
1158
|
+
notificationDispatcher.setCurrentAgent(connId, null);
|
|
1159
|
+
notificationDispatcher.dispatchAgentCurrentChanged(connId, name, null);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
// agent.removal.retired (observability): never log key material — only the name + agent_id.
|
|
1163
|
+
logger.info("agent.removal.retired", { agentName: name, agentId });
|
|
1164
|
+
notificationDispatcher.dispatchAgentStateChanged(name, "offline", "removed");
|
|
1165
|
+
}
|
|
1166
|
+
const baseLine = wasActive
|
|
1167
|
+
? "Agent retired. This is one-way: its identity and history are kept for accountability, but it can no longer connect. The name is now free to reuse with cello create-agent."
|
|
1168
|
+
: "Agent was already retired locally.";
|
|
1169
|
+
const dirLine = directoryRevocation === "recorded"
|
|
1170
|
+
? " A signed revocation was recorded at the directory — peers will see it as revoked."
|
|
1171
|
+
: directoryRevocation === "deferred"
|
|
1172
|
+
? ` The directory could NOT be reached to record the revocation (${revocationReason ?? "directory_unreachable"}) — peers do not yet see it as revoked. Re-run 'cello remove-agent ${name}' when the directory is reachable to push it.`
|
|
1173
|
+
: revocationReason === "registered_without_directory_id"
|
|
1174
|
+
? " WARNING: this agent appears registered but has no directory id recorded locally, so its revocation could NOT be pushed — peers may still see it as reachable. Check the daemon logs (agent.removal.failed)."
|
|
1175
|
+
: " It was never registered with a directory, so there is no directory revocation to record.";
|
|
1176
|
+
return { ok: true, name, agentId, oneWay: true, directoryRevocation, guidance: baseLine + dirLine };
|
|
994
1177
|
});
|
|
995
1178
|
// ─── MCP-001: cello_stop_agent handler ───
|
|
996
1179
|
handlers.set("cello_stop_agent", async (params, _connectionId) => {
|