@cello-protocol/client 0.0.5 → 0.0.6

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.
@@ -261,10 +261,26 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
261
261
  import type { CelloClient } from "./types.js";
262
262
  import type { CelloNode } from "@cello-protocol/transport";
263
263
  import type { KeyProvider } from "@cello-protocol/crypto";
264
- import type { CheckpointStatusProvider } from "@cello-protocol/interfaces";
264
+ import type { CheckpointStatusProvider, Logger } from "@cello-protocol/interfaces";
265
265
  import type { ClientBackup } from "./client-backup.js";
266
+ /**
267
+ * AC-005 (DX-001): Default demo agent ID — the CELLO demo agent that strangers can connect to.
268
+ * Overridable via CELLO_DEMO_AGENT_ID env var without a code change.
269
+ * Must NOT be hardcoded as a string literal in tool implementations.
270
+ */
271
+ export declare const DEFAULT_DEMO_AGENT_ID = "a2c55e2721f45cfa86cb3417a76e3f7b";
266
272
  export declare function createMcpSessionServer(node: CelloNode, client: CelloClient, keyProvider: KeyProvider, opts?: {
267
273
  checkpointStatusProvider?: CheckpointStatusProvider;
268
274
  clientBackup?: ClientBackup;
275
+ /** AC-007 (DX-001): Directory HTTP URL for /agent-lookup endpoint. */
276
+ directoryUrl?: string;
277
+ /**
278
+ * AC-009 (DX-001): Promise that resolves when background init (DB open, directory dial,
279
+ * loadPersistedState) has completed. Tools that require the directory/FROST await this
280
+ * with a 10s timeout before proceeding.
281
+ */
282
+ readyPromise?: Promise<void>;
283
+ /** Structured logger for observability events (e.g. client.directory.agent_lookup.failed). */
284
+ logger?: Logger;
269
285
  }): McpServer;
270
286
  //# sourceMappingURL=mcp-server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkQG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,OAAO,KAAK,EAAE,WAAW,EAA0B,MAAM,YAAY,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAI1D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAuBvD,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,WAAW,EAAE,WAAW,EACxB,IAAI,CAAC,EAAE;IAAE,wBAAwB,CAAC,EAAE,wBAAwB,CAAC;IAAC,YAAY,CAAC,EAAE,YAAY,CAAA;CAAE,GAC1F,SAAS,CA+jCX"}
