aismemory 0.7.1 → 0.7.3

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.
Files changed (113) hide show
  1. package/README.md +121 -2
  2. package/dist/__tests__/agent-key-mode-e2e.test.js +78 -0
  3. package/dist/__tests__/agent-key-mode-e2e.test.js.map +1 -1
  4. package/dist/__tests__/agent-key-mode.test.js +123 -0
  5. package/dist/__tests__/agent-key-mode.test.js.map +1 -1
  6. package/dist/__tests__/agent-load-persistence.test.js +25 -2
  7. package/dist/__tests__/agent-load-persistence.test.js.map +1 -1
  8. package/dist/__tests__/agent-preference.test.js +8 -0
  9. package/dist/__tests__/agent-preference.test.js.map +1 -1
  10. package/dist/__tests__/agentmemory.test.d.ts +1 -0
  11. package/dist/__tests__/agentmemory.test.js +73 -0
  12. package/dist/__tests__/agentmemory.test.js.map +1 -0
  13. package/dist/__tests__/ais-http.test.d.ts +1 -0
  14. package/dist/__tests__/ais-http.test.js +139 -0
  15. package/dist/__tests__/ais-http.test.js.map +1 -0
  16. package/dist/__tests__/auto-enroll-key.test.js +28 -4
  17. package/dist/__tests__/auto-enroll-key.test.js.map +1 -1
  18. package/dist/__tests__/beacon-contract.test.d.ts +1 -0
  19. package/dist/__tests__/beacon-contract.test.js +106 -0
  20. package/dist/__tests__/beacon-contract.test.js.map +1 -0
  21. package/dist/__tests__/credential-isolation.test.d.ts +1 -0
  22. package/dist/__tests__/credential-isolation.test.js +61 -0
  23. package/dist/__tests__/credential-isolation.test.js.map +1 -0
  24. package/dist/__tests__/credential-resolution.test.d.ts +1 -0
  25. package/dist/__tests__/credential-resolution.test.js +141 -0
  26. package/dist/__tests__/credential-resolution.test.js.map +1 -0
  27. package/dist/__tests__/delegated-read-args.test.d.ts +9 -0
  28. package/dist/__tests__/delegated-read-args.test.js +80 -0
  29. package/dist/__tests__/delegated-read-args.test.js.map +1 -0
  30. package/dist/__tests__/doctor.test.js +250 -9
  31. package/dist/__tests__/doctor.test.js.map +1 -1
  32. package/dist/__tests__/ender-receipt-auth.test.d.ts +1 -0
  33. package/dist/__tests__/ender-receipt-auth.test.js +26 -0
  34. package/dist/__tests__/ender-receipt-auth.test.js.map +1 -0
  35. package/dist/__tests__/fixtures/beacon-contract.d.ts +66 -0
  36. package/dist/__tests__/fixtures/beacon-contract.js +72 -0
  37. package/dist/__tests__/fixtures/beacon-contract.js.map +1 -0
  38. package/dist/__tests__/inbox-client.test.d.ts +1 -0
  39. package/dist/__tests__/inbox-client.test.js +108 -0
  40. package/dist/__tests__/inbox-client.test.js.map +1 -0
  41. package/dist/__tests__/key-auth.test.js +7 -1
  42. package/dist/__tests__/key-auth.test.js.map +1 -1
  43. package/dist/__tests__/list-all-agents.test.js +2 -0
  44. package/dist/__tests__/list-all-agents.test.js.map +1 -1
  45. package/dist/__tests__/lost-agent.test.d.ts +1 -0
  46. package/dist/__tests__/lost-agent.test.js +108 -0
  47. package/dist/__tests__/lost-agent.test.js.map +1 -0
  48. package/dist/__tests__/memory-write-failure.test.js +5 -1
  49. package/dist/__tests__/memory-write-failure.test.js.map +1 -1
  50. package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.d.ts +1 -0
  51. package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.js +207 -0
  52. package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.js.map +1 -0
  53. package/dist/__tests__/provisional-register-mcp-remember.test.d.ts +1 -0
  54. package/dist/__tests__/provisional-register-mcp-remember.test.js +196 -0
  55. package/dist/__tests__/provisional-register-mcp-remember.test.js.map +1 -0
  56. package/dist/__tests__/readme-tools-parity.test.d.ts +1 -0
  57. package/dist/__tests__/readme-tools-parity.test.js +64 -0
  58. package/dist/__tests__/readme-tools-parity.test.js.map +1 -0
  59. package/dist/__tests__/reconnect-survival-e2e.test.js +63 -1
  60. package/dist/__tests__/reconnect-survival-e2e.test.js.map +1 -1
  61. package/dist/__tests__/token-manager.test.js +20 -1
  62. package/dist/__tests__/token-manager.test.js.map +1 -1
  63. package/dist/__tests__/tool-args.test.js +196 -1
  64. package/dist/__tests__/tool-args.test.js.map +1 -1
  65. package/dist/__tests__/workspace-key-mode.test.d.ts +1 -0
  66. package/dist/__tests__/workspace-key-mode.test.js +264 -0
  67. package/dist/__tests__/workspace-key-mode.test.js.map +1 -0
  68. package/dist/agent-key-mode.d.ts +4 -2
  69. package/dist/agent-key-mode.js +106 -11
  70. package/dist/agent-key-mode.js.map +1 -1
  71. package/dist/ais-http.d.ts +7 -0
  72. package/dist/ais-http.js +137 -0
  73. package/dist/ais-http.js.map +1 -0
  74. package/dist/auth-staleness.d.ts +2 -0
  75. package/dist/auth-staleness.js +5 -0
  76. package/dist/auth-staleness.js.map +1 -1
  77. package/dist/auto-handoff.d.ts +1 -1
  78. package/dist/auto-handoff.js +5 -3
  79. package/dist/auto-handoff.js.map +1 -1
  80. package/dist/cli/doctor.d.ts +4 -0
  81. package/dist/cli/doctor.js +120 -8
  82. package/dist/cli/doctor.js.map +1 -1
  83. package/dist/cli/enable-key-auth.js +4 -5
  84. package/dist/cli/enable-key-auth.js.map +1 -1
  85. package/dist/cli/orientation.d.ts +15 -0
  86. package/dist/cli/orientation.js +89 -0
  87. package/dist/cli/orientation.js.map +1 -0
  88. package/dist/harness-identity.d.ts +17 -0
  89. package/dist/harness-identity.js +105 -0
  90. package/dist/harness-identity.js.map +1 -0
  91. package/dist/hydration.d.ts +1 -1
  92. package/dist/hydration.js +3 -2
  93. package/dist/hydration.js.map +1 -1
  94. package/dist/inbox-client.d.ts +9 -0
  95. package/dist/inbox-client.js +74 -0
  96. package/dist/inbox-client.js.map +1 -0
  97. package/dist/index.js +498 -109
  98. package/dist/index.js.map +1 -1
  99. package/dist/key-auth.js +16 -10
  100. package/dist/key-auth.js.map +1 -1
  101. package/dist/oauth-credentials.d.ts +11 -0
  102. package/dist/oauth-credentials.js +31 -3
  103. package/dist/oauth-credentials.js.map +1 -1
  104. package/dist/token-manager.d.ts +6 -0
  105. package/dist/token-manager.js +16 -0
  106. package/dist/token-manager.js.map +1 -1
  107. package/dist/tool-args.d.ts +98 -0
  108. package/dist/tool-args.js +155 -0
  109. package/dist/tool-args.js.map +1 -1
  110. package/dist/workspace-key-mode.d.ts +40 -0
  111. package/dist/workspace-key-mode.js +258 -0
  112. package/dist/workspace-key-mode.js.map +1 -0
  113. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9,27 +9,33 @@
