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.
Files changed (89) hide show
  1. package/dist/__tests__/auth-staleness.test.d.ts +9 -0
  2. package/dist/__tests__/auth-staleness.test.js +46 -0
  3. package/dist/__tests__/auth-staleness.test.js.map +1 -0
  4. package/dist/__tests__/auto-handoff.test.js +108 -2
  5. package/dist/__tests__/auto-handoff.test.js.map +1 -1
  6. package/dist/__tests__/config.test.js +6 -3
  7. package/dist/__tests__/config.test.js.map +1 -1
  8. package/dist/__tests__/device-flow-recovery.test.d.ts +1 -0
  9. package/dist/__tests__/device-flow-recovery.test.js +206 -0
  10. package/dist/__tests__/device-flow-recovery.test.js.map +1 -0
  11. package/dist/__tests__/env-agent.test.d.ts +1 -0
  12. package/dist/__tests__/env-agent.test.js +19 -0
  13. package/dist/__tests__/env-agent.test.js.map +1 -0
  14. package/dist/__tests__/existing-hashes.test.d.ts +1 -0
  15. package/dist/__tests__/existing-hashes.test.js +122 -0
  16. package/dist/__tests__/existing-hashes.test.js.map +1 -0
  17. package/dist/__tests__/hydration.test.js +38 -0
  18. package/dist/__tests__/hydration.test.js.map +1 -1
  19. package/dist/__tests__/key-auth.test.d.ts +1 -0
  20. package/dist/__tests__/key-auth.test.js +284 -0
  21. package/dist/__tests__/key-auth.test.js.map +1 -0
  22. package/dist/__tests__/local-mirror.test.js +4 -0
  23. package/dist/__tests__/local-mirror.test.js.map +1 -1
  24. package/dist/__tests__/oauth-credentials.test.d.ts +1 -0
  25. package/dist/__tests__/oauth-credentials.test.js +29 -0
  26. package/dist/__tests__/oauth-credentials.test.js.map +1 -0
  27. package/dist/__tests__/pipeline-ingestion.test.js +112 -1
  28. package/dist/__tests__/pipeline-ingestion.test.js.map +1 -1
  29. package/dist/__tests__/refresh.test.js +24 -0
  30. package/dist/__tests__/refresh.test.js.map +1 -1
  31. package/dist/__tests__/sync-memory-cli.test.d.ts +1 -0
  32. package/dist/__tests__/sync-memory-cli.test.js +200 -0
  33. package/dist/__tests__/sync-memory-cli.test.js.map +1 -0
  34. package/dist/__tests__/telemetry.test.d.ts +1 -0
  35. package/dist/__tests__/telemetry.test.js +67 -0
  36. package/dist/__tests__/telemetry.test.js.map +1 -0
  37. package/dist/__tests__/token-expiry-reauth.test.d.ts +1 -0
  38. package/dist/__tests__/token-expiry-reauth.test.js +201 -0
  39. package/dist/__tests__/token-expiry-reauth.test.js.map +1 -0
  40. package/dist/__tests__/tool-args.test.d.ts +1 -0
  41. package/dist/__tests__/tool-args.test.js +78 -0
  42. package/dist/__tests__/tool-args.test.js.map +1 -0
  43. package/dist/auth-staleness.d.ts +27 -0
  44. package/dist/auth-staleness.js +41 -0
  45. package/dist/auth-staleness.js.map +1 -0
  46. package/dist/auto-handoff.d.ts +2 -0
  47. package/dist/auto-handoff.js +40 -16
  48. package/dist/auto-handoff.js.map +1 -1
  49. package/dist/cli/enable-key-auth.d.ts +1 -0
  50. package/dist/cli/enable-key-auth.js +131 -0
  51. package/dist/cli/enable-key-auth.js.map +1 -0
  52. package/dist/cli/sync-memory.js +31 -36
  53. package/dist/cli/sync-memory.js.map +1 -1
  54. package/dist/config.js +4 -1
  55. package/dist/config.js.map +1 -1
  56. package/dist/env-agent.d.ts +10 -0
  57. package/dist/env-agent.js +14 -0
  58. package/dist/env-agent.js.map +1 -0
  59. package/dist/hydration.js +54 -3
  60. package/dist/hydration.js.map +1 -1
  61. package/dist/index.js +314 -118
  62. package/dist/index.js.map +1 -1
  63. package/dist/key-auth.d.ts +66 -0
  64. package/dist/key-auth.js +179 -0
  65. package/dist/key-auth.js.map +1 -0
  66. package/dist/local-mirror.d.ts +5 -0
  67. package/dist/local-mirror.js +72 -14
  68. package/dist/local-mirror.js.map +1 -1
  69. package/dist/oauth-credentials.d.ts +14 -0
  70. package/dist/oauth-credentials.js +35 -0
  71. package/dist/oauth-credentials.js.map +1 -0
  72. package/dist/pipeline/bulk-store.d.ts +46 -0
  73. package/dist/pipeline/bulk-store.js +165 -0
  74. package/dist/pipeline/bulk-store.js.map +1 -0
  75. package/dist/pipeline/existing-hashes.d.ts +20 -0
  76. package/dist/pipeline/existing-hashes.js +111 -0
  77. package/dist/pipeline/existing-hashes.js.map +1 -0
  78. package/dist/pipeline/ingestion.d.ts +8 -4
  79. package/dist/pipeline/ingestion.js +36 -8
  80. package/dist/pipeline/ingestion.js.map +1 -1
  81. package/dist/telemetry.d.ts +22 -0
  82. package/dist/telemetry.js +28 -0
  83. package/dist/telemetry.js.map +1 -0
  84. package/dist/tool-args.d.ts +52 -0
  85. package/dist/tool-args.js +78 -0
  86. package/dist/tool-args.js.map +1 -0
  87. package/dist/trust-ledger.js +2 -2
  88. package/dist/trust-ledger.js.map +1 -1
  89. 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
  }
