aismemory 0.7.2 → 0.7.4

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 (98) hide show
  1. package/README.md +110 -0
  2. package/dist/__tests__/agent-key-mode.test.js +60 -0
  3. package/dist/__tests__/agent-key-mode.test.js.map +1 -1
  4. package/dist/__tests__/agent-load-persistence.test.js +25 -2
  5. package/dist/__tests__/agent-load-persistence.test.js.map +1 -1
  6. package/dist/__tests__/agentmemory.test.d.ts +1 -0
  7. package/dist/__tests__/agentmemory.test.js +73 -0
  8. package/dist/__tests__/agentmemory.test.js.map +1 -0
  9. package/dist/__tests__/ais-http.test.js +73 -45
  10. package/dist/__tests__/ais-http.test.js.map +1 -1
  11. package/dist/__tests__/auto-enroll-key.test.js +10 -6
  12. package/dist/__tests__/auto-enroll-key.test.js.map +1 -1
  13. package/dist/__tests__/beacon-contract.test.d.ts +1 -0
  14. package/dist/__tests__/beacon-contract.test.js +106 -0
  15. package/dist/__tests__/beacon-contract.test.js.map +1 -0
  16. package/dist/__tests__/credential-isolation.test.d.ts +1 -0
  17. package/dist/__tests__/credential-isolation.test.js +61 -0
  18. package/dist/__tests__/credential-isolation.test.js.map +1 -0
  19. package/dist/__tests__/credential-resolution.test.d.ts +1 -0
  20. package/dist/__tests__/credential-resolution.test.js +141 -0
  21. package/dist/__tests__/credential-resolution.test.js.map +1 -0
  22. package/dist/__tests__/doctor.test.js +250 -9
  23. package/dist/__tests__/doctor.test.js.map +1 -1
  24. package/dist/__tests__/ender-receipt-auth.test.d.ts +1 -0
  25. package/dist/__tests__/ender-receipt-auth.test.js +26 -0
  26. package/dist/__tests__/ender-receipt-auth.test.js.map +1 -0
  27. package/dist/__tests__/fixtures/beacon-contract.d.ts +66 -0
  28. package/dist/__tests__/fixtures/beacon-contract.js +72 -0
  29. package/dist/__tests__/fixtures/beacon-contract.js.map +1 -0
  30. package/dist/__tests__/list-all-agents.test.js +2 -0
  31. package/dist/__tests__/list-all-agents.test.js.map +1 -1
  32. package/dist/__tests__/lost-agent.test.d.ts +1 -0
  33. package/dist/__tests__/lost-agent.test.js +108 -0
  34. package/dist/__tests__/lost-agent.test.js.map +1 -0
  35. package/dist/__tests__/mcp-init-no-auth.test.js +29 -5
  36. package/dist/__tests__/mcp-init-no-auth.test.js.map +1 -1
  37. package/dist/__tests__/memory-write-failure.test.js +5 -1
  38. package/dist/__tests__/memory-write-failure.test.js.map +1 -1
  39. package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.d.ts +1 -0
  40. package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.js +207 -0
  41. package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.js.map +1 -0
  42. package/dist/__tests__/provisional-register-mcp-remember.test.js +5 -1
  43. package/dist/__tests__/provisional-register-mcp-remember.test.js.map +1 -1
  44. package/dist/__tests__/readme-tools-parity.test.d.ts +1 -0
  45. package/dist/__tests__/readme-tools-parity.test.js +64 -0
  46. package/dist/__tests__/readme-tools-parity.test.js.map +1 -0
  47. package/dist/__tests__/reconnect-survival-e2e.test.js +80 -9
  48. package/dist/__tests__/reconnect-survival-e2e.test.js.map +1 -1
  49. package/dist/__tests__/token-manager.test.js +12 -0
  50. package/dist/__tests__/token-manager.test.js.map +1 -1
  51. package/dist/__tests__/tool-args.test.js +129 -1
  52. package/dist/__tests__/tool-args.test.js.map +1 -1
  53. package/dist/__tests__/workspace-key-mode.test.d.ts +1 -0
  54. package/dist/__tests__/workspace-key-mode.test.js +264 -0
  55. package/dist/__tests__/workspace-key-mode.test.js.map +1 -0
  56. package/dist/agent-key-mode.d.ts +4 -2
  57. package/dist/agent-key-mode.js +68 -7
  58. package/dist/agent-key-mode.js.map +1 -1
  59. package/dist/ais-http.d.ts +2 -0
  60. package/dist/ais-http.js +72 -5
  61. package/dist/ais-http.js.map +1 -1
  62. package/dist/auth-staleness.d.ts +2 -0
  63. package/dist/auth-staleness.js +5 -0
  64. package/dist/auth-staleness.js.map +1 -1
  65. package/dist/auto-handoff.d.ts +1 -1
  66. package/dist/auto-handoff.js +5 -3
  67. package/dist/auto-handoff.js.map +1 -1
  68. package/dist/cli/doctor.d.ts +4 -0
  69. package/dist/cli/doctor.js +122 -9
  70. package/dist/cli/doctor.js.map +1 -1
  71. package/dist/cli/enable-key-auth.js +6 -6
  72. package/dist/cli/enable-key-auth.js.map +1 -1
  73. package/dist/cli/orientation.d.ts +15 -0
  74. package/dist/cli/orientation.js +89 -0
  75. package/dist/cli/orientation.js.map +1 -0
  76. package/dist/harness-identity.d.ts +17 -0
  77. package/dist/harness-identity.js +105 -0
  78. package/dist/harness-identity.js.map +1 -0
  79. package/dist/hydration.d.ts +1 -1
  80. package/dist/hydration.js +3 -2
  81. package/dist/hydration.js.map +1 -1
  82. package/dist/index.js +365 -44
  83. package/dist/index.js.map +1 -1
  84. package/dist/key-auth.js +9 -9
  85. package/dist/key-auth.js.map +1 -1
  86. package/dist/oauth-credentials.d.ts +11 -0
  87. package/dist/oauth-credentials.js +31 -3
  88. package/dist/oauth-credentials.js.map +1 -1
  89. package/dist/token-manager.d.ts +3 -2
  90. package/dist/token-manager.js +7 -4
  91. package/dist/token-manager.js.map +1 -1
  92. package/dist/tool-args.d.ts +56 -0
  93. package/dist/tool-args.js +85 -0
  94. package/dist/tool-args.js.map +1 -1
  95. package/dist/workspace-key-mode.d.ts +40 -0
  96. package/dist/workspace-key-mode.js +258 -0
  97. package/dist/workspace-key-mode.js.map +1 -0
  98. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -9,29 +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';
