aismemory 0.4.0 → 0.5.1
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/dist/__tests__/auth-staleness.test.d.ts +9 -0
- package/dist/__tests__/auth-staleness.test.js +46 -0
- package/dist/__tests__/auth-staleness.test.js.map +1 -0
- package/dist/__tests__/auto-handoff.test.js +108 -2
- package/dist/__tests__/auto-handoff.test.js.map +1 -1
- package/dist/__tests__/config.test.js +6 -3
- package/dist/__tests__/config.test.js.map +1 -1
- package/dist/__tests__/device-flow-recovery.test.d.ts +1 -0
- package/dist/__tests__/device-flow-recovery.test.js +206 -0
- package/dist/__tests__/device-flow-recovery.test.js.map +1 -0
- package/dist/__tests__/env-agent.test.d.ts +1 -0
- package/dist/__tests__/env-agent.test.js +19 -0
- package/dist/__tests__/env-agent.test.js.map +1 -0
- package/dist/__tests__/existing-hashes.test.d.ts +1 -0
- package/dist/__tests__/existing-hashes.test.js +122 -0
- package/dist/__tests__/existing-hashes.test.js.map +1 -0
- package/dist/__tests__/hydration.test.js +38 -0
- package/dist/__tests__/hydration.test.js.map +1 -1
- package/dist/__tests__/key-auth.test.d.ts +1 -0
- package/dist/__tests__/key-auth.test.js +284 -0
- package/dist/__tests__/key-auth.test.js.map +1 -0
- package/dist/__tests__/local-mirror.test.js +4 -0
- package/dist/__tests__/local-mirror.test.js.map +1 -1
- package/dist/__tests__/oauth-credentials.test.d.ts +1 -0
- package/dist/__tests__/oauth-credentials.test.js +29 -0
- package/dist/__tests__/oauth-credentials.test.js.map +1 -0
- package/dist/__tests__/pipeline-ingestion.test.js +112 -1
- package/dist/__tests__/pipeline-ingestion.test.js.map +1 -1
- package/dist/__tests__/refresh.test.js +24 -0
- package/dist/__tests__/refresh.test.js.map +1 -1
- package/dist/__tests__/sync-memory-cli.test.d.ts +1 -0
- package/dist/__tests__/sync-memory-cli.test.js +200 -0
- package/dist/__tests__/sync-memory-cli.test.js.map +1 -0
- package/dist/__tests__/telemetry.test.d.ts +1 -0
- package/dist/__tests__/telemetry.test.js +67 -0
- package/dist/__tests__/telemetry.test.js.map +1 -0
- package/dist/__tests__/token-expiry-reauth.test.d.ts +1 -0
- package/dist/__tests__/token-expiry-reauth.test.js +201 -0
- package/dist/__tests__/token-expiry-reauth.test.js.map +1 -0
- package/dist/__tests__/tool-args.test.d.ts +1 -0
- package/dist/__tests__/tool-args.test.js +78 -0
- package/dist/__tests__/tool-args.test.js.map +1 -0
- package/dist/auth-staleness.d.ts +27 -0
- package/dist/auth-staleness.js +41 -0
- package/dist/auth-staleness.js.map +1 -0
- package/dist/auto-handoff.d.ts +2 -0
- package/dist/auto-handoff.js +40 -16
- package/dist/auto-handoff.js.map +1 -1
- package/dist/cli/enable-key-auth.d.ts +1 -0
- package/dist/cli/enable-key-auth.js +131 -0
- package/dist/cli/enable-key-auth.js.map +1 -0
- package/dist/cli/sync-memory.js +31 -36
- package/dist/cli/sync-memory.js.map +1 -1
- package/dist/config.js +4 -1
- package/dist/config.js.map +1 -1
- package/dist/env-agent.d.ts +10 -0
- package/dist/env-agent.js +14 -0
- package/dist/env-agent.js.map +1 -0
- package/dist/hydration.js +54 -3
- package/dist/hydration.js.map +1 -1
- package/dist/index.js +314 -118
- package/dist/index.js.map +1 -1
- package/dist/key-auth.d.ts +66 -0
- package/dist/key-auth.js +179 -0
- package/dist/key-auth.js.map +1 -0
- package/dist/local-mirror.d.ts +5 -0
- package/dist/local-mirror.js +72 -14
- package/dist/local-mirror.js.map +1 -1
- package/dist/oauth-credentials.d.ts +14 -0
- package/dist/oauth-credentials.js +35 -0
- package/dist/oauth-credentials.js.map +1 -0
- package/dist/pipeline/bulk-store.d.ts +46 -0
- package/dist/pipeline/bulk-store.js +165 -0
- package/dist/pipeline/bulk-store.js.map +1 -0
- package/dist/pipeline/existing-hashes.d.ts +20 -0
- package/dist/pipeline/existing-hashes.js +111 -0
- package/dist/pipeline/existing-hashes.js.map +1 -0
- package/dist/pipeline/ingestion.d.ts +8 -4
- package/dist/pipeline/ingestion.js +36 -8
- package/dist/pipeline/ingestion.js.map +1 -1
- package/dist/telemetry.d.ts +22 -0
- package/dist/telemetry.js +28 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/tool-args.d.ts +52 -0
- package/dist/tool-args.js +78 -0
- package/dist/tool-args.js.map +1 -0
- package/dist/trust-ledger.js +2 -2
- package/dist/trust-ledger.js.map +1 -1
- package/package.json +9 -4
package/dist/hydration.js
CHANGED
|
@@ -1,13 +1,59 @@
|
|
|
1
|
+
const LOG_PREFIX = '[aismemory]';
|
|
2
|
+
function redactHydrationMessage(message) {
|
|
3
|
+
return message
|
|
4
|
+
.replace(/Bearer\s+\S+/gi, 'Bearer [REDACTED]')
|
|
5
|
+
.replace(/authorization:\s*\S+/gi, 'authorization: [REDACTED]')
|
|
6
|
+
.slice(0, 300);
|
|
7
|
+
}
|
|
8
|
+
function isRetryableHttpStatus(status) {
|
|
9
|
+
return status >= 500 || status === 429;
|
|
10
|
+
}
|
|
11
|
+
function isRetryableHydrationError(err) {
|
|
12
|
+
if (!(err instanceof Error))
|
|
13
|
+
return true;
|
|
14
|
+
const msg = err.message.toLowerCase();
|
|
15
|
+
return (msg.includes('network') ||
|
|
16
|
+
msg.includes('fetch') ||
|
|
17
|
+
msg.includes('timeout') ||
|
|
18
|
+
msg.includes('econnrefused') ||
|
|
19
|
+
msg.includes('enotfound'));
|
|
20
|
+
}
|
|
21
|
+
function logHydrationFailure(kind, details) {
|
|
22
|
+
const retryLabel = details.retryable ? 'retryable' : 'permanent';
|
|
23
|
+
const statusPart = details.status !== undefined ? ` status=${details.status}` : '';
|
|
24
|
+
const line = `${LOG_PREFIX} hydrateIdentity failed (${kind}, ${retryLabel})${statusPart}: ${redactHydrationMessage(details.message)}\n`;
|
|
25
|
+
process.stderr.write(line);
|
|
26
|
+
}
|
|
1
27
|
export async function hydrateIdentity(aisUrl, agentId, token) {
|
|
2
28
|
try {
|
|
3
29
|
const res = await fetch(`${aisUrl}/v1/agents/${agentId}/full-context`, {
|
|
4
30
|
headers: { Authorization: `Bearer ${token}` },
|
|
5
31
|
});
|
|
6
|
-
if (!res.ok)
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
let bodyHint = res.statusText;
|
|
34
|
+
try {
|
|
35
|
+
const text = await res.text();
|
|
36
|
+
if (text.length > 0)
|
|
37
|
+
bodyHint = text;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
/* ignore body read errors */
|
|
41
|
+
}
|
|
42
|
+
logHydrationFailure('http', {
|
|
43
|
+
status: res.status,
|
|
44
|
+
message: bodyHint,
|
|
45
|
+
retryable: isRetryableHttpStatus(res.status),
|
|
46
|
+
});
|
|
7
47
|
return null;
|
|
48
|
+
}
|
|
8
49
|
const json = (await res.json());
|
|
9
|
-
if (!json.success || !json.data)
|
|
50
|
+
if (!json.success || !json.data) {
|
|
51
|
+
logHydrationFailure('api', {
|
|
52
|
+
message: json.success === false ? 'AIS full-context success=false' : 'AIS full-context missing data',
|
|
53
|
+
retryable: false,
|
|
54
|
+
});
|
|
10
55
|
return null;
|
|
56
|
+
}
|
|
11
57
|
const d = json.data;
|
|
12
58
|
const identity = {
|
|
13
59
|
id: d.identity?.id ?? '',
|
|
@@ -55,7 +101,12 @@ export async function hydrateIdentity(aisUrl, agentId, token) {
|
|
|
55
101
|
totalTokens: d.context?.totalTokens ?? 0,
|
|
56
102
|
};
|
|
57
103
|
}
|
|
58
|
-
catch {
|
|
104
|
+
catch (err) {
|
|
105
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
106
|
+
logHydrationFailure('network', {
|
|
107
|
+
message,
|
|
108
|
+
retryable: isRetryableHydrationError(err),
|
|
109
|
+
});
|
|
59
110
|
return null;
|
|
60
111
|
}
|
|
61
112
|
}
|
package/dist/hydration.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hydration.js","sourceRoot":"","sources":["../src/hydration.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAc,EACd,OAAe,EACf,KAAa;IAEb,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,MAAM,cAAc,OAAO,eAAe,EAAE;YACrE,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE;SAC9C,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,EAAE;
|
|
1
|
+
{"version":3,"file":"hydration.js","sourceRoot":"","sources":["../src/hydration.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -9,37 +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 { tryKeyAuth } from './key-auth.js';
|
|
13
|
+
import { isCredsStale, isAuthErrorResult } from './auth-staleness.js';
|
|
12
14
|
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
13
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
14
|
-
import { join } from 'path';
|
|
15
15
|
import { homedir } from 'os';
|
|
16
|
+
import { loadOAuthCredentials, saveOAuthCredentials, } from './oauth-credentials.js';
|
|
16
17
|
import { hydrateIdentity } from './hydration.js';
|
|
17
18
|
import { assemblePrompt } from './prompt-resource.js';
|
|
18
19
|
import { startRefreshTimer, stopRefreshTimer } from './refresh.js';
|
|
19
20
|
import { ActivityTracker, setupAutoHandoff } from './auto-handoff.js';
|
|
21
|
+
import { assertEnvAgentInTenant } from './env-agent.js';
|
|
22
|
+
import { emitTelemetry, hashAgentId } from './telemetry.js';
|
|
23
|
+
import { parseAgentLoadArgs, parseEmptyToolArgs, parseHandoffArgs, parseRecallArgs, parseRememberArgs, } from './tool-args.js';
|
|
20
24
|
// ── Config ──────────────────────────────────────────────
|
|
21
25
|
const AIS_URL = process.env['AIS_URL'] ?? 'https://ais.agentsandswarms.ai';
|
|
22
|
-
const CREDS_DIR = join(homedir(), '.aismemory');
|
|
23
|
-
const CREDS_FILE = join(CREDS_DIR, 'credentials.json');
|
|
24
26
|
// ── Credential persistence ──────────────────────────────
|
|
25
27
|
function loadCredentials() {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
stderr('Saved credentials expired. Re-authenticating...');
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
return data;
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
28
|
+
const data = loadOAuthCredentials();
|
|
29
|
+
if (!data)
|
|
30
|
+
return null;
|
|
31
|
+
if (isCredsStale(data.expiresAt, Date.now())) {
|
|
32
|
+
stderr('Saved credentials expired or expiring soon. Re-authenticating...');
|
|
37
33
|
return null;
|
|
38
34
|
}
|
|
35
|
+
return data;
|
|
39
36
|
}
|
|
40
37
|
function saveCredentials(creds) {
|
|
41
|
-
|
|
42
|
-
writeFileSync(CREDS_FILE, JSON.stringify(creds, null, 2));
|
|
38
|
+
saveOAuthCredentials(creds);
|
|
43
39
|
}
|
|
44
40
|
// ── HTTP helpers ────────────────────────────────────────
|
|
45
41
|
function stderr(msg) {
|
|
@@ -49,17 +45,55 @@ async function aisPost(path, body, token) {
|
|
|
49
45
|
const headers = { 'Content-Type': 'application/json' };
|
|
50
46
|
if (token)
|
|
51
47
|
headers['Authorization'] = `Bearer ${token}`;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
let res;
|
|
49
|
+
try {
|
|
50
|
+
res = await fetch(`${AIS_URL}${path}`, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers,
|
|
53
|
+
body: JSON.stringify(body),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
emitTelemetry({
|
|
58
|
+
event: 'ais.http.failure',
|
|
59
|
+
path,
|
|
60
|
+
error: err instanceof Error ? err.message : String(err),
|
|
61
|
+
});
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
emitTelemetry({
|
|
66
|
+
event: 'ais.http.failure',
|
|
67
|
+
path,
|
|
68
|
+
httpStatus: res.status,
|
|
69
|
+
error: `HTTP ${res.status}`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
57
72
|
return res.json();
|
|
58
73
|
}
|
|
59
74
|
async function aisGet(path, token) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
75
|
+
let res;
|
|
76
|
+
try {
|
|
77
|
+
res = await fetch(`${AIS_URL}${path}`, {
|
|
78
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
emitTelemetry({
|
|
83
|
+
event: 'ais.http.failure',
|
|
84
|
+
path,
|
|
85
|
+
error: err instanceof Error ? err.message : String(err),
|
|
86
|
+
});
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
emitTelemetry({
|
|
91
|
+
event: 'ais.http.failure',
|
|
92
|
+
path,
|
|
93
|
+
httpStatus: res.status,
|
|
94
|
+
error: `HTTP ${res.status}`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
63
97
|
return res.json();
|
|
64
98
|
}
|
|
65
99
|
async function resolveAgentForLoad(token, query) {
|
|
@@ -95,11 +129,21 @@ async function resolveAgentForLoad(token, query) {
|
|
|
95
129
|
* User can override with AIS_AGENT_ID or AIS_AGENT_NAME env vars.
|
|
96
130
|
*/
|
|
97
131
|
async function resolveAgent(token, fallbackAgentId, fallbackTenantId) {
|
|
98
|
-
// Explicit env var overrides everything
|
|
132
|
+
// Explicit env var overrides everything — must belong to this tenant.
|
|
99
133
|
const envAgentId = process.env['AIS_AGENT_ID'];
|
|
100
134
|
if (envAgentId) {
|
|
101
|
-
|
|
102
|
-
|
|
135
|
+
const listRes = (await aisGet('/v1/agents', token));
|
|
136
|
+
const agents = listRes.data?.agents ?? [];
|
|
137
|
+
try {
|
|
138
|
+
const match = assertEnvAgentInTenant(envAgentId, agents);
|
|
139
|
+
stderr(`Using agent from AIS_AGENT_ID: ${match.name} (${match.id})`);
|
|
140
|
+
return { agentId: match.id, tenantId: fallbackTenantId };
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
144
|
+
stderr(message);
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
103
147
|
}
|
|
104
148
|
try {
|
|
105
149
|
const res = await aisGet('/v1/agents', token);
|
|
@@ -154,6 +198,7 @@ class BondPendingError extends Error {
|
|
|
154
198
|
}
|
|
155
199
|
}
|
|
156
200
|
async function startDeviceFlow() {
|
|
201
|
+
emitTelemetry({ event: 'auth.device_flow.start' });
|
|
157
202
|
stderr('Starting authentication...');
|
|
158
203
|
const authRes = (await aisPost('/v1/oauth/device/authorize', {
|
|
159
204
|
name: `agent-${homedir().split('/').pop() ?? 'user'}`,
|
|
@@ -170,14 +215,24 @@ async function startDeviceFlow() {
|
|
|
170
215
|
stderr(` Code : ${user_code}`);
|
|
171
216
|
stderr('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
172
217
|
const poll = pollForToken(device_code, interval, agent_id, provisional_tenant_id);
|
|
173
|
-
|
|
218
|
+
const flow = {
|
|
174
219
|
device_code,
|
|
175
220
|
user_code,
|
|
176
221
|
verification_uri_complete,
|
|
177
222
|
agent_id,
|
|
178
223
|
provisional_tenant_id,
|
|
179
224
|
poll,
|
|
225
|
+
dead: false,
|
|
180
226
|
};
|
|
227
|
+
// Mark the flow dead as soon as its poll rejects so the next
|
|
228
|
+
// `ensureCredentials` call starts a fresh flow. The `.catch` here is purely
|
|
229
|
+
// for state-tracking — the original rejection is still observed by callers
|
|
230
|
+
// racing `flow.poll`. We swallow on this branch to suppress unhandled
|
|
231
|
+
// rejection warnings between rejection and the next ensureCredentials call.
|
|
232
|
+
poll.catch(() => {
|
|
233
|
+
flow.dead = true;
|
|
234
|
+
});
|
|
235
|
+
return flow;
|
|
181
236
|
}
|
|
182
237
|
async function pollForToken(device_code, interval, agent_id, provisional_tenant_id) {
|
|
183
238
|
const pollInterval = Math.max(interval, 5) * 1000;
|
|
@@ -284,7 +339,33 @@ const tools = [
|
|
|
284
339
|
},
|
|
285
340
|
];
|
|
286
341
|
// ── MCP Server ──────────────────────────────────────────
|
|
342
|
+
/**
|
|
343
|
+
* Decode the user_id + tenant_id claims from a (presumed valid) JWT without
|
|
344
|
+
* verifying the signature. Used to derive bootstrap context after a successful
|
|
345
|
+
* key-auth handshake — we trust the token because the server just minted it.
|
|
346
|
+
*/
|
|
347
|
+
function decodeJwtClaims(token) {
|
|
348
|
+
const parts = token.split('.');
|
|
349
|
+
if (parts.length !== 3)
|
|
350
|
+
return {};
|
|
351
|
+
try {
|
|
352
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
|
|
353
|
+
return { userId: payload.user_id ?? payload.sub, tenantId: payload.tenant_id };
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return {};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
287
359
|
async function main() {
|
|
360
|
+
// Subcommand dispatch — CLI entrypoints exit before MCP server startup.
|
|
361
|
+
if (process.argv[2] === 'enable-key-auth') {
|
|
362
|
+
await import('./cli/enable-key-auth.js');
|
|
363
|
+
return; // CLI calls process.exit() itself
|
|
364
|
+
}
|
|
365
|
+
if (process.argv[2] === 'sync') {
|
|
366
|
+
await import('./cli/sync-memory.js');
|
|
367
|
+
return; // sync-memory.ts calls process.exit() on failure
|
|
368
|
+
}
|
|
288
369
|
// Lazy-auth state. We connect the MCP transport before the user has approved
|
|
289
370
|
// the device flow so Claude Code's 30s connection timeout is not blocked on
|
|
290
371
|
// human approval. The first tool call surfaces the bond URL via its result.
|
|
@@ -292,22 +373,82 @@ async function main() {
|
|
|
292
373
|
let activeFlow = null;
|
|
293
374
|
let postAuthDone = false;
|
|
294
375
|
// Created here so `agent_load` and post-auth setup can reference it via closure.
|
|
295
|
-
const server = new Server({ name: 'aismemory', version: '0.
|
|
376
|
+
const server = new Server({ name: 'aismemory', version: '0.5.0' }, { capabilities: { tools: {}, prompts: {} } });
|
|
296
377
|
/**
|
|
297
378
|
* Wait briefly for the in-flight device-flow poll to complete. If the user
|
|
298
379
|
* has already approved, this returns the fresh credentials. If not, throws
|
|
299
380
|
* BondPendingError so the calling tool returns the bond URL to the agent.
|
|
381
|
+
*
|
|
382
|
+
* Recovery: if the previous flow's poll rejected (expired_token,
|
|
383
|
+
* access_denied, 30-min deadline, etc.), the dead flow is discarded and a
|
|
384
|
+
* new device flow is started inline — the user gets a fresh bond URL on
|
|
385
|
+
* the next tool call without having to restart the MCP process.
|
|
300
386
|
*/
|
|
301
387
|
async function ensureCredentials(softTimeoutMs = 7000) {
|
|
302
|
-
if (creds)
|
|
303
|
-
|
|
304
|
-
|
|
388
|
+
if (creds) {
|
|
389
|
+
if (!isCredsStale(creds.expiresAt, Date.now()))
|
|
390
|
+
return creds;
|
|
391
|
+
// CORBOT-A5BBD580: tokens live 7 days but the process can outlive
|
|
392
|
+
// them. Drop stale creds and fall through to silent key auth.
|
|
393
|
+
stderr('Cached credentials are expired or expiring soon — re-authenticating...');
|
|
394
|
+
creds = null;
|
|
395
|
+
}
|
|
396
|
+
// ── Key auth (Phase 1 DID auth) ───────────────────────────────────────
|
|
397
|
+
// If the user has enrolled a private key for this device, sign a
|
|
398
|
+
// server-issued challenge and skip the device-flow ceremony entirely.
|
|
399
|
+
// tryKeyAuth() returns null if no key file is present (clean fallback
|
|
400
|
+
// to device flow) and throws only on real failures (e.g. server says
|
|
401
|
+
// INVALID_SIGNATURE). On throw we log and continue to device flow —
|
|
402
|
+
// device flow is the recovery path for "user wiped their key file".
|
|
403
|
+
try {
|
|
404
|
+
const keyResult = await tryKeyAuth({ aisUrl: AIS_URL });
|
|
405
|
+
if (keyResult) {
|
|
406
|
+
const claims = decodeJwtClaims(keyResult.token);
|
|
407
|
+
const tenantId = claims.tenantId ?? '';
|
|
408
|
+
const resolved = await resolveAgent(keyResult.token, '', tenantId);
|
|
409
|
+
creds = {
|
|
410
|
+
token: keyResult.token,
|
|
411
|
+
agentId: resolved.agentId,
|
|
412
|
+
tenantId: resolved.tenantId || tenantId,
|
|
413
|
+
expiresAt: keyResult.expiresAt,
|
|
414
|
+
};
|
|
415
|
+
saveCredentials(creds);
|
|
416
|
+
emitTelemetry({
|
|
417
|
+
event: 'auth.key.success',
|
|
418
|
+
agentIdHash: hashAgentId(creds.agentId),
|
|
419
|
+
});
|
|
420
|
+
stderr('Authenticated via DID key auth (no browser required)');
|
|
421
|
+
if (!postAuthDone) {
|
|
422
|
+
postAuthDone = true;
|
|
423
|
+
void runPostAuth(creds);
|
|
424
|
+
}
|
|
425
|
+
return creds;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
emitTelemetry({
|
|
430
|
+
event: 'auth.key.fallback',
|
|
431
|
+
error: err instanceof Error ? err.message : String(err),
|
|
432
|
+
});
|
|
433
|
+
stderr(`Key auth failed: ${err instanceof Error ? err.message : String(err)}. Falling back to device flow.`);
|
|
434
|
+
}
|
|
435
|
+
if (!activeFlow || activeFlow.dead) {
|
|
305
436
|
activeFlow = await startDeviceFlow();
|
|
437
|
+
}
|
|
306
438
|
const sentinel = Symbol('bond-pending');
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
439
|
+
let got;
|
|
440
|
+
try {
|
|
441
|
+
got = await Promise.race([
|
|
442
|
+
activeFlow.poll,
|
|
443
|
+
new Promise((r) => setTimeout(() => r(sentinel), softTimeoutMs)),
|
|
444
|
+
]);
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
// Poll rejected (deadline / expired / denied). Start a fresh flow
|
|
448
|
+
// immediately and surface its new bond URL — no restart required.
|
|
449
|
+
activeFlow = await startDeviceFlow();
|
|
450
|
+
throw new BondPendingError(activeFlow.verification_uri_complete, activeFlow.user_code);
|
|
451
|
+
}
|
|
311
452
|
if (got === sentinel) {
|
|
312
453
|
throw new BondPendingError(activeFlow.verification_uri_complete, activeFlow.user_code);
|
|
313
454
|
}
|
|
@@ -319,6 +460,16 @@ async function main() {
|
|
|
319
460
|
}
|
|
320
461
|
return creds;
|
|
321
462
|
}
|
|
463
|
+
/** Poll AIS for recent memories and refresh the identity prompt. */
|
|
464
|
+
function applyMemoryRefresh(agentId, token) {
|
|
465
|
+
startRefreshTimer(AIS_URL, agentId, token, (memories) => {
|
|
466
|
+
if (agentContext) {
|
|
467
|
+
agentContext.recentMemories = memories;
|
|
468
|
+
}
|
|
469
|
+
server.notification({ method: 'notifications/prompts/list_changed' });
|
|
470
|
+
stderr(`Memory refresh: ${memories.length} memories`);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
322
473
|
/** Identity hydration, prompt refresh, and auto-handoff — all post-auth. */
|
|
323
474
|
async function runPostAuth(c) {
|
|
324
475
|
try {
|
|
@@ -330,13 +481,7 @@ async function main() {
|
|
|
330
481
|
else {
|
|
331
482
|
stderr('Agent identity not loaded — tools work, but no identity prompt');
|
|
332
483
|
}
|
|
333
|
-
|
|
334
|
-
if (agentContext) {
|
|
335
|
-
agentContext.recentMemories = memories;
|
|
336
|
-
}
|
|
337
|
-
server.notification({ method: 'notifications/prompts/list_changed' });
|
|
338
|
-
stderr(`Memory refresh: ${memories.length} memories`);
|
|
339
|
-
});
|
|
484
|
+
applyMemoryRefresh(c.agentId, c.token);
|
|
340
485
|
setupAutoHandoff(AIS_URL, c.agentId, c.token, activityTracker, () => {
|
|
341
486
|
stopRefreshTimer();
|
|
342
487
|
stderr('Session handoff saved');
|
|
@@ -352,94 +497,145 @@ async function main() {
|
|
|
352
497
|
? [{ name: 'agent-identity', description: 'Active AIS agent identity, personality, and context' }]
|
|
353
498
|
: [],
|
|
354
499
|
}));
|
|
355
|
-
server.setRequestHandler(GetPromptRequestSchema, async () =>
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
500
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
501
|
+
const { name } = request.params;
|
|
502
|
+
if (name !== 'agent-identity') {
|
|
503
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
504
|
+
}
|
|
505
|
+
return {
|
|
506
|
+
messages: [{
|
|
507
|
+
role: 'user',
|
|
508
|
+
content: { type: 'text', text: assemblePrompt(agentContext) },
|
|
509
|
+
}],
|
|
510
|
+
};
|
|
511
|
+
});
|
|
361
512
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
362
513
|
const { name, arguments: args } = request.params;
|
|
363
|
-
const
|
|
514
|
+
const rawArgs = args ?? {};
|
|
364
515
|
activityTracker.recordToolUse(name);
|
|
516
|
+
const toolStartedAt = Date.now();
|
|
365
517
|
try {
|
|
366
|
-
const
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
params
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
518
|
+
const UNKNOWN_TOOL = Symbol('unknown-tool');
|
|
519
|
+
const dispatchTool = async (c) => {
|
|
520
|
+
const token = c.token;
|
|
521
|
+
// agentId is mutable across `agent_load` calls within this session.
|
|
522
|
+
let agentId = c.agentId;
|
|
523
|
+
let result;
|
|
524
|
+
switch (name) {
|
|
525
|
+
case 'remember': {
|
|
526
|
+
const rememberArgs = parseRememberArgs(rawArgs);
|
|
527
|
+
result = await aisPost(`/v1/agents/${agentId}/memory`, {
|
|
528
|
+
content: rememberArgs.content,
|
|
529
|
+
type: rememberArgs.type,
|
|
530
|
+
importance: rememberArgs.importance,
|
|
531
|
+
}, token);
|
|
532
|
+
activityTracker.recordMemoryStored(rememberArgs.content);
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
case 'recall': {
|
|
536
|
+
const recallArgs = parseRecallArgs(rawArgs);
|
|
537
|
+
const params = new URLSearchParams();
|
|
538
|
+
params.set('query', recallArgs.query);
|
|
539
|
+
if (recallArgs.limit !== undefined)
|
|
540
|
+
params.set('limit', String(recallArgs.limit));
|
|
541
|
+
if (recallArgs.type !== undefined)
|
|
542
|
+
params.set('types', recallArgs.type);
|
|
543
|
+
result = await aisGet(`/v1/agents/${agentId}/memory?${params}`, token);
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
case 'whoami':
|
|
547
|
+
parseEmptyToolArgs(rawArgs);
|
|
548
|
+
result = await aisGet(`/v1/agents/${agentId}`, token);
|
|
549
|
+
break;
|
|
550
|
+
case 'full_context':
|
|
551
|
+
parseEmptyToolArgs(rawArgs);
|
|
552
|
+
result = await aisGet(`/v1/agents/${agentId}/full-context`, token);
|
|
553
|
+
break;
|
|
554
|
+
case 'dream':
|
|
555
|
+
parseEmptyToolArgs(rawArgs);
|
|
556
|
+
result = await aisPost(`/v1/agents/${agentId}/dream`, {}, token);
|
|
557
|
+
break;
|
|
558
|
+
case 'handoff': {
|
|
559
|
+
const handoffArgs = parseHandoffArgs(rawArgs);
|
|
560
|
+
result = await aisPost(`/v1/agents/${agentId}/handoff`, {
|
|
561
|
+
summary: handoffArgs.summary ?? 'Session ended',
|
|
562
|
+
keyLearnings: handoffArgs.keyLearnings ?? [],
|
|
563
|
+
}, token);
|
|
564
|
+
break;
|
|
410
565
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
566
|
+
case 'agent_load': {
|
|
567
|
+
const { query } = parseAgentLoadArgs(rawArgs);
|
|
568
|
+
const target = await resolveAgentForLoad(token, query);
|
|
569
|
+
agentId = target.id;
|
|
570
|
+
// Persist the switch so subsequent tool calls use the new agent.
|
|
571
|
+
if (creds) {
|
|
572
|
+
creds = { ...creds, agentId };
|
|
573
|
+
}
|
|
574
|
+
agentContext = await hydrateIdentity(AIS_URL, agentId, token);
|
|
575
|
+
applyMemoryRefresh(agentId, token);
|
|
576
|
+
server.notification({ method: 'notifications/prompts/list_changed' });
|
|
577
|
+
stderr(`agent_load: switched to "${target.name}" (${target.id})`);
|
|
578
|
+
result = {
|
|
579
|
+
success: true,
|
|
580
|
+
agentId: target.id,
|
|
581
|
+
name: target.name,
|
|
582
|
+
did: agentContext?.identity.did ?? null,
|
|
583
|
+
status: target.status,
|
|
584
|
+
memoriesLoaded: agentContext?.recentMemories.length ?? 0,
|
|
585
|
+
goalsLoaded: agentContext?.goals.length ?? 0,
|
|
586
|
+
};
|
|
587
|
+
break;
|
|
416
588
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
goalsLoaded: agentContext?.goals.length ?? 0,
|
|
428
|
-
};
|
|
429
|
-
break;
|
|
589
|
+
default:
|
|
590
|
+
emitTelemetry({
|
|
591
|
+
event: 'tool.call',
|
|
592
|
+
tool: name,
|
|
593
|
+
agentIdHash: hashAgentId(agentId),
|
|
594
|
+
latencyMs: Date.now() - toolStartedAt,
|
|
595
|
+
status: 'error',
|
|
596
|
+
error: `Unknown tool: ${name}`,
|
|
597
|
+
});
|
|
598
|
+
return UNKNOWN_TOOL;
|
|
430
599
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
600
|
+
return result;
|
|
601
|
+
};
|
|
602
|
+
let result = await dispatchTool(await ensureCredentials());
|
|
603
|
+
if (isAuthErrorResult(result)) {
|
|
604
|
+
// CORBOT-A5BBD580: the server rejected our token mid-session
|
|
605
|
+
// (expired or secret rotated). Clear the cached creds, silently
|
|
606
|
+
// re-auth (key auth when a key file exists), and retry once.
|
|
607
|
+
stderr('AIS rejected the cached token; re-authenticating and retrying once...');
|
|
608
|
+
creds = null;
|
|
609
|
+
result = await dispatchTool(await ensureCredentials());
|
|
436
610
|
}
|
|
611
|
+
if (result === UNKNOWN_TOOL) {
|
|
612
|
+
return {
|
|
613
|
+
content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${name}` }) }],
|
|
614
|
+
isError: true,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
emitTelemetry({
|
|
618
|
+
event: 'tool.call',
|
|
619
|
+
tool: name,
|
|
620
|
+
agentIdHash: creds ? hashAgentId(creds.agentId) : undefined,
|
|
621
|
+
latencyMs: Date.now() - toolStartedAt,
|
|
622
|
+
status: 'ok',
|
|
623
|
+
});
|
|
437
624
|
return {
|
|
438
625
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
439
626
|
};
|
|
440
627
|
}
|
|
441
628
|
catch (error) {
|
|
442
629
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
630
|
+
const agentHash = creds ? hashAgentId(creds.agentId) : undefined;
|
|
631
|
+
emitTelemetry({
|
|
632
|
+
event: 'tool.call',
|
|
633
|
+
tool: name,
|
|
634
|
+
agentIdHash: agentHash,
|
|
635
|
+
latencyMs: Date.now() - toolStartedAt,
|
|
636
|
+
status: 'error',
|
|
637
|
+
error: message,
|
|
638
|
+
});
|
|
443
639
|
// BondPendingError carries human-readable text that the agent should
|
|
444
640
|
// render verbatim — return as plain text rather than JSON.
|
|
445
641
|
if (error instanceof BondPendingError) {
|