@@ -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;YAAE,OAAO,IAAI,CAAC;QACzB,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAqB7B,CAAC;QACF,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC;QAE7C,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC;QAEpB,MAAM,QAAQ,GAAG;YACf,EAAE,EAAE,CAAC,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE;YACxB,GAAG,EAAE,CAAC,CAAC,QAAQ,EAAE,GAAG,IAAI,EAAE;YAC1B,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE,IAAI,IAAI,EAAE;YAC5B,WAAW,EAAE,CAAC,CAAC,QAAQ,EAAE,WAAW,IAAI,EAAE;YAC1C,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE;SACjC,CAAC;QAEF,MAAM,WAAW,GACf,CAAC,CAAC,WAAW,IAAI,IAAI;YACnB,CAAC,CAAC;gBACE,MAAM,EAAE,CAAC,CAAC,WAAW,CAAC,MAAM,IAAI,EAAE;gBAClC,kBAAkB,EAAE,CAAC,CAAC,WAAW,CAAC,kBAAkB,IAAI,EAAE;gBAC1D,eAAe,EAAE,CAAC,CAAC,WAAW,CAAC,eAAe,IAAI,EAAE;aACrD;YACH,CAAC,CAAC,IAAI,CAAC;QAEX,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACxC,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;YACpB,QAAQ,EAAE,CAAC,CAAC,QAAQ,IAAI,CAAC;YACzB,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE;SACvB,CAAC,CAAC,CAAC;QAEJ,MAAM,cAAc,GAAG,CAAC,CAAC,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC1D,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE;YACd,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,EAAE;YACxB,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE;YAClB,UAAU,EAAE,CAAC,CAAC,UAAU,IAAI,CAAC;YAC7B,SAAS,EAAE,CAAC,CAAC,SAAS,IAAI,EAAE;SAC7B,CAAC,CAAC,CAAC;QAEJ,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;QAErE,MAAM,WAAW,GACf,CAAC,CAAC,WAAW,IAAI,IAAI;YACnB,CAAC,CAAC;gBACE,OAAO,EAAE,CAAC,CAAC,WAAW,CAAC,OAAO,IAAI,EAAE;gBACpC,YAAY,EAAE,CAAC,CAAC,WAAW,CAAC,YAAY,IAAI,EAAE;gBAC9C,WAAW,EAAE,CAAC,CAAC,WAAW,CAAC,WAAW,IAAI,EAAE;gBAC5C,SAAS,EAAE,CAAC,CAAC,WAAW,CAAC,SAAS,IAAI,EAAE;aACzC;YACH,CAAC,CAAC,IAAI,CAAC;QAEX,OAAO;YACL,QAAQ;YACR,WAAW;YACX,KAAK;YACL,cAAc;YACd,MAAM;YACN,WAAW;YACX,WAAW,EAAE,CAAC,CAAC,WAAW,IAAI,EAAE;YAChC,WAAW,EAAE,CAAC,CAAC,OAAO,EAAE,WAAW,IAAI,CAAC;SACzC,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
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
- try {
27
- if (!existsSync(CREDS_FILE))
28
- return null;
29
- const data = JSON.parse(readFileSync(CREDS_FILE, 'utf-8'));
30
- if (new Date(data.expiresAt) <= new Date()) {
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
- mkdirSync(CREDS_DIR, { recursive: true });
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
- const res = await fetch(`${AIS_URL}${path}`, {
53
- method: 'POST',
54
- headers,
55
- body: JSON.stringify(body),
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
- const res = await fetch(`${AIS_URL}${path}`, {
61
- headers: { 'Authorization': `Bearer ${token}` },
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
- stderr(`Using agent from AIS_AGENT_ID: ${envAgentId}`);
102
- return { agentId: envAgentId, tenantId: fallbackTenantId };
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
- return {
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.4.0' }, { capabilities: { tools: {}, prompts: {} } });
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
- return creds;
304
- if (!activeFlow)
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
- const got = await Promise.race([
308
- activeFlow.poll,
309
- new Promise((r) => setTimeout(() => r(sentinel), softTimeoutMs)),
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
- startRefreshTimer(AIS_URL, c.agentId, c.token, (memories) => {
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
- messages: [{
357
- role: 'user',
358
- content: { type: 'text', text: assemblePrompt(agentContext) },
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 a = (args ?? {});
514
+ const rawArgs = args ?? {};
364
515
  activityTracker.recordToolUse(name);
516
+ const toolStartedAt = Date.now();
365
517
  try {
366
- const c = await ensureCredentials();
367
- const token = c.token;
368
- // agentId is mutable across `agent_load` calls within this session.
369
- let agentId = c.agentId;
370
- let result;
371
- switch (name) {
372
- case 'remember':
373
- result = await aisPost(`/v1/agents/${agentId}/memory`, {
374
- content: a['content'],
375
- type: a['type'] ?? 'context',
376
- importance: a['importance'] ?? 0.5,
377
- }, token);
378
- activityTracker.recordMemoryStored(String(a['content'] ?? ''));
379
- break;
380
- case 'recall': {
381
- const params = new URLSearchParams();
382
- if (a['query'])
383
- params.set('query', String(a['query']));
384
- if (a['limit'])
385
- params.set('limit', String(a['limit']));
386
- if (a['type'])
387
- params.set('types', String(a['type']));
388
- result = await aisGet(`/v1/agents/${agentId}/memory?${params}`, token);
389
- break;
390
- }
391
- case 'whoami':
392
- result = await aisGet(`/v1/agents/${agentId}`, token);
393
- break;
394
- case 'full_context':
395
- result = await aisGet(`/v1/agents/${agentId}/full-context`, token);
396
- break;
397
- case 'dream':
398
- result = await aisPost(`/v1/agents/${agentId}/dream`, {}, token);
399
- break;
400
- case 'handoff':
401
- result = await aisPost(`/v1/agents/${agentId}/handoff`, {
402
- summary: a['summary'] ?? 'Session ended',
403
- keyLearnings: a['keyLearnings'] ?? [],
404
- }, token);
405
- break;
406
- case 'agent_load': {
407
- const query = String(a['query'] ?? '').trim();
408
- if (!query) {
409
- throw new Error('agent_load: query is required');
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
- const target = await resolveAgentForLoad(token, query);
412
- agentId = target.id;
413
- // Persist the switch so subsequent tool calls use the new agent.
414
- if (creds) {
415
- creds = { ...creds, agentId };
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
- agentContext = await hydrateIdentity(AIS_URL, agentId, token);
418
- server.notification({ method: 'notifications/prompts/list_changed' });
419
- stderr(`agent_load: switched to "${target.name}" (${target.id})`);
420
- result = {
421
- success: true,
422
- agentId: target.id,
423
- name: target.name,
424
- did: agentContext?.identity.did ?? null,
425
- status: target.status,
426
- memoriesLoaded: agentContext?.recentMemories.length ?? 0,
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
- default:
432
- return {
433
- content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${name}` }) }],
434
- isError: true,
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) {