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.
Files changed (113) hide show
  1. package/README.md +121 -2
  2. package/dist/__tests__/agent-key-mode-e2e.test.js +78 -0
  3. package/dist/__tests__/agent-key-mode-e2e.test.js.map +1 -1
  4. package/dist/__tests__/agent-key-mode.test.js +123 -0
  5. package/dist/__tests__/agent-key-mode.test.js.map +1 -1
  6. package/dist/__tests__/agent-load-persistence.test.js +25 -2
  7. package/dist/__tests__/agent-load-persistence.test.js.map +1 -1
  8. package/dist/__tests__/agent-preference.test.js +8 -0
  9. package/dist/__tests__/agent-preference.test.js.map +1 -1
  10. package/dist/__tests__/agentmemory.test.d.ts +1 -0
  11. package/dist/__tests__/agentmemory.test.js +73 -0
  12. package/dist/__tests__/agentmemory.test.js.map +1 -0
  13. package/dist/__tests__/ais-http.test.d.ts +1 -0
  14. package/dist/__tests__/ais-http.test.js +139 -0
  15. package/dist/__tests__/ais-http.test.js.map +1 -0
  16. package/dist/__tests__/auto-enroll-key.test.js +28 -4
  17. package/dist/__tests__/auto-enroll-key.test.js.map +1 -1
  18. package/dist/__tests__/beacon-contract.test.d.ts +1 -0
  19. package/dist/__tests__/beacon-contract.test.js +106 -0
  20. package/dist/__tests__/beacon-contract.test.js.map +1 -0
  21. package/dist/__tests__/credential-isolation.test.d.ts +1 -0
  22. package/dist/__tests__/credential-isolation.test.js +61 -0
  23. package/dist/__tests__/credential-isolation.test.js.map +1 -0
  24. package/dist/__tests__/credential-resolution.test.d.ts +1 -0
  25. package/dist/__tests__/credential-resolution.test.js +141 -0
  26. package/dist/__tests__/credential-resolution.test.js.map +1 -0
  27. package/dist/__tests__/delegated-read-args.test.d.ts +9 -0
  28. package/dist/__tests__/delegated-read-args.test.js +80 -0
  29. package/dist/__tests__/delegated-read-args.test.js.map +1 -0
  30. package/dist/__tests__/doctor.test.js +250 -9
  31. package/dist/__tests__/doctor.test.js.map +1 -1
  32. package/dist/__tests__/ender-receipt-auth.test.d.ts +1 -0
  33. package/dist/__tests__/ender-receipt-auth.test.js +26 -0
  34. package/dist/__tests__/ender-receipt-auth.test.js.map +1 -0
  35. package/dist/__tests__/fixtures/beacon-contract.d.ts +66 -0
  36. package/dist/__tests__/fixtures/beacon-contract.js +72 -0
  37. package/dist/__tests__/fixtures/beacon-contract.js.map +1 -0
  38. package/dist/__tests__/inbox-client.test.d.ts +1 -0
  39. package/dist/__tests__/inbox-client.test.js +108 -0
  40. package/dist/__tests__/inbox-client.test.js.map +1 -0
  41. package/dist/__tests__/key-auth.test.js +7 -1
  42. package/dist/__tests__/key-auth.test.js.map +1 -1
  43. package/dist/__tests__/list-all-agents.test.js +2 -0
  44. package/dist/__tests__/list-all-agents.test.js.map +1 -1
  45. package/dist/__tests__/lost-agent.test.d.ts +1 -0
  46. package/dist/__tests__/lost-agent.test.js +108 -0
  47. package/dist/__tests__/lost-agent.test.js.map +1 -0
  48. package/dist/__tests__/memory-write-failure.test.js +5 -1
  49. package/dist/__tests__/memory-write-failure.test.js.map +1 -1
  50. package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.d.ts +1 -0
  51. package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.js +207 -0
  52. package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.js.map +1 -0
  53. package/dist/__tests__/provisional-register-mcp-remember.test.d.ts +1 -0
  54. package/dist/__tests__/provisional-register-mcp-remember.test.js +196 -0
  55. package/dist/__tests__/provisional-register-mcp-remember.test.js.map +1 -0
  56. package/dist/__tests__/readme-tools-parity.test.d.ts +1 -0
  57. package/dist/__tests__/readme-tools-parity.test.js +64 -0
  58. package/dist/__tests__/readme-tools-parity.test.js.map +1 -0
  59. package/dist/__tests__/reconnect-survival-e2e.test.js +63 -1
  60. package/dist/__tests__/reconnect-survival-e2e.test.js.map +1 -1
  61. package/dist/__tests__/token-manager.test.js +20 -1
  62. package/dist/__tests__/token-manager.test.js.map +1 -1
  63. package/dist/__tests__/tool-args.test.js +196 -1
  64. package/dist/__tests__/tool-args.test.js.map +1 -1
  65. package/dist/__tests__/workspace-key-mode.test.d.ts +1 -0
  66. package/dist/__tests__/workspace-key-mode.test.js +264 -0
  67. package/dist/__tests__/workspace-key-mode.test.js.map +1 -0
  68. package/dist/agent-key-mode.d.ts +4 -2
  69. package/dist/agent-key-mode.js +106 -11
  70. package/dist/agent-key-mode.js.map +1 -1
  71. package/dist/ais-http.d.ts +7 -0
  72. package/dist/ais-http.js +137 -0
  73. package/dist/ais-http.js.map +1 -0
  74. package/dist/auth-staleness.d.ts +2 -0
  75. package/dist/auth-staleness.js +5 -0
  76. package/dist/auth-staleness.js.map +1 -1
  77. package/dist/auto-handoff.d.ts +1 -1
  78. package/dist/auto-handoff.js +5 -3
  79. package/dist/auto-handoff.js.map +1 -1
  80. package/dist/cli/doctor.d.ts +4 -0
  81. package/dist/cli/doctor.js +120 -8
  82. package/dist/cli/doctor.js.map +1 -1
  83. package/dist/cli/enable-key-auth.js +4 -5
  84. package/dist/cli/enable-key-auth.js.map +1 -1
  85. package/dist/cli/orientation.d.ts +15 -0
  86. package/dist/cli/orientation.js +89 -0
  87. package/dist/cli/orientation.js.map +1 -0
  88. package/dist/harness-identity.d.ts +17 -0
  89. package/dist/harness-identity.js +105 -0
  90. package/dist/harness-identity.js.map +1 -0
  91. package/dist/hydration.d.ts +1 -1
  92. package/dist/hydration.js +3 -2
  93. package/dist/hydration.js.map +1 -1
  94. package/dist/inbox-client.d.ts +9 -0
  95. package/dist/inbox-client.js +74 -0
  96. package/dist/inbox-client.js.map +1 -0
  97. package/dist/index.js +498 -109
  98. package/dist/index.js.map +1 -1
  99. package/dist/key-auth.js +16 -10
  100. package/dist/key-auth.js.map +1 -1
  101. package/dist/oauth-credentials.d.ts +11 -0
  102. package/dist/oauth-credentials.js +31 -3
  103. package/dist/oauth-credentials.js.map +1 -1
  104. package/dist/token-manager.d.ts +6 -0
  105. package/dist/token-manager.js +16 -0
  106. package/dist/token-manager.js.map +1 -1
  107. package/dist/tool-args.d.ts +98 -0
  108. package/dist/tool-args.js +155 -0
  109. package/dist/tool-args.js.map +1 -1
  110. package/dist/workspace-key-mode.d.ts +40 -0
  111. package/dist/workspace-key-mode.js +258 -0
  112. package/dist/workspace-key-mode.js.map +1 -0
  113. package/package.json +1 -1
