aismemory 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -2
- package/dist/__tests__/agent-key-mode-e2e.test.js +78 -0
- package/dist/__tests__/agent-key-mode-e2e.test.js.map +1 -1
- package/dist/__tests__/agent-key-mode.test.js +123 -0
- package/dist/__tests__/agent-key-mode.test.js.map +1 -1
- package/dist/__tests__/agent-load-persistence.test.js +25 -2
- package/dist/__tests__/agent-load-persistence.test.js.map +1 -1
- package/dist/__tests__/agent-preference.test.js +8 -0
- package/dist/__tests__/agent-preference.test.js.map +1 -1
- package/dist/__tests__/agentmemory.test.d.ts +1 -0
- package/dist/__tests__/agentmemory.test.js +73 -0
- package/dist/__tests__/agentmemory.test.js.map +1 -0
- package/dist/__tests__/ais-http.test.d.ts +1 -0
- package/dist/__tests__/ais-http.test.js +139 -0
- package/dist/__tests__/ais-http.test.js.map +1 -0
- package/dist/__tests__/auto-enroll-key.test.js +28 -4
- package/dist/__tests__/auto-enroll-key.test.js.map +1 -1
- package/dist/__tests__/beacon-contract.test.d.ts +1 -0
- package/dist/__tests__/beacon-contract.test.js +106 -0
- package/dist/__tests__/beacon-contract.test.js.map +1 -0
- package/dist/__tests__/credential-isolation.test.d.ts +1 -0
- package/dist/__tests__/credential-isolation.test.js +61 -0
- package/dist/__tests__/credential-isolation.test.js.map +1 -0
- package/dist/__tests__/credential-resolution.test.d.ts +1 -0
- package/dist/__tests__/credential-resolution.test.js +141 -0
- package/dist/__tests__/credential-resolution.test.js.map +1 -0
- package/dist/__tests__/delegated-read-args.test.d.ts +9 -0
- package/dist/__tests__/delegated-read-args.test.js +80 -0
- package/dist/__tests__/delegated-read-args.test.js.map +1 -0
- package/dist/__tests__/doctor.test.js +250 -9
- package/dist/__tests__/doctor.test.js.map +1 -1
- package/dist/__tests__/ender-receipt-auth.test.d.ts +1 -0
- package/dist/__tests__/ender-receipt-auth.test.js +26 -0
- package/dist/__tests__/ender-receipt-auth.test.js.map +1 -0
- package/dist/__tests__/fixtures/beacon-contract.d.ts +66 -0
- package/dist/__tests__/fixtures/beacon-contract.js +72 -0
- package/dist/__tests__/fixtures/beacon-contract.js.map +1 -0
- package/dist/__tests__/inbox-client.test.d.ts +1 -0
- package/dist/__tests__/inbox-client.test.js +108 -0
- package/dist/__tests__/inbox-client.test.js.map +1 -0
- package/dist/__tests__/key-auth.test.js +7 -1
- package/dist/__tests__/key-auth.test.js.map +1 -1
- package/dist/__tests__/list-all-agents.test.js +2 -0
- package/dist/__tests__/list-all-agents.test.js.map +1 -1
- package/dist/__tests__/lost-agent.test.d.ts +1 -0
- package/dist/__tests__/lost-agent.test.js +108 -0
- package/dist/__tests__/lost-agent.test.js.map +1 -0
- package/dist/__tests__/memory-write-failure.test.js +5 -1
- package/dist/__tests__/memory-write-failure.test.js.map +1 -1
- package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.d.ts +1 -0
- package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.js +207 -0
- package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.js.map +1 -0
- package/dist/__tests__/provisional-register-mcp-remember.test.d.ts +1 -0
- package/dist/__tests__/provisional-register-mcp-remember.test.js +196 -0
- package/dist/__tests__/provisional-register-mcp-remember.test.js.map +1 -0
- package/dist/__tests__/readme-tools-parity.test.d.ts +1 -0
- package/dist/__tests__/readme-tools-parity.test.js +64 -0
- package/dist/__tests__/readme-tools-parity.test.js.map +1 -0
- package/dist/__tests__/reconnect-survival-e2e.test.js +63 -1
- package/dist/__tests__/reconnect-survival-e2e.test.js.map +1 -1
- package/dist/__tests__/token-manager.test.js +20 -1
- package/dist/__tests__/token-manager.test.js.map +1 -1
- package/dist/__tests__/tool-args.test.js +196 -1
- package/dist/__tests__/tool-args.test.js.map +1 -1
- package/dist/__tests__/workspace-key-mode.test.d.ts +1 -0
- package/dist/__tests__/workspace-key-mode.test.js +264 -0
- package/dist/__tests__/workspace-key-mode.test.js.map +1 -0
- package/dist/agent-key-mode.d.ts +4 -2
- package/dist/agent-key-mode.js +106 -11
- package/dist/agent-key-mode.js.map +1 -1
- package/dist/ais-http.d.ts +7 -0
- package/dist/ais-http.js +137 -0
- package/dist/ais-http.js.map +1 -0
- package/dist/auth-staleness.d.ts +2 -0
- package/dist/auth-staleness.js +5 -0
- package/dist/auth-staleness.js.map +1 -1
- package/dist/auto-handoff.d.ts +1 -1
- package/dist/auto-handoff.js +5 -3
- package/dist/auto-handoff.js.map +1 -1
- package/dist/cli/doctor.d.ts +4 -0
- package/dist/cli/doctor.js +120 -8
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/enable-key-auth.js +4 -5
- package/dist/cli/enable-key-auth.js.map +1 -1
- package/dist/cli/orientation.d.ts +15 -0
- package/dist/cli/orientation.js +89 -0
- package/dist/cli/orientation.js.map +1 -0
- package/dist/harness-identity.d.ts +17 -0
- package/dist/harness-identity.js +105 -0
- package/dist/harness-identity.js.map +1 -0
- package/dist/hydration.d.ts +1 -1
- package/dist/hydration.js +3 -2
- package/dist/hydration.js.map +1 -1
- package/dist/inbox-client.d.ts +9 -0
- package/dist/inbox-client.js +74 -0
- package/dist/inbox-client.js.map +1 -0
- package/dist/index.js +498 -109
- package/dist/index.js.map +1 -1
- package/dist/key-auth.js +16 -10
- package/dist/key-auth.js.map +1 -1
- package/dist/oauth-credentials.d.ts +11 -0
- package/dist/oauth-credentials.js +31 -3
- package/dist/oauth-credentials.js.map +1 -1
- package/dist/token-manager.d.ts +6 -0
- package/dist/token-manager.js +16 -0
- package/dist/token-manager.js.map +1 -1
- package/dist/tool-args.d.ts +98 -0
- package/dist/tool-args.js +155 -0
- package/dist/tool-args.js.map +1 -1
- package/dist/workspace-key-mode.d.ts +40 -0
- package/dist/workspace-key-mode.js +258 -0
- package/dist/workspace-key-mode.js.map +1 -0
- package/package.json +1 -1
|
@@ -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":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE7D,MAAM,OAAO,GAAG,sCAAsC,CAAC;AAEvD,QAAQ,CAAC,2BAA2B,EAAE,GAAS,EAAE;IAC/C,EAAE,CAAC,qDAAqD,EAAE,GAAS,EAAE;QACnE,MAAM,CAAC,qBAAqB,CAAC,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,CAAC;aACpF,OAAO,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,6CAA6C,EAAE,GAAS,EAAE;QAC3D,MAAM,CAAC,sBAAsB,CAAC,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,CAAC;aACrF,OAAO,CAAC,EAAE,YAAY,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,2DAA2D,EAAE,GAAS,EAAE;QACzE,MAAM,CAAC,sBAAsB,CAAC,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,CAAC;aACxH,OAAO,CAAC,EAAE,YAAY,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,wDAAwD,EAAE,GAAS,EAAE;QACtE,MAAM,CAAC,qBAAqB,CAAC,EAAE,WAAW,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,cAAc,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,CAAC;aACvH,OAAO,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,uBAAuB,EAAE,cAAc,CAAC,CAAC,CAAC,6DAA6D,EAAE,CAAC,GAAG,EAAQ,EAAE;QACzI,MAAM,CAAC,GAAY,EAAE,CAAC,qBAAqB,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IACnH,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,wDAAwD,EAAE,GAAS,EAAE;QACtE,MAAM,CAAC,GAAY,EAAE,CAAC,qBAAqB,CAAC,EAAE,WAAW,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,gDAAgD,CAAC,CAAC;QACvI,MAAM,CAAC,GAAY,EAAE,CAAC,sBAAsB,CAAC,EAAE,WAAW,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,gDAAgD,CAAC,CAAC;IAC1I,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,4EAA4E,EAAE,GAAS,EAAE;QAC1F,0EAA0E;QAC1E,yEAAyE;QACzE,0EAA0E;QAC1E,MAAM,GAAG,GAAG,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,IAAI,OAAO,IAAI,EAAE,CAAC;QAC1E,MAAM,QAAQ,GAAG,mBAAmB,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;QAC9C,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC;QACtE,MAAM,UAAU,GAAG,EAAE,GAAG,GAAG,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;QAC/D,MAAM,CAAC,qBAAqB,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,CAAC;IACxF,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,kEAAkE,EAAE,GAAS,EAAE;QAChF,MAAM,CAAC,mBAAmB,CAAC,EAAE,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QACnE,MAAM,CAAC,GAAY,EAAE,CAAC,qBAAqB,CAAC,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC,CAAC;aAClG,OAAO,CAAC,gDAAgD,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,oCAAoC,EAAE,GAAS,EAAE;QAClD,MAAM,GAAG,GAAG,EAAE,eAAe,EAAE,uBAAuB,EAAE,YAAY,EAAE,wBAAwB,EAAE,CAAC;QACjG,MAAM,CAAC,sBAAsB,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QAC/C,MAAM,CAAC,qBAAqB,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IAChD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,gCAAgC,EAAE,GAAS,EAAE;IACpD,EAAE,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,uEAAuE,EAAE,KAAK,EAAE,IAAI,EAAiB,EAAE;QACtK,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,4BAA4B,CAAC,CAAC,CAAC;QAC3E,MAAM,QAAQ,GAAiF,EAAE,CAAC;QAClG,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAQ,EAAE;YAC7C,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;YACjD,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;YAC7C,QAAQ,CAAC,IAAI,CAAC;gBACZ,IAAI,EAAE,GAAG,CAAC,GAAG,IAAI,EAAE;gBACnB,GAAG,CAAC,OAAO,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACvD,GAAG,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC/C,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC5E,CAAC,CAAC;YACH,GAAG,CAAC,SAAS,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;YACnF,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,IAAI,KAAK,MAAM,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;QAChF,CAAC,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,IAAI,EAAE,4BAA4B,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;QACpF,IAAI,CAAC;YACH,MAAM,IAAI,OAAO,CAAO,CAAC,IAAI,EAAQ,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAClF,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;YACjC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;gBAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;YAC5F,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAC;YACrD,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;gBACpB,aAAa,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,SAAS,EAAE,sBAAsB,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACnK,CAAC;YACD,MAAM,GAAG,GAA2B,EAAE,CAAC;YACvC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;gBACxD,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,IAAI,KAAK,cAAc;oBAAE,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;YACpG,CAAC;YACD,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE;gBACjB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,oBAAoB,OAAO,CAAC,IAAI,EAAE;gBAC3C,oBAAoB,EAAE,SAAS;gBAC/B,eAAe,EAAE,uBAAuB;gBACxC,YAAY,EAAE,wBAAwB;aACvC,CAAC,CAAC;YACH,IAAI,IAAI,KAAK,WAAW;gBAAE,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,iBAAiB,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,CAAC;YACvI,IAAI,IAAI,KAAK,OAAO;gBAAE,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,CAAC;YAChG,qEAAqE;YACrE,IAAI,IAAI,KAAK,cAAc;gBAAE,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,EAAE,IAAI,OAAO,IAAI,EAAE,CAAC,CAAC;YAClH,MAAM,SAAS,GAAG,IAAI,oBAAoB,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,OAAO,CAAC,SAAS,EAAE,qBAAqB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;YAClJ,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAChC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,EAAE,SAAS,EAAE,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;YAChK,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;YAC1C,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;gBACpB,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAClC,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,yBAAyB,CAAC,CAAC;gBACxD,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,iCAAiC,CAAC,CAAC;gBAChE,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,0BAA0B,CAAC,CAAC;gBACzD,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;gBAC7C,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,+BAA+B,CAAC,CAAC;gBAC9D,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,6BAA6B,CAAC,CAAC;gBAC5D,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,OAAO,EAAW,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,4BAA4B,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpK,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACtC,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAW,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;gBAC/F,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;gBACjD,KAAK,MAAM,OAAO,IAAI,cAAc,EAAE,CAAC;oBACrC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;oBACjF,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;oBACjF,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,4BAA4B,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;gBAC3F,CAAC;gBACD,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAW,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC5F,CAAC;YACD,MAAM,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,uBAAuB,CAAC,CAAC;YAC1D,MAAM,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,wBAAwB,CAAC,CAAC;YAC3D,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,uBAAuB,CAAC,CAAC;QAC1E,CAAC;gBAAS,CAAC;YACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YACrB,MAAM,IAAI,OAAO,CAAO,CAAC,IAAI,EAAE,MAAM,EAAQ,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,EAAQ,EAAE,GAAG,IAAI,KAAK;gBAAE,MAAM,CAAC,KAAK,CAAC,CAAC;;gBAAM,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAChI,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,CAAC;IACH,CAAC,EAAE,MAAM,CAAC,CAAC;AACb,CAAC,CAAC,CAAC"}
|
|
@@ -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":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,OAAO,GAAG,0BAA0B,CAAC;AAC3C,MAAM,MAAM,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC;AAC9D,MAAM,SAAS,GAAG,sCAAsC,CAAC;AAEzD,SAAS,YAAY,CAAC,MAAc,EAAE,IAAa;IACjD,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE;QACxC,MAAM;QACN,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;KAChD,CAAC,CAAC;AACL,CAAC;AAED,SAAS,SAAS;IAChB,OAAO,EAAE,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3E,CAAC;AAED,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,MAAM,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,SAAS,EAAE,CAAC,CAAC;QAC/E,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,gBAAgB,CAAC,CAAC,aAAa,EAAE,CAAC;QAC5E,MAAM,CAAC,oBAAoB,CAAC,EAAE,CAAC,CAAC,gBAAgB,CAAC,CAAC,aAAa,EAAE,CAAC;IACpE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,OAAO,CAClF,kBAAkB,CACnB,CAAC;QACF,MAAM,CAAC,GAAG,EAAE,CAAC,oBAAoB,CAAC,EAAE,gBAAgB,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,OAAO,CACxE,kBAAkB,CACnB,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,CAAC,oBAAoB,CAAC,EAAE,gBAAgB,EAAE,SAAS,EAAE,CAAC,CAAC,gBAAgB,CAAC,CAAC,IAAI,CACjF,SAAS,CACV,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,CAAC,GAAG,EAAE,CAAC,oBAAoB,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qEAAqE,EAAE,GAAG,EAAE;QAC7E,2EAA2E;QAC3E,+DAA+D;QAC/D,MAAM,MAAM,GAAG,iBAAiB,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,SAAS,EAAE,CAAC,CAAC;QACjF,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,sDAAsD,EAAE,GAAG,EAAE;IACpE,EAAE,CAAC,uDAAuD,EAAE,KAAK,IAAI,EAAE;QACrE,MAAM,CAAC,GAAG,SAAS,EAAE,CAAC;QACtB,MAAM,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,CAA4B,CAAC,CAAC;QACtG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CACtC,GAAG,OAAO,uCAAuC,CAClD,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mFAAmF,EAAE,KAAK,IAAI,EAAE;QACjG,MAAM,CAAC,GAAG,SAAS,EAAE,CAAC;QACtB,MAAM,mBAAmB,CACvB,OAAO,EACP,MAAM,EACN,QAAQ,EACR,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,SAAS,EAAE,EAC9C,CAA4B,CAC7B,CAAC;QACF,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QACxC,wEAAwE;QACxE,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,6BAA6B,CAAC,CAAC;QACrD,MAAM,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,oBAAoB,SAAS,EAAE,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;QAC7D,MAAM,CAAC,GAAG,SAAS,EAAE,CAAC;QACtB,MAAM,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,EAAE,EAAE,CAA4B,CAAC,CAAC;QAC7F,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,kCAAkC,CAAC,CAAC;IACzF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;QACtE,MAAM,CAAC,GAAG,SAAS,EAAE,CAAC;QACtB,MAAM,mBAAmB,CACvB,OAAO,EACP,MAAM,EACN,cAAc,EACd,EAAE,gBAAgB,EAAE,SAAS,EAAE,EAC/B,CAA4B,CAC7B,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CACtC,GAAG,OAAO,qDAAqD,SAAS,EAAE,CAC3E,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QAC/D,MAAM,CAAC,GAAG,SAAS,EAAE,CAAC;QACtB,MAAM,MAAM,CACV,mBAAmB,CACjB,OAAO,EACP,MAAM,EACN,QAAQ,EACR,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,YAAY,EAAE,EACjD,CAA4B,CAC7B,CACF,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IACnC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -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('
|
|
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: () =>
|
|
230
|
+
loadCreds: () => ({ token: 'synthetic-token', agentId: 'legacy-agent', tenantId: 'tenant-a', expiresAt: new Date(now + 3600000).toISOString() }),
|
|
196
231
|
});
|
|
197
|
-
expect(report.checks).toHaveLength(
|
|
198
|
-
expect(report.checks.
|
|
199
|
-
expect(report.verdict).toBe('
|
|
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
|
|
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
|