@cello-protocol/daemon 0.0.13 → 0.0.15
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 +124 -14
- package/dist/daemon.js.map +1 -1
- package/dist/db-identity-store.d.ts +15 -0
- package/dist/db-identity-store.d.ts.map +1 -1
- package/dist/db-identity-store.js +34 -0
- package/dist/db-identity-store.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-category.d.ts +19 -0
- package/dist/session-category.d.ts.map +1 -0
- package/dist/session-category.js +9 -0
- package/dist/session-category.js.map +1 -0
- package/dist/session-node-manager.d.ts +6 -0
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +12 -0
- package/dist/session-node-manager.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +3 -3
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,EAQrB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAM/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;AAqInF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,IAAI,oBAAoB,CAAC;IAClC;;;;OAIG;IACH,qBAAqB,IAAI,kBAAkB,CAAC;IAC5C;;;;;;;;OAQG;IACH,gBAAgB,IAAI,SAAS,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;CACtC;AAyCD,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA8vH7E"}
|
package/dist/daemon.js
CHANGED
|
@@ -32,6 +32,7 @@ import { loadAgents } from "./agent-loader.js";
|
|
|
32
32
|
import { acquireLock, removeLock } from "./lock-file.js";
|
|
33
33
|
import { createIpcServer } from "./ipc-server.js";
|
|
34
34
|
import { SessionNodeManager } from "./session-node-manager.js";
|
|
35
|
+
import { classifySession } from "./session-category.js";
|
|
35
36
|
import { RetryQueue } from "./retry-queue.js";
|
|
36
37
|
import { NonceDedupStore } from "./nonce-dedup.js";
|
|
37
38
|
import { ContentParkClient } from "./content-park-client.js";
|
|
@@ -42,7 +43,7 @@ import { RegistrationManager } from "./registration-manager.js";
|
|
|
42
43
|
import { DaemonRegistrationContext } from "./registration-context.js";
|
|
43
44
|
import { DbRegistrationPersistence, DbIdentityStore } from "./db-identity-store.js";
|
|
44
45
|
import { DbManifestVersionStore } from "./manifest-version-store-db.js";
|
|
45
|
-
import { verify as ed25519Verify, sealToRecipient, generateKLocalSeed, InMemoryKeyProvider } from "@cello-protocol/crypto";
|
|
46
|
+
import { verify as ed25519Verify, sealToRecipient, generateKLocalSeed, InMemoryKeyProvider, hash as cryptoHash } from "@cello-protocol/crypto";
|
|
46
47
|
import { attemptSealUpgrade as attemptSealUpgradeImpl, verifyUpgradeConfirmedCert } from "./seal-upgrade.js";
|
|
47
48
|
// CELLO-M7-MSG-001 (AC-013/AC-018): the single application content-size cap, enforced
|
|
48
49
|
// at the send point here (the receive point lives in the transport content decode).
|
|
@@ -468,6 +469,15 @@ export async function startDaemon(config) {
|
|
|
468
469
|
signaling: mgr,
|
|
469
470
|
logger,
|
|
470
471
|
});
|
|
472
|
+
// CELLO-M8-TRUST-001: receive sealed trust signals pushed from the directory pickup queue on
|
|
473
|
+
// THIS agent's stream. Open with k_local, verify the recomputed hash against the directory
|
|
474
|
+
// anchor, store locally, then ACK (so the directory deletes the ciphertext). The daemon is the
|
|
475
|
+
// ONLY party that can open the seal (SI-001); a hash mismatch is rejected without storing/ACKing.
|
|
476
|
+
mgr.registerInboundHandler((frame) => {
|
|
477
|
+
if (frame["type"] !== "trust_signal_pickup")
|
|
478
|
+
return;
|
|
479
|
+
void handleTrustSignalPickup(frame, agentKeyProvider, mgr, agentName);
|
|
480
|
+
});
|
|
471
481
|
// CELLO-M7-CONN-001 (DOD-CONN-2): inbound session_assignment + seal_interrupted_request
|
|
472
482
|
// on THIS agent's own stream, so a non-primary agent receives inbound sessions (SPINE-5).
|
|
473
483
|
wirePerAgentSessionInbound(mgr);
|
|
@@ -902,9 +912,18 @@ export async function startDaemon(config) {
|
|
|
902
912
|
// array from SQLite. Shared by both getStatus() (daemon-wide) and the
|
|
903
913
|
// cello_status MCP handler (per-connection) so live MCP clients see the same
|
|
904
914
|
// interrupted sessions a CLI `cello status` would.
|
|
915
|
+
// `cello status` is a health snapshot, not a session archive. It surfaces ONLY genuinely
|
|
916
|
+
// RESUMABLE sessions (interrupted with messages exchanged) — never failed inits (interrupted,
|
|
917
|
+
// 0 messages — a dead handshake), which classify as "failed" and would otherwise accumulate
|
|
918
|
+
// unbounded and confuse. The list is also capped; the full, queryable history is `cello sessions`
|
|
919
|
+
// / cello_list_sessions (with filter + limit flags).
|
|
920
|
+
const STATUS_RESUMABLE_CAP = 10;
|
|
905
921
|
function buildInterruptedSessions() {
|
|
906
|
-
|
|
907
|
-
|
|
922
|
+
return sessionNodeManager
|
|
923
|
+
.getSessionsByStatus("interrupted")
|
|
924
|
+
.filter((row) => (row.message_count ?? 0) > 0) // resumable only — drop failed 0-message inits
|
|
925
|
+
.slice(0, STATUS_RESUMABLE_CAP)
|
|
926
|
+
.map((row) => ({
|
|
908
927
|
sessionId: row.session_id,
|
|
909
928
|
agentName: row.agent_name,
|
|
910
929
|
counterpartyPubkey: row.counterparty_pubkey,
|
|
@@ -2114,27 +2133,56 @@ export async function startDaemon(config) {
|
|
|
2114
2133
|
// and the guidance strings that point here ("See cello_list_sessions") dead-end.
|
|
2115
2134
|
// Read from the persisted SQLite store, so it works after a daemon restart and
|
|
2116
2135
|
// from a fresh MCP connection (no in-memory session-node required).
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2136
|
+
// Classify → filter → cap a set of session rows into a SessionListResponse. Shared by the
|
|
2137
|
+
// per-agent MCP surface (cello_list_sessions) and the daemon-wide CLI surface (list_sessions),
|
|
2138
|
+
// so the operator-facing categories (open/closed/failed) and the default cap never drift.
|
|
2139
|
+
// Default filter = "open" (live/resumable only — the common case); default limit bounds the
|
|
2140
|
+
// response so a long-lived agent's history can't balloon it. `all` includes failed/closed.
|
|
2141
|
+
const DEFAULT_LIST_LIMIT = 50;
|
|
2142
|
+
const MAX_LIST_LIMIT = 500;
|
|
2143
|
+
function selectSessions(rows, params) {
|
|
2144
|
+
const rawFilter = typeof params?.filter === "string" ? params.filter : "open";
|
|
2145
|
+
const filter = rawFilter === "closed" || rawFilter === "failed" || rawFilter === "all" ? rawFilter : "open";
|
|
2146
|
+
let limit = Number(params?.limit);
|
|
2147
|
+
if (!Number.isFinite(limit) || limit < 1)
|
|
2148
|
+
limit = DEFAULT_LIST_LIMIT;
|
|
2149
|
+
limit = Math.min(Math.floor(limit), MAX_LIST_LIMIT);
|
|
2150
|
+
const classified = rows.map((row) => {
|
|
2151
|
+
const messageCount = row.message_count ?? 0;
|
|
2152
|
+
const category = classifySession(row.status, messageCount);
|
|
2153
|
+
return { row, messageCount, category };
|
|
2154
|
+
});
|
|
2155
|
+
const matched = filter === "all" ? classified : classified.filter((c) => c.category === filter);
|
|
2156
|
+
const sessions = matched.slice(0, limit).map(({ row, messageCount, category }) => ({
|
|
2126
2157
|
sessionId: row.session_id,
|
|
2127
2158
|
agentName: row.agent_name,
|
|
2128
2159
|
counterpartyPubkey: row.counterparty_pubkey,
|
|
2129
2160
|
status: row.status,
|
|
2130
|
-
|
|
2161
|
+
category,
|
|
2162
|
+
messageCount,
|
|
2131
2163
|
createdAt: new Date(row.created_at).toISOString(),
|
|
2132
2164
|
updatedAt: new Date(row.updated_at).toISOString(),
|
|
2133
2165
|
// interrupted_at is the canonical ISO interruption stamp; null for any
|
|
2134
2166
|
// session that was never interrupted (active/sealed).
|
|
2135
2167
|
interruptedAt: row.interrupted_at ?? null,
|
|
2136
2168
|
}));
|
|
2137
|
-
return { ok: true, sessions };
|
|
2169
|
+
return { ok: true, filter, limit, totalMatched: matched.length, sessions };
|
|
2170
|
+
}
|
|
2171
|
+
// cello_list_sessions (MCP, per current agent): the discovery surface for the by-id reads
|
|
2172
|
+
// (cello_get_transcript / cello_get_sealed_receipt). Accepts { filter?: open|closed|failed|all,
|
|
2173
|
+
// limit?: number } — defaults to open + DEFAULT_LIST_LIMIT so failed/dead handshakes don't drown
|
|
2174
|
+
// the live ones. Read from persisted SQLite, so it survives a daemon restart / fresh connection.
|
|
2175
|
+
handlers.set("cello_list_sessions", async (params, connectionId) => {
|
|
2176
|
+
const connState = perConnectionState.get(connectionId);
|
|
2177
|
+
if (!connState || !connState.currentAgent) {
|
|
2178
|
+
return NO_CURRENT_AGENT_RESPONSE;
|
|
2179
|
+
}
|
|
2180
|
+
return selectSessions(sessionNodeManager.getSessionsForAgent(connState.currentAgent), params);
|
|
2181
|
+
});
|
|
2182
|
+
// list_sessions (daemon-wide, for the `cello sessions` CLI which has no current agent): same
|
|
2183
|
+
// filter/limit semantics, across ALL agents.
|
|
2184
|
+
handlers.set("list_sessions", async (params) => {
|
|
2185
|
+
return selectSessions(sessionNodeManager.getAllSessions(), params);
|
|
2138
2186
|
});
|
|
2139
2187
|
// DAEMON-003 IPC handlers: queue_failed_send and check_nonce (AC-010)
|
|
2140
2188
|
handlers.set("queue_failed_send", async (params, _connectionId) => {
|
|
@@ -2852,6 +2900,68 @@ export async function startDaemon(config) {
|
|
|
2852
2900
|
// the keystone) received them. A frame arrives on exactly one agent's stream, so there is no
|
|
2853
2901
|
// double-dispatch; each handler resolves the local agent internally and that resolution
|
|
2854
2902
|
// matches the stream the directory routed the frame to.
|
|
2903
|
+
// CELLO-M8-TRUST-001: open + verify + store + ACK a sealed trust signal pushed from the directory
|
|
2904
|
+
// pickup queue. The daemon is the ONLY party that can open the seal (k_local — SI-001); it verifies
|
|
2905
|
+
// the recomputed hash against the directory's anchor before storing, and ACKs only on success so the
|
|
2906
|
+
// directory deletes the ciphertext (AC-001/AC-002). open-fail / hash-mismatch / store-fail → NO ACK
|
|
2907
|
+
// (the directory keeps the pickup for a later retry; the signal is re-mintable).
|
|
2908
|
+
async function handleTrustSignalPickup(frame, keyProvider, mgr, agentName) {
|
|
2909
|
+
const id = typeof frame["id"] === "string" ? frame["id"] : null;
|
|
2910
|
+
const signalKind = typeof frame["signal_kind"] === "string" ? frame["signal_kind"] : null;
|
|
2911
|
+
const signalHash = typeof frame["signal_hash"] === "string" ? frame["signal_hash"] : null;
|
|
2912
|
+
const ciphertext = frame["ciphertext"];
|
|
2913
|
+
if (!id || !signalKind || !signalHash || !(ciphertext instanceof Uint8Array)) {
|
|
2914
|
+
// Neither stores nor ACKs → the directory retains the row and re-delivers. Log it: a PERMANENTLY
|
|
2915
|
+
// malformed frame would otherwise be re-delivered forever with zero daemon-side signal (fallback-finder).
|
|
2916
|
+
logger.warn("daemon.trust_signal.malformed", {
|
|
2917
|
+
agentName,
|
|
2918
|
+
hasId: !!id,
|
|
2919
|
+
hasSignalKind: !!signalKind,
|
|
2920
|
+
hasSignalHash: !!signalHash,
|
|
2921
|
+
ciphertextOk: ciphertext instanceof Uint8Array,
|
|
2922
|
+
});
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
if (!keyProvider.openContentSeal) {
|
|
2926
|
+
// A session-node stub key cannot open content seals. No ACK → the directory re-delivers; log so a
|
|
2927
|
+
// pickup persistently routed to a stub-key agent is visible rather than a silent forever-retry.
|
|
2928
|
+
logger.warn("daemon.trust_signal.no_content_key", { agentName, signalKind, correlationId: id });
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
// The pickup id correlates the directory's deliver/ack with the daemon's receive (TRUST-001 obs).
|
|
2932
|
+
const correlationId = id;
|
|
2933
|
+
let recovered;
|
|
2934
|
+
try {
|
|
2935
|
+
recovered = await keyProvider.openContentSeal(ciphertext);
|
|
2936
|
+
}
|
|
2937
|
+
catch {
|
|
2938
|
+
recovered = null;
|
|
2939
|
+
}
|
|
2940
|
+
if (!recovered) {
|
|
2941
|
+
logger.warn("daemon.trust_signal.open_failed", { agentName, signalKind, correlationId });
|
|
2942
|
+
return;
|
|
2943
|
+
}
|
|
2944
|
+
const recomputed = Buffer.from(cryptoHash(recovered)).toString("hex");
|
|
2945
|
+
if (recomputed !== signalHash) {
|
|
2946
|
+
logger.error("daemon.trust_signal.hash_mismatch", { agentName, signalKind, correlationId });
|
|
2947
|
+
return;
|
|
2948
|
+
}
|
|
2949
|
+
try {
|
|
2950
|
+
const store = new DbIdentityStore(sessionNodeManager.getDb(), logger);
|
|
2951
|
+
store.storeTrustSignal({ signalHash, agentId: null, signalKind, payload: recovered });
|
|
2952
|
+
}
|
|
2953
|
+
catch (err) {
|
|
2954
|
+
logger.error("daemon.trust_signal.store_failed", {
|
|
2955
|
+
agentName,
|
|
2956
|
+
signalKind,
|
|
2957
|
+
correlationId,
|
|
2958
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2959
|
+
});
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
logger.info("daemon.trust_signal.received", { agentName, signalKind, verified: true, correlationId });
|
|
2963
|
+
await mgr.sendRaw({ type: "trust_signal_ack", id });
|
|
2964
|
+
}
|
|
2855
2965
|
function wirePerAgentSessionInbound(mgr) {
|
|
2856
2966
|
mgr.registerInboundHandler((frame) => {
|
|
2857
2967
|
if (frame["type"] !== "seal_interrupted_request")
|