@@ -0,0 +1,141 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
3
+ import { createServer } from 'node:http';
4
+ import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { join, resolve } from 'node:path';
7
+ import { describe, expect, it } from 'vitest';
8
+ import { loadAgentApiKeyConfig } from '../agent-key-mode.js';
9
+ import { loadWorkspaceKeyConfig } from '../workspace-key-mode.js';
10
+ import { loadHarnessIdentity } from '../harness-identity.js';
11
+ const agentId = '11111111-1111-4111-8111-111111111111';
12
+ describe('scoped credential aliases', () => {
13
+ it('accepts AIS_API_KEY for a single agent and trims it', () => {
14
+ expect(loadAgentApiKeyConfig({ AIS_API_KEY: ' ais_ak_test\n', AIS_AGENT_ID: agentId }))
15
+ .toEqual({ apiKey: 'ais_ak_test', agentId });
16
+ });
17
+ it('accepts a workspace key through AIS_API_KEY', () => {
18
+ expect(loadWorkspaceKeyConfig({ AIS_API_KEY: ' ais_wk_test\n', AIS_AGENT_ID: agentId }))
19
+ .toEqual({ workspaceKey: 'ais_wk_test', agentId });
20
+ });
21
+ it('prefers the explicit workspace key over the generic alias', () => {
22
+ expect(loadWorkspaceKeyConfig({ AIS_WORKSPACE_KEY: 'ais_wk_primary', AIS_API_KEY: 'ais_wk_other', AIS_AGENT_ID: agentId }))
23
+ .toEqual({ workspaceKey: 'ais_wk_primary', agentId });
24
+ });
25
+ it('prefers AIS_API_KEY over the legacy single-agent alias', () => {
26
+ expect(loadAgentApiKeyConfig({ AIS_API_KEY: 'ais_ak_primary', AIS_AGENT_API_KEY: 'ais_ak_other', AIS_AGENT_ID: agentId }))
27
+ .toEqual({ apiKey: 'ais_ak_primary', agentId });
28
+ });
29
+ it.each(['', ' ', 'service-test-sentinel', 'eyJ.fake.jwt'])('rejects an invalid generic key without falling through (%s)', (key) => {
30
+ expect(() => loadAgentApiKeyConfig({ AIS_API_KEY: key, AIS_AGENT_ID: agentId })).toThrow(/AIS_API_KEY/);
31
+ });
32
+ it('names the missing identity for either generic key type', () => {
33
+ expect(() => loadAgentApiKeyConfig({ AIS_API_KEY: 'ais_ak_test' })).toThrow(/AIS_API_KEY is set but AIS_AGENT_ID is missing/);
34
+ expect(() => loadWorkspaceKeyConfig({ AIS_API_KEY: 'ais_wk_test' })).toThrow(/AIS_API_KEY is set but AIS_AGENT_ID is missing/);
35
+ });
36
+ it('selects agent key mode when AIS_AGENT_ID arrives with injection whitespace', () => {
37
+ // main() resolves the harness identity BEFORE its agent-API-key check and
38
+ // rebuilds the adapter env from it, so an untrimmed AIS_AGENT_ID used to
39
+ // abort the process there and agent key mode never ran (CORBOT-D8D35CF0).
40
+ const env = { AIS_API_KEY: 'ais_ak_test', AIS_AGENT_ID: ` ${agentId}\n` };
41
+ const identity = loadHarnessIdentity([], env);
42
+ expect(identity).toEqual({ version: 1, harness: 'env-pin', agentId });
43
+ const adapterEnv = { ...env, AIS_AGENT_ID: identity?.agentId };
44
+ expect(loadAgentApiKeyConfig(adapterEnv)).toEqual({ apiKey: 'ais_ak_test', agentId });
45
+ });
46
+ it('treats a blank AIS_AGENT_ID as absent so the key loader names it', () => {
47
+ expect(loadHarnessIdentity([], { AIS_AGENT_ID: ' ' })).toBeNull();
48
+ expect(() => loadAgentApiKeyConfig({ AIS_API_KEY: 'ais_ak_test', AIS_AGENT_ID: undefined }))
49
+ .toThrow(/AIS_API_KEY is set but AIS_AGENT_ID is missing/);
50
+ });
51
+ it('ignores infrastructure credentials', () => {
52
+ const env = { AIS_SERVICE_KEY: 'service-test-sentinel', DATABASE_URL: 'database-test-sentinel' };
53
+ expect(loadWorkspaceKeyConfig(env)).toBeNull();
54
+ expect(loadAgentApiKeyConfig(env)).toBeNull();
55
+ });
56
+ });
57
+ describe('real MCP credential resolution', () => {
58
+ it.each(['workspace', 'agent', 'agent-padded', 'cache', 'cold'])('%s path uses only the selected credential and gives actionable errors', async (mode) => {
59
+ const taskHome = mkdtempSync(join(tmpdir(), 'ais-credential-resolution-'));
60
+ const requests = [];
61
+ const server = createServer((req, res) => {
62
+ const workspace = req.headers['x-workspace-key'];
63
+ const agent = req.headers['x-agent-api-key'];
64
+ requests.push({
65
+ path: req.url ?? '',
66
+ ...(typeof workspace === 'string' ? { workspace } : {}),
67
+ ...(typeof agent === 'string' ? { agent } : {}),
68
+ ...(req.headers.authorization ? { bearer: req.headers.authorization } : {}),
69
+ });
70
+ res.writeHead(mode === 'cold' ? 503 : 200, { 'content-type': 'application/json' });
71
+ res.end(JSON.stringify({ success: mode !== 'cold', data: { memories: [] } }));
72
+ });
73
+ const client = new Client({ name: 'credential-resolution-test', version: '1.0.0' });
74
+ try {
75
+ await new Promise((done) => { server.listen(0, '127.0.0.1', done); });
76
+ const address = server.address();
77
+ if (!address || typeof address === 'string')
78
+ throw new Error('Missing test server address');
79
+ const cacheFile = join(taskHome, 'credentials.json');
80
+ if (mode !== 'cold') {
81
+ writeFileSync(cacheFile, JSON.stringify({ token: 'cache-test-sentinel', agentId, tenantId: 'tenant-test', expiresAt: '2099-01-01T00:00:00Z' }), { mode: 0o600 });
82
+ }
83
+ const env = {};
84
+ for (const [name, value] of Object.entries(process.env)) {
85
+ if (value !== undefined && !name.startsWith('AIS_') && name !== 'DATABASE_URL')
86
+ env[name] = value;
87
+ }
88
+ Object.assign(env, {
89
+ HOME: taskHome,
90
+ AIS_URL: `http://127.0.0.1:${address.port}`,
91
+ AIS_CREDENTIALS_FILE: cacheFile,
92
+ AIS_SERVICE_KEY: 'service-test-sentinel',
93
+ DATABASE_URL: 'database-test-sentinel',
94
+ });
95
+ if (mode === 'workspace')
96
+ Object.assign(env, { AIS_WORKSPACE_KEY: 'ais_wk_test', AIS_API_KEY: 'ais_ak_other', AIS_AGENT_ID: agentId });
97
+ if (mode === 'agent')
98
+ Object.assign(env, { AIS_API_KEY: 'ais_ak_test', AIS_AGENT_ID: agentId });
99
+ // Same credential, injected the way a sourced .env file delivers it.
100
+ if (mode === 'agent-padded')
101
+ Object.assign(env, { AIS_API_KEY: ' ais_ak_test\n', AIS_AGENT_ID: ` ${agentId}\n` });
102
+ const transport = new StdioClientTransport({ command: process.execPath, args: [resolve(__dirname, '../../dist/index.js')], env, stderr: 'pipe' });
103
+ await client.connect(transport);
104
+ const result = await client.callTool({ name: mode === 'cold' ? 'agent_load' : 'recall', arguments: mode === 'cold' ? { query: 'corbot' } : { query: 'test' } });
105
+ const serialized = JSON.stringify(result);
106
+ if (mode === 'cold') {
107
+ expect(result.isError).toBe(true);
108
+ expect(serialized).toContain('Missing AIS credential:');
109
+ expect(serialized).toContain('AIS_WORKSPACE_KEY / AIS_API_KEY');
110
+ expect(serialized).toContain('ask your workspace owner');
111
+ expect(serialized).toContain('AIS_AGENT_ID');
112
+ expect(serialized).toContain('~/.aismemory/credentials.json');
113
+ expect(serialized).toContain('AIS sign-in could not start');
114
+ expect(requests.every((request) => request.path === '/v1/oauth/device/authorize' && !request.bearer && !request.workspace && !request.agent)).toBe(true);
115
+ }
116
+ else {
117
+ expect(result.isError).not.toBe(true);
118
+ const memoryRequests = requests.filter((request) => request.path.includes('/memory'));
119
+ expect(memoryRequests.length).toBeGreaterThan(0);
120
+ for (const request of memoryRequests) {
121
+ expect(request.workspace).toBe(mode === 'workspace' ? 'ais_wk_test' : undefined);
122
+ expect(request.agent).toBe(mode.startsWith('agent') ? 'ais_ak_test' : undefined);
123
+ expect(request.bearer).toBe(mode === 'cache' ? 'Bearer cache-test-sentinel' : undefined);
124
+ }
125
+ expect(requests.some((request) => request.path.includes('/oauth/'))).toBe(false);
126
+ }
127
+ expect(serialized).not.toContain('service-test-sentinel');
128
+ expect(serialized).not.toContain('database-test-sentinel');
129
+ expect(JSON.stringify(requests)).not.toContain('service-test-sentinel');
130
+ }
131
+ finally {
132
+ await client.close();
133
+ await new Promise((done, reject) => { server.close((error) => { if (error)
134
+ reject(error);
135
+ else
136
+ done(); }); });
137
+ rmSync(taskHome, { recursive: true, force: true });
138
+ }
139
+ }, 15_000);
140
+ });
141
+ //# sourceMappingURL=credential-resolution.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-resolution.test.js","sourceRoot":"","sources":["../../src/__tests__/credential-resolution.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Delegated cross-agent read pass-through (CORBOT-3785D10D).
3
+ *
4
+ * The aismemory MCP server is a thin client over the AIS HTTP API. Its job here
5
+ * is narrow: accept an optional `principalAgentId` on the two READ tools,
6
+ * validate its shape, and forward it as a query parameter. All authorization
7
+ * happens server-side — this package must never assume a grant exists.
8
+ */
9
+ export {};
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Delegated cross-agent read pass-through (CORBOT-3785D10D).
3
+ *
4
+ * The aismemory MCP server is a thin client over the AIS HTTP API. Its job here
5
+ * is narrow: accept an optional `principalAgentId` on the two READ tools,
6
+ * validate its shape, and forward it as a query parameter. All authorization
7
+ * happens server-side — this package must never assume a grant exists.
8
+ */
9
+ import { describe, expect, it, vi } from 'vitest';
10
+ import { dispatchToolKeyMode } from '../agent-key-mode.js';
11
+ import { parseRecallArgs, parseFullContextArgs, parseRememberArgs, } from '../tool-args.js';
12
+ const AIS_URL = 'https://ais.example.test';
13
+ const config = { apiKey: 'ais_ak_test', agentId: 'agent-42' };
14
+ const PRINCIPAL = 'a436450e-8349-4137-9bc3-08609acd13b8';
15
+ function jsonResponse(status, body) {
16
+ return new Response(JSON.stringify(body), {
17
+ status,
18
+ headers: { 'content-type': 'application/json' },
19
+ });
20
+ }
21
+ function fetchMock() {
22
+ return vi.fn(async () => jsonResponse(200, { success: true, data: {} }));
23
+ }
24
+ describe('argument parsing', () => {
25
+ it('accepts a well-formed principalAgentId on recall', () => {
26
+ const parsed = parseRecallArgs({ query: 'desk', principalAgentId: PRINCIPAL });
27
+ expect(parsed.principalAgentId).toBe(PRINCIPAL);
28
+ });
29
+ it('leaves principalAgentId undefined when omitted', () => {
30
+ expect(parseRecallArgs({ query: 'desk' }).principalAgentId).toBeUndefined();
31
+ expect(parseFullContextArgs({}).principalAgentId).toBeUndefined();
32
+ });
33
+ it('rejects a principalAgentId that is not a UUID', () => {
34
+ expect(() => parseRecallArgs({ query: 'desk', principalAgentId: 'corbot' })).toThrow(/valid agent UUID/);
35
+ expect(() => parseFullContextArgs({ principalAgentId: 'corbot' })).toThrow(/valid agent UUID/);
36
+ });
37
+ it('accepts a principalAgentId on full_context', () => {
38
+ expect(parseFullContextArgs({ principalAgentId: PRINCIPAL }).principalAgentId).toBe(PRINCIPAL);
39
+ });
40
+ it('full_context still rejects unknown keys', () => {
41
+ expect(() => parseFullContextArgs({ nope: 1 })).toThrow();
42
+ });
43
+ it('remember does NOT accept a principalAgentId — writes stay per-agent', () => {
44
+ // The write schema has no such field. Even if a client sends one it is not
45
+ // parsed into the request, and the server rejects it outright.
46
+ const parsed = parseRememberArgs({ content: 'hi', principalAgentId: PRINCIPAL });
47
+ expect(parsed).not.toHaveProperty('principalAgentId');
48
+ });
49
+ });
50
+ describe('dispatch forwards the principal as a query parameter', () => {
51
+ it('recall without a principal is byte-for-byte unchanged', async () => {
52
+ const f = fetchMock();
53
+ await dispatchToolKeyMode(AIS_URL, config, 'recall', { query: 'test' }, f);
54
+ expect(String(f.mock.calls[0][0])).toBe(`${AIS_URL}/v1/agents/agent-42/memory?query=test`);
55
+ });
56
+ it('recall with a principal appends principalAgentId and keeps the CALLER in the path', async () => {
57
+ const f = fetchMock();
58
+ await dispatchToolKeyMode(AIS_URL, config, 'recall', { query: 'test', principalAgentId: PRINCIPAL }, f);
59
+ const url = String(f.mock.calls[0][0]);
60
+ // The caller stays in the path — the agent API key must still match it.
61
+ expect(url).toContain('/v1/agents/agent-42/memory?');
62
+ expect(url).toContain(`principalAgentId=${PRINCIPAL}`);
63
+ });
64
+ it('full_context without a principal is unchanged', async () => {
65
+ const f = fetchMock();
66
+ await dispatchToolKeyMode(AIS_URL, config, 'full_context', {}, f);
67
+ expect(String(f.mock.calls[0][0])).toBe(`${AIS_URL}/v1/agents/agent-42/full-context`);
68
+ });
69
+ it('full_context with a principal appends principalAgentId', async () => {
70
+ const f = fetchMock();
71
+ await dispatchToolKeyMode(AIS_URL, config, 'full_context', { principalAgentId: PRINCIPAL }, f);
72
+ expect(String(f.mock.calls[0][0])).toBe(`${AIS_URL}/v1/agents/agent-42/full-context?principalAgentId=${PRINCIPAL}`);
73
+ });
74
+ it('a malformed principal never reaches the network', async () => {
75
+ const f = fetchMock();
76
+ await expect(dispatchToolKeyMode(AIS_URL, config, 'recall', { query: 'test', principalAgentId: 'not-a-uuid' }, f)).rejects.toThrow();
77
+ expect(f).not.toHaveBeenCalled();
78
+ });
79
+ });
80
+ //# sourceMappingURL=delegated-read-args.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delegated-read-args.test.js","sourceRoot":"","sources":["../../src/__tests__/delegated-read-args.test.ts"],"names":[],"mappings":"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"}
@@ -4,8 +4,8 @@
4
4
  * Every check function takes its network/fs/clock dependency as a parameter,
