@slashfi/agents-sdk 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-definitions/auth.d.ts.map +1 -1
- package/dist/agent-definitions/auth.js +44 -11
- package/dist/agent-definitions/auth.js.map +1 -1
- package/dist/agent-definitions/integrations.d.ts.map +1 -1
- package/dist/agent-definitions/integrations.js +106 -45
- package/dist/agent-definitions/integrations.js.map +1 -1
- package/dist/agent-definitions/remote-registry.d.ts.map +1 -1
- package/dist/agent-definitions/remote-registry.js +174 -45
- package/dist/agent-definitions/remote-registry.js.map +1 -1
- package/dist/agent-definitions/secrets.d.ts.map +1 -1
- package/dist/agent-definitions/secrets.js +1 -4
- package/dist/agent-definitions/secrets.js.map +1 -1
- package/dist/agent-definitions/users.d.ts.map +1 -1
- package/dist/agent-definitions/users.js +14 -3
- package/dist/agent-definitions/users.js.map +1 -1
- package/dist/define-config.d.ts +125 -0
- package/dist/define-config.d.ts.map +1 -0
- package/dist/define-config.js +75 -0
- package/dist/define-config.js.map +1 -0
- package/dist/define.d.ts +11 -2
- package/dist/define.d.ts.map +1 -1
- package/dist/define.js +57 -26
- package/dist/define.js.map +1 -1
- package/dist/events.d.ts +133 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +57 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +15 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/integration-interface.d.ts +3 -3
- package/dist/integration-interface.d.ts.map +1 -1
- package/dist/integration-interface.js +29 -21
- package/dist/integration-interface.js.map +1 -1
- package/dist/integrations-store.d.ts +2 -2
- package/dist/integrations-store.d.ts.map +1 -1
- package/dist/integrations-store.js +3 -3
- package/dist/integrations-store.js.map +1 -1
- package/dist/jwt.d.ts.map +1 -1
- package/dist/jwt.js +7 -5
- package/dist/jwt.js.map +1 -1
- package/dist/key-manager.d.ts.map +1 -1
- package/dist/key-manager.js +5 -3
- package/dist/key-manager.js.map +1 -1
- package/dist/oidc-signin.d.ts +32 -0
- package/dist/oidc-signin.d.ts.map +1 -0
- package/dist/oidc-signin.js +138 -0
- package/dist/oidc-signin.js.map +1 -0
- package/dist/registry-consumer.d.ts +104 -0
- package/dist/registry-consumer.d.ts.map +1 -0
- package/dist/registry-consumer.js +230 -0
- package/dist/registry-consumer.js.map +1 -0
- package/dist/registry.d.ts +5 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +76 -4
- package/dist/registry.js.map +1 -1
- package/dist/secret-collection.d.ts.map +1 -1
- package/dist/secret-collection.js.map +1 -1
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +222 -27
- package/dist/server.js.map +1 -1
- package/dist/test-utils/mock-oidc-server.d.ts +36 -0
- package/dist/test-utils/mock-oidc-server.d.ts.map +1 -0
- package/dist/test-utils/mock-oidc-server.js +96 -0
- package/dist/test-utils/mock-oidc-server.js.map +1 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agent-definitions/auth.ts +106 -38
- package/src/agent-definitions/integrations.ts +201 -73
- package/src/agent-definitions/remote-registry.ts +262 -65
- package/src/agent-definitions/secrets.ts +22 -8
- package/src/agent-definitions/users.ts +16 -4
- package/src/consumer.test.ts +536 -0
- package/src/define-config.ts +205 -0
- package/src/define.ts +134 -46
- package/src/events.ts +237 -0
- package/src/index.ts +89 -8
- package/src/integration-interface.ts +52 -28
- package/src/integrations-store.ts +9 -5
- package/src/jwt.ts +48 -19
- package/src/key-manager.test.ts +22 -13
- package/src/key-manager.ts +8 -10
- package/src/oidc-signin.ts +223 -0
- package/src/registry-consumer.ts +413 -0
- package/src/registry.ts +115 -9
- package/src/secret-collection.ts +2 -1
- package/src/server.test.ts +304 -238
- package/src/server.ts +371 -69
- package/src/test-utils/mock-oidc-server.ts +123 -0
- package/src/types.ts +69 -18
package/src/server.test.ts
CHANGED
|
@@ -1,284 +1,350 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* E2E: atlas.slash.com ↔ registry.slash.com
|
|
3
|
+
*
|
|
4
|
+
* Tests the full production scenario:
|
|
5
|
+
* - registry.slash.com hosts public agents (notion, linear)
|
|
6
|
+
* - atlas.slash.com uses a ConsumerConfig with refs + file:// secrets
|
|
7
|
+
* - createRegistryConsumer connects atlas to the registry
|
|
8
|
+
* - Consumer discovers agents, resolves secrets, calls tools
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
12
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
2
15
|
import {
|
|
3
|
-
createAgentServer,
|
|
4
16
|
createAgentRegistry,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
method: 'tools/call',
|
|
44
|
-
params: { name: toolName, arguments: args },
|
|
17
|
+
createAgentServer,
|
|
18
|
+
createRegistryConsumer,
|
|
19
|
+
defineAgent,
|
|
20
|
+
defineTool,
|
|
21
|
+
isSecretUri,
|
|
22
|
+
} from "./index";
|
|
23
|
+
import type { AgentServer, ConsumerConfig } from "./index";
|
|
24
|
+
import {
|
|
25
|
+
type MockOIDCServer,
|
|
26
|
+
startMockOIDC,
|
|
27
|
+
} from "./test-utils/mock-oidc-server";
|
|
28
|
+
|
|
29
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
30
|
+
// registry.slash.com — the public agent registry
|
|
31
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
32
|
+
|
|
33
|
+
const notionAgent = defineAgent({
|
|
34
|
+
path: "notion",
|
|
35
|
+
entrypoint: "Notion workspace integration",
|
|
36
|
+
config: {
|
|
37
|
+
name: "Notion",
|
|
38
|
+
description: "Search pages, query databases, create content",
|
|
39
|
+
},
|
|
40
|
+
visibility: "public" as const,
|
|
41
|
+
tools: [
|
|
42
|
+
defineTool({
|
|
43
|
+
name: "search_pages",
|
|
44
|
+
description: "Search for pages in Notion",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
query: { type: "string", description: "Search query" },
|
|
49
|
+
},
|
|
50
|
+
required: ["query"],
|
|
51
|
+
},
|
|
52
|
+
execute: async (input: { query: string }) => ({
|
|
53
|
+
results: [{ title: `Found: ${input.query}`, id: "page-abc" }],
|
|
54
|
+
}),
|
|
45
55
|
}),
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
56
|
+
defineTool({
|
|
57
|
+
name: "api",
|
|
58
|
+
description: "Raw Notion API call",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
method: { type: "string" },
|
|
63
|
+
path: { type: "string" },
|
|
64
|
+
body: { type: "object" },
|
|
65
|
+
},
|
|
66
|
+
required: ["method", "path"],
|
|
67
|
+
},
|
|
68
|
+
execute: async (input: { method: string; path: string }) => ({
|
|
69
|
+
status: 200,
|
|
70
|
+
method: input.method,
|
|
71
|
+
path: input.path,
|
|
72
|
+
}),
|
|
73
|
+
}),
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const linearAgent = defineAgent({
|
|
78
|
+
path: "linear",
|
|
79
|
+
entrypoint: "Linear project management",
|
|
80
|
+
config: { name: "Linear", description: "Track issues, manage projects" },
|
|
81
|
+
visibility: "public" as const,
|
|
82
|
+
tools: [
|
|
83
|
+
defineTool({
|
|
84
|
+
name: "list_issues",
|
|
85
|
+
description: "List issues",
|
|
86
|
+
inputSchema: { type: "object", properties: {} },
|
|
87
|
+
execute: async () => ({ issues: [{ id: "ENG-1", title: "Ship it" }] }),
|
|
88
|
+
}),
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Internal system agent — not public
|
|
93
|
+
const secretsAgent = defineAgent({
|
|
94
|
+
path: "@secrets",
|
|
95
|
+
entrypoint: "Internal secrets store",
|
|
96
|
+
visibility: "internal" as const,
|
|
97
|
+
tools: [
|
|
98
|
+
defineTool({
|
|
99
|
+
name: "get",
|
|
100
|
+
description: "Get a secret",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: "object",
|
|
103
|
+
properties: { key: { type: "string" } },
|
|
104
|
+
required: ["key"],
|
|
105
|
+
},
|
|
106
|
+
execute: async () => ({ value: "s3cr3t" }),
|
|
107
|
+
}),
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
112
|
+
// Test suite
|
|
113
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
114
|
+
|
|
115
|
+
describe("atlas ↔ registry E2E", () => {
|
|
116
|
+
// --- registry.slash.com ---
|
|
117
|
+
let registry: AgentServer;
|
|
118
|
+
const REGISTRY_PORT = 19890;
|
|
119
|
+
const REGISTRY_URL = `http://localhost:${REGISTRY_PORT}`;
|
|
120
|
+
|
|
121
|
+
// --- atlas.slash.com secrets (file:// on disk) ---
|
|
122
|
+
let secretsDir: string;
|
|
123
|
+
let authToken: string;
|
|
124
|
+
|
|
125
|
+
// --- OIDC provider ---
|
|
126
|
+
let oidc: MockOIDCServer;
|
|
73
127
|
|
|
74
128
|
beforeAll(async () => {
|
|
75
|
-
// 1.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
129
|
+
// 1. Write secrets to disk (simulating atlas.slash.com's secret store)
|
|
130
|
+
secretsDir = await mkdtemp(join(tmpdir(), "atlas-secrets-"));
|
|
131
|
+
await writeFile(join(secretsDir, "notion-client-id"), "notion_cid_prod");
|
|
132
|
+
await writeFile(join(secretsDir, "notion-client-secret"), "notion_cs_prod");
|
|
133
|
+
process.env.LINEAR_API_KEY = "lin_key_prod";
|
|
134
|
+
|
|
135
|
+
// 2. Start mock OIDC provider
|
|
136
|
+
oidc = await startMockOIDC({ port: 19891 });
|
|
137
|
+
|
|
138
|
+
// 3. Start registry.slash.com
|
|
139
|
+
const reg = createAgentRegistry();
|
|
140
|
+
reg.register(notionAgent);
|
|
141
|
+
reg.register(linearAgent);
|
|
142
|
+
reg.register(secretsAgent);
|
|
143
|
+
|
|
144
|
+
registry = createAgentServer(reg, {
|
|
145
|
+
port: REGISTRY_PORT,
|
|
146
|
+
oidcProvider: {
|
|
147
|
+
issuer: oidc.issuer,
|
|
148
|
+
clientId: oidc.clientId,
|
|
149
|
+
clientSecret: oidc.clientSecret,
|
|
93
150
|
},
|
|
94
151
|
});
|
|
152
|
+
await registry.initKeys();
|
|
153
|
+
await registry.start();
|
|
95
154
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// 3. Create server with trusted issuer — NO @auth agent registered
|
|
102
|
-
// This is the exact scenario that was broken.
|
|
103
|
-
server = createAgentServer(registry, {
|
|
104
|
-
port: SDK_PORT,
|
|
105
|
-
trustedIssuers: [{
|
|
106
|
-
issuer: ISSUER_URL,
|
|
107
|
-
scopes: ['agents:admin'],
|
|
108
|
-
}],
|
|
155
|
+
// 4. Get auth token (simulating atlas user signing in)
|
|
156
|
+
authToken = await registry.signJwt({
|
|
157
|
+
sub: "atlas-user-001",
|
|
158
|
+
email: "user@slash.com",
|
|
109
159
|
});
|
|
110
|
-
await server.start();
|
|
111
160
|
});
|
|
112
161
|
|
|
113
|
-
afterAll(() => {
|
|
114
|
-
|
|
115
|
-
|
|
162
|
+
afterAll(async () => {
|
|
163
|
+
await registry?.stop?.();
|
|
164
|
+
await oidc?.stop();
|
|
165
|
+
await rm(secretsDir, { recursive: true, force: true });
|
|
166
|
+
process.env.LINEAR_API_KEY = undefined;
|
|
116
167
|
});
|
|
117
168
|
|
|
118
|
-
|
|
119
|
-
return new SignJWT({ sub: 'atlas-api', ...claims } as any)
|
|
120
|
-
.setProtectedHeader({ alg: 'ES256', kid: KID })
|
|
121
|
-
.setIssuer(ISSUER_URL)
|
|
122
|
-
.setIssuedAt()
|
|
123
|
-
.setExpirationTime('5m')
|
|
124
|
-
.sign(privateKey);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ─── Core auth flow tests ───────────────────────────────────
|
|
128
|
-
|
|
129
|
-
test('system token → can load internal agent', async () => {
|
|
130
|
-
const token = await signToken();
|
|
131
|
-
const rpc = await mcpCall(SDK_PORT, 'call_agent', {
|
|
132
|
-
request: { action: 'load', path: '/agents/@clock' },
|
|
133
|
-
}, token);
|
|
134
|
-
|
|
135
|
-
const result = parseResult(rpc);
|
|
136
|
-
expect(result.success).toBe(true);
|
|
137
|
-
});
|
|
169
|
+
// ── Discovery ──────────────────────────────────────────────────
|
|
138
170
|
|
|
139
|
-
test(
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
171
|
+
test("consumer discovers public agents on registry", async () => {
|
|
172
|
+
const consumer = await createRegistryConsumer(
|
|
173
|
+
{ registries: [REGISTRY_URL] },
|
|
174
|
+
{ token: authToken },
|
|
175
|
+
);
|
|
143
176
|
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
expect(
|
|
177
|
+
const agents = await consumer.list();
|
|
178
|
+
const paths = agents.map((a) => a.path);
|
|
179
|
+
expect(paths).toContain("notion");
|
|
180
|
+
expect(paths).toContain("linear");
|
|
147
181
|
});
|
|
148
182
|
|
|
149
|
-
test(
|
|
150
|
-
const
|
|
151
|
-
|
|
183
|
+
test("unauthenticated consumer sees only public agents", async () => {
|
|
184
|
+
const consumer = await createRegistryConsumer({
|
|
185
|
+
registries: [REGISTRY_URL],
|
|
152
186
|
});
|
|
153
187
|
|
|
154
|
-
const
|
|
155
|
-
|
|
188
|
+
const agents = await consumer.list();
|
|
189
|
+
const paths = agents.map((a) => a.path);
|
|
190
|
+
expect(paths).toContain("notion");
|
|
191
|
+
expect(paths).not.toContain("@secrets");
|
|
156
192
|
});
|
|
157
193
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
194
|
+
// ── Consumer config with refs + secrets ─────────────────────────
|
|
195
|
+
|
|
196
|
+
test("consumer config with agent URL + file:// secrets", async () => {
|
|
197
|
+
const config: ConsumerConfig = {
|
|
198
|
+
refs: [
|
|
199
|
+
{
|
|
200
|
+
ref: "notion",
|
|
201
|
+
url: `${REGISTRY_URL}/agents/notion`,
|
|
202
|
+
config: {
|
|
203
|
+
clientId: `file://${join(secretsDir, "notion-client-id")}`,
|
|
204
|
+
clientSecret: `file://${join(secretsDir, "notion-client-secret")}`,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const consumer = await createRegistryConsumer(config, { token: authToken });
|
|
211
|
+
|
|
212
|
+
// Resolve secrets
|
|
213
|
+
const ref = config.refs?.[0] as { config: Record<string, string> };
|
|
214
|
+
const resolved = await consumer.resolveConfig(ref.config);
|
|
215
|
+
expect(resolved.clientId).toBe("notion_cid_prod");
|
|
216
|
+
expect(resolved.clientSecret).toBe("notion_cs_prod");
|
|
166
217
|
});
|
|
167
218
|
|
|
168
|
-
test(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
219
|
+
test("consumer config with env:// secrets", async () => {
|
|
220
|
+
const config: ConsumerConfig = {
|
|
221
|
+
refs: [
|
|
222
|
+
{
|
|
223
|
+
ref: "linear",
|
|
224
|
+
url: `${REGISTRY_URL}/agents/linear`,
|
|
225
|
+
config: {
|
|
226
|
+
apiKey: "env://LINEAR_API_KEY",
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const consumer = await createRegistryConsumer(config, { token: authToken });
|
|
233
|
+
const ref = config.refs?.[0] as { config: Record<string, string> };
|
|
234
|
+
const resolved = await consumer.resolveConfig(ref.config);
|
|
235
|
+
expect(resolved.apiKey).toBe("lin_key_prod");
|
|
184
236
|
});
|
|
185
237
|
|
|
186
|
-
//
|
|
238
|
+
// ── Calling agent tools ────────────────────────────────────────
|
|
187
239
|
|
|
188
|
-
test(
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
240
|
+
test("consumer calls notion/search_pages via registry", async () => {
|
|
241
|
+
const consumer = await createRegistryConsumer(
|
|
242
|
+
{
|
|
243
|
+
registries: [REGISTRY_URL],
|
|
244
|
+
refs: ["notion"],
|
|
245
|
+
},
|
|
246
|
+
{ token: authToken },
|
|
247
|
+
);
|
|
192
248
|
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
249
|
+
const result = await consumer.call("notion", "search_pages", {
|
|
250
|
+
query: "meeting notes",
|
|
251
|
+
});
|
|
252
|
+
expect(result).toBeDefined();
|
|
196
253
|
});
|
|
197
254
|
|
|
198
|
-
test(
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
255
|
+
test("consumer calls linear/list_issues via registry", async () => {
|
|
256
|
+
const consumer = await createRegistryConsumer(
|
|
257
|
+
{
|
|
258
|
+
registries: [REGISTRY_URL],
|
|
259
|
+
refs: ["linear"],
|
|
260
|
+
},
|
|
261
|
+
{ token: authToken },
|
|
262
|
+
);
|
|
203
263
|
|
|
204
|
-
const
|
|
205
|
-
expect(
|
|
206
|
-
expect(paths).toContain('/agents/@clock');
|
|
264
|
+
const result = await consumer.call("linear", "list_issues", {});
|
|
265
|
+
expect(result).toBeDefined();
|
|
207
266
|
});
|
|
208
267
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
268
|
+
test("consumer calls notion/api proxy tool", async () => {
|
|
269
|
+
const consumer = await createRegistryConsumer(
|
|
270
|
+
{
|
|
271
|
+
registries: [REGISTRY_URL],
|
|
272
|
+
refs: ["notion"],
|
|
273
|
+
},
|
|
274
|
+
{ token: authToken },
|
|
275
|
+
);
|
|
216
276
|
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
issuer: ISSUER_URL,
|
|
221
|
-
scopes: ['agents:read'], // NOT agents:admin or *
|
|
222
|
-
}],
|
|
277
|
+
const result = await consumer.call("notion", "api", {
|
|
278
|
+
method: "GET",
|
|
279
|
+
path: "/v1/pages/page-123",
|
|
223
280
|
});
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
try {
|
|
227
|
-
const token = await signToken();
|
|
228
|
-
|
|
229
|
-
// agents:read grants agent-level access (not system)
|
|
230
|
-
// Internal agents are accessible to authenticated agents
|
|
231
|
-
const internalRpc = await mcpCall(19882, 'call_agent', {
|
|
232
|
-
request: { action: 'load', path: '/agents/@internal-agent' },
|
|
233
|
-
}, token);
|
|
234
|
-
expect(parseResult(internalRpc).success).toBe(true);
|
|
235
|
-
|
|
236
|
-
// Private agents should be denied (only self can access)
|
|
237
|
-
const privateRpc = await mcpCall(19882, 'call_agent', {
|
|
238
|
-
request: { action: 'load', path: '/agents/@private-agent' },
|
|
239
|
-
}, token);
|
|
240
|
-
expect(parseResult(privateRpc).success).toBe(false);
|
|
241
|
-
expect(parseResult(privateRpc).code).toBe('ACCESS_DENIED');
|
|
242
|
-
} finally {
|
|
243
|
-
limitedServer?.stop?.();
|
|
244
|
-
}
|
|
281
|
+
expect(result).toBeDefined();
|
|
245
282
|
});
|
|
246
|
-
});
|
|
247
283
|
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
284
|
+
// ── OIDC sign-in flow ──────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
test("OIDC sign-in returns JWT usable by consumer", async () => {
|
|
287
|
+
// Step 1: authorize
|
|
288
|
+
const r1 = await fetch(
|
|
289
|
+
`${REGISTRY_URL}/signin/authorize?redirect_uri=http://localhost:9999/done`,
|
|
290
|
+
{ redirect: "manual" },
|
|
291
|
+
);
|
|
292
|
+
expect(r1.status).toBe(302);
|
|
293
|
+
|
|
294
|
+
// Step 2: OIDC provider
|
|
295
|
+
const r2 = await fetch(r1.headers.get("location")!, { redirect: "manual" });
|
|
296
|
+
expect(r2.status).toBe(302);
|
|
297
|
+
|
|
298
|
+
// Step 3: callback → JWT
|
|
299
|
+
const r3 = await fetch(r2.headers.get("location")!, { redirect: "manual" });
|
|
300
|
+
expect(r3.status).toBe(302);
|
|
301
|
+
const jwt = new URL(r3.headers.get("location")!).searchParams.get("token")!;
|
|
302
|
+
expect(jwt.split(".")).toHaveLength(3);
|
|
303
|
+
|
|
304
|
+
// Step 4: JWT works with consumer
|
|
305
|
+
const consumer = await createRegistryConsumer(
|
|
306
|
+
{ registries: [REGISTRY_URL] },
|
|
307
|
+
{ token: jwt },
|
|
308
|
+
);
|
|
309
|
+
const agents = await consumer.list();
|
|
310
|
+
expect(agents.map((a) => a.path)).toContain("notion");
|
|
256
311
|
});
|
|
257
312
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
313
|
+
// ── Auth guards ────────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
test("tools/call without auth returns 401", async () => {
|
|
316
|
+
const res = await fetch(`${REGISTRY_URL}/agents/notion`, {
|
|
317
|
+
method: "POST",
|
|
318
|
+
headers: { "Content-Type": "application/json" },
|
|
319
|
+
body: JSON.stringify({
|
|
320
|
+
jsonrpc: "2.0",
|
|
321
|
+
id: 1,
|
|
322
|
+
method: "tools/call",
|
|
323
|
+
params: { name: "search_pages", arguments: { query: "test" } },
|
|
324
|
+
}),
|
|
325
|
+
});
|
|
326
|
+
expect(res.status).toBe(401);
|
|
263
327
|
});
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
// ─── Unit: canSeeAgent ───────────────────────────────────────────
|
|
267
328
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const auth = { callerId: 'api', callerType: 'system' as const, scopes: ['*'], isRoot: true };
|
|
272
|
-
expect(canSeeAgent(agent, auth)).toBe(true);
|
|
329
|
+
test("internal agent not visible without auth", async () => {
|
|
330
|
+
const res = await fetch(`${REGISTRY_URL}/agents/@secrets`);
|
|
331
|
+
expect(res.status).toBe(404);
|
|
273
332
|
});
|
|
274
333
|
|
|
275
|
-
test(
|
|
276
|
-
const
|
|
277
|
-
|
|
334
|
+
test("internal agent visible with auth", async () => {
|
|
335
|
+
const res = await fetch(`${REGISTRY_URL}/agents/@secrets`, {
|
|
336
|
+
headers: { Authorization: `Bearer ${authToken}` },
|
|
337
|
+
});
|
|
338
|
+
expect(res.status).toBe(200);
|
|
278
339
|
});
|
|
279
340
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
341
|
+
// ── Secret URI helpers ─────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
test("isSecretUri recognizes supported schemes", () => {
|
|
344
|
+
expect(isSecretUri("file:///tmp/key")).toBe(true);
|
|
345
|
+
expect(isSecretUri("env://VAR")).toBe(true);
|
|
346
|
+
expect(isSecretUri("https://vault/key")).toBe(true);
|
|
347
|
+
expect(isSecretUri("just-a-string")).toBe(false);
|
|
348
|
+
expect(isSecretUri(42)).toBe(false);
|
|
283
349
|
});
|
|
284
350
|
});
|