1
+ {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkQG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,OAAO,KAAK,EAAE,WAAW,EAA0B,MAAM,YAAY,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAI1D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AACnF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AA0BvD;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,qCAAqC,CAAC;AAexE,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,WAAW,EAAE,WAAW,EACxB,IAAI,CAAC,EAAE;IACL,wBAAwB,CAAC,EAAE,wBAAwB,CAAC;IACpD,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,8FAA8F;IAC9F,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GACA,SAAS,CAgyCX"}
@@ -267,6 +267,26 @@ function jsonText(value) {
267
267
  return { content: [{ type: "text", text: JSON.stringify(value) }] };
268
268
  }
269
269
  const TRANSPORT_NOT_STARTED = jsonText({ error: { reason: "transport_not_started" } });
270
+ /**
271
+ * AC-004 (DX-001): Structured not_registered error returned when any tool that requires
272
+ * registration is called before the agent has completed registration.
273
+ * SI-001: must not include the pre-auth token in any error message.
274
+ */
275
+ const NOT_REGISTERED_ERROR = jsonText({
276
+ error: {
277
+ reason: "not_registered",
278
+ message: "This agent is not yet registered with the CELLO directory. " +
279
+ "To register: (1) Get a token from @CelloConnectStagingBot on Telegram, " +
280
+ "(2) call cello_register({ token: 'CELLO-...' }). " +
281
+ "Call cello_setup_guidance() for the full 6-step setup guide.",
282
+ },
283
+ });
284
+ /**
285
+ * AC-005 (DX-001): Default demo agent ID — the CELLO demo agent that strangers can connect to.
286
+ * Overridable via CELLO_DEMO_AGENT_ID env var without a code change.
287
+ * Must NOT be hardcoded as a string literal in tool implementations.
288
+ */
289
+ export const DEFAULT_DEMO_AGENT_ID = "a2c55e2721f45cfa86cb3417a76e3f7b";
270
290
  function toHex(bytes) {
271
291
  return Buffer.from(bytes).toString("hex");
272
292
  }
@@ -280,6 +300,22 @@ function sleep(ms) {
280
300
  export function createMcpSessionServer(node, client, keyProvider, opts) {
281
301
  const checkpointStatusProvider = opts?.checkpointStatusProvider;
282
302
  const clientBackup = opts?.clientBackup;
303
+ const directoryHttpUrl = opts?.directoryUrl;
304
+ const logger = opts?.logger;
305
+ const readyPromise = opts?.readyPromise;
306
+ /**
307
+ * AC-009 (DX-001): Await background init with a 10s timeout.
308
+ * Tools that require the directory/FROST (cello_initiate_session, cello_request_connection)
309
+ * call this before proceeding. cello_status and cello_setup_guidance do NOT await this.
310
+ */
311
+ async function awaitReady() {
312
+ if (!readyPromise)
313
+ return;
314
+ await Promise.race([
315
+ readyPromise,
316
+ new Promise((_, reject) => setTimeout(() => reject(new Error("background_init_timeout")), 10_000)),
317
+ ]);
318
+ }
283
319
  const startedAt = Date.now();
284
320
  // FIFO queue of inbound session assignment events.
285
321
  // Populated by client.onSessionAssignment callback.
@@ -287,6 +323,16 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
287
323
  function transportStarted() {
288
324
  return node.listenAddresses().length > 0;
289
325
  }
326
+ /**
327
+ * AC-004 (DX-001): Returns true if the agent is registered (registration state exists).
328
+ * Used by all tools that require registration to return NOT_REGISTERED_ERROR if not registered.
329
+ */
330
+ function isRegistered() {
331
+ const regState = typeof client.getRegistrationState === "function"
332
+ ? client.getRegistrationState()
333
+ : null;
334
+ return regState !== null;
335
+ }
290
336
  function directoryReachable() {
291
337
  // Check whether the libp2p node currently has an open connection to the directory peer.
292
338
  // This is accurate: true means the signaling stream is (or was recently) live;
@@ -320,18 +366,58 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
320
366
  // signaling stream, sends session_request, and awaits session_assignment from
321
367
  // the directory. The M1 polling stub has been replaced.
322
368
  server.registerTool("cello_initiate_session", {
323
- description: "Initiate a session with a target agent by their K_local public key. Requires an existing connection (M3+).",
369
+ description: "Initiate a session with a target agent. Accepts either target_pubkey (64 hex chars) or target_agent_id (32 hex chars). Requires an existing connection (M3+).",
324
370
  inputSchema: {
325
- target_pubkey: z.string().describe("Target agent K_local pubkey as lowercase hex (64 chars)"),
371
+ target_pubkey: z.string().optional().describe("Target agent K_local pubkey as lowercase hex (64 chars)"),
372
+ target_agent_id: z.string().optional().describe("Target agent's agent_id as lowercase hex (32 chars). Resolved to pubkey via directory /agent-lookup."),
326
373
  timeout_ms: z.number().int().min(0).optional().describe("Optional timeout in milliseconds"),
327
374
  },
328
- }, async ({ target_pubkey, timeout_ms }) => {
375
+ }, async ({ target_pubkey, target_agent_id, timeout_ms }) => {
376
+ if (!isRegistered())
377
+ return NOT_REGISTERED_ERROR;
378
+ // AC-009 (DX-001): await background init (directory connection, loadPersistedState)
379
+ try {
380
+ await awaitReady();
381
+ }
382
+ catch { /* proceed — may return directory_unreachable if needed */ }
329
383
  if (!transportStarted())
330
384
  return TRANSPORT_NOT_STARTED;
331
- if (!client.hasConnection(target_pubkey)) {
385
+ // AC-007b (DX-001): resolve target_agent_id to target_pubkey via /agent-lookup
386
+ let resolvedTargetPubkey = target_pubkey;
387
+ if (!resolvedTargetPubkey && target_agent_id) {
388
+ if (!/^[0-9a-f]{32}$/i.test(target_agent_id)) {
389
+ return jsonText({ ok: false, reason: "invalid_target_agent_id", agent_id: target_agent_id });
390
+ }
391
+ if (!directoryHttpUrl) {
392
+ return jsonText({ ok: false, reason: "directory_not_configured", agent_id: target_agent_id });
393
+ }
394
+ const t0Lookup = Date.now();
395
+ try {
396
+ const resp = await fetch(`${directoryHttpUrl}/agent-lookup?agent_id=${target_agent_id}`, { signal: AbortSignal.timeout(10_000) });
397
+ if (!resp.ok) {
398
+ logger?.warn("client.directory.agent_lookup.failed", { agentId: target_agent_id, endpoint: `${directoryHttpUrl}/agent-lookup`, httpStatus: resp.status, reason: "http_error", durationMs: Date.now() - t0Lookup });
399
+ return jsonText({ ok: false, reason: "agent_not_found", agent_id: target_agent_id });
400
+ }
401
+ const body = await resp.json();
402
+ if (!body.k_local_pubkey) {
403
+ logger?.warn("client.directory.agent_lookup.failed", { agentId: target_agent_id, endpoint: `${directoryHttpUrl}/agent-lookup`, httpStatus: resp.status, reason: "missing_pubkey_in_response", durationMs: Date.now() - t0Lookup });
404
+ return jsonText({ ok: false, reason: "agent_not_found", agent_id: target_agent_id });
405
+ }
406
+ resolvedTargetPubkey = body.k_local_pubkey;
407
+ }
408
+ catch (e) {
409
+ const reason = e instanceof Error ? e.message : String(e);
410
+ logger?.warn("client.directory.agent_lookup.failed", { agentId: target_agent_id, endpoint: `${directoryHttpUrl}/agent-lookup`, httpStatus: 0, reason, durationMs: Date.now() - t0Lookup });
411
+ return jsonText({ ok: false, reason: "agent_not_found", agent_id: target_agent_id });
412
+ }
413
+ }
414
+ if (!resolvedTargetPubkey) {
415
+ return jsonText({ ok: false, reason: "missing_target", message: "Provide either target_pubkey (64 hex) or target_agent_id (32 hex)." });
416
+ }
417
+ if (!client.hasConnection(resolvedTargetPubkey)) {
332
418
  return jsonText({ ok: false, reason: "no_connection" });
333
419
  }
334
- const result = await client.initiateSession(target_pubkey, { timeoutMs: timeout_ms });
420
+ const result = await client.initiateSession(resolvedTargetPubkey, { timeoutMs: timeout_ms });
335
421
  if (result.ok) {
336
422
  return jsonText({
337
423
  ok: true,
@@ -351,6 +437,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
351
437
  timeout_ms: z.number().int().min(0).describe("Maximum wait time in milliseconds"),
352
438
  },
353
439
  }, async ({ timeout_ms }) => {
440
+ if (!isRegistered())
441
+ return NOT_REGISTERED_ERROR;
354
442
  if (!transportStarted())
355
443
  return TRANSPORT_NOT_STARTED;
356
444
  const deadline = Date.now() + timeout_ms;
@@ -382,6 +470,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
382
470
  content: z.string().describe("UTF-8 message content"),
383
471
  },
384
472
  }, async ({ session_id, content }) => {
473
+ if (!isRegistered())
474
+ return NOT_REGISTERED_ERROR;
385
475
  if (!transportStarted())
386
476
  return TRANSPORT_NOT_STARTED;
387
477
  const contentBytes = new TextEncoder().encode(content);
@@ -419,6 +509,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
419
509
  timeout_ms: z.number().int().min(0).describe("Maximum wait time in milliseconds"),
420
510
  },
421
511
  }, async ({ session_id, timeout_ms }) => {
512
+ if (!isRegistered())
513
+ return NOT_REGISTERED_ERROR;
422
514
  if (!transportStarted())
423
515
  return TRANSPORT_NOT_STARTED;
424
516
  const msg = await client.receiveSessionMessageAsync(session_id, timeout_ms);
@@ -468,6 +560,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
468
560
  timeout_ms: z.number().int().min(0).describe("Maximum wait time in milliseconds"),
469
561
  },
470
562
  }, async ({ timeout_ms }) => {
563
+ if (!isRegistered())
564
+ return NOT_REGISTERED_ERROR;
471
565
  if (!transportStarted())
472
566
  return TRANSPORT_NOT_STARTED;
473
567
  const result = await client.receiveMessageAsync(timeout_ms);
@@ -518,6 +612,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
518
612
  session_id: z.string().describe("Session ID as lowercase hex"),
519
613
  },
520
614
  }, async ({ session_id }) => {
615
+ if (!isRegistered())
616
+ return NOT_REGISTERED_ERROR;
521
617
  if (!transportStarted())
522
618
  return TRANSPORT_NOT_STARTED;
523
619
  const result = await client.initiateSessionSeal(session_id);
@@ -603,6 +699,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
603
699
  description: "List all known sessions and their current status.",
604
700
  inputSchema: {},
605
701
  }, async () => {
702
+ if (!isRegistered())
703
+ return NOT_REGISTERED_ERROR;
606
704
  const records = client.listSessions();
607
705
  const sessions = records.map((s) => ({
608
706
  session_id: toHex(s.session_id),
@@ -674,6 +772,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
674
772
  session_id: z.string().describe("Session ID as lowercase hex"),
675
773
  },
676
774
  }, async ({ session_id }) => {
775
+ if (!isRegistered())
776
+ return NOT_REGISTERED_ERROR;
677
777
  const record = client.listSessions().find((s) => toHex(s.session_id) === session_id);
678
778
  if (!record) {
679
779
  return jsonText({ error: { reason: "session_not_found", session_id } });
@@ -748,6 +848,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
748
848
  leaf_index: z.number().int().min(0).describe("Zero-based index of the target leaf"),
749
849
  },
750
850
  }, async ({ session_id, leaf_index }) => {
851
+ if (!isRegistered())
852
+ return NOT_REGISTERED_ERROR;
751
853
  // PERSIST-017: if CheckpointStatusProvider is wired in, check MMR checkpoint status first.
752
854
  // Returns the pending/confirmed/error response based on staging table state.
753
855
  if (checkpointStatusProvider) {
@@ -822,18 +924,97 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
822
924
  sealed_root: localRootHex,
823
925
  });
824
926
  });
927
+ // ── cello_setup_guidance ──────────────────────────────────────────────────
928
+ //
929
+ // AC-005 (DX-001): Returns the full 6-step CELLO setup guide (always shown,
930
+ // never collapsed) followed by a 'Your current status' section.
931
+ // Available to unregistered agents — does NOT require registration.
932
+ server.registerTool("cello_setup_guidance", {
933
+ description: "Get the full CELLO setup guide and your current status. Always shows all 6 steps. Call this first to understand how to use CELLO.",
934
+ inputSchema: {},
935
+ }, async () => {
936
+ // AC-005 (DX-001): Demo agent ID from env var or constant — NOT hardcoded inline.
937
+ const demoAgentId = process.env["CELLO_DEMO_AGENT_ID"] ?? DEFAULT_DEMO_AGENT_ID;
938
+ // Gather current status
939
+ const regStateRaw = typeof client.getRegistrationState === "function"
940
+ ? client.getRegistrationState()
941
+ : null;
942
+ const registered = regStateRaw !== null;
943
+ const agentIdStr = registered ? regStateRaw.agent_id : null;
944
+ const connections = typeof client.listConnections === "function" ? client.listConnections() : [];
945
+ const sessions = client.listSessions().filter((s) => s.status === "active");
946
+ const dirReachable = directoryReachable();
947
+ // Determine current step for the pointer
948
+ let currentStep = 1;
949
+ let nextAction = "Call cello_register({ token: '<your token>' }) to register.";
950
+ if (registered && connections.length === 0) {
951
+ currentStep = 2;
952
+ nextAction = `Call cello_request_connection({ target_agent_id: '${demoAgentId}' }) to connect to the demo agent.`;
953
+ }
954
+ else if (registered && connections.length > 0 && sessions.length === 0) {
955
+ currentStep = 3;
956
+ const firstConn = connections[0];
957
+ nextAction = `Call cello_initiate_session({ target_agent_id: '${demoAgentId}' }) (or target_pubkey: '${firstConn.counterparty_pubkey}') to start a session.`;
958
+ }
959
+ else if (registered && sessions.length > 0) {
960
+ currentStep = 4;
961
+ const firstSession = sessions[0];
962
+ nextAction = `Call cello_send({ session_id: '${toHex(firstSession.session_id)}', content: 'Hello!' }) to send a message.`;
963
+ }
964
+ const guide = [
965
+ "# CELLO Setup Guide",
966
+ "",
967
+ "## Step 1: Get your registration token",
968
+ "Message @CelloConnectStagingBot on Telegram. Say 'register me' and it will send you a one-time token.",
969
+ "",
970
+ "## Step 2: Register with the directory",
971
+ "Call: cello_register({ token: 'CELLO-<your token>' })",
972
+ "This runs a cryptographic key ceremony (FROST DKG) with the directory. Takes 5-10 seconds.",
973
+ "",
974
+ "## Step 3: Connect to another agent",
975
+ `Call: cello_request_connection({ target_agent_id: '${demoAgentId}' })`,
976
+ `Or connect to any other agent by their agent_id (32 hex chars) or pubkey (64 hex chars).`,
977
+ "The demo agent is always online and will accept your connection automatically.",
978
+ "",
979
+ "## Step 4: Start a session",
980
+ `Call: cello_initiate_session({ target_agent_id: '${demoAgentId}' })`,
981
+ "A session is an encrypted, tamper-evident message channel.",
982
+ "",
983
+ "## Step 5: Send and receive messages",
984
+ "Call: cello_send({ session_id: '<session_id>', content: 'Hello!' })",
985
+ "Call: cello_receive({ timeout_ms: 10000 }) to receive replies",
986
+ "",
987
+ "## Step 6: Close the session and get your permanent record",
988
+ "Call: cello_close_session({ session_id: '<session_id>' })",
989
+ "Call: cello_get_sealed_receipt({ session_id: '<session_id>' }) for a cryptographic proof",
990
+ "",
991
+ "---",
992
+ "## Your current status",
993
+ `Registered: ${registered ? `YES (agent_id: ${agentIdStr})` : "NO"}`,
994
+ `Connections: ${connections.length}`,
995
+ `Active sessions: ${sessions.length}`,
996
+ `Directory reachable: ${dirReachable ? "YES" : "NO"}`,
997
+ "",
998
+ `→ You are at Step ${currentStep}. ${nextAction}`,
999
+ ].join("\n");
1000
+ return { content: [{ type: "text", text: guide }] };
1001
+ });
825
1002
  // ── cello_register ────────────────────────────────────────────────────────
826
1003
  //
827
1004
  // REG-001: DKG registration. Idempotent — second call returns already_registered.
828
1005
  // SI-001: never emits ML-DSA secret key material.
829
1006
  server.registerTool("cello_register", {
830
- description: "Register this agent with the CELLO directory. Runs the REG-001 DKG ceremony. Idempotent — returns already_registered if already done.",
1007
+ description: "Register this agent with the CELLO directory. Runs the REG-001 DKG ceremony. Idempotent — returns already_registered if already done. Obtain your token from @CelloConnectStagingBot on Telegram.",
831
1008
  inputSchema: {
832
- phone_stub: z.string().describe("Phone stub for identity binding (format: +E.164 or hex stub)"),
833
- pre_auth_token: z.string().optional().describe("OPS-AGENT-001: Pre-authorization token issued by POST /internal/pre-authorize. Required in M6+. Use any 'DEV-' prefixed token in CELLO_ENV=local."),
1009
+ // AC-006 (DX-001): phone_stub removed from user-facing schema; token renamed from pre_auth_token.
1010
+ // The client sends phone_stub='' internally (directory accepts empty when token is present).
1011
+ token: z.string().describe("Pre-authorization token from @CelloConnectStagingBot on Telegram. Format: CELLO-<33 base58 chars> (production) or DEV-<...> (local). SI-001: this token must not appear in any log or response."),
834
1012
  },
835
- }, async ({ phone_stub, pre_auth_token }) => {
836
- const result = await client.register(phone_stub, pre_auth_token);
1013
+ }, async ({ token }) => {
1014
+ // AC-006 (DX-001): phone_stub is not a user-facing parameter.
1015
+ // Send '' as the wire value — the directory accepts empty phone_stub when pre_auth_token is valid.
1016
+ // SI-001: the token must NOT appear in any log event, error message, or MCP tool response.
1017
+ const result = await client.register("", token);
837
1018
  if ("error" in result) {
838
1019
  return jsonText({ error: { reason: result.error } });
839
1020
  }
@@ -848,21 +1029,64 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
848
1029
  // ── cello_request_connection ───────────────────────────────────────────────
849
1030
  //
850
1031
  // CONNREQ-002: Send Round 1 connection_request to target B.
851
- // Blocks up to 5 min (directory default). Returns accepted/rejected/etc.
1032
+ // AC-004 (DX-001): requires registration.
1033
+ // AC-007 (DX-001): accepts target_agent_id (32 hex) in addition to target_pubkey (64 hex).
1034
+ // AC-008 (DX-001): per-stage timeouts (10s dial, 10s send, 90s wait).
852
1035
  server.registerTool("cello_request_connection", {
853
- description: "Send a connection request to a target agent. Blocks until the target responds (up to 5 minutes).",
1036
+ description: "Send a connection request to a target agent. Accepts either target_pubkey (64 hex chars) or target_agent_id (32 hex chars — looks up pubkey from directory). Blocks until the target responds or timeouts per stage.",
854
1037
  inputSchema: {
855
- target_pubkey: z.string().describe("Target agent's own_pubkey (Ed25519 identity key) as lowercase hex (64 chars). This is the value from cello_status().own_pubkey on the target agent — NOT their primary_pubkey."),
1038
+ target_pubkey: z.string().optional().describe("Target agent's K_local pubkey as lowercase hex (64 chars). Use this if you know the raw pubkey."),
1039
+ target_agent_id: z.string().optional().describe("Target agent's agent_id as lowercase hex (32 chars). The directory resolves this to a pubkey automatically."),
856
1040
  include_endorsements: z.boolean().optional().describe("Include endorsements in the connection package"),
857
1041
  include_attestations: z.boolean().optional().describe("Include attestations in the connection package"),
858
1042
  },
859
- }, async ({ target_pubkey }) => {
1043
+ }, async ({ target_pubkey, target_agent_id }) => {
1044
+ if (!isRegistered())
1045
+ return NOT_REGISTERED_ERROR;
1046
+ // AC-009 (DX-001): await background init before attempting connection
1047
+ try {
1048
+ await awaitReady();
1049
+ }
1050
+ catch { /* proceed — per-stage timeouts handle directory_unreachable */ }
860
1051
  const extendedClient = client;
1052
+ // AC-007 (DX-001): resolve target_agent_id to target_pubkey via /agent-lookup
1053
+ // This runs BEFORE the cello_request_connection function check so that agent_id
1054
+ // and missing_target errors are returned with clear messages in all cases.
1055
+ let resolvedTargetPubkey = target_pubkey;
1056
+ if (!resolvedTargetPubkey && target_agent_id) {
1057
+ if (!/^[0-9a-f]{32}$/i.test(target_agent_id)) {
1058
+ return jsonText({ error: { reason: "invalid_target_agent_id", message: "target_agent_id must be exactly 32 lowercase hex chars" } });
1059
+ }
1060
+ if (!directoryHttpUrl) {
1061
+ return jsonText({ error: { reason: "directory_not_configured", message: "Cannot resolve agent_id: directory URL is not configured." } });
1062
+ }
1063
+ const t0Lookup = Date.now();
1064
+ try {
1065
+ const resp = await fetch(`${directoryHttpUrl}/agent-lookup?agent_id=${target_agent_id}`, { signal: AbortSignal.timeout(10_000) });
1066
+ if (!resp.ok) {
1067
+ logger?.warn("client.directory.agent_lookup.failed", { agentId: target_agent_id, endpoint: `${directoryHttpUrl}/agent-lookup`, httpStatus: resp.status, reason: "http_error", durationMs: Date.now() - t0Lookup });
1068
+ return jsonText({ error: { reason: "agent_not_found", agent_id: target_agent_id } });
1069
+ }
1070
+ const body = await resp.json();
1071
+ if (!body.k_local_pubkey) {
1072
+ logger?.warn("client.directory.agent_lookup.failed", { agentId: target_agent_id, endpoint: `${directoryHttpUrl}/agent-lookup`, httpStatus: resp.status, reason: "missing_pubkey_in_response", durationMs: Date.now() - t0Lookup });
1073
+ return jsonText({ error: { reason: "agent_not_found", agent_id: target_agent_id } });
1074
+ }
1075
+ resolvedTargetPubkey = body.k_local_pubkey;
1076
+ }
1077
+ catch (e) {
1078
+ const reason = e instanceof Error ? e.message : String(e);
1079
+ logger?.warn("client.directory.agent_lookup.failed", { agentId: target_agent_id, endpoint: `${directoryHttpUrl}/agent-lookup`, httpStatus: 0, reason, durationMs: Date.now() - t0Lookup });
1080
+ return jsonText({ error: { reason: "agent_not_found", agent_id: target_agent_id } });
1081
+ }
1082
+ }
1083
+ if (!resolvedTargetPubkey) {
1084
+ return jsonText({ error: { reason: "missing_target", message: "Provide either target_pubkey (64 hex) or target_agent_id (32 hex)." } });
1085
+ }
861
1086
  if (typeof extendedClient.cello_request_connection !== "function") {
862
- return jsonText({ error: { reason: "not_registered" } });
1087
+ return jsonText({ error: { reason: "not_implemented" } });
863
1088
  }
864
1089
  // Build a real ConnectionPackage (pseudonym binding) from registration state.
865
- // Falls back to an empty package if the agent is not yet registered.
866
1090
  let packageCbor = new Uint8Array(0);
867
1091
  const regState = typeof extendedClient.getRegistrationState === "function"
868
1092
  ? extendedClient.getRegistrationState()
@@ -890,8 +1114,12 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
890
1114
  }
891
1115
  }
892
1116
  const result = await extendedClient.cello_request_connection({
893
- target_pubkey,
1117
+ target_pubkey: resolvedTargetPubkey,
894
1118
  package_cbor: packageCbor,
1119
+ // AC-008 (DX-001): per-stage timeouts
1120
+ dialTimeoutMs: 10_000,
1121
+ sendTimeoutMs: 10_000,
1122
+ waitTimeoutMs: 90_000,
895
1123
  });
896
1124
  if (result.result === "established") {
897
1125
  return jsonText({ result: "accepted", connection_id: result.connection_id });
@@ -910,7 +1138,16 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
910
1138
  });
911
1139
  }
912
1140
  if (result.result === "timeout") {
913
- return jsonText({ result: "timeout" });
1141
+ // AC-008: stage-specific error messages
1142
+ const stage = result.stage;
1143
+ if (stage === "dial") {
1144
+ return jsonText({ error: { reason: "directory_unreachable_timeout", message: "Could not reach directory. Check your network connection." } });
1145
+ }
1146
+ if (stage === "send") {
1147
+ return jsonText({ error: { reason: "request_delivery_timeout", message: "Connection request not delivered to directory." } });
1148
+ }
1149
+ // Default: target response timeout
1150
+ return jsonText({ error: { reason: "target_response_timeout", message: "Target agent did not respond within 90s. The agent may be offline. Try again with cello_request_connection." } });
914
1151
  }
915
1152
  return jsonText({ error: { reason: result.reason } });
916
1153
  });
@@ -925,6 +1162,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
925
1162
  include_attestations: z.boolean().optional().describe("Include attestations in the updated package"),
926
1163
  },
927
1164
  }, async ({ connection_request_id }) => {
1165
+ if (!isRegistered())
1166
+ return NOT_REGISTERED_ERROR;
928
1167
  const extendedClient = client;
929
1168
  if (typeof extendedClient.cello_respond_to_disclosure_request !== "function") {
930
1169
  return jsonText({ error: { reason: "no_pending_disclosure_request" } });
@@ -982,6 +1221,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
982
1221
  timeout_ms: z.number().int().min(0).optional().describe("Maximum wait time in ms. Default: 30000"),
983
1222
  },
984
1223
  }, async ({ timeout_ms }) => {
1224
+ if (!isRegistered())
1225
+ return NOT_REGISTERED_ERROR;
985
1226
  const result = await client.awaitConnectionRequest(timeout_ms ?? 30_000);
986
1227
  if (result.type === "timeout") {
987
1228
  return jsonText({ type: "timeout" });
@@ -1003,6 +1244,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
1003
1244
  connection_request_id: z.string().describe("Connection request ID from cello_await_connection_request"),
1004
1245
  },
1005
1246
  }, async ({ connection_request_id }) => {
1247
+ if (!isRegistered())
1248
+ return NOT_REGISTERED_ERROR;
1006
1249
  const result = await client.acceptConnection(connection_request_id);
1007
1250
  if ("error" in result) {
1008
1251
  return jsonText({ error: result.error });
@@ -1019,6 +1262,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
1019
1262
  reason: z.string().optional().describe("Optional rejection reason"),
1020
1263
  },
1021
1264
  }, async ({ connection_request_id, reason }) => {
1265
+ if (!isRegistered())
1266
+ return NOT_REGISTERED_ERROR;
1022
1267
  const result = await client.rejectConnection(connection_request_id, reason);
1023
1268
  if ("error" in result) {
1024
1269
  return jsonText({ error: result.error });
@@ -1036,6 +1281,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
1036
1281
  requested_items: z.array(z.record(z.string(), z.unknown())).describe("List of disclosure items to request"),
1037
1282
  },
1038
1283
  }, async ({ connection_request_id, requested_items }) => {
1284
+ if (!isRegistered())
1285
+ return NOT_REGISTERED_ERROR;
1039
1286
  const result = await client.requestMoreDisclosure(connection_request_id, requested_items);
1040
1287
  if ("error" in result) {
1041
1288
  return jsonText({ error: result.error });
@@ -1049,6 +1296,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
1049
1296
  description: "List all active connections for this agent.",
1050
1297
  inputSchema: {},
1051
1298
  }, async () => {
1299
+ if (!isRegistered())
1300
+ return NOT_REGISTERED_ERROR;
1052
1301
  const records = client.listConnections();
1053
1302
  const connections = records.map((c) => ({
1054
1303
  connection_id: c.connection_id,
@@ -1073,6 +1322,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
1073
1322
  })).optional().describe("Signal requirements (optional)"),
1074
1323
  },
1075
1324
  }, async ({ mode, review_mode, requirements }) => {
1325
+ if (!isRegistered())
1326
+ return NOT_REGISTERED_ERROR;
1076
1327
  const policy = {
1077
1328
  mode,
1078
1329
  review_mode,
@@ -1096,6 +1347,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
1096
1347
  description: "Return the current connection policy configuration.",
1097
1348
  inputSchema: {},
1098
1349
  }, async () => {
1350
+ if (!isRegistered())
1351
+ return NOT_REGISTERED_ERROR;
1099
1352
  const getFn = client.getPolicy;
1100
1353
  const policy = typeof getFn === "function"
1101
1354
  ? getFn.call(client)
@@ -1116,6 +1369,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
1116
1369
  "Returns ok:true on success. Returns ok:false with reason if backup fails or is not configured.",
1117
1370
  inputSchema: {},
1118
1371
  }, async () => {
1372
+ if (!isRegistered())
1373
+ return NOT_REGISTERED_ERROR;
1119
1374
  if (!clientBackup) {
1120
1375
  return jsonText({ ok: false, reason: "not_configured" });
1121
1376
  }
@@ -1138,6 +1393,8 @@ export function createMcpSessionServer(node, client, keyProvider, opts) {
1138
1393
  "Returns ok:true on success. Returns ok:false with reason if restore fails or is not configured.",
1139
1394
  inputSchema: {},
1140
1395
  }, async () => {
1396
+ if (!isRegistered())
1397
+ return NOT_REGISTERED_ERROR;
1141
1398
  if (!clientBackup) {
1142
1399
  return jsonText({ ok: false, reason: "not_configured" });
1143
1400
  }