16
- import { aisGet, aisPost } from './ais-http.js';
17
+ import { aisGet, aisPost, aisPatch, DEFAULT_AIS_URL } from './ais-http.js';
17
18
  import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
18
- import { homedir } from 'os';
19
- import { loadOAuthCredentials, saveOAuthCredentials } from './oauth-credentials.js';
19
+ import { homedir } from 'node:os';
20
+ import { loadOAuthCredentials, loadNonStaleDiskBearer, saveOAuthCredentials, } from './oauth-credentials.js';
20
21
  import { saveAgentPreference, loadAgentPreference, migrateCredentialsFileIfNeeded } from './agent-preference.js';
21
- import { createTokenManager } from './token-manager.js';
22
+ import { createTokenManager, isNoRegisteredKeyFailure } from './token-manager.js';
22
23
  import { hydrateIdentity } from './hydration.js';
23
24
  import { assemblePrompt } from './prompt-resource.js';
24
25
  import { startRefreshTimer, stopRefreshTimer } from './refresh.js';
25
26
  import { ActivityTracker, setupAutoHandoff } from './auto-handoff.js';
26
27
  import { assertEnvAgentInTenant, selectAgentForSession } from './env-agent.js';
27
28
  import { emitTelemetry, hashAgentId } from './telemetry.js';
