@cello-protocol/daemon 0.0.48 → 0.0.50

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.
@@ -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,EAQrB,MAAM,YAAY,CAAC;AAKpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAU/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAsB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAQ/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA0InF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,IAAI,oBAAoB,CAAC;IAClC;;;;OAIG;IACH,qBAAqB,IAAI,kBAAkB,CAAC;IAC5C;;;;;;;;OAQG;IACH,gBAAgB,IAAI,SAAS,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;CACtC;AA2CD,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAE1D,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA4gM7E"}
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;AAKpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAU/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAuB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAQ/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA0InF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,IAAI,oBAAoB,CAAC;IAClC;;;;OAIG;IACH,qBAAqB,IAAI,kBAAkB,CAAC;IAC5C;;;;;;;;OAQG;IACH,gBAAgB,IAAI,SAAS,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;CACtC;AA2CD,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAE1D,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA6kM7E"}
package/dist/daemon.js CHANGED
@@ -48,12 +48,13 @@ import { RegistrationManager } from "./registration-manager.js";
48
48
  import { DaemonRegistrationContext } from "./registration-context.js";
49
49
  import { DbRegistrationPersistence, DbIdentityStore } from "./db-identity-store.js";
50
50
  import { DbManifestVersionStore } from "./manifest-version-store-db.js";
51
- import { verify as ed25519Verify, sealToRecipient, generateKLocalSeed, InMemoryKeyProvider, hash as cryptoHash } from "@cello-protocol/crypto";
51
+ import { verify as ed25519Verify, generateKLocalSeed, InMemoryKeyProvider, hash as cryptoHash } from "@cello-protocol/crypto";
52
52
  import { attemptSealUpgrade as attemptSealUpgradeImpl, verifyUpgradeConfirmedCert } from "./seal-upgrade.js";
53
53
  import { upgradeAbsentToRecovered, hasAbsentParticipant } from "./seal-receipt-upgrade.js";
54
54
  // CELLO-M7-MSG-001 (AC-013/AC-018): the single application content-size cap, enforced
55
55
  // at the send point here (the receive point lives in the transport content decode).
56
56
  import { MAX_CONTENT_BYTES, computeGenesisPrevRoot, buildAgentRevocationTbs, MONIKER_RE, validateMoniker } from "@cello-protocol/protocol-types";
57
+ import { sealParkEnvelope } from "./park-envelope.js";
57
58
  import { resolveCelloEnv, createTransportSelector, isProductionVariant, } from "./transport-composition.js";
58
59
  import { selectAdvertisedAddress } from "./transport-selector.js";
59
60
  import { parseSessionAssignment, sessionRequestErrorReason, parseDiscoveryLookupResult, discoveryLookupErrorReason, extractOfferedMoniker, resolveOutboundMoniker } from "./session-assignment-parser.js";