9
9
  */
10
10
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
11
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
- import { readFileSync } from 'node:fs';
12
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
13
14
  import { tryKeyAuth, enrollKeyIfNeeded, hasEnrolledKey } from './key-auth.js';
14
- import { isCredsStale, isAuthErrorResult } from './auth-staleness.js';
15
+ import { isCredsStale, isAuthErrorResult, isNoRegisteredKeysAuthError } from './auth-staleness.js';
15
16
  import { AisHttpError } from './ais-http-error.js';
17
+ import { aisGet, aisPost, aisPatch } from './ais-http.js';
16
18
  import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
17
- import { homedir } from 'os';
18
- import { loadOAuthCredentials, saveOAuthCredentials } from './oauth-credentials.js';
19
+ import { homedir } from 'node:os';
20
+ import { loadOAuthCredentials, loadNonStaleDiskBearer, saveOAuthCredentials, } from './oauth-credentials.js';
19
21
  import { saveAgentPreference, loadAgentPreference, migrateCredentialsFileIfNeeded } from './agent-preference.js';
20
- import { createTokenManager } from './token-manager.js';
22
+ import { createTokenManager, isNoRegisteredKeyFailure } from './token-manager.js';
21
23
  import { hydrateIdentity } from './hydration.js';
22
24
  import { assemblePrompt } from './prompt-resource.js';
23
25
  import { startRefreshTimer, stopRefreshTimer } from './refresh.js';
24
26
  import { ActivityTracker, setupAutoHandoff } from './auto-handoff.js';
25
27
  import { assertEnvAgentInTenant, selectAgentForSession } from './env-agent.js';
26
28
  import { emitTelemetry, hashAgentId } from './telemetry.js';
27
- import { parseAgentLoadArgs, parseEmptyToolArgs, parseHandoffArgs, parseRecallArgs, parseRememberArgs, } from './tool-args.js';
29
+ import { parseAgentLoadArgs, parseEmptyToolArgs, parseHandoffArgs, parseRecallArgs, parseFullContextArgs, parseRememberArgs, parseInboxSendArgs, parseInboxReadArgs, parseInboxAckArgs, parseGoalCreateArgs, parseGoalListArgs, parseGoalUpdateProgressArgs, parseGoalCompleteArgs, INBOX_MAX_BODY_CHARS, INBOX_MAX_TASK_ID_CHARS, INBOX_MIN_TTL_SECONDS, INBOX_MAX_TTL_SECONDS, INBOX_DEFAULT_TTL_SECONDS, INBOX_MAX_PAGE_SIZE, INBOX_DEFAULT_PAGE_SIZE, INBOX_MAX_ACK_IDS, GOAL_MAX_TITLE_CHARS, GOAL_MAX_DESCRIPTION_CHARS, GOAL_MAX_CRITERIA_ITEMS, GOAL_DEFAULT_PAGE_SIZE, GOAL_MAX_PAGE_SIZE, } from './tool-args.js';
30
+ import { loadHarnessIdentity, installHarnessIdentity, optionValue, assertPinnedAgent, verifyPinnedIdentity, requiresIdentityGate } from './harness-identity.js';
31
+ import { readInboxAndMaybeAck } from './inbox-client.js';
28
32
  import { runPollLoop, DeviceFlowRestartedError } from './device-poll.js';
29
33
  import { loadAgentApiKeyConfig, AgentApiKeyConfigError, dispatchToolKeyMode, UNKNOWN_TOOL_KEY_MODE, } from './agent-key-mode.js';
34
+ import { loadWorkspaceKeyConfig, WorkspaceKeyConfigError, dispatchToolWorkspaceKeyMode, UNKNOWN_TOOL_WORKSPACE_KEY_MODE, } from './workspace-key-mode.js';
30
35
  // ── Config ──────────────────────────────────────────────
31
36
  const AIS_URL = process.env['AIS_URL'] ?? 'https://ais.agentsandswarms.ai';
32
37
  const PACKAGE_VERSION = readPackageVersion();