28
- import { parseAgentLoadArgs, parseEmptyToolArgs, parseHandoffArgs, parseRecallArgs, parseFullContextArgs, parseRememberArgs, parseInboxSendArgs, parseInboxReadArgs, 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, } 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';
29
31
  import { readInboxAndMaybeAck } from './inbox-client.js';
30
32
  import { runPollLoop, DeviceFlowRestartedError } from './device-poll.js';
31
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';
32
35
  // ── Config ──────────────────────────────────────────────
33
- const AIS_URL = process.env['AIS_URL'] ?? 'https://ais.agentsandswarms.ai';
36
+ const AIS_URL = process.env['AIS_URL']?.trim() || DEFAULT_AIS_URL;
34
37
  const PACKAGE_VERSION = readPackageVersion();
38
+ let harnessIdentity = null;
35
39
  function readPackageVersion() {
36
40
  const packageJsonUrl = new URL('../package.json', import.meta.url);
37
41
  const packageJson = JSON.parse(readFileSync(packageJsonUrl, 'utf-8'));
@@ -67,6 +71,25 @@ function loadCredentials() {
67
71
  function stderr(msg) {
68
72
  process.stderr.write(`[aismemory] ${msg}\n`);
69
73
  }
74
+ function isSafeClaudeSessionId(sessionId) {
75
+ return sessionId !== '.' && sessionId !== '..' && /^[A-Za-z0-9._-]+$/.test(sessionId);
76
+ }
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;
83
+ }
84
+ try {
85
+ const sessionsDir = join(homedir(), '.claude', 'ais-agent-sessions');
86
+ mkdirSync(sessionsDir, { recursive: true });
87
+ writeFileSync(join(sessionsDir, sessionId), agentName, 'utf-8');
88
+ }
89
+ catch (error) {
90
+ stderr(`Skipping Claude session marker: ${error instanceof Error ? error.message : String(error)}`);
91
+ }
92
+ }
70
93
  /**
71
94
  * Fetch EVERY agent in the tenant, following server pagination.
72
95
  * The server defaults to limit=20 and silently truncates (2026-08-24
@@ -118,13 +141,13 @@ async function resolveAgentForLoad(token, query) {
118
141
  * User can override with AIS_AGENT_ID or AIS_AGENT_NAME env vars.
119
142
  */