@@ -1508,7 +1509,7 @@ export async function startDaemon(config) {
1508
1509
  // the session's relay endpoint; we seal the content to the recipient (E2E — the relay never sees
1509
1510
  // plaintext, INV-3) and deposit it to that relay's store-and-forward mailbox via the standing
1510
1511
  // receiver node. The recipient pulls + recovers it at the witnessed sequence (R1) on next online.
1511
- sessionNodeManager.setContentParkHook(async ({ sessionId, recipientPubkeyHex, relayPeerId, relayAddrs, contentHashHex, content, structure1Cbor, structure2Cbor }) => {
1512
+ sessionNodeManager.setContentParkHook(async ({ agentName, sessionId, recipientPubkeyHex, relayPeerId, relayAddrs, contentHashHex, content, structure1Cbor, structure2Cbor }) => {
1512
1513
  const node = sessionNodeManager.getStandingReceiverNode();
1513
1514
  if (!node) {
1514
1515
  const reason = "standing_receiver_unavailable";
@@ -1519,10 +1520,32 @@ export async function startDaemon(config) {
1519
1520
  // parked when nothing was ever deposited.
1520
1521
  return { ok: false, reason };
1521
1522
  }
1523
+ // SEC-1: sign the entry as the SENDING agent. Without a key we cannot produce an envelope the
1524
+ // recipient will accept, so fail LOUD rather than depositing something that will be refused on
1525
+ // recovery (a deposit the recipient must reject is worse than no deposit — it looks delivered).
1526
+ const senderKp = keyProviders.get(agentName);
1527
+ if (!senderKp) {
1528
+ const reason = "signing_key_unavailable";
1529
+ logger.warn("content.park.deposit.failed", { agentName, sessionId, contentHash: contentHashHex, reason });
1530
+ return { ok: false, reason };
1531
+ }
1522
1532
  const recipientPubkey = Buffer.from(recipientPubkeyHex, "hex");
1533
+ const contentHashBytes = Buffer.from(contentHashHex, "hex");
1534
+ logger.info("content.park.signed", { agentName, sessionId, contentHash: contentHashHex });
1523
1535
  // DOD-MSG-4 (2b): seal the ORDERING ENVELOPE (content + the relay's signed Structure2), not bare
1524
1536
  // content, so the parked entry is self-ordering on recover. The relay still holds only ciphertext.
1525
- const ciphertext = sealToRecipient(recipientPubkey, sessionNodeManager.encodeParkEnvelope(content, structure1Cbor, structure2Cbor));
1537
+ // SEC-1: sealParkEnvelope is the SOLE producer — it signs (sender's K_local, over the
1538
+ // session/recipient/content binding) and seals in one place, so the two park sites cannot drift
1539
+ // apart on what gets signed.
1540
+ const ciphertext = await sealParkEnvelope({
1541
+ signer: senderKp,
1542
+ sessionIdHex: sessionId,
1543
+ recipientPubkey,
1544
+ contentHash: contentHashBytes,
1545
+ content,
1546
+ structure1Cbor,
1547
+ structure2Cbor,
1548
+ });
1526
1549
  const client = new ContentParkClient({ relayPeerId, relayAddrs: [...relayAddrs], logger });
1527
1550
  const res = await client.deposit(node, {
1528
1551
  recipientPubkey,
@@ -1573,11 +1596,29 @@ export async function startDaemon(config) {
1573
1596
  const node = sessionNodeManager.getStandingReceiverNode(record.agent_name);
1574
1597
  if (!node)
1575
1598
  return { parked: false, error: "standing_receiver_unavailable" };
1599
+ // SEC-1: the crash backstop must sign too — an unsigned re-park would be REFUSED on recovery,
1600
+ // which would turn the message-loss backstop into a message-loss cause. This is why the SEC-1
1601
+ // signature binds to the sender's own K_local and NOT to the relay's ordering record: the
1602
+ // ordering record is not persisted in retry_queue (see below) and so cannot be reproduced here,
1603
+ // but the KEY can — the owning agent still holds it after a crash. Hence: NO schema migration.
1604
+ const senderKp = keyProviders.get(ownerName);
1605
+ if (!senderKp)
1606
+ return { parked: false, error: "signing_key_unavailable" };
1576
1607
  const recipientPubkey = Buffer.from(record.counterparty_pubkey, "hex");
1608
+ const contentHashBytes = Buffer.from(entry.contentHashHex, "hex");
1609
+ logger.info("content.park.signed", { agentName: ownerName, sessionId: entry.sessionId, contentHash: entry.contentHashHex, source: "startup_flush" });
1577
1610
  // DOD-MSG-4 (2b): seal the envelope shape too (content only — the durable awaiting queue does not
1578
1611
  // persist the ordering record, so a crash-backstop re-park recovers in arrival order; the common
1579
1612
  // live-park path above carries the full Structure2). Keeps ONE envelope format on the recover side.
1580
- const ciphertext = sealToRecipient(recipientPubkey, sessionNodeManager.encodeParkEnvelope(entry.contentBlob));
1613
+ // SEC-1: same sole producer as the live hook — the backstop signs from the persisted
1614
+ // (sessionId, recipient, contentHash), which is why it survives a crash with NO schema migration.
1615
+ const ciphertext = await sealParkEnvelope({
1616
+ signer: senderKp,
1617
+ sessionIdHex: entry.sessionId,
1618
+ recipientPubkey,
1619
+ contentHash: contentHashBytes,
1620
+ content: entry.contentBlob,
1621
+ });
1581
1622
  const client = new ContentParkClient({ relayPeerId: ep.relayPeerId, relayAddrs: [...ep.relayAddrs], logger });
1582
1623
  const res = await client.deposit(node, {
1583
1624
  recipientPubkey,
@@ -2904,9 +2945,13 @@ export async function startDaemon(config) {
2904
2945
  // NOTE: cello_await_session is NOT in this stub list — Seam 2 registers a real
2905
2946
  // handler for it below (inbound session establishment), with its own inline
2906
2947
  // no_current_agent guard.
2907
- // F1-a2: cello_receive_session is NO LONGER stubbed here it is registered below as a true
2908
- // alias of the real (blocking) cello_receive handler. Kept as an (empty) extension point for
2909
- // future tools that need the plain no_current_agent guard without their own handler.
2948
+ // DOD-ONBOARD-HELP-1: cello_receive_session is DELETED (Andre, 2026-07-11). It was a literal
2949
+ // alias the same handler object as cello_receive that claimed an "accept/join" step CELLO
2950
+ // does not have (inbound sessions are auto-accepted by the standing receiver). No alias, no
2951
+ // deprecated shim, no dead handler.
2952
+ //
2953
+ // The list below stays as an (empty) extension point: a future tool that needs only the plain
2954
+ // no_current_agent guard, with no handler of its own, is registered here.
2910
2955
  const SESSION_TOOLS_REQUIRING_AGENT = [];
2911
2956
  const NO_CURRENT_AGENT_RESPONSE = {
2912
2957
  ok: false,
@@ -3702,22 +3747,28 @@ export async function startDaemon(config) {
3702
3747
  const client = new ContentParkClient({ relayPeerId, relayAddrs, logger });
3703
3748
  const entries = await client.pull(node, Buffer.from(recipientPubkey, "hex"), kp);
3704
3749
  let recovered = 0;
3750
+ // SEC-1 / review M2: a REFUSED entry must not vanish behind `ok:true`. Report it — an operator
3751
+ // seeing {ok:true, pulled:3, recovered:0} would otherwise have three messages evaporate with the
3752
+ // only explanation buried in the daemon log. This is both the injection signal and, given the
3753
+ // deliberate no-tolerant-window rollout, the lagging-peer signal.
3754
+ const refusals = [];
3755
+ // AC7: one correlationId per recover flow, threaded through every event it emits.
3756
+ const correlationId = randomUUID();
3705
3757
  for (const e of entries) {
3706
3758
  const unsealed = await kp.openContentSeal(e.ciphertext);
3707
3759
  if (!unsealed) {
3708
3760
  logger.warn("content.recover.unseal_failed", { sessionId: e.sessionIdHex, contentHash: e.contentHashHex });
3709
3761
  continue;
3710
3762
  }
3711
- // DOD-MSG-4 (2b): the unsealed blob is the ordering envelope (content + the relay's signed
3712
- // Structure2). Extract the content; if the record is present, verify it and feed the strict-in-
3713
- // order gate the canonical sequence BEFORE ingest so recovered messages order the same way a
3714
- // direct frame does (closes review finding #3). Old/bare-content seals decode to content alone.
3715
- const env = sessionNodeManager.decodeParkEnvelope(unsealed);
3763
+ // SEC-1: authenticate THEN ingest fused in recoverParkedEntry, which is now the ONLY way
3764
+ // parked content can enter the transcript. It verifies the sender's signature over
3765
+ // (session_id, recipient_pubkey, content_hash) and that the signer is this session's
3766
+ // counterparty, and it still verifies + records the DOD-MSG-4 ordering record when present.
3767
+ // Relay deposit is unauthenticated and the seal is anonymous, so WITHOUT this gate anyone
3768
+ // holding the recipient's public key — the relay above all — could inject content that gets
3769
+ // attributed to the honest counterparty and then notarized by the bilateral seal.
3716
3770
  const contentHashBytes = Buffer.from(e.contentHashHex, "hex");
3717
- if (env.structure1Cbor && env.structure2Cbor) {
3718
- sessionNodeManager.recordOrderingRecord(recipientAgent.name, e.sessionIdHex, env.structure1Cbor, env.structure2Cbor, contentHashBytes);
3719
- }
3720
- const ingest = await sessionNodeManager.ingestReceivedContent(recipientAgent.name, e.sessionIdHex, env.content, contentHashBytes);
3771
+ const ingest = await sessionNodeManager.recoverParkedEntry(recipientAgent.name, e.sessionIdHex, Buffer.from(recipientPubkey, "hex"), unsealed, contentHashBytes, correlationId);
3721
3772
  if (ingest.ok && ingest.held) {
3722
3773
  // DOD-MSG-4 (review finding #4): a held entry is NOT yet an appended leaf — its sequence is
3723
3774
  // the FUTURE canonical index, not a completed recovery. Do not count it as recovered; log it
@@ -3754,10 +3805,14 @@ export async function startDaemon(config) {
3754
3805
  }
3755
3806
  }
3756
3807
  else {
3757
- logger.warn("content.recover.ingest_failed", { sessionId: e.sessionIdHex, contentHash: e.contentHashHex, reason: ingest.reason });
3808
+ // SEC-1 (M2): carry the refusal out to the caller, not just to the log. Deliberately still
3809
+ // NOT confirm-deleted — a forgery must not be able to evict itself from the mailbox, and a
3810
+ // genuine v1-peer message must not be destroyed by our refusing it.
3811
+ refusals.push({ contentHash: e.contentHashHex, sessionId: e.sessionIdHex, reason: ingest.reason });
3812
+ logger.warn("content.recover.ingest_failed", { sessionId: e.sessionIdHex, contentHash: e.contentHashHex, reason: ingest.reason, correlationId });
3758
3813
  }
3759
3814
  }
3760
- return { ok: true, recovered, pulled: entries.length };
3815
+ return { ok: true, recovered, pulled: entries.length, refused: refusals.length, refusals };
3761
3816
  }
3762
3817
  // DOD-MSG-4 (auto-recover-on-reconnect): when an agent comes online, drain its parked mailbox from
3763
3818
  // every relay it has sessions on — symmetric to the SENDER's flushAwaitingContent. Without this,
@@ -5292,11 +5347,14 @@ export async function startDaemon(config) {
5292
5347
  });
5293
5348
  // ─── CELLO-M7-DAEMON-004 / F1-a: cello_receive (BLOCKING, session-scoped) ────────
5294
5349
  // F1-a fix: the daemon port had dropped the blocking receive (the handler was a
5295
- // non-blocking buf.shift and cello_receive_session was a not_implemented stub). It now
5296
- // BLOCKS up to timeout_ms, polling the received-content buffer resolved by the next
5297
- // arrival, a terminal seal answer (F1-b), or timeout. This is the "blocking receive
5298
- // variant" the guidance names. Registered under both cello_receive and cello_receive_session
5299
- // (F1-a2: one implementation, both names the redundant tools are collapsed).
5350
+ // non-blocking buf.shift). It now BLOCKS up to timeout_ms, polling the received-content
5351
+ // buffer resolved by the next arrival, a terminal seal answer (F1-b), or timeout. This is
5352
+ // the "blocking receive variant" the guidance names.
5353
+ //
5354
+ // ONE name (DOD-ONBOARD-HELP-1, Andre 2026-07-11). This was briefly registered under
5355
+ // `cello_receive_session` as well — one implementation behind two names. That alias is DELETED:
5356
+ // it accepted or joined nothing (inbound sessions are auto-accepted by the standing receiver),
5357
+ // so it was a second name for a step that does not exist, and its help said otherwise.
5300
5358
  const RECEIVE_DEFAULT_TIMEOUT_MS = 30000; // matches the cello-mcp shim's documented default
5301
5359
  const handleReceive = async (params, connectionId) => {
5302
5360
  const connState = perConnectionState.get(connectionId);
@@ -5432,7 +5490,6 @@ export async function startDaemon(config) {
5432
5490
  }
5433
5491
  };
5434
5492
  handlers.set("cello_receive", handleReceive);
5435
- handlers.set("cello_receive_session", handleReceive);
5436
5493
  // ─── M8C-INBOX-1 (N1/N4): cello_check_notifications — push-loss reconciler + poll-only inbox ───
5437
5494
  // Notifications are fire-and-forget (no ack, no redelivery); this is how a client discovers what
5438
5495
  // it missed while its shim was down/busy, and the primary inbox for poll-only clients (Bedrock,