@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.
Files changed (93) hide show
  1. package/dist/agent-definitions/auth.d.ts.map +1 -1
  2. package/dist/agent-definitions/auth.js +44 -11
  3. package/dist/agent-definitions/auth.js.map +1 -1
  4. package/dist/agent-definitions/integrations.d.ts.map +1 -1
  5. package/dist/agent-definitions/integrations.js +106 -45
  6. package/dist/agent-definitions/integrations.js.map +1 -1
  7. package/dist/agent-definitions/remote-registry.d.ts.map +1 -1
  8. package/dist/agent-definitions/remote-registry.js +174 -45
  9. package/dist/agent-definitions/remote-registry.js.map +1 -1
  10. package/dist/agent-definitions/secrets.d.ts.map +1 -1
  11. package/dist/agent-definitions/secrets.js +1 -4
  12. package/dist/agent-definitions/secrets.js.map +1 -1
  13. package/dist/agent-definitions/users.d.ts.map +1 -1
  14. package/dist/agent-definitions/users.js +14 -3
  15. package/dist/agent-definitions/users.js.map +1 -1
  16. package/dist/define-config.d.ts +125 -0
  17. package/dist/define-config.d.ts.map +1 -0
  18. package/dist/define-config.js +75 -0
  19. package/dist/define-config.js.map +1 -0
  20. package/dist/define.d.ts +11 -2
  21. package/dist/define.d.ts.map +1 -1
  22. package/dist/define.js +57 -26
  23. package/dist/define.js.map +1 -1
  24. package/dist/events.d.ts +133 -0
  25. package/dist/events.d.ts.map +1 -0
  26. package/dist/events.js +57 -0
  27. package/dist/events.js.map +1 -0
  28. package/dist/index.d.ts +15 -7
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +9 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/integration-interface.d.ts +3 -3
  33. package/dist/integration-interface.d.ts.map +1 -1
  34. package/dist/integration-interface.js +29 -21
  35. package/dist/integration-interface.js.map +1 -1
  36. package/dist/integrations-store.d.ts +2 -2
  37. package/dist/integrations-store.d.ts.map +1 -1
  38. package/dist/integrations-store.js +3 -3
  39. package/dist/integrations-store.js.map +1 -1
  40. package/dist/jwt.d.ts.map +1 -1
  41. package/dist/jwt.js +7 -5
  42. package/dist/jwt.js.map +1 -1
  43. package/dist/key-manager.d.ts.map +1 -1
  44. package/dist/key-manager.js +5 -3
  45. package/dist/key-manager.js.map +1 -1
  46. package/dist/oidc-signin.d.ts +32 -0
  47. package/dist/oidc-signin.d.ts.map +1 -0
  48. package/dist/oidc-signin.js +138 -0
  49. package/dist/oidc-signin.js.map +1 -0
  50. package/dist/registry-consumer.d.ts +104 -0
  51. package/dist/registry-consumer.d.ts.map +1 -0
  52. package/dist/registry-consumer.js +230 -0
  53. package/dist/registry-consumer.js.map +1 -0
  54. package/dist/registry.d.ts +5 -0
  55. package/dist/registry.d.ts.map +1 -1
  56. package/dist/registry.js +76 -4
  57. package/dist/registry.js.map +1 -1
  58. package/dist/secret-collection.d.ts.map +1 -1
  59. package/dist/secret-collection.js.map +1 -1
  60. package/dist/server.d.ts +3 -0
  61. package/dist/server.d.ts.map +1 -1
  62. package/dist/server.js +222 -27
  63. package/dist/server.js.map +1 -1
  64. package/dist/test-utils/mock-oidc-server.d.ts +36 -0
  65. package/dist/test-utils/mock-oidc-server.d.ts.map +1 -0
  66. package/dist/test-utils/mock-oidc-server.js +96 -0
  67. package/dist/test-utils/mock-oidc-server.js.map +1 -0
  68. package/dist/types.d.ts +17 -0
  69. package/dist/types.d.ts.map +1 -1
  70. package/package.json +1 -1
  71. package/src/agent-definitions/auth.ts +106 -38
  72. package/src/agent-definitions/integrations.ts +201 -73
  73. package/src/agent-definitions/remote-registry.ts +262 -65
  74. package/src/agent-definitions/secrets.ts +22 -8
  75. package/src/agent-definitions/users.ts +16 -4
  76. package/src/consumer.test.ts +536 -0
  77. package/src/define-config.ts +205 -0
  78. package/src/define.ts +134 -46
  79. package/src/events.ts +237 -0
  80. package/src/index.ts +89 -8
  81. package/src/integration-interface.ts +52 -28
  82. package/src/integrations-store.ts +9 -5
  83. package/src/jwt.ts +48 -19
  84. package/src/key-manager.test.ts +22 -13
  85. package/src/key-manager.ts +8 -10
  86. package/src/oidc-signin.ts +223 -0
  87. package/src/registry-consumer.ts +413 -0
  88. package/src/registry.ts +115 -9
  89. package/src/secret-collection.ts +2 -1
  90. package/src/server.test.ts +304 -238
  91. package/src/server.ts +371 -69
  92. package/src/test-utils/mock-oidc-server.ts +123 -0
  93. package/src/types.ts +69 -18