120
143
  async function resolveAgent(token, fallbackAgentId, fallbackTenantId, preferredAgentId) {
121
- // Explicit env var overrides everything — must belong to this tenant.
122
- 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;
123
146
  if (envAgentId) {
124
147
  const agents = await listAllAgents(token);
125
148
  try {
126
149
  const match = assertEnvAgentInTenant(envAgentId, agents);
127
- stderr(`Using agent from AIS_AGENT_ID: ${match.name} (${match.id})`);
150
+ stderr(`Using pinned agent: ${match.name} (${match.id})`);
128
151
  return { agentId: match.id, tenantId: fallbackTenantId };
129
152
  }
130
153
  catch (error) {
@@ -173,6 +196,11 @@ async function resolveAgent(token, fallbackAgentId, fallbackTenantId, preferredA
173
196
  return { agentId: fallbackAgentId, tenantId: fallbackTenantId };
174
197
  }
175
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.';
176
204
  /**
177
205
  * Bond URL/code surfaced when a tool is called before the user has approved
178
206
  * the device flow. Caller catches this and returns a friendly error to the MCP
@@ -183,7 +211,7 @@ class BondPendingError extends Error {
183
211
  userCode;
184
212
  bondPending = true;
185
213
  constructor(verificationUri, userCode) {
186
- 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` +
187
215
  `Visit: ${verificationUri}\n` +
188
216
  `Code: ${userCode}\n\n` +
189
217
  `Enter your email and tap Approve, then retry this tool. Takes about 30 seconds.\n` +
@@ -332,9 +360,10 @@ flowRef) {
332
360
  // reconnected silently. migrateCredentialsFileIfNeeded() (agent-preference.ts)
333
361
  // is what later removes this token from disk, once a key gets enrolled.
334
362
  if (hasEnrolledKey()) {
335
- saveAgentPreference({ agentId: creds.agentId, tenantId: creds.tenantId });
363
+ if (!harnessIdentity)
364
+ saveAgentPreference({ agentId: creds.agentId, tenantId: creds.tenantId });
336
365
  }
337
- else {
366
+ else if (!harnessIdentity) {
338
367
  saveOAuthCredentials(creds);
339
368
  }
340
369
  stderr('Authenticated successfully!');
@@ -416,6 +445,7 @@ const tools = [
416
445
  type: 'string',
417
446
  description: 'Agent name to look up (e.g., "Corbot", "Data", "code-assistant")',
418
447
  },
448
+ memoriesLimit: { type: 'integer', minimum: 1, maximum: 50, description: 'Number of memories to load (1-50). Omit to use the AIS default.' },
419
449
  },
420
450
  required: ['query'],
421
451
  },
@@ -508,6 +538,92 @@ const tools = [
508
538
  required: [],
509
539
  },
510
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
+ },
511
627
  ];
512
628
  // ── MCP Server ──────────────────────────────────────────
513
629
  /**
@@ -527,6 +643,82 @@ function decodeJwtClaims(token) {
527
643
  return {};
528
644
  }
529
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
+ }
530
722
  /**
531
723
  * MCP server entrypoint for agent-API-key auth mode (CORBOT-B1D3244C).
532
724
  *
@@ -552,6 +744,9 @@ async function runAgentKeyModeServer(config) {
552
744
  activityTracker.recordToolUse(name);
553
745
  const toolStartedAt = Date.now();
554
746
  try {
747
+ if (requiresIdentityGate(name, rawArgs)) {
748
+ await verifyPinnedIdentity(harnessIdentity, config.agentId, () => dispatchToolKeyMode(AIS_URL, config, 'whoami', {}));
749
+ }
555
750
  const result = await dispatchToolKeyMode(AIS_URL, config, name, rawArgs);
556
751
  if (result === UNKNOWN_TOOL_KEY_MODE) {
557
752
  return {
@@ -599,16 +794,43 @@ async function main() {
599
794
  return; // sync-memory.ts calls process.exit() on failure
600
795
  }
601
796
  if (process.argv[2] === 'doctor') {
602
- await import('./cli/doctor.js');
797
+ await (await import('./cli/doctor.js')).main(process.argv.slice(3));
603
798
  return; // doctor.ts calls process.exit() itself
604
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
+ }
605
827
  // Agent API key auth mode (CORBOT-B1D3244C) — unattended machines. Checked
606
828
  // before any device-flow / OAuth-credential / DID-key-auth setup below so
607
829
  // this mode NEVER falls back to those paths (that fallback is what used to
608
830
  // silently mint duplicate provisional agents in prod).
609
831
  let agentKeyConfig;
610
832
  try {
611
- agentKeyConfig = loadAgentApiKeyConfig();
833
+ agentKeyConfig = loadAgentApiKeyConfig(adapterEnv);
612
834
  }
613
835
  catch (err) {
614
836
  if (err instanceof AgentApiKeyConfigError) {
@@ -624,6 +846,11 @@ async function main() {
624
846
  // Lazy-auth state. We connect the MCP transport before the user has approved
625
847
  // the device flow so Claude Code's 30s connection timeout is not blocked on
626
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();
627
854
  let creds = loadCredentials();
628
855
  let activeFlow = null;
629
856
  let postAuthDone = false;
@@ -649,16 +876,30 @@ async function main() {
649
876
  // background, retries with backoff, and never serializes a token to disk.
650
877
  const tokenManager = createTokenManager({
651
878
  authenticate: async () => {
652
- const keyResult = await tryKeyAuth({ aisUrl: AIS_URL });
653
- if (!keyResult)
654
- throw new Error('no enrolled device key');
655
- 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
+ }
656
896
  },
657
897
  resolveIdentity: async (token) => {
658
898
  const claims = decodeJwtClaims(token);
659
899
  const tenantId = claims.tenantId ?? '';
660
900
  const resolved = await resolveAgent(token, '', tenantId, loadAgentPreference()?.agentId);
661
- saveAgentPreference({ agentId: resolved.agentId, tenantId: resolved.tenantId || tenantId });
901
+ if (!harnessIdentity)
902
+ saveAgentPreference({ agentId: resolved.agentId, tenantId: resolved.tenantId || tenantId });
662
903
  return { agentId: resolved.agentId, tenantId: resolved.tenantId || tenantId };
663
904
  },
664
905
  onEvent: (event, detail) => {
@@ -688,30 +929,53 @@ async function main() {
688
929
  // propagate to the tool result verbatim — that IS the structured
689
930
  // "no action needed, self-healing in the background" message.
690
931
  if (hasEnrolledKey()) {
691
- const mc = await tokenManager.getCredentials();
692
- if (!firstKeyAcquireDone) {
693
- firstKeyAcquireDone = true;
694
- emitTelemetry({ event: 'auth.key.success', agentIdHash: hashAgentId(mc.agentId) });
695
- 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;
696
950
  }
697
- // Compose token/expiresAt from the manager, but let a live agent_load
698
- // switch win over the manager's cached identity — never let a renewal
699
- // silently revert the session back to the originally-resolved agent.
700
- creds = sessionAgentOverride
701
- ? { token: mc.token, expiresAt: mc.expiresAt, agentId: sessionAgentOverride.agentId, tenantId: sessionAgentOverride.tenantId }
702
- : mc;
703
- if (!postAuthDone) {
704
- postAuthDone = true;
705
- 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;
706
966
  }
707
- return creds;
708
967
  }
709
968
  // ── No-key path: browser device flow (unchanged bond URL semantics) ──
710
969
  if (creds && !isCredsStale(creds.expiresAt, Date.now())) {
711
970
  return creds;
712
971
  }
713
972
  if (!activeFlow || activeFlow.dead) {
714
- 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
+ }
715
979
  }
716
980
  const sentinel = Symbol('bond-pending');
717
981
  // poll is always set by startDeviceFlow before it returns; this guard
@@ -757,7 +1021,8 @@ async function main() {
757
1021
  });
758
1022
  }
759
1023
  /** Identity hydration, prompt refresh, and auto-handoff — all post-auth. */
760
- async function runPostAuth(c) {
1024
+ async function runPostAuth(credentials) {
1025
+ const c = { ...credentials, agentId: harnessIdentity?.agentId ?? credentials.agentId };
761
1026
  // Auto-enroll a DID key so re-auth stays silent after the 7-day bond
762
1027
  // token expires. Without this, key enrollment was a manual opt-in step
763
1028
  // (`enable-key-auth`) and every normal user was re-prompted to bond at
@@ -785,7 +1050,7 @@ async function main() {
785
1050
  setupAutoHandoff(AIS_URL, c.agentId, c.token, activityTracker, () => {
786
1051
  stopRefreshTimer();
787
1052
  stderr('Session handoff saved');
788
- });
1053
+ }, () => verifyPinnedIdentity(harnessIdentity, c.agentId, () => aisGet(`/v1/agents/${c.agentId}`, c.token)));
789
1054
  }
790
1055
  catch (e) {
791
1056
  stderr(`Post-auth setup failed: ${e instanceof Error ? e.message : String(e)}`);
@@ -815,11 +1080,17 @@ async function main() {
815
1080
  activityTracker.recordToolUse(name);
816
1081
  const toolStartedAt = Date.now();
817
1082
  try {
1083
+ if (requiresIdentityGate(name, rawArgs)) {
1084
+ assertPinnedAgent(harnessIdentity, harnessIdentity?.agentId ?? 'unselected');
1085
+ }
818
1086
  const UNKNOWN_TOOL = Symbol('unknown-tool');
819
1087
  const dispatchTool = async (c) => {
820
1088
  const token = c.token;
821
1089
  // agentId is mutable across `agent_load` calls within this session.
822
- 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
+ }
823
1094
  let result;
824
1095
  switch (name) {
825
1096
  case 'remember': {
@@ -857,6 +1128,8 @@ async function main() {
857
1128
  fcParams.set('principalAgentId', fcArgs.principalAgentId);
858
1129
  if (fcArgs.memoriesLimit !== undefined)
859
1130
  fcParams.set('memoriesLimit', String(fcArgs.memoriesLimit));
1131
+ if (fcArgs.view !== undefined)
1132
+ fcParams.set('view', fcArgs.view);
860
1133
  const fcQs = fcParams.toString();
861
1134
  const fcPath = `/v1/agents/${agentId}/full-context${fcQs ? `?${fcQs}` : ''}`;
862
1135
  result = await aisGet(fcPath, token);
@@ -875,8 +1148,11 @@ async function main() {
875
1148
  break;
876
1149
  }
877
1150
  case 'agent_load': {
878
- const { query } = parseAgentLoadArgs(rawArgs);
1151
+ const { query, memoriesLimit } = parseAgentLoadArgs(rawArgs);
879
1152
  const target = await resolveAgentForLoad(token, query);
1153
+ if (harnessIdentity)
1154
+ assertPinnedAgent(harnessIdentity, target.id);
1155
+ persistClaudeSessionAgentName(process.env.CLAUDE_SESSION_ID, target.name);
880
1156
  agentId = target.id;
881
1157
  // Persist the switch so subsequent tool calls use the new agent.
882
1158
  // saveAgentPreference() MUST be called here so that a process
@@ -890,9 +1166,10 @@ async function main() {
890
1166
  if (creds) {
891
1167
  creds = { ...creds, agentId };
892
1168
  sessionAgentOverride = { agentId, tenantId: creds.tenantId };
893
- saveAgentPreference({ agentId, tenantId: creds.tenantId });
1169
+ if (!harnessIdentity)
1170
+ saveAgentPreference({ agentId, tenantId: creds.tenantId });
894
1171
  }
895
- agentContext = await hydrateIdentity(AIS_URL, agentId, token);
1172
+ agentContext = await hydrateIdentity(AIS_URL, agentId, token, memoriesLimit);
896
1173
  applyMemoryRefresh(agentId, token);
897
1174
  server.notification({ method: 'notifications/prompts/list_changed' });
898
1175
  stderr(`agent_load: switched to "${target.name}" (${target.id})`);
@@ -931,6 +1208,50 @@ async function main() {
931
1208
  result = await readInboxAndMaybeAck(inboxReadArgs, () => aisGet(`/v1/agents/${agentId}/inbox?${inboxParams}`, token), (messageIds) => aisPost(`/v1/agents/${agentId}/inbox/ack`, { messageIds }, token));
932
1209
  break;
933
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
+ }
934
1255
  default:
935
1256
  emitTelemetry({
936
1257
  event: 'tool.call',
@@ -1029,7 +1350,7 @@ async function main() {
1029
1350
  stderr('Device key enrolled — deferring session setup to the first key-auth token acquire.');
1030
1351
  }
1031
1352
  else {
1032
- 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.`);
1033
1354
  }
1034
1355
  }
1035
1356
  main().catch((error) => {