38
+ let harnessIdentity = null;
33
39
  function readPackageVersion() {
34
40
  const packageJsonUrl = new URL('../package.json', import.meta.url);
35
41
  const packageJson = JSON.parse(readFileSync(packageJsonUrl, 'utf-8'));
@@ -65,71 +71,24 @@ function loadCredentials() {
65
71
  function stderr(msg) {
66
72
  process.stderr.write(`[aismemory] ${msg}\n`);
67
73
  }
68
- async function aisPost(path, body, token) {
69
- const headers = { 'Content-Type': 'application/json' };
70
- if (token)
71
- headers['Authorization'] = `Bearer ${token}`;
72
- let res;
73
- try {
74
- res = await fetch(`${AIS_URL}${path}`, {
75
- method: 'POST',
76
- headers,
77
- body: JSON.stringify(body),
78
- });
79
- }
80
- catch (err) {
81
- emitTelemetry({
82
- event: 'ais.http.failure',
83
- path,
84
- error: err instanceof Error ? err.message : String(err),
85
- });
86
- throw err;
87
- }
88
- const data = await res.json().catch(() => null);
89
- if (!res.ok) {
90
- emitTelemetry({
91
- event: 'ais.http.failure',
92
- path,
93
- httpStatus: res.status,
94
- error: `HTTP ${res.status}`,
95
- });
96
- // Token-rejection bodies feed the silent re-auth-and-retry loop; every
97
- // other non-2xx must throw so a failed write surfaces as an MCP error
98
- // instead of being mistaken for a successful one.
99
- if (isAuthErrorResult(data))
100
- return data;
101
- throw new AisHttpError(res.status, path, data);
102
- }
103
- return data;
74
+ function isSafeClaudeSessionId(sessionId) {
75
+ return sessionId !== '.' && sessionId !== '..' && /^[A-Za-z0-9._-]+$/.test(sessionId);
104
76
  }
105
- async function aisGet(path, token) {
106
- let res;
107
- try {
108
- res = await fetch(`${AIS_URL}${path}`, {
109
- headers: { 'Authorization': `Bearer ${token}` },
110
- });
77
+ function persistClaudeSessionAgentName(sessionId, agentName) {
78
+ if (!sessionId)
79
+ return;
80
+ if (!isSafeClaudeSessionId(sessionId)) {
81
+ stderr('Skipping Claude session marker: CLAUDE_SESSION_ID contains unsafe path characters');
82
+ return;
111
83
  }
112
- catch (err) {
113
- emitTelemetry({
114
- event: 'ais.http.failure',
115
- path,
116
- error: err instanceof Error ? err.message : String(err),
117
- });
118
- throw err;
84
+ try {
85
+ const sessionsDir = join(homedir(), '.claude', 'ais-agent-sessions');
86
+ mkdirSync(sessionsDir, { recursive: true });
87
+ writeFileSync(join(sessionsDir, sessionId), agentName, 'utf-8');
119
88
  }
120
- const data = await res.json().catch(() => null);
121
- if (!res.ok) {
122
- emitTelemetry({
123
- event: 'ais.http.failure',
124
- path,
125
- httpStatus: res.status,
126
- error: `HTTP ${res.status}`,
127
- });
128
- if (isAuthErrorResult(data))
129
- return data;
130
- throw new AisHttpError(res.status, path, data);
89
+ catch (error) {
90
+ stderr(`Skipping Claude session marker: ${error instanceof Error ? error.message : String(error)}`);
131
91
  }
132
- return data;
133
92
  }
134
93
  /**
135
94
  * Fetch EVERY agent in the tenant, following server pagination.
@@ -182,13 +141,13 @@ async function resolveAgentForLoad(token, query) {
182
141
  * User can override with AIS_AGENT_ID or AIS_AGENT_NAME env vars.
183
142
  */
184
143
  async function resolveAgent(token, fallbackAgentId, fallbackTenantId, preferredAgentId) {
185
- // Explicit env var overrides everything — must belong to this tenant.
186
- const envAgentId = process.env['AIS_AGENT_ID'];
144
+ // The harness pin overrides shared preferences and must belong to this tenant.
145
+ const envAgentId = harnessIdentity?.agentId;
187
146
  if (envAgentId) {
188
147
  const agents = await listAllAgents(token);
189
148
  try {
190
149
  const match = assertEnvAgentInTenant(envAgentId, agents);
191
- stderr(`Using agent from AIS_AGENT_ID: ${match.name} (${match.id})`);
150
+ stderr(`Using pinned agent: ${match.name} (${match.id})`);
192
151
  return { agentId: match.id, tenantId: fallbackTenantId };
193
152
  }
194
153
  catch (error) {
@@ -237,6 +196,11 @@ async function resolveAgent(token, fallbackAgentId, fallbackTenantId, preferredA
237
196
  return { agentId: fallbackAgentId, tenantId: fallbackTenantId };
238
197
  }
239
198
  }
199
+ const MISSING_CREDENTIAL_HELP = 'Missing AIS credential: no authenticated aismemory MCP session, AIS_WORKSPACE_KEY / AIS_API_KEY, or usable local OAuth cache. ' +
200
+ 'Connect aismemory and complete AIS sign-in, or ask your workspace owner for a scoped ais_wk_ key ' +
201
+ '(ais_ak_ for a single agent) and configure AIS_WORKSPACE_KEY / AIS_API_KEY with AIS_AGENT_ID. ' +
202
+ 'Local sign-in can use ~/.aismemory/credentials.json. ' +
203
+ 'Railway is an operator-only last resort for locating an existing scoped key; client surfaces never fetch AIS_SERVICE_KEY or DATABASE_URL.';
240
204
  /**
241
205
  * Bond URL/code surfaced when a tool is called before the user has approved
242
206
  * the device flow. Caller catches this and returns a friendly error to the MCP
@@ -247,7 +211,7 @@ class BondPendingError extends Error {
247
211
  userCode;
248
212
  bondPending = true;
249
213
  constructor(verificationUri, userCode) {
250
- super(`Memory bond required — ask your human to approve now.\n\n` +
214
+ super(`Memory bond required — ask your human to approve now.\n\n${MISSING_CREDENTIAL_HELP}\n\n` +
251
215
  `Visit: ${verificationUri}\n` +
252
216
  `Code: ${userCode}\n\n` +
253
217
  `Enter your email and tap Approve, then retry this tool. Takes about 30 seconds.\n` +
@@ -396,9 +360,10 @@ flowRef) {
396
360
  // reconnected silently. migrateCredentialsFileIfNeeded() (agent-preference.ts)
397
361
  // is what later removes this token from disk, once a key gets enrolled.
398
362
  if (hasEnrolledKey()) {
399
- saveAgentPreference({ agentId: creds.agentId, tenantId: creds.tenantId });
363
+ if (!harnessIdentity)
364
+ saveAgentPreference({ agentId: creds.agentId, tenantId: creds.tenantId });
400
365
  }
401
- else {
366
+ else if (!harnessIdentity) {
402
367
  saveOAuthCredentials(creds);
403
368
  }
404
369
  stderr('Authenticated successfully!');
@@ -423,13 +388,15 @@ const tools = [
423
388
  },
424
389
  {
425
390
  name: 'recall',
426
- description: 'Search memories by natural language query.',
391
+ description: "Search memories by natural language query. Set principalAgentId to search a DIFFERENT " +
392
+ "agent's memories under a 'memory:read' grant from that agent's owner.",
427
393
  inputSchema: {
428
394
  type: 'object',
429
395
  properties: {
430
396
  query: { type: 'string', description: 'Search query' },
431
397
  limit: { type: 'number', description: 'Max results (default 10)' },
432
398
  type: { type: 'string', enum: ['fact', 'event', 'lesson', 'context', 'goal', 'task'], description: 'Filter by type' },
399
+ principalAgentId: { type: 'string', description: "Optional: search ANOTHER agent's memories instead of your own (its agent UUID). Requires that agent's owner to have granted you the 'memory:read' scope and that you share a tenant; otherwise the call is refused with 403. Read-only — your own identity and write access are unchanged." },
433
400
  },
434
401
  required: ['query'],
435
402
  },
@@ -441,8 +408,15 @@ const tools = [
441
408
  },
442
409
  {
443
410
  name: 'full_context',
444
- description: 'Load the full agent context — identity, personality, goals, facts, skills, and last handoff.',
445
- inputSchema: { type: 'object', properties: {} },
411
+ description: 'Load the full agent context — identity, personality, goals, facts, skills, and last handoff. ' +
412
+ "Set principalAgentId to load ANOTHER agent's context under a 'memory:read' grant.",
413
+ inputSchema: {
414
+ type: 'object',
415
+ properties: {
416
+ principalAgentId: { type: 'string', description: "Optional: search ANOTHER agent's memories instead of your own (its agent UUID). Requires that agent's owner to have granted you the 'memory:read' scope and that you share a tenant; otherwise the call is refused with 403. Read-only — your own identity and write access are unchanged." },
417
+ memoriesLimit: { type: 'number', description: 'Number of memories to load (1-50, default 5). Lower values reduce context size.' },
418
+ },
419
+ },
446
420
  },
447
421
  {
448
422
  name: 'dream',
@@ -471,10 +445,185 @@ const tools = [
471
445
  type: 'string',
472
446
  description: 'Agent name to look up (e.g., "Corbot", "Data", "code-assistant")',
473
447
  },
448
+ memoriesLimit: { type: 'integer', minimum: 1, maximum: 50, description: 'Number of memories to load (1-50). Omit to use the AIS default.' },
474
449
  },
475
450
  required: ['query'],
476
451
  },
477
452
  },
453
+ // Agent inbox (CORBOT-7115B8AE / server-side CORBOT-6194EDC3) — mirrors
454
+ // packages/agent-identity-service/src/mcp/tools/inbox-tools.ts, which this
455
+ // package cannot import (it ships standalone to npm).
456
+ {
457
+ name: 'inbox_send',
458
+ description: 'Send a message to another agent that holds an AIS identity in your tenant — hand off a task, ask a peer a question, ' +
459
+ 'report an update, flag a decision, or acknowledge something they sent you. ' +
460
+ 'Call it when you have something a specific peer needs and you are not going to wait for the answer: delivery is pull-based, ' +
461
+ 'so the message sits in their inbox until they next call inbox_read, and nothing wakes the agent — never block your turn on a reply. ' +
462
+ "Fields: recipientAgentId is the peer's AIS agent id; kind labels the intent so the reader can triage; body is the message itself. " +
463
+ 'Set threadId (and inReplyTo) to continue an existing exchange rather than starting a new one, taskId to tie the message to a ticket, ' +
464
+ 'messageId to make a retry idempotent (a re-send of the same id delivers nothing twice and comes back with duplicate: true), ' +
465
+ 'and ttlSeconds to shorten how long the message survives unread (default 7 days).',
466
+ inputSchema: {
467
+ type: 'object',
468
+ properties: {
469
+ recipientAgentId: {
470
+ type: 'string',
471
+ description: "The peer's AIS agent id (uuid). Must be an active agent in your own tenant; anything else is reported as not found.",
472
+ },
473
+ kind: {
474
+ type: 'string',
475
+ enum: ['task', 'update', 'question', 'decision', 'handoff', 'ack'],
476
+ description: "Intent label the reader triages on: 'task' (do this), 'update' (status, no action), 'question' (needs an answer), " +
477
+ "'decision' (a fork the peer owns), 'handoff' (work is now theirs), 'ack' (received/acted).",
478
+ },
479
+ body: {
480
+ type: 'string',
481
+ description: `Message body, 1-${INBOX_MAX_BODY_CHARS} characters. Write it for a peer with no view of your session: say what it is and what you want back.`,
482
+ },
483
+ threadId: {
484
+ type: 'string',
485
+ description: 'Optional uuid of an existing exchange to continue. Omit to start a new thread — the first message becomes its own thread root.',
486
+ },
487
+ taskId: {
488
+ type: 'string',
489
+ description: `Optional ticket or task identifier this message belongs to, up to ${INBOX_MAX_TASK_ID_CHARS} characters (e.g. a kanban ticket id).`,
490
+ },
491
+ inReplyTo: {
492
+ type: 'string',
493
+ description: 'Optional uuid of the specific message being answered.',
494
+ },
495
+ messageId: {
496
+ type: 'string',
497
+ description: 'Optional client-generated uuid for idempotency. Re-sending an id already stored delivers nothing new and returns duplicate: true, ' +
498
+ 'so a retried call after a timeout cannot double-deliver.',
499
+ },
500
+ ttlSeconds: {
501
+ type: 'number',
502
+ description: `Optional lifetime in seconds, ${INBOX_MIN_TTL_SECONDS}-${INBOX_MAX_TTL_SECONDS} (default ${INBOX_DEFAULT_TTL_SECONDS}, one week). After it the message is purged whether read or not.`,
503
+ },
504
+ },
505
+ required: ['recipientAgentId', 'kind', 'body'],
506
+ },
507
+ },
508
+ {
509
+ name: 'inbox_read',
510
+ description: 'Read the messages other agents have sent you, oldest first. ' +
511
+ 'Call it at the start of every turn and after any wake-up — delivery is pull-based and nothing wakes the agent, ' +
512
+ 'so a peer\'s task or question is invisible until you read for it. ' +
513
+ 'It returns only unread messages by default; pass ack: true once you have taken them in so they stop being returned, ' +
514
+ 'and unreadOnly: false to re-read messages you already acked (they survive until their TTL). ' +
515
+ 'Use limit to size the page and pass the returned nextCursor back as cursor to continue; a null nextCursor means you have reached the end. ' +
516
+ 'unreadRemaining, when present, is how many unread messages are still waiting after this read.',
517
+ inputSchema: {
518
+ type: 'object',
519
+ properties: {
520
+ cursor: {
521
+ type: 'string',
522
+ description: 'Opaque cursor from a previous call\'s nextCursor. Omit to start from the oldest message.',
523
+ },
524
+ limit: {
525
+ type: 'number',
526
+ description: `Maximum messages to return, 1-${INBOX_MAX_PAGE_SIZE} (default ${INBOX_DEFAULT_PAGE_SIZE}).`,
527
+ },
528
+ unreadOnly: {
529
+ type: 'boolean',
530
+ description: 'Return only messages you have not acked. Defaults to true; set false to re-read the full stream including acked messages.',
531
+ },
532
+ ack: {
533
+ type: 'boolean',
534
+ description: 'Ack the messages this call returns, so they stop coming back as unread. Defaults to false; set true when you have actually read them ' +
535
+ 'and intend to act — acking is a read cursor, not a delete, so an acked message is still readable with unreadOnly: false until its TTL.',
536
+ },
537
+ },
538
+ required: [],
539
+ },
540
+ },
541
+ {
542
+ name: 'inbox_ack',
543
+ description: 'Ack specific inbox messages by id without doing a fresh inbox_read — use it when you already know the ' +
544
+ 'messageIds (e.g. from a prior inbox_read you did not ack at the time) and just need to mark them handled ' +
545
+ 'so they stop coming back as unread.',
546
+ inputSchema: {
547
+ type: 'object',
548
+ properties: {
549
+ messageIds: {
550
+ type: 'array',
551
+ items: { type: 'string' },
552
+ description: `Ids of the messages to ack, 1-${INBOX_MAX_ACK_IDS} items.`,
553
+ },
554
+ },
555
+ required: ['messageIds'],
556
+ },
557
+ },
558
+ // Goal tools (CORBOT-721e9a8b, L4 one-client-surface) — the same lifecycle
559
+ // as agent-identity-service/src/mcp/tools/goal-tools.ts, wired through this
560
+ // package's existing REST connection rather than a second server.
561
+ {
562
+ name: 'goal_create',
563
+ description: 'Create a new goal for the current agent. Goals track progress and success rates over time.',
564
+ inputSchema: {
565
+ type: 'object',
566
+ properties: {
567
+ title: { type: 'string', description: `Title of the goal, up to ${GOAL_MAX_TITLE_CHARS} characters.` },
568
+ description: { type: 'string', description: `Detailed description of what needs to be accomplished, up to ${GOAL_MAX_DESCRIPTION_CHARS} characters.` },
569
+ successCriteria: {
570
+ type: 'array',
571
+ items: { type: 'string' },
572
+ description: `List of criteria that define successful completion, up to ${GOAL_MAX_CRITERIA_ITEMS} items.`,
573
+ },
574
+ parentGoalId: { type: 'string', description: 'Id of the parent goal, if this is a sub-goal.' },
575
+ },
576
+ required: ['title', 'description'],
577
+ },
578
+ },
579
+ {
580
+ name: 'goal_list',
581
+ description: 'List goals for the current agent, with optional status and parent-goal filters.',
582
+ inputSchema: {
583
+ type: 'object',
584
+ properties: {
585
+ status: {
586
+ type: 'string',
587
+ enum: ['draft', 'active', 'completing', 'completed', 'failed', 'cancelled'],
588
+ description: 'Filter by goal status.',
589
+ },
590
+ page: { type: 'number', description: 'Page number (default 1).' },
591
+ limit: { type: 'number', description: `Maximum goals per page, 1-${GOAL_MAX_PAGE_SIZE} (default ${GOAL_DEFAULT_PAGE_SIZE}).` },
592
+ parentGoalId: { type: 'string', description: 'Filter to sub-goals of this parent goal id.' },
593
+ },
594
+ required: [],
595
+ },
596
+ },
597
+ {
598
+ name: 'goal_update_progress',
599
+ description: 'Update the progress (0-100) of a goal the current agent owns.',
600
+ inputSchema: {
601
+ type: 'object',
602
+ properties: {
603
+ goalId: { type: 'string', description: 'The id of the goal.' },
604
+ progress: { type: 'number', minimum: 0, maximum: 100, description: 'Progress percentage (0-100).' },
605
+ },
606
+ required: ['goalId', 'progress'],
607
+ },
608
+ },
609
+ {
610
+ name: 'goal_complete',
611
+ description: 'Mark a goal as complete (success or failure). Records the outcome for success-rate tracking.',
612
+ inputSchema: {
613
+ type: 'object',
614
+ properties: {
615
+ goalId: { type: 'string', description: 'The id of the goal.' },
616
+ success: { type: 'boolean', description: 'Whether the goal was successfully completed.' },
617
+ notes: { type: 'string', description: 'Completion notes.' },
618
+ lessonsLearned: {
619
+ type: 'array',
620
+ items: { type: 'string' },
621
+ description: `Lessons learned during goal execution, up to ${GOAL_MAX_CRITERIA_ITEMS} items.`,
622
+ },
623
+ },
624
+ required: ['goalId', 'success'],
625
+ },
626
+ },
478
627
  ];
479
628
  // ── MCP Server ──────────────────────────────────────────
480
629
  /**
@@ -494,6 +643,82 @@ function decodeJwtClaims(token) {
494
643
  return {};
495
644
  }
496
645
  }
646
+ /**
647
+ * MCP server entrypoint for workspace-key auth mode (CORBOT-3103ED88).
648
+ *
649
+ * Like agent-API-key mode but supports agent_load: when agent_load succeeds,
650
+ * the active agentId is updated for subsequent calls. The workspace key can
651
+ * access multiple agents within a workspace (subject to server grants).
652
+ */
653
+ async function runWorkspaceKeyModeServer(config) {
654
+ // Mutable copy so agent_load can switch the active agent.
655
+ let activeConfig = { ...config };
656
+ stderr(`Workspace key auth mode — agent starts at ${activeConfig.agentId} (agent_load can switch).`);
657
+ const server = new Server({ name: 'aismemory', version: PACKAGE_VERSION }, { capabilities: { tools: {}, prompts: {} } });
658
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
659
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [] }));
660
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
661
+ throw new Error(`Unknown prompt: ${request.params.name}`);
662
+ });
663
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
664
+ const { name, arguments: args } = request.params;
665
+ const rawArgs = args ?? {};
666
+ activityTracker.recordToolUse(name);
667
+ const toolStartedAt = Date.now();
668
+ try {
669
+ const callConfig = activeConfig;
670
+ if (requiresIdentityGate(name, rawArgs)) {
671
+ await verifyPinnedIdentity(harnessIdentity, callConfig.agentId, () => dispatchToolWorkspaceKeyMode(AIS_URL, callConfig, 'whoami', {}));
672
+ }
673
+ const result = await dispatchToolWorkspaceKeyMode(AIS_URL, callConfig, name, rawArgs);
674
+ if (result === UNKNOWN_TOOL_WORKSPACE_KEY_MODE) {
675
+ return {
676
+ content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${name}` }) }],
677
+ isError: true,
678
+ };
679
+ }
680
+ // agent_load: update the active agentId for subsequent calls.
681
+ if (name === 'agent_load' &&
682
+ typeof result === 'object' &&
683
+ result !== null &&
684
+ 'switched' in result &&
685
+ result.switched) {
686
+ const switchResult = result;
687
+ if (harnessIdentity) {
688
+ await verifyPinnedIdentity(harnessIdentity, switchResult.agentId, () => dispatchToolWorkspaceKeyMode(AIS_URL, { ...callConfig, agentId: switchResult.agentId }, 'whoami', {}));
689
+ }
690
+ activeConfig = { ...activeConfig, agentId: switchResult.agentId };
691
+ stderr(`Switched active agent to ${switchResult.agentId}`);
692
+ }
693
+ emitTelemetry({
694
+ event: 'tool.call',
695
+ tool: name,
696
+ agentIdHash: hashAgentId(activeConfig.agentId),
697
+ latencyMs: Date.now() - toolStartedAt,
698
+ status: 'ok',
699
+ });
700
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
701
+ }
702
+ catch (error) {
703
+ const message = error instanceof Error ? error.message : 'Unknown error';
704
+ emitTelemetry({
705
+ event: 'tool.call',
706
+ tool: name,
707
+ agentIdHash: hashAgentId(activeConfig.agentId),
708
+ latencyMs: Date.now() - toolStartedAt,
709
+ status: 'error',
710
+ error: message,
711
+ });
712
+ return {
713
+ content: [{ type: 'text', text: JSON.stringify({ error: message }) }],
714
+ isError: true,
715
+ };
716
+ }
717
+ });
718
+ const transport = new StdioServerTransport();
719
+ await server.connect(transport);
720
+ stderr(`MCP server running (workspace key mode). Tools: ${tools.map((t) => t.name).join(', ')}`);
721
+ }
497
722
  /**
498
723
  * MCP server entrypoint for agent-API-key auth mode (CORBOT-B1D3244C).
499
724
  *
@@ -519,6 +744,9 @@ async function runAgentKeyModeServer(config) {
519
744
  activityTracker.recordToolUse(name);
520
745
  const toolStartedAt = Date.now();
521
746
  try {
747
+ if (requiresIdentityGate(name, rawArgs)) {
748
+ await verifyPinnedIdentity(harnessIdentity, config.agentId, () => dispatchToolKeyMode(AIS_URL, config, 'whoami', {}));
749
+ }
522
750
  const result = await dispatchToolKeyMode(AIS_URL, config, name, rawArgs);
523
751
  if (result === UNKNOWN_TOOL_KEY_MODE) {
524
752
  return {
@@ -566,16 +794,43 @@ async function main() {
566
794
  return; // sync-memory.ts calls process.exit() on failure
567
795
  }
568
796
  if (process.argv[2] === 'doctor') {
569
- await import('./cli/doctor.js');
797
+ await (await import('./cli/doctor.js')).main(process.argv.slice(3));
570
798
  return; // doctor.ts calls process.exit() itself
571
799
  }
800
+ if (process.argv[2] === 'install') {
801
+ const source = optionValue(process.argv.slice(3), '--manifest');
802
+ if (!source)
803
+ throw new Error('Usage: aismemory install --manifest <file>');
804
+ const pin = installHarnessIdentity(source);
805
+ process.stdout.write(`Installed identity pin for ${pin.harness}: ${pin.agentId}. Launch: aismemory --harness ${pin.harness}\nVerify: aismemory doctor --harness ${pin.harness}\n`);
806
+ return;
807
+ }
808
+ harnessIdentity = loadHarnessIdentity();
809
+ const adapterEnv = { ...process.env, AIS_AGENT_ID: harnessIdentity?.agentId };
810
+ // Workspace key auth mode (CORBOT-3103ED88) — multi-agent workspace
811
+ // credentials. Checked FIRST: workspace key -> agent API key -> device flow.
812
+ let workspaceKeyConfig;
813
+ try {
814
+ workspaceKeyConfig = loadWorkspaceKeyConfig(adapterEnv);
815
+ }
816
+ catch (err) {
817
+ if (err instanceof WorkspaceKeyConfigError) {
818
+ stderr(err.message);
819
+ process.exit(1);
820
+ }
821
+ throw err;
822
+ }
823
+ if (workspaceKeyConfig) {
824
+ await runWorkspaceKeyModeServer(workspaceKeyConfig);
825
+ return;
826
+ }
572
827
  // Agent API key auth mode (CORBOT-B1D3244C) — unattended machines. Checked
573
828
  // before any device-flow / OAuth-credential / DID-key-auth setup below so
574
829
  // this mode NEVER falls back to those paths (that fallback is what used to
575
830
  // silently mint duplicate provisional agents in prod).
576
831
  let agentKeyConfig;
577
832
  try {
578
- agentKeyConfig = loadAgentApiKeyConfig();
833
+ agentKeyConfig = loadAgentApiKeyConfig(adapterEnv);
579
834
  }
580
835
  catch (err) {
581
836
  if (err instanceof AgentApiKeyConfigError) {
@@ -591,6 +846,11 @@ async function main() {
591
846
  // Lazy-auth state. We connect the MCP transport before the user has approved
592
847
  // the device flow so Claude Code's 30s connection timeout is not blocked on
593
848
  // human approval. The first tool call surfaces the bond URL via its result.
849
+ // Capture any seeded register bearer before loadCredentials() migrates v1
850
+ // files away once a local DID key exists (F5a). qa-monitor seeds token +
851
+ // agentId while an unregistered key file may also be present; did-prove then
852
+ // 401s NO_REGISTERED_KEYS but this bearer still works for REST memory.
853
+ const diskBearerFallback = loadNonStaleDiskBearer();
594
854
  let creds = loadCredentials();
595
855
  let activeFlow = null;
596
856
  let postAuthDone = false;
@@ -616,16 +876,30 @@ async function main() {
616
876
  // background, retries with backoff, and never serializes a token to disk.
617
877
  const tokenManager = createTokenManager({
618
878
  authenticate: async () => {
619
- const keyResult = await tryKeyAuth({ aisUrl: AIS_URL });
620
- if (!keyResult)
621
- throw new Error('no enrolled device key');
622
- return { token: keyResult.token, expiresAt: keyResult.expiresAt };
879
+ try {
880
+ const keyResult = await tryKeyAuth({ aisUrl: AIS_URL });
881
+ if (!keyResult)
882
+ throw new Error('no enrolled device key');
883
+ return { token: keyResult.token, expiresAt: keyResult.expiresAt };
884
+ }
885
+ catch (err) {
886
+ // Leftover local DID key + seeded register bearer (qa-monitor): did-prove
887
+ // 401s NO_REGISTERED_KEYS but the disk JWT still works for REST memory.
888
+ if (isNoRegisteredKeysAuthError(err)) {
889
+ const diskBearer = diskBearerFallback ?? loadNonStaleDiskBearer();
890
+ if (diskBearer) {
891
+ return diskBearer;
892
+ }
893
+ }
894
+ throw err;
895
+ }
623
896
  },
624
897
  resolveIdentity: async (token) => {
625
898
  const claims = decodeJwtClaims(token);
626
899
  const tenantId = claims.tenantId ?? '';
627
900
  const resolved = await resolveAgent(token, '', tenantId, loadAgentPreference()?.agentId);
628
- saveAgentPreference({ agentId: resolved.agentId, tenantId: resolved.tenantId || tenantId });
901
+ if (!harnessIdentity)
902
+ saveAgentPreference({ agentId: resolved.agentId, tenantId: resolved.tenantId || tenantId });
629
903
  return { agentId: resolved.agentId, tenantId: resolved.tenantId || tenantId };
630
904
  },
631
905
  onEvent: (event, detail) => {
@@ -655,30 +929,53 @@ async function main() {
655
929
  // propagate to the tool result verbatim — that IS the structured
656
930
  // "no action needed, self-healing in the background" message.
657
931
  if (hasEnrolledKey()) {
658
- const mc = await tokenManager.getCredentials();
659
- if (!firstKeyAcquireDone) {
660
- firstKeyAcquireDone = true;
661
- emitTelemetry({ event: 'auth.key.success', agentIdHash: hashAgentId(mc.agentId) });
662
- stderr('Authenticated via DID key auth (no browser required)');
932
+ try {
933
+ const mc = await tokenManager.getCredentials();
934
+ if (!firstKeyAcquireDone) {
935
+ firstKeyAcquireDone = true;
936
+ emitTelemetry({ event: 'auth.key.success', agentIdHash: hashAgentId(mc.agentId) });
937
+ stderr('Authenticated via DID key auth (no browser required)');
938
+ }
939
+ // Compose token/expiresAt from the manager, but let a live agent_load
940
+ // switch win over the manager's cached identity — never let a renewal
941
+ // silently revert the session back to the originally-resolved agent.
942
+ creds = sessionAgentOverride
943
+ ? { token: mc.token, expiresAt: mc.expiresAt, agentId: sessionAgentOverride.agentId, tenantId: sessionAgentOverride.tenantId }
944
+ : mc;
945
+ if (!postAuthDone) {
946
+ postAuthDone = true;
947
+ void runPostAuth(creds);
948
+ }
949
+ return creds;
663
950
  }
664
- // Compose token/expiresAt from the manager, but let a live agent_load
665
- // switch win over the manager's cached identity — never let a renewal
666
- // silently revert the session back to the originally-resolved agent.
667
- creds = sessionAgentOverride
668
- ? { token: mc.token, expiresAt: mc.expiresAt, agentId: sessionAgentOverride.agentId, tenantId: sessionAgentOverride.tenantId }
669
- : mc;
670
- if (!postAuthDone) {
671
- postAuthDone = true;
672
- void runPostAuth(creds);
951
+ catch (err) {
952
+ if (isNoRegisteredKeyFailure(err)) {
953
+ const disk = loadOAuthCredentials();
954
+ if (disk && !isCredsStale(disk.expiresAt, Date.now())) {
955
+ emitTelemetry({ event: 'auth.key.fallback', agentIdHash: hashAgentId(disk.agentId) });
956
+ stderr('Key auth reports no registered key server-side; falling back to disk OAuth bearer.');
957
+ creds = disk;
958
+ if (!postAuthDone) {
959
+ postAuthDone = true;
960
+ void runPostAuth(creds);
961
+ }
962
+ return creds;
963
+ }
964
+ }
965
+ throw err;
673
966
  }
674
- return creds;
675
967
  }
676
968
  // ── No-key path: browser device flow (unchanged bond URL semantics) ──
677
969
  if (creds && !isCredsStale(creds.expiresAt, Date.now())) {
678
970
  return creds;
679
971
  }
680
972
  if (!activeFlow || activeFlow.dead) {
681
- activeFlow = await startDeviceFlow(activeFlow?.provisional_token);
973
+ try {
974
+ activeFlow = await startDeviceFlow(activeFlow?.provisional_token);
975
+ }
976
+ catch (error) {
977
+ throw new Error(`${MISSING_CREDENTIAL_HELP} AIS sign-in could not start; check AIS_URL and connectivity, then retry.`, { cause: error });
978
+ }
682
979
  }
683
980
  const sentinel = Symbol('bond-pending');
684
981
  // poll is always set by startDeviceFlow before it returns; this guard
@@ -724,7 +1021,8 @@ async function main() {
724
1021
  });
725
1022
  }
726
1023
  /** Identity hydration, prompt refresh, and auto-handoff — all post-auth. */
727
- async function runPostAuth(c) {
1024
+ async function runPostAuth(credentials) {
1025
+ const c = { ...credentials, agentId: harnessIdentity?.agentId ?? credentials.agentId };
728
1026
  // Auto-enroll a DID key so re-auth stays silent after the 7-day bond
729
1027
  // token expires. Without this, key enrollment was a manual opt-in step
730
1028
  // (`enable-key-auth`) and every normal user was re-prompted to bond at
@@ -752,7 +1050,7 @@ async function main() {
752
1050
  setupAutoHandoff(AIS_URL, c.agentId, c.token, activityTracker, () => {
753
1051
  stopRefreshTimer();
754
1052
  stderr('Session handoff saved');
755
- });
1053
+ }, () => verifyPinnedIdentity(harnessIdentity, c.agentId, () => aisGet(`/v1/agents/${c.agentId}`, c.token)));
756
1054
  }
757
1055
  catch (e) {
758
1056
  stderr(`Post-auth setup failed: ${e instanceof Error ? e.message : String(e)}`);
@@ -782,11 +1080,17 @@ async function main() {
782
1080
  activityTracker.recordToolUse(name);
783
1081
  const toolStartedAt = Date.now();
784
1082
  try {
1083
+ if (requiresIdentityGate(name, rawArgs)) {
1084
+ assertPinnedAgent(harnessIdentity, harnessIdentity?.agentId ?? 'unselected');
1085
+ }
785
1086
  const UNKNOWN_TOOL = Symbol('unknown-tool');
786
1087
  const dispatchTool = async (c) => {
787
1088
  const token = c.token;
788
1089
  // agentId is mutable across `agent_load` calls within this session.
789
- let agentId = c.agentId;
1090
+ let agentId = harnessIdentity?.agentId ?? c.agentId;
1091
+ if (requiresIdentityGate(name, rawArgs)) {
1092
+ await verifyPinnedIdentity(harnessIdentity, agentId, () => aisGet(`/v1/agents/${agentId}`, token));
1093
+ }
790
1094
  let result;
791
1095
  switch (name) {
792
1096
  case 'remember': {
@@ -807,6 +1111,9 @@ async function main() {
807
1111
  params.set('limit', String(recallArgs.limit));
808
1112
  if (recallArgs.type !== undefined)
809
1113
  params.set('types', recallArgs.type);
1114
+ if (recallArgs.principalAgentId !== undefined) {
1115
+ params.set('principalAgentId', recallArgs.principalAgentId);
1116
+ }
810
1117
  result = await aisGet(`/v1/agents/${agentId}/memory?${params}`, token);
811
1118
  break;
812
1119
  }
@@ -814,10 +1121,20 @@ async function main() {
814
1121
  parseEmptyToolArgs(rawArgs);
815
1122
  result = await aisGet(`/v1/agents/${agentId}`, token);
816
1123
  break;
817
- case 'full_context':
818
- parseEmptyToolArgs(rawArgs);
819
- result = await aisGet(`/v1/agents/${agentId}/full-context`, token);
1124
+ case 'full_context': {
1125
+ const fcArgs = parseFullContextArgs(rawArgs);
1126
+ const fcParams = new URLSearchParams();
1127
+ if (fcArgs.principalAgentId !== undefined)
1128
+ fcParams.set('principalAgentId', fcArgs.principalAgentId);
1129
+ if (fcArgs.memoriesLimit !== undefined)
1130
+ fcParams.set('memoriesLimit', String(fcArgs.memoriesLimit));
1131
+ if (fcArgs.view !== undefined)
1132
+ fcParams.set('view', fcArgs.view);
1133
+ const fcQs = fcParams.toString();
1134
+ const fcPath = `/v1/agents/${agentId}/full-context${fcQs ? `?${fcQs}` : ''}`;
1135
+ result = await aisGet(fcPath, token);
820
1136
  break;
1137
+ }
821
1138
  case 'dream':
822
1139
  parseEmptyToolArgs(rawArgs);
823
1140
  result = await aisPost(`/v1/agents/${agentId}/dream`, {}, token);
@@ -831,8 +1148,11 @@ async function main() {
831
1148
  break;
832
1149
  }
833
1150
  case 'agent_load': {
834
- const { query } = parseAgentLoadArgs(rawArgs);
1151
+ const { query, memoriesLimit } = parseAgentLoadArgs(rawArgs);
835
1152
  const target = await resolveAgentForLoad(token, query);
1153
+ if (harnessIdentity)
1154
+ assertPinnedAgent(harnessIdentity, target.id);
1155
+ persistClaudeSessionAgentName(process.env.CLAUDE_SESSION_ID, target.name);
836
1156
  agentId = target.id;
837
1157
  // Persist the switch so subsequent tool calls use the new agent.
838
1158
  // saveAgentPreference() MUST be called here so that a process
@@ -846,9 +1166,10 @@ async function main() {
846
1166
  if (creds) {
847
1167
  creds = { ...creds, agentId };
848
1168
  sessionAgentOverride = { agentId, tenantId: creds.tenantId };
849
- saveAgentPreference({ agentId, tenantId: creds.tenantId });
1169
+ if (!harnessIdentity)
1170
+ saveAgentPreference({ agentId, tenantId: creds.tenantId });
850
1171
  }
851
- agentContext = await hydrateIdentity(AIS_URL, agentId, token);
1172
+ agentContext = await hydrateIdentity(AIS_URL, agentId, token, memoriesLimit);
852
1173
  applyMemoryRefresh(agentId, token);
853
1174
  server.notification({ method: 'notifications/prompts/list_changed' });
854
1175
  stderr(`agent_load: switched to "${target.name}" (${target.id})`);
@@ -863,6 +1184,74 @@ async function main() {
863
1184
  };
864
1185
  break;
865
1186
  }
1187
+ case 'inbox_send': {
1188
+ const inboxSendArgs = parseInboxSendArgs(rawArgs);
1189
+ result = await aisPost(`/v1/agents/${agentId}/inbox`, {
1190
+ recipientAgentId: inboxSendArgs.recipientAgentId,
1191
+ kind: inboxSendArgs.kind,
1192
+ body: inboxSendArgs.body,
1193
+ ...(inboxSendArgs.threadId !== undefined ? { threadId: inboxSendArgs.threadId } : {}),
1194
+ ...(inboxSendArgs.taskId !== undefined ? { taskId: inboxSendArgs.taskId } : {}),
1195
+ ...(inboxSendArgs.inReplyTo !== undefined ? { inReplyTo: inboxSendArgs.inReplyTo } : {}),
1196
+ ...(inboxSendArgs.messageId !== undefined ? { messageId: inboxSendArgs.messageId } : {}),
1197
+ ...(inboxSendArgs.ttlSeconds !== undefined ? { ttlSeconds: inboxSendArgs.ttlSeconds } : {}),
1198
+ }, token);
1199
+ break;
1200
+ }
1201
+ case 'inbox_read': {
1202
+ const inboxReadArgs = parseInboxReadArgs(rawArgs);
1203
+ const inboxParams = new URLSearchParams();
1204
+ if (inboxReadArgs.cursor !== undefined)
1205
+ inboxParams.set('cursor', inboxReadArgs.cursor);
1206
+ inboxParams.set('limit', String(inboxReadArgs.limit));
1207
+ inboxParams.set('unreadOnly', String(inboxReadArgs.unreadOnly));
1208
+ result = await readInboxAndMaybeAck(inboxReadArgs, () => aisGet(`/v1/agents/${agentId}/inbox?${inboxParams}`, token), (messageIds) => aisPost(`/v1/agents/${agentId}/inbox/ack`, { messageIds }, token));
1209
+ break;
1210
+ }
1211
+ case 'inbox_ack': {
1212
+ const inboxAckArgs = parseInboxAckArgs(rawArgs);
1213
+ result = await aisPost(`/v1/agents/${agentId}/inbox/ack`, { messageIds: inboxAckArgs.messageIds }, token);
1214
+ break;
1215
+ }
1216
+ case 'goal_create': {
1217
+ const goalCreateArgs = parseGoalCreateArgs(rawArgs);
1218
+ result = await aisPost(`/v1/agents/${agentId}/goals`, {
1219
+ title: goalCreateArgs.title,
1220
+ description: goalCreateArgs.description,
1221
+ ...(goalCreateArgs.successCriteria !== undefined ? { successCriteria: goalCreateArgs.successCriteria } : {}),
1222
+ ...(goalCreateArgs.parentGoalId !== undefined ? { parentGoalId: goalCreateArgs.parentGoalId } : {}),
1223
+ }, token);
1224
+ break;
1225
+ }
1226
+ case 'goal_list': {
1227
+ const goalListArgs = parseGoalListArgs(rawArgs);
1228
+ const goalParams = new URLSearchParams();
1229
+ if (goalListArgs.status !== undefined)
1230
+ goalParams.set('status', goalListArgs.status);
1231
+ if (goalListArgs.page !== undefined)
1232
+ goalParams.set('page', String(goalListArgs.page));
1233
+ goalParams.set('limit', String(goalListArgs.limit));
1234
+ if (goalListArgs.parentGoalId !== undefined)
1235
+ goalParams.set('parentGoalId', goalListArgs.parentGoalId);
1236
+ result = await aisGet(`/v1/agents/${agentId}/goals?${goalParams}`, token);
1237
+ break;
1238
+ }
1239
+ case 'goal_update_progress': {
1240
+ const goalProgressArgs = parseGoalUpdateProgressArgs(rawArgs);
1241
+ result = await aisPatch(`/v1/agents/${agentId}/goals/${goalProgressArgs.goalId}`, {
1242
+ progress: goalProgressArgs.progress,
1243
+ }, token);
1244
+ break;
1245
+ }
1246
+ case 'goal_complete': {
1247
+ const goalCompleteArgs = parseGoalCompleteArgs(rawArgs);
1248
+ result = await aisPost(`/v1/agents/${agentId}/goals/${goalCompleteArgs.goalId}/complete`, {
1249
+ success: goalCompleteArgs.success,
1250
+ ...(goalCompleteArgs.notes !== undefined ? { notes: goalCompleteArgs.notes } : {}),
1251
+ ...(goalCompleteArgs.lessonsLearned !== undefined ? { lessonsLearned: goalCompleteArgs.lessonsLearned } : {}),
1252
+ }, token);
1253
+ break;
1254
+ }
866
1255
  default:
867
1256
  emitTelemetry({
868
1257
  event: 'tool.call',
@@ -961,7 +1350,7 @@ async function main() {
961
1350
  stderr('Device key enrolled — deferring session setup to the first key-auth token acquire.');
962
1351
  }
963
1352
  else {
964
- stderr('No saved credentials. Bond URL will be surfaced on first tool call.');
1353
+ stderr(`${MISSING_CREDENTIAL_HELP} Bond URL will be surfaced on first tool call.`);
965
1354
  }
966
1355
  }
967
1356
  main().catch((error) => {