@@ -1,284 +1,350 @@
1
- import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
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
- detectAuth,
6
- resolveAuth,
7
- canSeeAgent,
8
- } from './index';
9
- import type { AgentDefinition, TrustedIssuer, AgentServer } from './index';
10
- import { generateKeyPair, exportJWK, SignJWT } from 'jose';
11
-
12
- // ─── Helpers ─────────────────────────────────────────────────────
13
-
14
- function makeAgent(
15
- path: string,
16
- opts: Partial<AgentDefinition> = {},
17
- ): AgentDefinition {
18
- return {
19
- path,
20
- entrypoint: 'test',
21
- tools: [],
22
- visibility: 'internal',
23
- config: { name: path.split('/').pop(), supportedActions: ['load'] },
24
- ...opts,
25
- } as AgentDefinition;
26
- }
27
-
28
- async function mcpCall(
29
- port: number,
30
- toolName: string,
31
- args: Record<string, unknown>,
32
- authToken?: string,
33
- ) {
34
- const headers: Record<string, string> = { 'Content-Type': 'application/json' };
35
- if (authToken) headers.Authorization = `Bearer ${authToken}`;
36
-
37
- const res = await fetch(`http://localhost:${port}`, {
38
- method: 'POST',
39
- headers,
40
- body: JSON.stringify({
41
- jsonrpc: '2.0',
42
- id: Date.now(),
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
- return res.json() as Promise<any>;
48
- }
49
-
50
- function parseResult(rpc: any): any {
51
- const text = rpc.result?.content?.[0]?.text;
52
- return text ? JSON.parse(text) : null;
53
- }
54
-
55
- // ─── E2E: Full server auth flow ──────────────────────────────────
56
- //
57
- // These tests spin up a real createAgentServer, send actual HTTP
58
- // requests, and verify the complete path:
59
- // HTTP request → auth resolution → handleToolCall → registry.call → access check
60
- //
61
- // This is what actually broke today: authConfig was null → resolveAuth
62
- // was skipped → trusted issuer tokens were ignored.
63
-
64
- describe('E2E: createAgentServer with trusted issuers', () => {
65
- let privateKey: CryptoKey;
66
- let publicJwk: any;
67
- let jwksHttpServer: ReturnType<typeof Bun.serve>;
68
- let server: AgentServer;
69
- const JWKS_PORT = 19880;
70
- const SDK_PORT = 19881;
71
- const ISSUER_URL = `http://localhost:${JWKS_PORT}`;
72
- const KID = 'test-e2e-key';
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. Generate ES256 keypair and serve JWKS
76
- const keyPair = await generateKeyPair('ES256', { extractable: true });
77
- privateKey = keyPair.privateKey;
78
- publicJwk = await exportJWK(keyPair.publicKey);
79
- publicJwk.kid = KID;
80
- publicJwk.alg = 'ES256';
81
- publicJwk.use = 'sig';
82
-
83
- jwksHttpServer = Bun.serve({
84
- port: JWKS_PORT,
85
- fetch(req) {
86
- const url = new URL(req.url);
87
- if (url.pathname === '/.well-known/jwks.json') {
88
- return new Response(JSON.stringify({ keys: [publicJwk] }), {
89
- headers: { 'Content-Type': 'application/json' },
90
- });
91
- }
92
- return new Response('Not found', { status: 404 });
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
- // 2. Create registry with internal + public agents
97
- const registry = createAgentRegistry();
98
- registry.register(makeAgent('/agents/@clock', { visibility: 'internal' }));
99
- registry.register(makeAgent('/agents/public-bot', { visibility: 'public' }));
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
- server?.stop?.();
115
- jwksHttpServer?.stop();
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
- async function signToken(claims: Record<string, unknown> = {}): Promise<string> {
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('no token access denied for internal agent', async () => {
140
- const rpc = await mcpCall(SDK_PORT, 'call_agent', {
141
- request: { action: 'load', path: '/agents/@clock' },
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 result = parseResult(rpc);
145
- expect(result.success).toBe(false);
146
- expect(result.code).toBe('ACCESS_DENIED');
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('no token public agent still accessible', async () => {
150
- const rpc = await mcpCall(SDK_PORT, 'call_agent', {
151
- request: { action: 'load', path: '/agents/public-bot' },
183
+ test("unauthenticated consumer sees only public agents", async () => {
184
+ const consumer = await createRegistryConsumer({
185
+ registries: [REGISTRY_URL],
152
186
  });
153
187
 
154
- const result = parseResult(rpc);
155
- expect(result.success).toBe(true);
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
- test('garbage token access denied', async () => {
159
- const rpc = await mcpCall(SDK_PORT, 'call_agent', {
160
- request: { action: 'load', path: '/agents/@clock' },
161
- }, 'not.a.valid.jwt');
162
-
163
- const result = parseResult(rpc);
164
- expect(result.success).toBe(false);
165
- expect(result.code).toBe('ACCESS_DENIED');
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('token with wrong issuer → access denied', async () => {
169
- // Sign with correct key but wrong iss claim
170
- const token = await new SignJWT({ sub: 'evil' } as any)
171
- .setProtectedHeader({ alg: 'ES256', kid: KID })
172
- .setIssuer('http://evil:9999') // not in trustedIssuers
173
- .setIssuedAt()
174
- .setExpirationTime('5m')
175
- .sign(privateKey);
176
-
177
- const rpc = await mcpCall(SDK_PORT, 'call_agent', {
178
- request: { action: 'load', path: '/agents/@clock' },
179
- }, token);
180
-
181
- const result = parseResult(rpc);
182
- expect(result.success).toBe(false);
183
- expect(result.code).toBe('ACCESS_DENIED');
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
- // ─── Visibility in list_agents ─────────────────────────────
238
+ // ── Calling agent tools ────────────────────────────────────────
187
239
 
188
- test('list_agents without token only public agents', async () => {
189
- const rpc = await mcpCall(SDK_PORT, 'list_agents', {});
190
- const result = parseResult(rpc);
191
- expect(result.success).toBe(true);
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 paths = result.agents.map((a: any) => a.path);
194
- expect(paths).toContain('/agents/public-bot');
195
- expect(paths).not.toContain('/agents/@clock');
249
+ const result = await consumer.call("notion", "search_pages", {
250
+ query: "meeting notes",
251
+ });
252
+ expect(result).toBeDefined();
196
253
  });
197
254
 
198
- test('list_agents with system token → all agents visible', async () => {
199
- const token = await signToken();
200
- const rpc = await mcpCall(SDK_PORT, 'list_agents', {}, token);
201
- const result = parseResult(rpc);
202
- expect(result.success).toBe(true);
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 paths = result.agents.map((a: any) => a.path);
205
- expect(paths).toContain('/agents/public-bot');
206
- expect(paths).toContain('/agents/@clock');
264
+ const result = await consumer.call("linear", "list_issues", {});
265
+ expect(result).toBeDefined();
207
266
  });
208
267
 
209
- // ─── Scopes: limited issuer ────────────────────────────────
210
-
211
- test('issuer with limited scopes → resolves as agent, not system', async () => {
212
- // Create a separate server with limited-scope issuer
213
- const limitedRegistry = createAgentRegistry();
214
- limitedRegistry.register(makeAgent('/agents/@private-agent', { visibility: 'private' }));
215
- limitedRegistry.register(makeAgent('/agents/@internal-agent', { visibility: 'internal' }));
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 limitedServer = createAgentServer(limitedRegistry, {
218
- port: 19882,
219
- trustedIssuers: [{
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
- await limitedServer.start();
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
- // ─── Unit: detectAuth ────────────────────────────────────────────
249
-
250
- describe('detectAuth', () => {
251
- test('returns non-null even without @auth agent', () => {
252
- const registry = createAgentRegistry();
253
- const config = detectAuth(registry);
254
- expect(config).toBeDefined();
255
- expect(config).not.toBeNull();
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
- test('returns empty config (no store, no rootKey) without @auth agent', () => {
259
- const registry = createAgentRegistry();
260
- const config = detectAuth(registry);
261
- expect(config.store).toBeUndefined();
262
- expect(config.rootKey).toBeUndefined();
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
- describe('canSeeAgent', () => {
269
- test('system auth can see internal agents', () => {
270
- const agent = makeAgent('/agents/@clock', { visibility: 'internal' });
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('null auth cannot see internal agents', () => {
276
- const agent = makeAgent('/agents/@clock', { visibility: 'internal' });
277
- expect(canSeeAgent(agent, null)).toBe(false);
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
- test('null auth can see public agents', () => {
281
- const agent = makeAgent('/agents/public', { visibility: 'public' });
282
- expect(canSeeAgent(agent, null)).toBe(true);
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
  });