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