@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
@@ -0,0 +1,223 @@
1
+ /**
2
+ * OIDC Sign-In Provider
3
+ *
4
+ * Implements the full OIDC authorization code flow for user sign-in.
5
+ * The server acts as an OIDC Relying Party — users authenticate with
6
+ * an external Identity Provider and receive a server-signed JWT.
7
+ *
8
+ * Flow:
9
+ * GET /signin/authorize → 302 redirect to IdP
10
+ * GET /signin/callback → exchange code → fetch userinfo → sign JWT → return
11
+ */
12
+
13
+ import { type SigningKey, signJwtES256 } from "./jwt.js";
14
+
15
+ export interface OIDCProviderConfig {
16
+ /** OIDC issuer URL (used for discovery) */
17
+ issuer: string;
18
+ /** OAuth client ID */
19
+ clientId: string;
20
+ /** OAuth client secret */
21
+ clientSecret: string;
22
+ /** Scopes to request (default: ["openid", "email", "profile"]) */
23
+ scopes?: string[];
24
+ }
25
+
26
+ /** Cached OIDC discovery document */
27
+ interface OIDCDiscovery {
28
+ authorization_endpoint: string;
29
+ token_endpoint: string;
30
+ userinfo_endpoint: string;
31
+ }
32
+
33
+ /** In-flight state for pending auth flows */
34
+ interface PendingFlow {
35
+ redirectUri: string;
36
+ nonce: string;
37
+ createdAt: number;
38
+ }
39
+
40
+ export interface OIDCSignInHandler {
41
+ /** Handle incoming request — call from server fetch */
42
+ handleRequest(
43
+ req: Request,
44
+ params: {
45
+ baseUrl: string;
46
+ signingKey: SigningKey;
47
+ issuerUrl: string;
48
+ },
49
+ ): Promise<Response | null>;
50
+ }
51
+
52
+ export function createOIDCSignIn(
53
+ config: OIDCProviderConfig,
54
+ ): OIDCSignInHandler {
55
+ let discoveryCache: OIDCDiscovery | null = null;
56
+ const pendingFlows = new Map<string, PendingFlow>();
57
+
58
+ async function fetchDiscovery(): Promise<OIDCDiscovery> {
59
+ if (discoveryCache) return discoveryCache;
60
+ const url = `${config.issuer}/.well-known/openid-configuration`;
61
+ const res = await fetch(url);
62
+ if (!res.ok) throw new Error(`OIDC discovery failed: ${res.status}`);
63
+ discoveryCache = (await res.json()) as OIDCDiscovery;
64
+ return discoveryCache;
65
+ }
66
+
67
+ function generateState(): string {
68
+ const bytes = crypto.getRandomValues(new Uint8Array(24));
69
+ return Buffer.from(bytes).toString("base64url");
70
+ }
71
+
72
+ return {
73
+ async handleRequest(req, { baseUrl, signingKey, issuerUrl }) {
74
+ const url = new URL(req.url);
75
+
76
+ // ── GET /signin/authorize → redirect to IdP ──
77
+ if (url.pathname === "/signin/authorize" && req.method === "GET") {
78
+ const redirectUri = url.searchParams.get("redirect_uri") ?? "";
79
+ if (!redirectUri) {
80
+ return Response.json(
81
+ {
82
+ error: "invalid_request",
83
+ error_description: "Missing redirect_uri",
84
+ },
85
+ { status: 400 },
86
+ );
87
+ }
88
+
89
+ const discovery = await fetchDiscovery();
90
+ const state = generateState();
91
+ const nonce = generateState();
92
+
93
+ pendingFlows.set(state, {
94
+ redirectUri,
95
+ nonce,
96
+ createdAt: Date.now(),
97
+ });
98
+
99
+ // Clean up stale flows (> 10 min)
100
+ const now = Date.now();
101
+ for (const [k, v] of pendingFlows) {
102
+ if (now - v.createdAt > 600_000) pendingFlows.delete(k);
103
+ }
104
+
105
+ const scopes = config.scopes ?? ["openid", "email", "profile"];
106
+ const callbackUrl = `${baseUrl}/signin/callback`;
107
+ const authUrl = new URL(discovery.authorization_endpoint);
108
+ authUrl.searchParams.set("response_type", "code");
109
+ authUrl.searchParams.set("client_id", config.clientId);
110
+ authUrl.searchParams.set("redirect_uri", callbackUrl);
111
+ authUrl.searchParams.set("scope", scopes.join(" "));
112
+ authUrl.searchParams.set("state", state);
113
+ authUrl.searchParams.set("nonce", nonce);
114
+
115
+ return Response.redirect(authUrl.toString(), 302);
116
+ }
117
+
118
+ // ── GET /signin/callback → exchange code → userinfo → JWT ──
119
+ if (url.pathname === "/signin/callback" && req.method === "GET") {
120
+ const code = url.searchParams.get("code");
121
+ const state = url.searchParams.get("state");
122
+ const error = url.searchParams.get("error");
123
+
124
+ if (error) {
125
+ return Response.json(
126
+ {
127
+ error,
128
+ error_description:
129
+ url.searchParams.get("error_description") ?? "",
130
+ },
131
+ { status: 400 },
132
+ );
133
+ }
134
+
135
+ if (!code || !state) {
136
+ return Response.json(
137
+ {
138
+ error: "invalid_request",
139
+ error_description: "Missing code or state",
140
+ },
141
+ { status: 400 },
142
+ );
143
+ }
144
+
145
+ const flow = pendingFlows.get(state);
146
+ if (!flow) {
147
+ return Response.json(
148
+ {
149
+ error: "invalid_state",
150
+ error_description: "Unknown or expired state",
151
+ },
152
+ { status: 400 },
153
+ );
154
+ }
155
+ pendingFlows.delete(state);
156
+
157
+ const discovery = await fetchDiscovery();
158
+ const callbackUrl = `${baseUrl}/signin/callback`;
159
+
160
+ // Exchange code for tokens
161
+ const tokenRes = await fetch(discovery.token_endpoint, {
162
+ method: "POST",
163
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
164
+ body: new URLSearchParams({
165
+ grant_type: "authorization_code",
166
+ code,
167
+ redirect_uri: callbackUrl,
168
+ client_id: config.clientId,
169
+ client_secret: config.clientSecret,
170
+ }),
171
+ });
172
+
173
+ if (!tokenRes.ok) {
174
+ const text = await tokenRes.text();
175
+ return Response.json(
176
+ { error: "token_exchange_failed", error_description: text },
177
+ { status: 502 },
178
+ );
179
+ }
180
+
181
+ const tokens = (await tokenRes.json()) as { access_token: string };
182
+
183
+ // Fetch userinfo
184
+ const userinfoRes = await fetch(discovery.userinfo_endpoint, {
185
+ headers: { Authorization: `Bearer ${tokens.access_token}` },
186
+ });
187
+
188
+ if (!userinfoRes.ok) {
189
+ return Response.json(
190
+ {
191
+ error: "userinfo_failed",
192
+ error_description: `Status ${userinfoRes.status}`,
193
+ },
194
+ { status: 502 },
195
+ );
196
+ }
197
+
198
+ const userinfo = (await userinfoRes.json()) as Record<string, unknown>;
199
+
200
+ // Sign server JWT with user's identity
201
+ const jwt = await signJwtES256(
202
+ {
203
+ sub: (userinfo.sub as string) ?? "unknown",
204
+ name: (userinfo.name as string) ?? "unknown",
205
+ scopes: ["*"],
206
+ iss: issuerUrl,
207
+ } as Parameters<typeof signJwtES256>[0],
208
+ signingKey.privateKey,
209
+ signingKey.kid,
210
+ issuerUrl,
211
+ "1h",
212
+ );
213
+
214
+ // Redirect back to the caller with the JWT
215
+ const sep = flow.redirectUri.includes("?") ? "&" : "?";
216
+ return Response.redirect(`${flow.redirectUri}${sep}token=${jwt}`, 302);
217
+ }
218
+
219
+ // Not a signin route
220
+ return null;
221
+ },
222
+ };
223
+ }
@@ -0,0 +1,413 @@
1
+ /**
2
+ * Registry Consumer — Connects to registries and resolves refs.
3
+ *
4
+ * The consumer reads a `ConsumerConfig`, discovers registries via
5
+ * `/.well-known/configuration`, resolves refs to agent definitions,
6
+ * and provides a unified interface for calling tools across all connected agents.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { createRegistryConsumer, defineConfig } from '@slashfi/agents-sdk';
11
+ *
12
+ * const config = defineConfig({
13
+ * registries: ['https://registry.slash.com'],
14
+ * refs: ['notion', { ref: 'postgres', as: 'prod-db', config: { url: '...' } }],
15
+ * });
16
+ *
17
+ * const consumer = await createRegistryConsumer(config);
18
+ *
19
+ * // List all available agents across registries
20
+ * const available = await consumer.list();
21
+ *
22
+ * // List configured refs
23
+ * const refs = consumer.refs();
24
+ *
25
+ * // Call a tool on a ref
26
+ * const result = await consumer.call('notion', 'search', { query: 'meeting notes' });
27
+ *
28
+ * // Resolve secrets in config values
29
+ * const dbUrl = await consumer.resolveSecret('https://twin.slash.com/secrets/crdb-url');
30
+ * ```
31
+ */
32
+
33
+ import type {
34
+ ConsumerConfig,
35
+ RefConfig,
36
+ ResolvedConfig,
37
+ ResolvedRef,
38
+ ResolvedRegistry,
39
+ } from "./define-config.js";
40
+ import {
41
+ isSecretUrl,
42
+ normalizeRef,
43
+ normalizeRegistry,
44
+ } from "./define-config.js";
45
+
46
+ // ============================================
47
+ // Registry Discovery Types
48
+ // ============================================
49
+
50
+ /** Registry well-known configuration (from /.well-known/configuration) */
51
+ export interface RegistryConfiguration {
52
+ issuer: string;
53
+ jwks_uri?: string;
54
+ token_endpoint?: string;
55
+ agents_endpoint?: string;
56
+ call_endpoint?: string;
57
+ supported_grant_types?: string[];
58
+ /** @deprecated Use agents_endpoint + GET /list instead */
59
+ agents?: string[];
60
+ }
61
+
62
+ /** An agent definition as listed by a registry */
63
+ export interface AgentListing {
64
+ /** Agent path (e.g., '@notion') */
65
+ path: string;
66
+ /** Description */
67
+ description?: string;
68
+ /** Publisher (registry name) */
69
+ publisher: string;
70
+ /** Tools available */
71
+ tools?: Array<{
72
+ name: string;
73
+ description?: string;
74
+ }>;
75
+ /** Whether it requires auth */
76
+ requiresAuth?: boolean;
77
+ /** Integration config if applicable */
78
+ integration?: {
79
+ provider: string;
80
+ displayName: string;
81
+ category?: string;
82
+ };
83
+ }
84
+
85
+ // ============================================
86
+ // Secret Resolver
87
+ // ============================================
88
+
89
+ /** Resolves secret URLs to their values */
90
+ import { readFile } from "node:fs/promises";
91
+
92
+ export type SecretResolver = (
93
+ uri: string,
94
+ auth?: { token?: string },
95
+ ) => Promise<string>;
96
+
97
+ /**
98
+ * Default secret resolver — dispatches on URI scheme:
99
+ * file:// → read from filesystem
100
+ * env:// → read from environment variable
101
+ * https:// → HTTP GET with optional bearer token
102
+ * http:// → HTTP GET (dev only)
103
+ */
104
+ async function defaultSecretResolver(
105
+ uri: string,
106
+ auth?: { token?: string },
107
+ ): Promise<string> {
108
+ const parsed = new URL(uri);
109
+
110
+ switch (parsed.protocol) {
111
+ case "file:": {
112
+ const filePath = parsed.pathname;
113
+ return (await readFile(filePath, "utf-8")).trim();
114
+ }
115
+ case "env:": {
116
+ // env://VAR_NAME or env:///VAR_NAME
117
+ const varName = parsed.hostname || parsed.pathname.replace(/^\//, "");
118
+ const value = process.env[varName];
119
+ if (!value) {
120
+ throw new Error(`Environment variable not set: ${varName}`);
121
+ }
122
+ return value;
123
+ }
124
+ case "https:":
125
+ case "http:": {
126
+ const headers: Record<string, string> = {};
127
+ if (auth?.token) {
128
+ headers.Authorization = `Bearer ${auth.token}`;
129
+ }
130
+ const res = await fetch(uri, { headers });
131
+ if (!res.ok) {
132
+ throw new Error(
133
+ `Failed to resolve secret ${uri}: ${res.status} ${res.statusText}`,
134
+ );
135
+ }
136
+ return res.text();
137
+ }
138
+ default:
139
+ throw new Error(`Unsupported secret URI scheme: ${parsed.protocol}`);
140
+ }
141
+ }
142
+
143
+ // ============================================
144
+ // Consumer Options
145
+ // ============================================
146
+
147
+ export interface RegistryConsumerOptions {
148
+ /** Override the secret resolver (default: HTTP GET + JWT) */
149
+ resolveSecret?: SecretResolver;
150
+
151
+ /** Bearer token for authenticated registries */
152
+ token?: string;
153
+
154
+ /** Custom fetch implementation */
155
+ fetch?: typeof globalThis.fetch;
156
+ }
157
+
158
+ // ============================================
159
+ // Registry Consumer
160
+ // ============================================
161
+
162
+ export interface RegistryConsumer {
163
+ /** List all available agents across all connected registries */
164
+ list(): Promise<AgentListing[]>;
165
+
166
+ /** List configured refs (from the consumer's config) */
167
+ refs(): ResolvedRef[];
168
+
169
+ /** Get the resolved registries */
170
+ registries(): ResolvedRegistry[];
171
+
172
+ /** Call a tool on a configured ref */
173
+ call(
174
+ refName: string,
175
+ tool: string,
176
+ params?: Record<string, unknown>,
177
+ ): Promise<unknown>;
178
+
179
+ /** Discover a registry's configuration */
180
+ discover(registryUrl: string): Promise<RegistryConfiguration>;
181
+
182
+ /** Resolve a secret URL to its value */
183
+ resolveSecret(url: string): Promise<string>;
184
+
185
+ /** Resolve all secret URLs in a config object, returning resolved values */
186
+ resolveConfig(
187
+ config: RefConfig,
188
+ ): Promise<Record<string, string | number | boolean>>;
189
+
190
+ /** Produce the indexed/serialized config output */
191
+ index(): ResolvedConfig;
192
+
193
+ /** Diff: what's available vs what's configured */
194
+ available(): Promise<AgentListing[]>;
195
+ }
196
+
197
+ /**
198
+ * Create a registry consumer from a config.
199
+ *
200
+ * The consumer connects to registries, discovers available agents,
201
+ * and provides a unified interface for calling tools.
202
+ */
203
+ export async function createRegistryConsumer(
204
+ config: ConsumerConfig,
205
+ options: RegistryConsumerOptions = {},
206
+ ): Promise<RegistryConsumer> {
207
+ const fetchFn = options.fetch ?? globalThis.fetch;
208
+ const resolveSecretFn = options.resolveSecret ?? defaultSecretResolver;
209
+
210
+ // Normalize registries
211
+ const resolvedRegistries = (config.registries ?? []).map(normalizeRegistry);
212
+
213
+ // Normalize refs
214
+ const resolvedRefs: ResolvedRef[] = (config.refs ?? []).map((entry) => {
215
+ const normalized = normalizeRef(entry);
216
+ return {
217
+ ref: normalized.ref,
218
+ name: normalized.name,
219
+ registry: normalized.registry ?? resolvedRegistries[0]?.url ?? "unknown",
220
+ config: normalized.config,
221
+ };
222
+ });
223
+
224
+ // Cache for registry configurations
225
+ const discoveryCache = new Map<string, RegistryConfiguration>();
226
+
227
+ // Discover a registry
228
+ async function discover(registryUrl: string): Promise<RegistryConfiguration> {
229
+ const cached = discoveryCache.get(registryUrl);
230
+ if (cached) return cached;
231
+
232
+ const url = `${registryUrl.replace(/\/$/, "")}/.well-known/configuration`;
233
+ const res = await fetchFn(url);
234
+ if (!res.ok) {
235
+ throw new Error(
236
+ `Failed to discover registry ${registryUrl}: ${res.status}`,
237
+ );
238
+ }
239
+ const configuration = (await res.json()) as RegistryConfiguration;
240
+ discoveryCache.set(registryUrl, configuration);
241
+ return configuration;
242
+ }
243
+
244
+ // List agents from a single registry
245
+ async function listFromRegistry(
246
+ registry: ResolvedRegistry,
247
+ ): Promise<AgentListing[]> {
248
+ const configuration = await discover(registry.url);
249
+ const listUrl =
250
+ configuration.agents_endpoint ??
251
+ `${registry.url.replace(/\/$/, "")}/list`;
252
+
253
+ const headers: Record<string, string> = {};
254
+ if (registry.auth.type === "bearer" && "token" in registry.auth) {
255
+ headers.Authorization = `Bearer ${registry.auth.token}`;
256
+ } else if (options.token) {
257
+ headers.Authorization = `Bearer ${options.token}`;
258
+ }
259
+
260
+ const res = await fetchFn(listUrl, { headers });
261
+ if (!res.ok) {
262
+ throw new Error(
263
+ `Failed to list agents from ${registry.url}: ${res.status}`,
264
+ );
265
+ }
266
+
267
+ const agents = (await res.json()) as Array<{
268
+ path: string;
269
+ description?: string;
270
+ tools?: Array<{ name: string; description?: string }>;
271
+ integration?: {
272
+ provider: string;
273
+ displayName: string;
274
+ category?: string;
275
+ };
276
+ }>;
277
+
278
+ return agents.map((agent) => ({
279
+ ...agent,
280
+ publisher: registry.publisher,
281
+ }));
282
+ }
283
+
284
+ // Call a tool via a registry
285
+ async function callTool(
286
+ registry: ResolvedRegistry,
287
+ agentPath: string,
288
+ tool: string,
289
+ params: Record<string, unknown>,
290
+ ): Promise<unknown> {
291
+ const configuration = await discover(registry.url);
292
+ const callUrl =
293
+ configuration.call_endpoint ?? `${registry.url.replace(/\/$/, "")}/call`;
294
+
295
+ const headers: Record<string, string> = {
296
+ "Content-Type": "application/json",
297
+ };
298
+ if (registry.auth.type === "bearer" && "token" in registry.auth) {
299
+ headers.Authorization = `Bearer ${registry.auth.token}`;
300
+ } else if (options.token) {
301
+ headers.Authorization = `Bearer ${options.token}`;
302
+ }
303
+
304
+ const res = await fetchFn(callUrl, {
305
+ method: "POST",
306
+ headers,
307
+ body: JSON.stringify({ path: agentPath, tool, params }),
308
+ });
309
+
310
+ if (!res.ok) {
311
+ const text = await res.text().catch(() => "unknown error");
312
+ throw new Error(
313
+ `Tool call failed (${registry.url}/${agentPath}/${tool}): ${res.status} ${text}`,
314
+ );
315
+ }
316
+
317
+ return res.json();
318
+ }
319
+
320
+ // Build the consumer
321
+ const consumer: RegistryConsumer = {
322
+ async list(): Promise<AgentListing[]> {
323
+ const results = await Promise.allSettled(
324
+ resolvedRegistries.map(listFromRegistry),
325
+ );
326
+ return results.flatMap((r) => (r.status === "fulfilled" ? r.value : []));
327
+ },
328
+
329
+ refs(): ResolvedRef[] {
330
+ return resolvedRefs;
331
+ },
332
+
333
+ registries(): ResolvedRegistry[] {
334
+ return resolvedRegistries;
335
+ },
336
+
337
+ async call(
338
+ refName: string,
339
+ tool: string,
340
+ params: Record<string, unknown> = {},
341
+ ): Promise<unknown> {
342
+ const ref = resolvedRefs.find((r) => r.name === refName);
343
+ if (!ref) {
344
+ throw new Error(
345
+ `Ref "${refName}" not found in config. Available: ${resolvedRefs.map((r) => r.name).join(", ")}`,
346
+ );
347
+ }
348
+
349
+ const registry = resolvedRegistries.find((r) => r.url === ref.registry);
350
+ if (!registry) {
351
+ throw new Error(
352
+ `Registry "${ref.registry}" not found for ref "${refName}"`,
353
+ );
354
+ }
355
+
356
+ return callTool(registry, ref.ref, tool, params);
357
+ },
358
+
359
+ discover,
360
+
361
+ async resolveSecret(url: string): Promise<string> {
362
+ return resolveSecretFn(url, { token: options.token });
363
+ },
364
+
365
+ async resolveConfig(
366
+ config: RefConfig,
367
+ ): Promise<Record<string, string | number | boolean>> {
368
+ const resolved: Record<string, string | number | boolean> = {};
369
+ for (const [key, value] of Object.entries(config)) {
370
+ if (isSecretUrl(value)) {
371
+ resolved[key] = await resolveSecretFn(value as string, {
372
+ token: options.token,
373
+ });
374
+ } else {
375
+ resolved[key] = value;
376
+ }
377
+ }
378
+ return resolved;
379
+ },
380
+
381
+ index(): ResolvedConfig {
382
+ return {
383
+ resolvedAt: new Date().toISOString(),
384
+ sourceHash: simpleHash(JSON.stringify(config)),
385
+ registries: resolvedRegistries,
386
+ refs: resolvedRefs,
387
+ meta: config.meta,
388
+ };
389
+ },
390
+
391
+ async available(): Promise<AgentListing[]> {
392
+ const all = await consumer.list();
393
+ const configuredRefs = new Set(resolvedRefs.map((r) => r.ref));
394
+ return all.filter((a) => !configuredRefs.has(a.path));
395
+ },
396
+ };
397
+
398
+ return consumer;
399
+ }
400
+
401
+ // ============================================
402
+ // Utilities
403
+ // ============================================
404
+
405
+ /** Simple hash for cache invalidation (not cryptographic) */
406
+ function simpleHash(str: string): string {
407
+ let hash = 0;
408
+ for (let i = 0; i < str.length; i++) {
409
+ const char = str.charCodeAt(i);
410
+ hash = ((hash << 5) - hash + char) | 0;
411
+ }
412
+ return Math.abs(hash).toString(36);
413
+ }