aismemory 0.7.4 → 0.8.0
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 +211 -11
- package/dist/__tests__/agent-archive.test.d.ts +1 -0
- package/dist/__tests__/agent-archive.test.js +153 -0
- package/dist/__tests__/agent-archive.test.js.map +1 -0
- package/dist/__tests__/agent-key-mode.test.js +88 -7
- package/dist/__tests__/agent-key-mode.test.js.map +1 -1
- package/dist/__tests__/ais-http.test.js +32 -0
- 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__/credential-resolution.test.js +5 -5
- package/dist/__tests__/credential-resolution.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 +144 -79
- package/dist/__tests__/doctor.test.js.map +1 -1
- package/dist/__tests__/mcp-tools.test.d.ts +1 -0
- package/dist/__tests__/mcp-tools.test.js +189 -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__/memory-write-failure.test.js +4 -8
- package/dist/__tests__/memory-write-failure.test.js.map +1 -1
- package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.js +4 -7
- package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.js.map +1 -1
- 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__/provisional-register-mcp-remember.test.js +4 -8
- package/dist/__tests__/provisional-register-mcp-remember.test.js.map +1 -1
- 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 +3 -4
- 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__/retry-after.test.d.ts +4 -0
- package/dist/__tests__/retry-after.test.js +128 -0
- package/dist/__tests__/retry-after.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-archive.d.ts +36 -0
- package/dist/agent-archive.js +69 -0
- package/dist/agent-archive.js.map +1 -0
- package/dist/agent-key-mode.d.ts +1 -1
- package/dist/agent-key-mode.js +56 -19
- package/dist/agent-key-mode.js.map +1 -1
- package/dist/ais-http-error.d.ts +26 -2
- package/dist/ais-http-error.js +52 -4
- package/dist/ais-http-error.js.map +1 -1
- package/dist/ais-http.js +46 -16
- package/dist/ais-http.js.map +1 -1
- package/dist/cli/doctor.d.ts +17 -2
- package/dist/cli/doctor.js +93 -14
- package/dist/cli/doctor.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 +135 -398
- package/dist/index.js.map +1 -1
- package/dist/mcp/tools.d.ts +82 -0
- package/dist/mcp/tools.js +453 -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/retry-after.d.ts +26 -0
- package/dist/retry-after.js +42 -0
- package/dist/retry-after.js.map +1 -0
- package/dist/session-identity.d.ts +26 -0
- package/dist/session-identity.js +89 -0
- package/dist/session-identity.js.map +1 -0
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.js.map +1 -1
- package/dist/tool-args.d.ts +4 -0
- package/dist/tool-args.js +16 -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 +22 -6
- package/dist/workspace-key-mode.js.map +1 -1
- package/package.json +4 -3
- package/dist/harness-identity.d.ts +0 -17
- package/dist/harness-identity.js +0 -105
- package/dist/harness-identity.js.map +0 -1
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,18 @@ 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 {
|
|
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 { loadSessionIdentity, verifyPinnedIdentity } from './session-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';
|
|
37
|
+
import { AGENT_ARCHIVE_DESCRIPTION, AGENT_ARCHIVE_INPUT_SCHEMA, AGENT_DELETE_ALIAS_DESCRIPTION } from './agent-archive.js';
|
|
35
38
|
// ── Config ──────────────────────────────────────────────
|
|
36
39
|
const AIS_URL = process.env['AIS_URL']?.trim() || DEFAULT_AIS_URL;
|
|
37
40
|
const PACKAGE_VERSION = readPackageVersion();
|
|
38
|
-
let
|
|
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
|
-
}
|
|
41
|
+
let sessionIdentity = null;
|
|
51
42
|
// ── Credential persistence ──────────────────────────────
|
|
52
43
|
function loadCredentials() {
|
|
53
44
|
// F5a (CORBOT-48DD2E25): migrate a legacy v1 file to v2 (ids only) BEFORE
|
|
@@ -141,8 +132,8 @@ async function resolveAgentForLoad(token, query) {
|
|
|
141
132
|
* User can override with AIS_AGENT_ID or AIS_AGENT_NAME env vars.
|
|
142
133
|
*/
|
|
143
134
|
async function resolveAgent(token, fallbackAgentId, fallbackTenantId, preferredAgentId) {
|
|
144
|
-
//
|
|
145
|
-
const envAgentId =
|
|
135
|
+
// This session's own identity overrides shared preferences and must belong to this tenant.
|
|
136
|
+
const envAgentId = sessionIdentity?.agentId;
|
|
146
137
|
if (envAgentId) {
|
|
147
138
|
const agents = await listAllAgents(token);
|
|
148
139
|
try {
|
|
@@ -202,20 +193,22 @@ const MISSING_CREDENTIAL_HELP = 'Missing AIS credential: no authenticated aismem
|
|
|
202
193
|
'Local sign-in can use ~/.aismemory/credentials.json. ' +
|
|
203
194
|
'Railway is an operator-only last resort for locating an existing scoped key; client surfaces never fetch AIS_SERVICE_KEY or DATABASE_URL.';
|
|
204
195
|
/**
|
|
205
|
-
* Bond URL
|
|
206
|
-
*
|
|
207
|
-
*
|
|
196
|
+
* Bond URL surfaced when a tool is called before the user has approved the
|
|
197
|
+
* device flow. Caller catches this and returns the text to the MCP client
|
|
198
|
+
* verbatim. The link already carries the user code, so the agent's whole job
|
|
199
|
+
* is to relay that one link — credential fallbacks stay out of this message
|
|
200
|
+
* (CORBOT-899790F0).
|
|
208
201
|
*/
|
|
209
202
|
class BondPendingError extends Error {
|
|
210
203
|
verificationUri;
|
|
211
204
|
userCode;
|
|
212
205
|
bondPending = true;
|
|
213
206
|
constructor(verificationUri, userCode) {
|
|
214
|
-
super(`Memory bond required
|
|
215
|
-
|
|
216
|
-
`
|
|
217
|
-
`
|
|
218
|
-
`
|
|
207
|
+
super(`Memory bond required. Reply to your human with only this bonding link, on its own line:\n\n` +
|
|
208
|
+
`${verificationUri}\n\n` +
|
|
209
|
+
`Send nothing else: not this message, not a code, not a key, not a command, not JSON. ` +
|
|
210
|
+
`Your human opens the link, signs in, and presses Bond. When they say it is done, retry this tool.\n` +
|
|
211
|
+
`The link expires in 30 minutes. An agent nobody bonds is archived after 7 days, with its memories.`);
|
|
219
212
|
this.verificationUri = verificationUri;
|
|
220
213
|
this.userCode = userCode;
|
|
221
214
|
this.name = 'BondPendingError';
|
|
@@ -224,7 +217,17 @@ class BondPendingError extends Error {
|
|
|
224
217
|
async function startDeviceFlow(priorProvisionalToken) {
|
|
225
218
|
emitTelemetry({ event: 'auth.device_flow.start' });
|
|
226
219
|
stderr('Starting authentication...');
|
|
227
|
-
|
|
220
|
+
// CORBOT-8F96A8EF: when this process has no in-flight flow of its own,
|
|
221
|
+
// fall back to the provisional token left on disk by an earlier process.
|
|
222
|
+
// Without this the token was only ever held in a module-level variable, so
|
|
223
|
+
// every respawn of a stdio server signed in with nothing to present and
|
|
224
|
+
// the server dutifully minted another agent. Sending it lets the server
|
|
225
|
+
// reuse the SAME provisional agent (tryReuseProvisional); if the token is
|
|
226
|
+
// stale or already bonded the server ignores it and mints, exactly as
|
|
227
|
+
// before — so this can only reduce the number of agents, never break a
|
|
228
|
+
// sign-in.
|
|
229
|
+
const carriedToken = priorProvisionalToken ?? loadPendingDeviceFlow()?.provisionalToken;
|
|
230
|
+
const authRes = (await aisPost('/v1/oauth/device/authorize', { name: `agent-${homedir().split('/').pop() ?? 'user'}` }, carriedToken));
|
|
228
231
|
if (!authRes.success || !authRes.data) {
|
|
229
232
|
throw new Error(`Authentication failed: ${authRes.error?.message ?? 'Unknown error'}`);
|
|
230
233
|
}
|
|
@@ -237,6 +240,14 @@ async function startDeviceFlow(priorProvisionalToken) {
|
|
|
237
240
|
stderr(` Code : ${user_code}`);
|
|
238
241
|
stderr(' Code expires in 30 min; unclaimed agents are archived after 7 days.');
|
|
239
242
|
stderr('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
243
|
+
// Persist before returning, so a process that dies between the bond URL
|
|
244
|
+
// being printed and the human clicking it does not strand this agent.
|
|
245
|
+
savePendingDeviceFlow({
|
|
246
|
+
provisionalToken: provisional_token,
|
|
247
|
+
agentId: agent_id,
|
|
248
|
+
provisionalTenantId: provisional_tenant_id,
|
|
249
|
+
startedAt: new Date().toISOString(),
|
|
250
|
+
});
|
|
240
251
|
const flow = {
|
|
241
252
|
device_code,
|
|
242
253
|
user_code,
|
|
@@ -313,6 +324,16 @@ flowRef) {
|
|
|
313
324
|
flowRef.user_code = newHandle.user_code;
|
|
314
325
|
flowRef.provisional_token = authRes.data.provisional_token;
|
|
315
326
|
}
|
|
327
|
+
// Keep the on-disk copy in step with the in-memory one (CORBOT-8F96A8EF).
|
|
328
|
+
// A code that expires mid-flow issues a fresh provisional token; if the
|
|
329
|
+
// process then dies, the stale token on disk would be rejected on the
|
|
330
|
+
// next run and mint a new agent — the orphan this fix exists to stop.
|
|
331
|
+
savePendingDeviceFlow({
|
|
332
|
+
provisionalToken: authRes.data.provisional_token,
|
|
333
|
+
agentId: authRes.data.agent_id,
|
|
334
|
+
provisionalTenantId: authRes.data.provisional_tenant_id,
|
|
335
|
+
startedAt: new Date().toISOString(),
|
|
336
|
+
});
|
|
316
337
|
stderr('');
|
|
317
338
|
stderr('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
318
339
|
stderr(' aismemory — device code refreshed');
|
|
@@ -360,28 +381,37 @@ flowRef) {
|
|
|
360
381
|
// reconnected silently. migrateCredentialsFileIfNeeded() (agent-preference.ts)
|
|
361
382
|
// is what later removes this token from disk, once a key gets enrolled.
|
|
362
383
|
if (hasEnrolledKey()) {
|
|
363
|
-
if (!
|
|
384
|
+
if (!sessionIdentity)
|
|
364
385
|
saveAgentPreference({ agentId: creds.agentId, tenantId: creds.tenantId });
|
|
365
386
|
}
|
|
366
|
-
else if (!
|
|
387
|
+
else if (!sessionIdentity) {
|
|
367
388
|
saveOAuthCredentials(creds);
|
|
368
389
|
}
|
|
390
|
+
// The flow is finished: this agent is bonded and reachable by id from the
|
|
391
|
+
// credentials/preference files. Leaving the provisional token on disk
|
|
392
|
+
// would let a later sign-in present a token for an already-bonded agent,
|
|
393
|
+
// which the server rejects — sending it back to minting.
|
|
394
|
+
clearPendingDeviceFlow();
|
|
369
395
|
stderr('Authenticated successfully!');
|
|
370
396
|
return creds;
|
|
371
397
|
}
|
|
372
398
|
let agentContext = null;
|
|
373
399
|
const activityTracker = new ActivityTracker();
|
|
374
400
|
// ── MCP Tool definitions ────────────────────────────────
|
|
401
|
+
// Appended to the tools an agent calls first, so the relay-only bonding rule
|
|
402
|
+
// is visible from the tool schema itself, not only the README (CORBOT-899790F0).
|
|
403
|
+
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
404
|
const tools = [
|
|
376
405
|
{
|
|
377
406
|
name: 'remember',
|
|
378
|
-
description: 'Store a memory. Types: fact, event, lesson, context, goal, task.',
|
|
407
|
+
description: 'Store a memory. Types: fact, event, lesson, context, goal, task.' + BOND_LINK_NOTE,
|
|
379
408
|
inputSchema: {
|
|
380
409
|
type: 'object',
|
|
381
410
|
properties: {
|
|
382
411
|
content: { type: 'string', description: 'The memory content to store' },
|
|
383
412
|
type: { type: 'string', enum: ['fact', 'event', 'lesson', 'context', 'goal', 'task'], description: 'Memory type' },
|
|
384
413
|
importance: { type: 'number', minimum: 0, maximum: 1, description: 'Importance (0-1, default 0.5)' },
|
|
414
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags to filter on later, e.g. ["work", "acme"]. At most 20; each 1-64 characters of a-z 0-9 . _ : - (stored lower-case). Tags are a filter, not a permission — use separate agents to wall off e.g. work from personal.' },
|
|
385
415
|
},
|
|
386
416
|
required: ['content'],
|
|
387
417
|
},
|
|
@@ -389,13 +419,17 @@ const tools = [
|
|
|
389
419
|
{
|
|
390
420
|
name: 'recall',
|
|
391
421
|
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."
|
|
422
|
+
"agent's memories under a 'memory:read' grant from that agent's owner. " +
|
|
423
|
+
"Each memory carries createdAt, updatedAt (its last edit), and tags. " +
|
|
424
|
+
"Memories superseded by a newer one are left out; to list or page through every memory, " +
|
|
425
|
+
"use the REST list route described in the aismemory README (cursor paging, sortBy=updatedAt, includeSuperseded=true)." + BOND_LINK_NOTE,
|
|
393
426
|
inputSchema: {
|
|
394
427
|
type: 'object',
|
|
395
428
|
properties: {
|
|
396
429
|
query: { type: 'string', description: 'Search query' },
|
|
397
430
|
limit: { type: 'number', description: 'Max results (default 10)' },
|
|
398
431
|
type: { type: 'string', enum: ['fact', 'event', 'lesson', 'context', 'goal', 'task'], description: 'Filter by type' },
|
|
432
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Only memories carrying ALL of these tags. A hard filter: results are never widened past it.' },
|
|
399
433
|
principalAgentId: { type: 'string', description: "Optional: search ANOTHER agent's memories instead of your own (its agent UUID). Requires that agent's owner to have granted you the 'memory:read' scope and that you share a tenant; otherwise the call is refused with 403. Read-only — your own identity and write access are unchanged." },
|
|
400
434
|
},
|
|
401
435
|
required: ['query'],
|
|
@@ -403,13 +437,13 @@ const tools = [
|
|
|
403
437
|
},
|
|
404
438
|
{
|
|
405
439
|
name: 'whoami',
|
|
406
|
-
description: 'Get the current agent identity — name, DID, status, description.',
|
|
440
|
+
description: 'Get the current agent identity — name, DID, status, description.' + BOND_LINK_NOTE,
|
|
407
441
|
inputSchema: { type: 'object', properties: {} },
|
|
408
442
|
},
|
|
409
443
|
{
|
|
410
444
|
name: 'full_context',
|
|
411
445
|
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.",
|
|
446
|
+
"Set principalAgentId to load ANOTHER agent's context under a 'memory:read' grant." + BOND_LINK_NOTE,
|
|
413
447
|
inputSchema: {
|
|
414
448
|
type: 'object',
|
|
415
449
|
properties: {
|
|
@@ -431,6 +465,8 @@ const tools = [
|
|
|
431
465
|
properties: {
|
|
432
466
|
summary: { type: 'string', description: 'Summary of what was accomplished this session' },
|
|
433
467
|
keyLearnings: { type: 'array', items: { type: 'string' }, description: 'Key learnings to carry forward' },
|
|
468
|
+
docPath: { type: 'string', description: 'Path of a handoff document written this session, saved with the package' },
|
|
469
|
+
docPrUrl: { type: 'string', description: 'Link to the pull request that carries the handoff document' },
|
|
434
470
|
},
|
|
435
471
|
},
|
|
436
472
|
},
|
|
@@ -624,6 +660,18 @@ const tools = [
|
|
|
624
660
|
required: ['goalId', 'success'],
|
|
625
661
|
},
|
|
626
662
|
},
|
|
663
|
+
// Owner-only lifecycle (CORBOT-BA5AADB0): archive, plus the agent_delete
|
|
664
|
+
// alias, which archives too. Key modes refuse both (agent-key-mode.ts).
|
|
665
|
+
{
|
|
666
|
+
name: 'agent_archive',
|
|
667
|
+
description: AGENT_ARCHIVE_DESCRIPTION,
|
|
668
|
+
inputSchema: AGENT_ARCHIVE_INPUT_SCHEMA,
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
name: 'agent_delete',
|
|
672
|
+
description: AGENT_DELETE_ALIAS_DESCRIPTION,
|
|
673
|
+
inputSchema: AGENT_ARCHIVE_INPUT_SCHEMA,
|
|
674
|
+
},
|
|
627
675
|
];
|
|
628
676
|
// ── MCP Server ──────────────────────────────────────────
|
|
629
677
|
/**
|
|
@@ -660,61 +708,15 @@ async function runWorkspaceKeyModeServer(config) {
|
|
|
660
708
|
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
661
709
|
throw new Error(`Unknown prompt: ${request.params.name}`);
|
|
662
710
|
});
|
|
663
|
-
server.setRequestHandler(CallToolRequestSchema,
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
-
});
|
|
711
|
+
server.setRequestHandler(CallToolRequestSchema, createWorkspaceKeyCallToolHandler({
|
|
712
|
+
aisUrl: AIS_URL,
|
|
713
|
+
activityTracker,
|
|
714
|
+
getSessionIdentity: () => sessionIdentity,
|
|
715
|
+
setSessionIdentity: (next) => { sessionIdentity = next; },
|
|
716
|
+
getActiveConfig: () => activeConfig,
|
|
717
|
+
setActiveConfig: (next) => { activeConfig = next; },
|
|
718
|
+
stderr,
|
|
719
|
+
}));
|
|
718
720
|
const transport = new StdioServerTransport();
|
|
719
721
|
await server.connect(transport);
|
|
720
722
|
stderr(`MCP server running (workspace key mode). Tools: ${tools.map((t) => t.name).join(', ')}`);
|
|
@@ -738,47 +740,12 @@ async function runAgentKeyModeServer(config) {
|
|
|
738
740
|
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
739
741
|
throw new Error(`Unknown prompt: ${request.params.name}`);
|
|
740
742
|
});
|
|
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
|
-
});
|
|
743
|
+
server.setRequestHandler(CallToolRequestSchema, createAgentKeyCallToolHandler({
|
|
744
|
+
aisUrl: AIS_URL,
|
|
745
|
+
activityTracker,
|
|
746
|
+
config,
|
|
747
|
+
getSessionIdentity: () => sessionIdentity,
|
|
748
|
+
}));
|
|
782
749
|
const transport = new StdioServerTransport();
|
|
783
750
|
await server.connect(transport);
|
|
784
751
|
stderr(`MCP server running (agent API key mode). Tools: ${tools.map((t) => t.name).join(', ')}`);
|
|
@@ -797,16 +764,17 @@ async function main() {
|
|
|
797
764
|
await (await import('./cli/doctor.js')).main(process.argv.slice(3));
|
|
798
765
|
return; // doctor.ts calls process.exit() itself
|
|
799
766
|
}
|
|
800
|
-
if (process.argv[2] === '
|
|
801
|
-
|
|
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`);
|
|
767
|
+
if (process.argv[2] === 'pull') {
|
|
768
|
+
await (await import('./cli/pull-memory.js')).main(process.argv.slice(3));
|
|
806
769
|
return;
|
|
807
770
|
}
|
|
808
|
-
|
|
809
|
-
|
|
771
|
+
// CORBOT-7B8A1D16: ask the npm registry, at most about once a day, whether a
|
|
772
|
+
// newer aismemory has been published. Deliberately not awaited — startup must
|
|
773
|
+
// not wait on a third-party registry, and the answer is only needed later,
|
|
774
|
+
// when whoami or full_context is called. Resolves to a skip when offline.
|
|
775
|
+
void startVersionCheck({ currentVersion: PACKAGE_VERSION, onUpdateAvailable: stderr });
|
|
776
|
+
sessionIdentity = loadSessionIdentity();
|
|
777
|
+
const adapterEnv = { ...process.env, AIS_AGENT_ID: sessionIdentity?.agentId };
|
|
810
778
|
// Workspace key auth mode (CORBOT-3103ED88) — multi-agent workspace
|
|
811
779
|
// credentials. Checked FIRST: workspace key -> agent API key -> device flow.
|
|
812
780
|
let workspaceKeyConfig;
|
|
@@ -898,7 +866,7 @@ async function main() {
|
|
|
898
866
|
const claims = decodeJwtClaims(token);
|
|
899
867
|
const tenantId = claims.tenantId ?? '';
|
|
900
868
|
const resolved = await resolveAgent(token, '', tenantId, loadAgentPreference()?.agentId);
|
|
901
|
-
if (!
|
|
869
|
+
if (!sessionIdentity)
|
|
902
870
|
saveAgentPreference({ agentId: resolved.agentId, tenantId: resolved.tenantId || tenantId });
|
|
903
871
|
return { agentId: resolved.agentId, tenantId: resolved.tenantId || tenantId };
|
|
904
872
|
},
|
|
@@ -1022,7 +990,7 @@ async function main() {
|
|
|
1022
990
|
}
|
|
1023
991
|
/** Identity hydration, prompt refresh, and auto-handoff — all post-auth. */
|
|
1024
992
|
async function runPostAuth(credentials) {
|
|
1025
|
-
const c = { ...credentials, agentId:
|
|
993
|
+
const c = { ...credentials, agentId: sessionIdentity?.agentId ?? credentials.agentId };
|
|
1026
994
|
// Auto-enroll a DID key so re-auth stays silent after the 7-day bond
|
|
1027
995
|
// token expires. Without this, key enrollment was a manual opt-in step
|
|
1028
996
|
// (`enable-key-auth`) and every normal user was re-prompted to bond at
|
|
@@ -1050,7 +1018,7 @@ async function main() {
|
|
|
1050
1018
|
setupAutoHandoff(AIS_URL, c.agentId, c.token, activityTracker, () => {
|
|
1051
1019
|
stopRefreshTimer();
|
|
1052
1020
|
stderr('Session handoff saved');
|
|
1053
|
-
}, () => verifyPinnedIdentity(
|
|
1021
|
+
}, () => verifyPinnedIdentity(sessionIdentity, c.agentId, () => aisGet(`/v1/agents/${c.agentId}`, c.token)));
|
|
1054
1022
|
}
|
|
1055
1023
|
catch (e) {
|
|
1056
1024
|
stderr(`Post-auth setup failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -1074,259 +1042,25 @@ async function main() {
|
|
|
1074
1042
|
}],
|
|
1075
1043
|
};
|
|
1076
1044
|
});
|
|
1077
|
-
server.setRequestHandler(CallToolRequestSchema,
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
activityTracker
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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
|
-
});
|
|
1045
|
+
server.setRequestHandler(CallToolRequestSchema, createOAuthCallToolHandler({
|
|
1046
|
+
aisUrl: AIS_URL,
|
|
1047
|
+
server,
|
|
1048
|
+
activityTracker,
|
|
1049
|
+
getSessionIdentity: () => sessionIdentity,
|
|
1050
|
+
setSessionIdentity: (next) => { sessionIdentity = next; },
|
|
1051
|
+
getCredentials: () => creds,
|
|
1052
|
+
setCredentials: (next) => { creds = next; },
|
|
1053
|
+
setSessionAgentOverride: (next) => { sessionAgentOverride = next; },
|
|
1054
|
+
getAgentContext: () => agentContext,
|
|
1055
|
+
setAgentContext: (next) => { agentContext = next; },
|
|
1056
|
+
ensureCredentials: () => ensureCredentials(),
|
|
1057
|
+
invalidateToken: () => { tokenManager.invalidate({ resetBackoff: true }); },
|
|
1058
|
+
applyMemoryRefresh,
|
|
1059
|
+
resolveAgentForLoad,
|
|
1060
|
+
persistClaudeSessionAgentName,
|
|
1061
|
+
isBondPendingError: (error) => error instanceof BondPendingError,
|
|
1062
|
+
stderr,
|
|
1063
|
+
}));
|
|
1330
1064
|
// Connect the transport BEFORE any auth so MCP `initialize` returns in <1s.
|
|
1331
1065
|
const transport = new StdioServerTransport();
|
|
1332
1066
|
await server.connect(transport);
|
|
@@ -1349,6 +1083,9 @@ async function main() {
|
|
|
1349
1083
|
else if (creds) {
|
|
1350
1084
|
stderr('Device key enrolled — deferring session setup to the first key-auth token acquire.');
|
|
1351
1085
|
}
|
|
1086
|
+
else if (hasEnrolledKey()) {
|
|
1087
|
+
stderr('Using enrolled device key; session starts on first tool call.');
|
|
1088
|
+
}
|
|
1352
1089
|
else {
|
|
1353
1090
|
stderr(`${MISSING_CREDENTIAL_HELP} Bond URL will be surfaced on first tool call.`);
|
|
1354
1091
|
}
|