5
5
  * so tests inject fakes instead of hitting the real network or filesystem.
6
6
  */
7
- import { describe, it, expect } from 'vitest';
8
- import { checkNodeVersion, checkDnsResolution, checkTlsReachability, checkRestHealth, checkMcpEndpoint, checkCredentials, checkProxyEnv, checkClockSkew, runDoctor, } from '../cli/doctor.js';
7
+ import { afterEach, describe, it, expect } from 'vitest';
8
+ import { checkHarnessIdentity, checkNodeVersion, checkDnsResolution, checkTlsReachability, checkRestHealth, checkMcpEndpoint, checkCredentials, checkProxyEnv, checkClockSkew, runDoctor, } from '../cli/doctor.js';
9
9
  function fakeResponse(status, headers = {}) {
10
10
  const headerMap = new Headers(headers);
11
11
  return {
@@ -81,6 +81,27 @@ describe('checkRestHealth', () => {
81
81
  });
82
82
  });
83
83
  describe('checkMcpEndpoint', () => {
84
+ it.each([200, 204, 403])('passes expected HTTP %i responses', async (status) => {
85
+ const result = await checkMcpEndpoint('https://ais.example', async () => fakeResponse(status));
86
+ expect(result.status).toBe('PASS');
87
+ });
88
+ it.each([404, 405, 429, 500, 503])('fails HTTP %i with a repair hint', async (status) => {
89
+ const result = await checkMcpEndpoint('https://ais.example', async () => fakeResponse(status));
90
+ expect(result.status).toBe('FAIL');
91
+ expect(result.message).toContain(`HTTP ${status}`);
92
+ expect(result.hint).toBeTruthy();
93
+ });
94
+ it.each([301, 302, 307, 308])('does not follow HTTP %i redirects to another endpoint', async (status) => {
95
+ const fetchImpl = async (input, init) => {
96
+ expect(String(input)).toBe('https://ais.example/mcp');
97
+ expect(init?.redirect).toBe('manual');
98
+ return fakeResponse(status, { location: 'https://other.example/login' });
99
+ };
100
+ const result = await checkMcpEndpoint('https://ais.example', fetchImpl);
101
+ expect(result.status).toBe('FAIL');
102
+ expect(result.message).toContain(`HTTP ${status}`);
103
+ expect(result.hint).toBeTruthy();
104
+ });
84
105
  it('passes (reachable) on a 401 — auth required is not the same as down', async () => {
85
106
  const fetchImpl = async () => fakeResponse(401);
86
107
  const result = await checkMcpEndpoint('https://ais.agentsandswarms.ai', fetchImpl);
@@ -174,7 +195,21 @@ describe('checkClockSkew', () => {
174
195
  });
175
196
  });
176
197
  describe('runDoctor', () => {
177
- it('produces a report with one entry per check, exit code 0 when all pass', async () => {
198
+ it.each([404, 500])('fails the report when MCP returns HTTP %i', async (status) => {
199
+ const report = await runDoctor({
200
+ aisUrl: 'https://ais.example',
201
+ env: {},
202
+ nodeVersion: 'v20.10.0',
203
+ fetchImpl: async (input) => fakeResponse(String(input).endsWith('/mcp') ? status : 200),
204
+ dnsLookup: async () => ({ address: '1.2.3.4', family: 4 }),
205
+ tlsProbe: async () => undefined,
206
+ loadCreds: () => null,
207
+ });
208
+ expect(report.exitCode).toBe(1);
209
+ expect(report.verdict).toBe('problems');
210
+ expect(report.checks.find((check) => check.name === 'MCP HTTP endpoint')?.status).toBe('FAIL');
211
+ });
212
+ it('passes an otherwise healthy legacy environment without a harness pin', async () => {
178
213
  const now = Date.now();
179
214
  const fetchImpl = async (input) => {
180
215
  const url = String(input);
@@ -192,11 +227,11 @@ describe('runDoctor', () => {
192
227
  fetchImpl,
193
228
  dnsLookup: async () => ({ address: '1.2.3.4', family: 4 }),
194
229
  tlsProbe: async () => undefined,
195
- loadCreds: () => null,
230
+ loadCreds: () => ({ token: 'synthetic-token', agentId: 'legacy-agent', tenantId: 'tenant-a', expiresAt: new Date(now + 3600000).toISOString() }),
196
231
  });
197
- expect(report.checks).toHaveLength(8);
198
- expect(report.checks.every((c) => c.status !== 'FAIL')).toBe(true);
199
- expect(report.verdict).toBe('problems'); // credentials WARN (not bonded) keeps this non-zero
232
+ expect(report.checks).toHaveLength(9);
233
+ expect(report.checks.find((c) => c.name === 'Harness identity gate')?.status).toBe('PASS');
234
+ expect(report.verdict).toBe('pass');
200
235
  expect(report.exitCode).toBe(0);
201
236
  });
202
237
  it('sets exit code 1 when any check fails', async () => {
@@ -228,11 +263,11 @@ describe('runDoctor', () => {
228
263
  return fakeResponse(200, { date: new Date(now).toUTCString() });
229
264
  if (url.endsWith('/mcp'))
230
265
  return fakeResponse(401);
231
- return fakeResponse(200); // credentials whoami call
266
+ return new Response(JSON.stringify({ success: true, data: { id: 'agent-1' } }), { status: 200 });
232
267
  };
233
268
  const report = await runDoctor({
234
269
  aisUrl: 'https://ais.agentsandswarms.ai',
235
- env: {},
270
+ env: { AIS_AGENT_ID: 'agent-1' },
236
271
  nodeVersion: 'v20.10.0',
237
272
  now: () => now,
238
273
  fetchImpl,
@@ -240,9 +275,215 @@ describe('runDoctor', () => {
240
275
  tlsProbe: async () => undefined,
241
276
  loadCreds: () => creds,
242
277
  });
278
+ expect(report).toMatchObject({ scope: 'cli-environment', runtimeEvidence: 'unavailable' });
243
279
  expect(report.problemCount).toBe(0);
244
280
  expect(report.verdict).toBe('pass');
245
281
  expect(report.exitCode).toBe(0);
246
282
  });
247
283
  });
284
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
285
+ import { tmpdir } from 'node:os';
286
+ import { join, resolve } from 'node:path';
287
+ import { createServer } from 'node:http';
288
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
289
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
290
+ import { installHarnessIdentity, loadHarnessIdentity, verifyPinnedIdentity, requiresIdentityGate } from '../harness-identity.js';
291
+ const homes = [];
292
+ const clients = [];
293
+ let server;
294
+ function homeFixture() {
295
+ const home = mkdtempSync(join(tmpdir(), 'ais-harness-gate-'));
296
+ homes.push(home);
297
+ return home;
298
+ }
299
+ function install(home, harness, agentId) {
300
+ const source = join(home, `${harness}-source.json`);
301
+ writeFileSync(source, JSON.stringify({ version: 1, harness, agentId }));
302
+ installHarnessIdentity(source, home);
303
+ }
304
+ function sharedDefault(home, agentId) {
305
+ const path = join(home, '.aismemory', 'credentials.json');
306
+ mkdirSync(join(home, '.aismemory'), { recursive: true });
307
+ writeFileSync(path, JSON.stringify({ agentId, tenantId: 'synthetic-tenant', token: 'synthetic-bearer', expiresAt: new Date(Date.now() + 3600000).toISOString() }));
308
+ return path;
309
+ }
310
+ async function connect(home, url, harness, authEnv = {}) {
311
+ const client = new Client({ name: 'synthetic-harness', version: '1' });
312
+ clients.push(client);
313
+ await client.connect(new StdioClientTransport({
314
+ command: process.execPath,
315
+ args: [resolve(__dirname, '../../dist/index.js'), ...(harness ? ['--harness', harness] : [])],
316
+ env: { PATH: process.env['PATH'] ?? '', HOME: home, AIS_URL: url, AIS_TELEMETRY: '0', ...authEnv },
317
+ stderr: 'pipe',
318
+ }));
319
+ return client;
320
+ }
321
+ afterEach(async () => {
322
+ for (const client of clients.splice(0))
323
+ await client.close();
324
+ if (server) {
325
+ server.closeAllConnections();
326
+ await new Promise((resolveClose) => { server?.close(() => resolveClose()); });
327
+ server = undefined;
328
+ }
329
+ for (const home of homes.splice(0))
330
+ rmSync(home, { recursive: true, force: true });
331
+ });
332
+ describe('harness identity manifest', () => {
333
+ it('installs separate immutable pins and rejects missing, conflicting, and malformed manifests', () => {
334
+ const home = homeFixture();
335
+ install(home, 'codex', 'agent-a');
336
+ install(home, 'claude', 'agent-b');
337
+ expect(loadHarnessIdentity(['--harness', 'codex'], {}, home)?.agentId).toBe('agent-a');
338
+ expect(loadHarnessIdentity(['--harness', 'claude'], {}, home)?.agentId).toBe('agent-b');
339
+ expect(() => install(home, 'codex', 'agent-b')).toThrow();
340
+ expect(() => loadHarnessIdentity(['--harness', 'missing'], {}, home)).toThrow('aismemory install');
341
+ expect(() => loadHarnessIdentity(['--harness', '../codex'], {}, home)).toThrow();
342
+ expect(() => loadHarnessIdentity(['--harness', 'codex'], { AIS_AGENT_ID: 'agent-b' }, home)).toThrow('conflicting pin');
343
+ writeFileSync(join(home, 'invalid.json'), JSON.stringify({ version: 1, harness: 'codex', agentId: 'agent-a', secret: 'synthetic' }));
344
+ expect(() => installHarnessIdentity(join(home, 'invalid.json'), home)).toThrow();
345
+ });
346
+ it('enforces configured pins and leaves legacy unpinned installs ungated', async () => {
347
+ const pin = { version: 1, harness: 'codex', agentId: 'agent-a' };
348
+ await expect(verifyPinnedIdentity(null, 'agent-a', async () => {
349
+ throw new Error('whoami should not be called without a pin');
350
+ })).resolves.toBeUndefined();
351
+ await expect(verifyPinnedIdentity(pin, 'agent-b', async () => ({}))).rejects.toThrow('expected agent-a');
352
+ for (const result of [{ success: true, data: { id: 'agent-b' } }, {}, { success: false, data: { id: 'agent-a' } }]) {
353
+ await expect(verifyPinnedIdentity(pin, 'agent-a', async () => result)).rejects.toThrow('whoami did not confirm');
354
+ }
355
+ await expect(verifyPinnedIdentity(pin, 'agent-a', async () => { throw new Error('offline'); })).rejects.toThrow('whoami unavailable');
356
+ expect(requiresIdentityGate('inbox_read', {})).toBe(false);
357
+ expect(requiresIdentityGate('inbox_read', { ack: true })).toBe(true);
358
+ });
359
+ it('doctor reports a verified pin and rejects the wrong remote identity', async () => {
360
+ const home = homeFixture();
361
+ install(home, 'codex', 'agent-a');
362
+ const options = {
363
+ home, argv: ['--harness', 'codex'], env: {},
364
+ loadCreds: () => ({ token: 'synthetic', agentId: 'shared-other', tenantId: 'synthetic', expiresAt: new Date(Date.now() + 3600000).toISOString() }),
365
+ };
366
+ const good = await checkHarnessIdentity('http://synthetic.invalid', { ...options, fetchImpl: async () => Response.json({ success: true, data: { id: 'agent-a' } }) });
367
+ expect(good.status).toBe('PASS');
368
+ const bad = await checkHarnessIdentity('http://synthetic.invalid', { ...options, fetchImpl: async () => Response.json({ success: true, data: { id: 'agent-b' } }) });
369
+ expect(bad.status).toBe('FAIL');
370
+ expect(bad.hint).toContain('aismemory doctor --harness');
371
+ });
372
+ it('doctor leaves legacy unpinned installs healthy', async () => {
373
+ const result = await checkHarnessIdentity('http://synthetic.invalid', {
374
+ env: {},
375
+ fetchImpl: async () => {
376
+ throw new Error('whoami should not be called without a pin');
377
+ },
378
+ });
379
+ expect(result.status).toBe('PASS');
380
+ expect(result.message).toContain('legacy unpinned mode remains allowed');
381
+ });
382
+ it('doctor reports scoped-key config errors without the identity repair hint', async () => {
383
+ const home = homeFixture();
384
+ install(home, 'codex', 'agent-a');
385
+ const result = await checkHarnessIdentity('http://synthetic.invalid', {
386
+ home,
387
+ argv: ['--harness', 'codex'],
388
+ env: { AIS_WORKSPACE_KEY: 'not-a-workspace-key' },
389
+ });
390
+ expect(result.status).toBe('FAIL');
391
+ expect(result.message).toContain('does not look like a workspace key');
392
+ expect(result.hint).toContain('Fix the scoped key environment');
393
+ expect(result.hint).not.toContain('aismemory install');
394
+ });
395
+ });
396
+ describe('real MCP processes with synthetic AIS', () => {
397
+ it('keeps two harnesses pinned after shared-default changes and restarts; rejects writes and acknowledgements on mismatch', async () => {
398
+ const home = homeFixture();
399
+ install(home, 'codex', 'agent-a');
400
+ install(home, 'claude', 'agent-b');
401
+ const requests = [];
402
+ let mismatch = false;
403
+ server = createServer((req, res) => {
404
+ requests.push(`${req.method} ${req.url}`);
405
+ const agent = req.url?.match(/^\/v1\/agents\/([^/?]+)$/)?.[1];
406
+ res.setHeader('Content-Type', 'application/json');
407
+ if (agent)
408
+ res.end(JSON.stringify({ success: true, data: { id: mismatch ? 'wrong-agent' : agent, name: agent, status: 'active' } }));
409
+ else if (req.url?.startsWith('/v1/resolve'))
410
+ res.end(JSON.stringify({ success: true, data: { id: 'agent-b', name: 'B', status: 'active' } }));
411
+ else if (req.url?.includes('/inbox?'))
412
+ res.end(JSON.stringify({ success: true, data: { messages: [{ id: 'message-a' }] } }));
413
+ else
414
+ res.end(JSON.stringify({ success: true, data: {} }));
415
+ });
416
+ await new Promise((r) => { server?.listen(0, '127.0.0.1', () => r()); });
417
+ const address = server.address();
418
+ if (!address || typeof address === 'string')
419
+ throw new Error('No synthetic server port');
420
+ const url = `http://127.0.0.1:${address.port}`;
421
+ for (const defaultAgent of ['shared-first', 'shared-changed']) {
422
+ const preference = sharedDefault(home, defaultAgent);
423
+ for (const [harness, agentId] of [['codex', 'agent-a'], ['claude', 'agent-b']]) {
424
+ const client = await connect(home, url, harness);
425
+ const whoami = await client.callTool({ name: 'whoami', arguments: {} });
426
+ expect(JSON.stringify(whoami)).toContain(agentId);
427
+ const before = requests.length;
428
+ const result = await client.callTool({ name: 'remember', arguments: { content: 'synthetic memory' } });
429
+ expect(result.isError).not.toBe(true);
430
+ expect(requests.slice(before)).toEqual([`GET /v1/agents/${agentId}`, `POST /v1/agents/${agentId}/memory`]);
431
+ expect(JSON.parse(readFileSync(preference, 'utf8')).agentId).toBe(defaultAgent);
432
+ await client.close();
433
+ }
434
+ }
435
+ const client = await connect(home, url, 'codex');
436
+ const switched = await client.callTool({ name: 'agent_load', arguments: { query: 'agent-b' } });
437
+ expect(switched.isError).toBe(true);
438
+ mismatch = true;
439
+ for (const [name, args] of [['remember', { content: 'must not write' }], ['inbox_read', { ack: true }], ['handoff', {}]]) {
440
+ const before = requests.length;
441
+ const result = await client.callTool({ name, arguments: args });
442
+ expect(result.isError).toBe(true);
443
+ expect(JSON.stringify(result)).toContain('aismemory doctor --harness codex');
444
+ expect(requests.slice(before).some((request) => request.startsWith('POST'))).toBe(false);
445
+ }
446
+ const unpinned = await connect(home, url);
447
+ const unpinnedBefore = requests.length;
448
+ const allowed = await unpinned.callTool({ name: 'remember', arguments: { content: 'legacy write remains allowed' } });
449
+ expect(allowed.isError).not.toBe(true);
450
+ expect(requests.slice(unpinnedBefore)).not.toContain('GET /v1/agents/shared-changed');
451
+ expect(requests.at(-1)).toBe('POST /v1/agents/shared-changed/memory');
452
+ }, 30000);
453
+ });
454
+ describe('scoped key adapters', () => {
455
+ it.each(['AIS_AGENT_API_KEY', 'AIS_WORKSPACE_KEY'])('verifies the pinned grant before writes with %s', async (keyName) => {
456
+ const home = homeFixture();
457
+ install(home, 'codex', 'agent-a');
458
+ let allowed = true;
459
+ const requests = [];
460
+ server = createServer((req, res) => {
461
+ requests.push(`${req.method} ${req.url}`);
462
+ res.writeHead(allowed ? 200 : 403, { 'Content-Type': 'application/json' });
463
+ res.end(JSON.stringify({ success: allowed, data: {}, error: allowed ? undefined : { code: 'AGENT_NOT_GRANTED' } }));
464
+ });
465
+ await new Promise((r) => { server?.listen(0, '127.0.0.1', () => r()); });
466
+ const address = server.address();
467
+ if (!address || typeof address === 'string')
468
+ throw new Error('No synthetic server port');
469
+ const client = await connect(home, `http://127.0.0.1:${address.port}`, 'codex', {
470
+ [keyName]: keyName === 'AIS_AGENT_API_KEY' ? 'ais_ak_synthetic-fixture' : 'ais_wk_synthetic-fixture',
471
+ });
472
+ const result = await client.callTool({ name: 'remember', arguments: { content: 'synthetic' } });
473
+ expect(result.isError).not.toBe(true);
474
+ expect(requests).toEqual(['GET /v1/agents/agent-a/memory?limit=1', 'POST /v1/agents/agent-a/memory']);
475
+ if (keyName === 'AIS_WORKSPACE_KEY') {
476
+ const crossPin = await client.callTool({ name: 'agent_load', arguments: { query: 'agent-b' } });
477
+ expect(crossPin.isError).toBe(true);
478
+ expect(JSON.stringify(crossPin)).toContain('expected agent-a, received agent-b');
479
+ const ownPin = await client.callTool({ name: 'agent_load', arguments: { query: 'agent-a' } });
480
+ expect(ownPin.isError).not.toBe(true);
481
+ }
482
+ allowed = false;
483
+ const denied = await client.callTool({ name: 'remember', arguments: { content: 'denied' } });
484
+ expect(denied.isError).toBe(true);
485
+ expect(JSON.stringify(denied)).toContain('whoami unavailable');
486
+ expect(requests.filter((request) => request.startsWith('POST'))).toHaveLength(1);
487
+ });
488
+ });
248
489
  //# sourceMappingURL=doctor.test.js.map