aismemory 0.7.3 → 0.7.5
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 +51 -11
- package/dist/__tests__/agent-key-mode.test.js +72 -7
- package/dist/__tests__/agent-key-mode.test.js.map +1 -1
- package/dist/__tests__/ais-http.test.js +42 -2
- package/dist/__tests__/ais-http.test.js.map +1 -1
- package/dist/__tests__/credential-isolation.test.js +117 -3
- package/dist/__tests__/credential-isolation.test.js.map +1 -1
- package/dist/__tests__/device-flow-gate-e2e.test.js +5 -0
- package/dist/__tests__/device-flow-gate-e2e.test.js.map +1 -1
- package/dist/__tests__/doctor.test.js +1 -1
- package/dist/__tests__/doctor.test.js.map +1 -1
- 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__/mcp-tools.test.d.ts +1 -0
- package/dist/__tests__/mcp-tools.test.js +155 -0
- package/dist/__tests__/mcp-tools.test.js.map +1 -0
- package/dist/__tests__/memory-pull.test.d.ts +1 -0
- package/dist/__tests__/memory-pull.test.js +380 -0
- package/dist/__tests__/memory-pull.test.js.map +1 -0
- package/dist/__tests__/pending-device-flow-e2e.test.d.ts +19 -0
- package/dist/__tests__/pending-device-flow-e2e.test.js +159 -0
- package/dist/__tests__/pending-device-flow-e2e.test.js.map +1 -0
- package/dist/__tests__/pending-device-flow.test.d.ts +15 -0
- package/dist/__tests__/pending-device-flow.test.js +108 -0
- package/dist/__tests__/pending-device-flow.test.js.map +1 -0
- package/dist/__tests__/readme-quickstart-default-ais-url.test.d.ts +1 -0
- package/dist/__tests__/readme-quickstart-default-ais-url.test.js +80 -0
- package/dist/__tests__/readme-quickstart-default-ais-url.test.js.map +1 -0
- package/dist/__tests__/reconnect-survival-e2e.test.js +17 -8
- package/dist/__tests__/reconnect-survival-e2e.test.js.map +1 -1
- package/dist/__tests__/remember-duplicate-truth.test.d.ts +13 -0
- package/dist/__tests__/remember-duplicate-truth.test.js +106 -0
- package/dist/__tests__/remember-duplicate-truth.test.js.map +1 -0
- package/dist/__tests__/startup-credential-warning-e2e.test.d.ts +1 -0
- package/dist/__tests__/startup-credential-warning-e2e.test.js +102 -0
- package/dist/__tests__/startup-credential-warning-e2e.test.js.map +1 -0
- package/dist/__tests__/tool-args.test.js +10 -0
- package/dist/__tests__/tool-args.test.js.map +1 -1
- package/dist/__tests__/version-check.test.d.ts +1 -0
- package/dist/__tests__/version-check.test.js +349 -0
- package/dist/__tests__/version-check.test.js.map +1 -0
- package/dist/agent-key-mode.d.ts +1 -1
- package/dist/agent-key-mode.js +30 -10
- package/dist/agent-key-mode.js.map +1 -1
- package/dist/ais-http-error.d.ts +22 -0
- package/dist/ais-http-error.js +42 -0
- package/dist/ais-http-error.js.map +1 -1
- package/dist/ais-http.d.ts +1 -0
- package/dist/ais-http.js +39 -16
- package/dist/ais-http.js.map +1 -1
- package/dist/cli/doctor.d.ts +16 -0
- package/dist/cli/doctor.js +66 -1
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/enable-key-auth.js +2 -1
- package/dist/cli/enable-key-auth.js.map +1 -1
- package/dist/cli/pull-memory.d.ts +17 -0
- package/dist/cli/pull-memory.js +141 -0
- package/dist/cli/pull-memory.js.map +1 -0
- package/dist/index.js +108 -383
- package/dist/index.js.map +1 -1
- package/dist/mcp/tools.d.ts +78 -0
- package/dist/mcp/tools.js +433 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/memory-pull.d.ts +114 -0
- package/dist/memory-pull.js +171 -0
- package/dist/memory-pull.js.map +1 -0
- package/dist/pending-device-flow.d.ts +49 -0
- package/dist/pending-device-flow.js +96 -0
- package/dist/pending-device-flow.js.map +1 -0
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.js.map +1 -1
- package/dist/tool-args.d.ts +2 -0
- package/dist/tool-args.js +6 -0
- package/dist/tool-args.js.map +1 -1
- package/dist/version-check.d.ts +72 -0
- package/dist/version-check.js +237 -0
- package/dist/version-check.js.map +1 -0
- package/dist/workspace-key-mode.js +15 -6
- package/dist/workspace-key-mode.js.map +1 -1
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -9,15 +9,16 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
11
11
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
12
|
-
import { mkdirSync,
|
|
12
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { tryKeyAuth, enrollKeyIfNeeded, hasEnrolledKey } from './key-auth.js';
|
|
15
|
-
import { isCredsStale,
|
|
15
|
+
import { isCredsStale, isNoRegisteredKeysAuthError } from './auth-staleness.js';
|
|
16
16
|
import { AisHttpError } from './ais-http-error.js';
|
|
17
|
-
import { aisGet, aisPost,
|
|
17
|
+
import { aisGet, aisPost, DEFAULT_AIS_URL } from './ais-http.js';
|
|
18
18
|
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
19
19
|
import { homedir } from 'node:os';
|
|
20
20
|
import { loadOAuthCredentials, loadNonStaleDiskBearer, saveOAuthCredentials, } from './oauth-credentials.js';
|
|
21
|
+
import { clearPendingDeviceFlow, loadPendingDeviceFlow, savePendingDeviceFlow, } from './pending-device-flow.js';
|
|
21
22
|
import { saveAgentPreference, loadAgentPreference, migrateCredentialsFileIfNeeded } from './agent-preference.js';
|
|
22
23
|
import { createTokenManager, isNoRegisteredKeyFailure } from './token-manager.js';
|
|
23
24
|
import { hydrateIdentity } from './hydration.js';
|
|
@@ -26,28 +27,17 @@ import { startRefreshTimer, stopRefreshTimer } from './refresh.js';
|
|
|
26
27
|
import { ActivityTracker, setupAutoHandoff } from './auto-handoff.js';
|
|
27
28
|
import { assertEnvAgentInTenant, selectAgentForSession } from './env-agent.js';
|
|
28
29
|
import { emitTelemetry, hashAgentId } from './telemetry.js';
|
|
29
|
-
import {
|
|
30
|
-
import { loadHarnessIdentity, installHarnessIdentity, optionValue,
|
|
31
|
-
import { readInboxAndMaybeAck } from './inbox-client.js';
|
|
30
|
+
import { 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';
|
|
31
|
+
import { loadHarnessIdentity, installHarnessIdentity, optionValue, verifyPinnedIdentity } from './harness-identity.js';
|
|
32
32
|
import { runPollLoop, DeviceFlowRestartedError } from './device-poll.js';
|
|
33
|
-
import { loadAgentApiKeyConfig, AgentApiKeyConfigError,
|
|
34
|
-
import { loadWorkspaceKeyConfig, WorkspaceKeyConfigError,
|
|
33
|
+
import { loadAgentApiKeyConfig, AgentApiKeyConfigError, } from './agent-key-mode.js';
|
|
34
|
+
import { loadWorkspaceKeyConfig, WorkspaceKeyConfigError, } from './workspace-key-mode.js';
|
|
35
|
+
import { createAgentKeyCallToolHandler, createOAuthCallToolHandler, createWorkspaceKeyCallToolHandler, } from './mcp/tools.js';
|
|
36
|
+
import { readPackageVersion, startVersionCheck } from './version-check.js';
|
|
35
37
|
// ── Config ──────────────────────────────────────────────
|
|
36
|
-
const AIS_URL = process.env['AIS_URL']
|
|
38
|
+
const AIS_URL = process.env['AIS_URL']?.trim() || DEFAULT_AIS_URL;
|
|
37
39
|
const PACKAGE_VERSION = readPackageVersion();
|
|
38
40
|
let harnessIdentity = null;
|
|
39
|
-
function readPackageVersion() {
|
|
40
|
-
const packageJsonUrl = new URL('../package.json', import.meta.url);
|
|
41
|
-
const packageJson = JSON.parse(readFileSync(packageJsonUrl, 'utf-8'));
|
|
42
|
-
if (typeof packageJson !== 'object' ||
|
|
43
|
-
packageJson === null ||
|
|
44
|
-
!('version' in packageJson) ||
|
|
45
|
-
typeof packageJson.version !== 'string' ||
|
|
46
|
-
packageJson.version.length === 0) {
|
|
47
|
-
throw new Error('Unable to load aismemory package version from package.json');
|
|
48
|
-
}
|
|
49
|
-
return packageJson.version;
|
|
50
|
-
}
|
|
51
41
|
// ── Credential persistence ──────────────────────────────
|
|
52
42
|
function loadCredentials() {
|
|
53
43
|
// F5a (CORBOT-48DD2E25): migrate a legacy v1 file to v2 (ids only) BEFORE
|
|
@@ -202,20 +192,22 @@ const MISSING_CREDENTIAL_HELP = 'Missing AIS credential: no authenticated aismem
|
|
|
202
192
|
'Local sign-in can use ~/.aismemory/credentials.json. ' +
|
|
203
193
|
'Railway is an operator-only last resort for locating an existing scoped key; client surfaces never fetch AIS_SERVICE_KEY or DATABASE_URL.';
|
|
204
194
|
/**
|
|
205
|
-
* Bond URL
|
|
206
|
-
*
|
|
207
|
-
*
|
|
195
|
+
* Bond URL surfaced when a tool is called before the user has approved the
|
|
196
|
+
* device flow. Caller catches this and returns the text to the MCP client
|
|
197
|
+
* verbatim. The link already carries the user code, so the agent's whole job
|
|
198
|
+
* is to relay that one link — credential fallbacks stay out of this message
|
|
199
|
+
* (CORBOT-899790F0).
|
|
208
200
|
*/
|
|
209
201
|
class BondPendingError extends Error {
|
|
210
202
|
verificationUri;
|
|
211
203
|
userCode;
|
|
212
204
|
bondPending = true;
|
|
213
205
|
constructor(verificationUri, userCode) {
|
|
214
|
-
super(`Memory bond required
|
|
215
|
-
|
|
216
|
-
`
|
|
217
|
-
`
|
|
218
|
-
`
|
|
206
|
+
super(`Memory bond required. Reply to your human with only this bonding link, on its own line:\n\n` +
|
|
207
|
+
`${verificationUri}\n\n` +
|
|
208
|
+
`Send nothing else: not this message, not a code, not a key, not a command, not JSON. ` +
|
|
209
|
+
`Your human opens the link, signs in, and presses Bond. When they say it is done, retry this tool.\n` +
|
|
210
|
+
`The link expires in 30 minutes. An agent nobody bonds is archived after 7 days, with its memories.`);
|
|
219
211
|
this.verificationUri = verificationUri;
|
|
220
212
|
this.userCode = userCode;
|
|
221
213
|
this.name = 'BondPendingError';
|
|
@@ -224,7 +216,17 @@ class BondPendingError extends Error {
|
|
|
224
216
|
async function startDeviceFlow(priorProvisionalToken) {
|
|
225
217
|
emitTelemetry({ event: 'auth.device_flow.start' });
|
|
226
218
|
stderr('Starting authentication...');
|
|
227
|
-
|
|
219
|
+
// CORBOT-8F96A8EF: when this process has no in-flight flow of its own,
|
|
220
|
+
// fall back to the provisional token left on disk by an earlier process.
|
|
221
|
+
// Without this the token was only ever held in a module-level variable, so
|
|
222
|
+
// every respawn of a stdio server signed in with nothing to present and
|
|
223
|
+
// the server dutifully minted another agent. Sending it lets the server
|
|
224
|
+
// reuse the SAME provisional agent (tryReuseProvisional); if the token is
|
|
225
|
+
// stale or already bonded the server ignores it and mints, exactly as
|
|
226
|
+
// before — so this can only reduce the number of agents, never break a
|
|
227
|
+
// sign-in.
|
|
228
|
+
const carriedToken = priorProvisionalToken ?? loadPendingDeviceFlow()?.provisionalToken;
|
|
229
|
+
const authRes = (await aisPost('/v1/oauth/device/authorize', { name: `agent-${homedir().split('/').pop() ?? 'user'}` }, carriedToken));
|
|
228
230
|
if (!authRes.success || !authRes.data) {
|
|
229
231
|
throw new Error(`Authentication failed: ${authRes.error?.message ?? 'Unknown error'}`);
|
|
230
232
|
}
|
|
@@ -237,6 +239,14 @@ async function startDeviceFlow(priorProvisionalToken) {
|
|
|
237
239
|
stderr(` Code : ${user_code}`);
|
|
238
240
|
stderr(' Code expires in 30 min; unclaimed agents are archived after 7 days.');
|
|
239
241
|
stderr('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
242
|
+
// Persist before returning, so a process that dies between the bond URL
|
|
243
|
+
// being printed and the human clicking it does not strand this agent.
|
|
244
|
+
savePendingDeviceFlow({
|
|
245
|
+
provisionalToken: provisional_token,
|
|
246
|
+
agentId: agent_id,
|
|
247
|
+
provisionalTenantId: provisional_tenant_id,
|
|
248
|
+
startedAt: new Date().toISOString(),
|
|
249
|
+
});
|
|
240
250
|
const flow = {
|
|
241
251
|
device_code,
|
|
242
252
|
user_code,
|
|
@@ -313,6 +323,16 @@ flowRef) {
|
|
|
313
323
|
flowRef.user_code = newHandle.user_code;
|
|
314
324
|
flowRef.provisional_token = authRes.data.provisional_token;
|
|
315
325
|
}
|
|
326
|
+
// Keep the on-disk copy in step with the in-memory one (CORBOT-8F96A8EF).
|
|
327
|
+
// A code that expires mid-flow issues a fresh provisional token; if the
|
|
328
|
+
// process then dies, the stale token on disk would be rejected on the
|
|
329
|
+
// next run and mint a new agent — the orphan this fix exists to stop.
|
|
330
|
+
savePendingDeviceFlow({
|
|
331
|
+
provisionalToken: authRes.data.provisional_token,
|
|
332
|
+
agentId: authRes.data.agent_id,
|
|
333
|
+
provisionalTenantId: authRes.data.provisional_tenant_id,
|
|
334
|
+
startedAt: new Date().toISOString(),
|
|
335
|
+
});
|
|
316
336
|
stderr('');
|
|
317
337
|
stderr('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
318
338
|
stderr(' aismemory — device code refreshed');
|
|
@@ -366,16 +386,24 @@ flowRef) {
|
|
|
366
386
|
else if (!harnessIdentity) {
|
|
367
387
|
saveOAuthCredentials(creds);
|
|
368
388
|
}
|
|
389
|
+
// The flow is finished: this agent is bonded and reachable by id from the
|
|
390
|
+
// credentials/preference files. Leaving the provisional token on disk
|
|
391
|
+
// would let a later sign-in present a token for an already-bonded agent,
|
|
392
|
+
// which the server rejects — sending it back to minting.
|
|
393
|
+
clearPendingDeviceFlow();
|
|
369
394
|
stderr('Authenticated successfully!');
|
|
370
395
|
return creds;
|
|
371
396
|
}
|
|
372
397
|
let agentContext = null;
|
|
373
398
|
const activityTracker = new ActivityTracker();
|
|
374
399
|
// ── MCP Tool definitions ────────────────────────────────
|
|
400
|
+
// Appended to the tools an agent calls first, so the relay-only bonding rule
|
|
401
|
+
// is visible from the tool schema itself, not only the README (CORBOT-899790F0).
|
|
402
|
+
const BOND_LINK_NOTE = ' If this agent is not bonded yet, the result is a bonding link: reply to your human with only that link, nothing else.';
|
|
375
403
|
const tools = [
|
|
376
404
|
{
|
|
377
405
|
name: 'remember',
|
|
378
|
-
description: 'Store a memory. Types: fact, event, lesson, context, goal, task.',
|
|
406
|
+
description: 'Store a memory. Types: fact, event, lesson, context, goal, task.' + BOND_LINK_NOTE,
|
|
379
407
|
inputSchema: {
|
|
380
408
|
type: 'object',
|
|
381
409
|
properties: {
|
|
@@ -389,7 +417,7 @@ const tools = [
|
|
|
389
417
|
{
|
|
390
418
|
name: 'recall',
|
|
391
419
|
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.",
|
|
420
|
+
"agent's memories under a 'memory:read' grant from that agent's owner." + BOND_LINK_NOTE,
|
|
393
421
|
inputSchema: {
|
|
394
422
|
type: 'object',
|
|
395
423
|
properties: {
|
|
@@ -403,13 +431,13 @@ const tools = [
|
|
|
403
431
|
},
|
|
404
432
|
{
|
|
405
433
|
name: 'whoami',
|
|
406
|
-
description: 'Get the current agent identity — name, DID, status, description.',
|
|
434
|
+
description: 'Get the current agent identity — name, DID, status, description.' + BOND_LINK_NOTE,
|
|
407
435
|
inputSchema: { type: 'object', properties: {} },
|
|
408
436
|
},
|
|
409
437
|
{
|
|
410
438
|
name: 'full_context',
|
|
411
439
|
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.",
|
|
440
|
+
"Set principalAgentId to load ANOTHER agent's context under a 'memory:read' grant." + BOND_LINK_NOTE,
|
|
413
441
|
inputSchema: {
|
|
414
442
|
type: 'object',
|
|
415
443
|
properties: {
|
|
@@ -431,6 +459,8 @@ const tools = [
|
|
|
431
459
|
properties: {
|
|
432
460
|
summary: { type: 'string', description: 'Summary of what was accomplished this session' },
|
|
433
461
|
keyLearnings: { type: 'array', items: { type: 'string' }, description: 'Key learnings to carry forward' },
|
|
462
|
+
docPath: { type: 'string', description: 'Path of a handoff document written this session, saved with the package' },
|
|
463
|
+
docPrUrl: { type: 'string', description: 'Link to the pull request that carries the handoff document' },
|
|
434
464
|
},
|
|
435
465
|
},
|
|
436
466
|
},
|
|
@@ -660,61 +690,14 @@ async function runWorkspaceKeyModeServer(config) {
|
|
|
660
690
|
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
661
691
|
throw new Error(`Unknown prompt: ${request.params.name}`);
|
|
662
692
|
});
|
|
663
|
-
server.setRequestHandler(CallToolRequestSchema,
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
});
|
|
693
|
+
server.setRequestHandler(CallToolRequestSchema, createWorkspaceKeyCallToolHandler({
|
|
694
|
+
aisUrl: AIS_URL,
|
|
695
|
+
activityTracker,
|
|
696
|
+
getHarnessIdentity: () => harnessIdentity,
|
|
697
|
+
getActiveConfig: () => activeConfig,
|
|
698
|
+
setActiveConfig: (next) => { activeConfig = next; },
|
|
699
|
+
stderr,
|
|
700
|
+
}));
|
|
718
701
|
const transport = new StdioServerTransport();
|
|
719
702
|
await server.connect(transport);
|
|
720
703
|
stderr(`MCP server running (workspace key mode). Tools: ${tools.map((t) => t.name).join(', ')}`);
|
|
@@ -738,47 +721,12 @@ async function runAgentKeyModeServer(config) {
|
|
|
738
721
|
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
739
722
|
throw new Error(`Unknown prompt: ${request.params.name}`);
|
|
740
723
|
});
|
|
741
|
-
server.setRequestHandler(CallToolRequestSchema,
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
if (requiresIdentityGate(name, rawArgs)) {
|
|
748
|
-
await verifyPinnedIdentity(harnessIdentity, config.agentId, () => dispatchToolKeyMode(AIS_URL, config, 'whoami', {}));
|
|
749
|
-
}
|
|
750
|
-
const result = await dispatchToolKeyMode(AIS_URL, config, name, rawArgs);
|
|
751
|
-
if (result === UNKNOWN_TOOL_KEY_MODE) {
|
|
752
|
-
return {
|
|
753
|
-
content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${name}` }) }],
|
|
754
|
-
isError: true,
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
emitTelemetry({
|
|
758
|
-
event: 'tool.call',
|
|
759
|
-
tool: name,
|
|
760
|
-
agentIdHash: hashAgentId(config.agentId),
|
|
761
|
-
latencyMs: Date.now() - toolStartedAt,
|
|
762
|
-
status: 'ok',
|
|
763
|
-
});
|
|
764
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
765
|
-
}
|
|
766
|
-
catch (error) {
|
|
767
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
768
|
-
emitTelemetry({
|
|
769
|
-
event: 'tool.call',
|
|
770
|
-
tool: name,
|
|
771
|
-
agentIdHash: hashAgentId(config.agentId),
|
|
772
|
-
latencyMs: Date.now() - toolStartedAt,
|
|
773
|
-
status: 'error',
|
|
774
|
-
error: message,
|
|
775
|
-
});
|
|
776
|
-
return {
|
|
777
|
-
content: [{ type: 'text', text: JSON.stringify({ error: message }) }],
|
|
778
|
-
isError: true,
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
});
|
|
724
|
+
server.setRequestHandler(CallToolRequestSchema, createAgentKeyCallToolHandler({
|
|
725
|
+
aisUrl: AIS_URL,
|
|
726
|
+
activityTracker,
|
|
727
|
+
config,
|
|
728
|
+
getHarnessIdentity: () => harnessIdentity,
|
|
729
|
+
}));
|
|
782
730
|
const transport = new StdioServerTransport();
|
|
783
731
|
await server.connect(transport);
|
|
784
732
|
stderr(`MCP server running (agent API key mode). Tools: ${tools.map((t) => t.name).join(', ')}`);
|
|
@@ -797,6 +745,10 @@ async function main() {
|
|
|
797
745
|
await (await import('./cli/doctor.js')).main(process.argv.slice(3));
|
|
798
746
|
return; // doctor.ts calls process.exit() itself
|
|
799
747
|
}
|
|
748
|
+
if (process.argv[2] === 'pull') {
|
|
749
|
+
await (await import('./cli/pull-memory.js')).main(process.argv.slice(3));
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
800
752
|
if (process.argv[2] === 'install') {
|
|
801
753
|
const source = optionValue(process.argv.slice(3), '--manifest');
|
|
802
754
|
if (!source)
|
|
@@ -805,6 +757,11 @@ async function main() {
|
|
|
805
757
|
process.stdout.write(`Installed identity pin for ${pin.harness}: ${pin.agentId}. Launch: aismemory --harness ${pin.harness}\nVerify: aismemory doctor --harness ${pin.harness}\n`);
|
|
806
758
|
return;
|
|
807
759
|
}
|
|
760
|
+
// CORBOT-7B8A1D16: ask the npm registry, at most about once a day, whether a
|
|
761
|
+
// newer aismemory has been published. Deliberately not awaited — startup must
|
|
762
|
+
// not wait on a third-party registry, and the answer is only needed later,
|
|
763
|
+
// when whoami or full_context is called. Resolves to a skip when offline.
|
|
764
|
+
void startVersionCheck({ currentVersion: PACKAGE_VERSION, onUpdateAvailable: stderr });
|
|
808
765
|
harnessIdentity = loadHarnessIdentity();
|
|
809
766
|
const adapterEnv = { ...process.env, AIS_AGENT_ID: harnessIdentity?.agentId };
|
|
810
767
|
// Workspace key auth mode (CORBOT-3103ED88) — multi-agent workspace
|
|
@@ -1074,259 +1031,24 @@ async function main() {
|
|
|
1074
1031
|
}],
|
|
1075
1032
|
};
|
|
1076
1033
|
});
|
|
1077
|
-
server.setRequestHandler(CallToolRequestSchema,
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
activityTracker
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
switch (name) {
|
|
1096
|
-
case 'remember': {
|
|
1097
|
-
const rememberArgs = parseRememberArgs(rawArgs);
|
|
1098
|
-
result = await aisPost(`/v1/agents/${agentId}/memory`, {
|
|
1099
|
-
content: rememberArgs.content,
|
|
1100
|
-
type: rememberArgs.type,
|
|
1101
|
-
importance: rememberArgs.importance,
|
|
1102
|
-
}, token);
|
|
1103
|
-
activityTracker.recordMemoryStored(rememberArgs.content);
|
|
1104
|
-
break;
|
|
1105
|
-
}
|
|
1106
|
-
case 'recall': {
|
|
1107
|
-
const recallArgs = parseRecallArgs(rawArgs);
|
|
1108
|
-
const params = new URLSearchParams();
|
|
1109
|
-
params.set('query', recallArgs.query);
|
|
1110
|
-
if (recallArgs.limit !== undefined)
|
|
1111
|
-
params.set('limit', String(recallArgs.limit));
|
|
1112
|
-
if (recallArgs.type !== undefined)
|
|
1113
|
-
params.set('types', recallArgs.type);
|
|
1114
|
-
if (recallArgs.principalAgentId !== undefined) {
|
|
1115
|
-
params.set('principalAgentId', recallArgs.principalAgentId);
|
|
1116
|
-
}
|
|
1117
|
-
result = await aisGet(`/v1/agents/${agentId}/memory?${params}`, token);
|
|
1118
|
-
break;
|
|
1119
|
-
}
|
|
1120
|
-
case 'whoami':
|
|
1121
|
-
parseEmptyToolArgs(rawArgs);
|
|
1122
|
-
result = await aisGet(`/v1/agents/${agentId}`, token);
|
|
1123
|
-
break;
|
|
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);
|
|
1136
|
-
break;
|
|
1137
|
-
}
|
|
1138
|
-
case 'dream':
|
|
1139
|
-
parseEmptyToolArgs(rawArgs);
|
|
1140
|
-
result = await aisPost(`/v1/agents/${agentId}/dream`, {}, token);
|
|
1141
|
-
break;
|
|
1142
|
-
case 'handoff': {
|
|
1143
|
-
const handoffArgs = parseHandoffArgs(rawArgs);
|
|
1144
|
-
result = await aisPost(`/v1/agents/${agentId}/handoff`, {
|
|
1145
|
-
summary: handoffArgs.summary ?? 'Session ended',
|
|
1146
|
-
keyLearnings: handoffArgs.keyLearnings ?? [],
|
|
1147
|
-
}, token);
|
|
1148
|
-
break;
|
|
1149
|
-
}
|
|
1150
|
-
case 'agent_load': {
|
|
1151
|
-
const { query, memoriesLimit } = parseAgentLoadArgs(rawArgs);
|
|
1152
|
-
const target = await resolveAgentForLoad(token, query);
|
|
1153
|
-
if (harnessIdentity)
|
|
1154
|
-
assertPinnedAgent(harnessIdentity, target.id);
|
|
1155
|
-
persistClaudeSessionAgentName(process.env.CLAUDE_SESSION_ID, target.name);
|
|
1156
|
-
agentId = target.id;
|
|
1157
|
-
// Persist the switch so subsequent tool calls use the new agent.
|
|
1158
|
-
// saveAgentPreference() MUST be called here so that a process
|
|
1159
|
-
// restart (silent MCP reconnect) restores the switched agent from
|
|
1160
|
-
// disk instead of silently reverting to the default
|
|
1161
|
-
// (CORBOT-497C7FA3). Ids only — no token is ever written to disk.
|
|
1162
|
-
// sessionAgentOverride MUST also be set so that a key-enrolled
|
|
1163
|
-
// TokenManager doesn't clobber this switch on the very next
|
|
1164
|
-
// ensureCredentials() call (the manager resolves identity once
|
|
1165
|
-
// and never re-resolves it).
|
|
1166
|
-
if (creds) {
|
|
1167
|
-
creds = { ...creds, agentId };
|
|
1168
|
-
sessionAgentOverride = { agentId, tenantId: creds.tenantId };
|
|
1169
|
-
if (!harnessIdentity)
|
|
1170
|
-
saveAgentPreference({ agentId, tenantId: creds.tenantId });
|
|
1171
|
-
}
|
|
1172
|
-
agentContext = await hydrateIdentity(AIS_URL, agentId, token, memoriesLimit);
|
|
1173
|
-
applyMemoryRefresh(agentId, token);
|
|
1174
|
-
server.notification({ method: 'notifications/prompts/list_changed' });
|
|
1175
|
-
stderr(`agent_load: switched to "${target.name}" (${target.id})`);
|
|
1176
|
-
result = {
|
|
1177
|
-
success: true,
|
|
1178
|
-
agentId: target.id,
|
|
1179
|
-
name: target.name,
|
|
1180
|
-
did: agentContext?.identity.did ?? null,
|
|
1181
|
-
status: target.status,
|
|
1182
|
-
memoriesLoaded: agentContext?.recentMemories.length ?? 0,
|
|
1183
|
-
goalsLoaded: agentContext?.goals.length ?? 0,
|
|
1184
|
-
};
|
|
1185
|
-
break;
|
|
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
|
-
}
|
|
1255
|
-
default:
|
|
1256
|
-
emitTelemetry({
|
|
1257
|
-
event: 'tool.call',
|
|
1258
|
-
tool: name,
|
|
1259
|
-
agentIdHash: hashAgentId(agentId),
|
|
1260
|
-
latencyMs: Date.now() - toolStartedAt,
|
|
1261
|
-
status: 'error',
|
|
1262
|
-
error: `Unknown tool: ${name}`,
|
|
1263
|
-
});
|
|
1264
|
-
return UNKNOWN_TOOL;
|
|
1265
|
-
}
|
|
1266
|
-
return result;
|
|
1267
|
-
};
|
|
1268
|
-
let result = await dispatchTool(await ensureCredentials());
|
|
1269
|
-
if (isAuthErrorResult(result)) {
|
|
1270
|
-
// CORBOT-A5BBD580: the server rejected our token mid-session
|
|
1271
|
-
// (expired or secret rotated). Clear the cached creds, silently
|
|
1272
|
-
// re-auth (key auth when a key file exists), and retry once.
|
|
1273
|
-
stderr('AIS rejected the cached token; re-authenticating and retrying once...');
|
|
1274
|
-
creds = null;
|
|
1275
|
-
// Drop the TokenManager's cached token too — without this, the
|
|
1276
|
-
// key-enrolled path's ensureCredentials() would just re-serve the
|
|
1277
|
-
// same still-time-valid-but-server-rejected token from `current`
|
|
1278
|
-
// instead of re-running challenge/prove (CORBOT-A5BBD580: a JWT
|
|
1279
|
-
// rejection mid-session, e.g. AIS rotating JWT_SECRET, must force a
|
|
1280
|
-
// real re-auth, not a no-op retry with the same token). Also reset
|
|
1281
|
-
// any escalated backoff window (CORBOT-4F7CAA8E / F2b): the server
|
|
1282
|
-
// just responded with a 401, so it is reachable right now — a stale
|
|
1283
|
-
// backoff window computed from unrelated prior renewal failures must
|
|
1284
|
-
// not hold this re-prove behind the getCredentials() gate.
|
|
1285
|
-
tokenManager.invalidate({ resetBackoff: true });
|
|
1286
|
-
result = await dispatchTool(await ensureCredentials());
|
|
1287
|
-
}
|
|
1288
|
-
if (result === UNKNOWN_TOOL) {
|
|
1289
|
-
return {
|
|
1290
|
-
content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${name}` }) }],
|
|
1291
|
-
isError: true,
|
|
1292
|
-
};
|
|
1293
|
-
}
|
|
1294
|
-
emitTelemetry({
|
|
1295
|
-
event: 'tool.call',
|
|
1296
|
-
tool: name,
|
|
1297
|
-
agentIdHash: creds ? hashAgentId(creds.agentId) : undefined,
|
|
1298
|
-
latencyMs: Date.now() - toolStartedAt,
|
|
1299
|
-
status: 'ok',
|
|
1300
|
-
});
|
|
1301
|
-
return {
|
|
1302
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
1303
|
-
};
|
|
1304
|
-
}
|
|
1305
|
-
catch (error) {
|
|
1306
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
1307
|
-
const agentHash = creds ? hashAgentId(creds.agentId) : undefined;
|
|
1308
|
-
emitTelemetry({
|
|
1309
|
-
event: 'tool.call',
|
|
1310
|
-
tool: name,
|
|
1311
|
-
agentIdHash: agentHash,
|
|
1312
|
-
latencyMs: Date.now() - toolStartedAt,
|
|
1313
|
-
status: 'error',
|
|
1314
|
-
error: message,
|
|
1315
|
-
});
|
|
1316
|
-
// BondPendingError carries human-readable text that the agent should
|
|
1317
|
-
// render verbatim — return as plain text rather than JSON.
|
|
1318
|
-
if (error instanceof BondPendingError) {
|
|
1319
|
-
return {
|
|
1320
|
-
content: [{ type: 'text', text: message }],
|
|
1321
|
-
isError: true,
|
|
1322
|
-
};
|
|
1323
|
-
}
|
|
1324
|
-
return {
|
|
1325
|
-
content: [{ type: 'text', text: JSON.stringify({ error: message }) }],
|
|
1326
|
-
isError: true,
|
|
1327
|
-
};
|
|
1328
|
-
}
|
|
1329
|
-
});
|
|
1034
|
+
server.setRequestHandler(CallToolRequestSchema, createOAuthCallToolHandler({
|
|
1035
|
+
aisUrl: AIS_URL,
|
|
1036
|
+
server,
|
|
1037
|
+
activityTracker,
|
|
1038
|
+
getHarnessIdentity: () => harnessIdentity,
|
|
1039
|
+
getCredentials: () => creds,
|
|
1040
|
+
setCredentials: (next) => { creds = next; },
|
|
1041
|
+
setSessionAgentOverride: (next) => { sessionAgentOverride = next; },
|
|
1042
|
+
getAgentContext: () => agentContext,
|
|
1043
|
+
setAgentContext: (next) => { agentContext = next; },
|
|
1044
|
+
ensureCredentials: () => ensureCredentials(),
|
|
1045
|
+
invalidateToken: () => { tokenManager.invalidate({ resetBackoff: true }); },
|
|
1046
|
+
applyMemoryRefresh,
|
|
1047
|
+
resolveAgentForLoad,
|
|
1048
|
+
persistClaudeSessionAgentName,
|
|
1049
|
+
isBondPendingError: (error) => error instanceof BondPendingError,
|
|
1050
|
+
stderr,
|
|
1051
|
+
}));
|
|
1330
1052
|
// Connect the transport BEFORE any auth so MCP `initialize` returns in <1s.
|
|
1331
1053
|
const transport = new StdioServerTransport();
|
|
1332
1054
|
await server.connect(transport);
|
|
@@ -1349,6 +1071,9 @@ async function main() {
|
|
|
1349
1071
|
else if (creds) {
|
|
1350
1072
|
stderr('Device key enrolled — deferring session setup to the first key-auth token acquire.');
|
|
1351
1073
|
}
|
|
1074
|
+
else if (hasEnrolledKey()) {
|
|
1075
|
+
stderr('Using enrolled device key; session starts on first tool call.');
|
|
1076
|
+
}
|
|
1352
1077
|
else {
|
|
1353
1078
|
stderr(`${MISSING_CREDENTIAL_HELP} Bond URL will be surfaced on first tool call.`);
|
|
1354
1079
|
}
|