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.
- package/README.md +110 -0
- package/dist/__tests__/agent-key-mode.test.js +60 -0
- package/dist/__tests__/agent-key-mode.test.js.map +1 -1
- package/dist/__tests__/agent-load-persistence.test.js +25 -2
- package/dist/__tests__/agent-load-persistence.test.js.map +1 -1
- package/dist/__tests__/agentmemory.test.d.ts +1 -0
- package/dist/__tests__/agentmemory.test.js +73 -0
- package/dist/__tests__/agentmemory.test.js.map +1 -0
- package/dist/__tests__/ais-http.test.js +73 -45
- package/dist/__tests__/ais-http.test.js.map +1 -1
- package/dist/__tests__/auto-enroll-key.test.js +10 -6
- package/dist/__tests__/auto-enroll-key.test.js.map +1 -1
- package/dist/__tests__/beacon-contract.test.d.ts +1 -0
- package/dist/__tests__/beacon-contract.test.js +106 -0
- package/dist/__tests__/beacon-contract.test.js.map +1 -0
- package/dist/__tests__/credential-isolation.test.d.ts +1 -0
- package/dist/__tests__/credential-isolation.test.js +61 -0
- package/dist/__tests__/credential-isolation.test.js.map +1 -0
- package/dist/__tests__/credential-resolution.test.d.ts +1 -0
- package/dist/__tests__/credential-resolution.test.js +141 -0
- package/dist/__tests__/credential-resolution.test.js.map +1 -0
- package/dist/__tests__/doctor.test.js +250 -9
- package/dist/__tests__/doctor.test.js.map +1 -1
- package/dist/__tests__/ender-receipt-auth.test.d.ts +1 -0
- package/dist/__tests__/ender-receipt-auth.test.js +26 -0
- package/dist/__tests__/ender-receipt-auth.test.js.map +1 -0
- package/dist/__tests__/fixtures/beacon-contract.d.ts +66 -0
- package/dist/__tests__/fixtures/beacon-contract.js +72 -0
- package/dist/__tests__/fixtures/beacon-contract.js.map +1 -0
- package/dist/__tests__/list-all-agents.test.js +2 -0
- package/dist/__tests__/list-all-agents.test.js.map +1 -1
- package/dist/__tests__/lost-agent.test.d.ts +1 -0
- package/dist/__tests__/lost-agent.test.js +108 -0
- package/dist/__tests__/lost-agent.test.js.map +1 -0
- package/dist/__tests__/mcp-init-no-auth.test.js +29 -5
- package/dist/__tests__/mcp-init-no-auth.test.js.map +1 -1
- package/dist/__tests__/memory-write-failure.test.js +5 -1
- package/dist/__tests__/memory-write-failure.test.js.map +1 -1
- package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.d.ts +1 -0
- package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.js +207 -0
- package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.js.map +1 -0
- package/dist/__tests__/provisional-register-mcp-remember.test.js +5 -1
- package/dist/__tests__/provisional-register-mcp-remember.test.js.map +1 -1
- package/dist/__tests__/readme-tools-parity.test.d.ts +1 -0
- package/dist/__tests__/readme-tools-parity.test.js +64 -0
- package/dist/__tests__/readme-tools-parity.test.js.map +1 -0
- package/dist/__tests__/reconnect-survival-e2e.test.js +80 -9
- package/dist/__tests__/reconnect-survival-e2e.test.js.map +1 -1
- package/dist/__tests__/token-manager.test.js +12 -0
- package/dist/__tests__/token-manager.test.js.map +1 -1
- package/dist/__tests__/tool-args.test.js +129 -1
- package/dist/__tests__/tool-args.test.js.map +1 -1
- package/dist/__tests__/workspace-key-mode.test.d.ts +1 -0
- package/dist/__tests__/workspace-key-mode.test.js +264 -0
- package/dist/__tests__/workspace-key-mode.test.js.map +1 -0
- package/dist/agent-key-mode.d.ts +4 -2
- package/dist/agent-key-mode.js +68 -7
- package/dist/agent-key-mode.js.map +1 -1
- package/dist/ais-http.d.ts +2 -0
- package/dist/ais-http.js +72 -5
- package/dist/ais-http.js.map +1 -1
- package/dist/auth-staleness.d.ts +2 -0
- package/dist/auth-staleness.js +5 -0
- package/dist/auth-staleness.js.map +1 -1
- package/dist/auto-handoff.d.ts +1 -1
- package/dist/auto-handoff.js +5 -3
- package/dist/auto-handoff.js.map +1 -1
- package/dist/cli/doctor.d.ts +4 -0
- package/dist/cli/doctor.js +122 -9
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/enable-key-auth.js +6 -6
- package/dist/cli/enable-key-auth.js.map +1 -1
- package/dist/cli/orientation.d.ts +15 -0
- package/dist/cli/orientation.js +89 -0
- package/dist/cli/orientation.js.map +1 -0
- package/dist/harness-identity.d.ts +17 -0
- package/dist/harness-identity.js +105 -0
- package/dist/harness-identity.js.map +1 -0
- package/dist/hydration.d.ts +1 -1
- package/dist/hydration.js +3 -2
- package/dist/hydration.js.map +1 -1
- package/dist/index.js +365 -44
- package/dist/index.js.map +1 -1
- package/dist/key-auth.js +9 -9
- package/dist/key-auth.js.map +1 -1
- package/dist/oauth-credentials.d.ts +11 -0
- package/dist/oauth-credentials.js +31 -3
- package/dist/oauth-credentials.js.map +1 -1
- package/dist/token-manager.d.ts +3 -2
- package/dist/token-manager.js +7 -4
- package/dist/token-manager.js.map +1 -1
- package/dist/tool-args.d.ts +56 -0
- package/dist/tool-args.js +85 -0
- package/dist/tool-args.js.map +1 -1
- package/dist/workspace-key-mode.d.ts +40 -0
- package/dist/workspace-key-mode.js +258 -0
- package/dist/workspace-key-mode.js.map +1 -0
- 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']
|
|
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
|
-
//
|
|
122
|
-
const envAgentId =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
-
|
|
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
|
-
|
|
692
|
-
|
|
693
|
-
firstKeyAcquireDone
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
1353
|
+
stderr(`${MISSING_CREDENTIAL_HELP} Bond URL will be surfaced on first tool call.`);
|
|
1033
1354
|
}
|
|
1034
1355
|
}
|
|
1035
1356
|
main().catch((error) => {
|