@slashfi/agents-sdk 0.75.0 → 0.76.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/adk.js +34 -70
- package/dist/adk.js.map +1 -1
- package/dist/cjs/config-store.js +154 -30
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/cjs/define-config.js.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/registry-consumer.js +8 -0
- package/dist/cjs/registry-consumer.js.map +1 -1
- package/dist/cjs/server.js +8 -0
- package/dist/cjs/server.js.map +1 -1
- package/dist/config-store.d.ts +7 -7
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +154 -30
- package/dist/config-store.js.map +1 -1
- package/dist/define-config.d.ts +29 -17
- package/dist/define-config.d.ts.map +1 -1
- package/dist/define-config.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/registry-consumer.d.ts +10 -0
- package/dist/registry-consumer.d.ts.map +1 -1
- package/dist/registry-consumer.js +8 -0
- package/dist/registry-consumer.js.map +1 -1
- package/dist/server.d.ts +11 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +8 -0
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/src/adk.ts +37 -63
- package/src/config-store.test.ts +169 -1
- package/src/config-store.ts +183 -42
- package/src/define-config.ts +31 -23
- package/src/index.ts +0 -2
- package/src/registry-consumer.ts +27 -0
- package/src/server.ts +19 -0
package/src/adk.ts
CHANGED
|
@@ -54,12 +54,8 @@ function wantsHelp(): boolean {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
const HELP_SECTIONS: Record<string, string> = {
|
|
57
|
-
proxy: `Proxy operations:
|
|
58
|
-
adk proxy add <url> --name <name> [--type mcp|registry] [--agent @config] [--default]
|
|
59
|
-
adk proxy remove <name>
|
|
60
|
-
adk proxy list`,
|
|
61
57
|
registry: `Registry operations:
|
|
62
|
-
adk registry add <url> --name <name> [--auth-type bearer|api-key|none]
|
|
58
|
+
adk registry add <url> --name <name> [--auth-type bearer|api-key|none] [--proxy [--proxy-agent @config]]
|
|
63
59
|
adk registry remove <name>
|
|
64
60
|
adk registry list
|
|
65
61
|
adk registry browse <name> [--query <q>]
|
|
@@ -98,19 +94,13 @@ adk — Agent Development Kit
|
|
|
98
94
|
Usage:
|
|
99
95
|
adk init [--target <agent>:<path>] Setup + install skills for coding agents
|
|
100
96
|
adk sync [--ref <name>] Materialize tool docs for all refs in config
|
|
101
|
-
adk proxy <op> [options] Manage remote adk proxies
|
|
102
97
|
adk registry <op> [options] Manage registry connections
|
|
103
98
|
adk ref <op> [options] Manage agent refs
|
|
104
99
|
adk config-path Print config directory path
|
|
105
100
|
adk error [id] View recent errors or a specific error
|
|
106
101
|
|
|
107
|
-
Proxy operations:
|
|
108
|
-
adk proxy add <url> --name <name> [--type mcp|registry] [--agent @config] [--default]
|
|
109
|
-
adk proxy remove <name>
|
|
110
|
-
adk proxy list
|
|
111
|
-
|
|
112
102
|
Registry operations:
|
|
113
|
-
adk registry add <url> --name <name> [--auth-type bearer|api-key|none]
|
|
103
|
+
adk registry add <url> --name <name> [--auth-type bearer|api-key|none] [--proxy [--proxy-agent @config]]
|
|
114
104
|
adk registry remove <name>
|
|
115
105
|
adk registry list
|
|
116
106
|
adk registry browse <name> [--query <q>]
|
|
@@ -157,52 +147,6 @@ Examples:
|
|
|
157
147
|
// Commands
|
|
158
148
|
// ============================================
|
|
159
149
|
|
|
160
|
-
async function runProxy() {
|
|
161
|
-
if (wantsHelp()) { console.log(HELP_SECTIONS.proxy); process.exit(0); }
|
|
162
|
-
const op = args[1];
|
|
163
|
-
const adk = getAdk();
|
|
164
|
-
|
|
165
|
-
switch (op) {
|
|
166
|
-
case "add": {
|
|
167
|
-
const url = args[2];
|
|
168
|
-
const name = getArg("--name");
|
|
169
|
-
if (!url || !name) { console.error("Usage: adk proxy add <url> --name <name> [--type mcp|registry] [--agent @config] [--default]"); process.exit(1); }
|
|
170
|
-
const type = (getArg("--type") ?? (url.startsWith("http") ? "mcp" : "registry")) as "mcp" | "registry";
|
|
171
|
-
const agent = getArg("--agent");
|
|
172
|
-
const isDefault = hasFlag("--default");
|
|
173
|
-
await adk.proxy.add({ name, url, type, ...(agent && { agent }), ...(isDefault && { default: true }) });
|
|
174
|
-
console.log(`Added proxy: ${name} → ${url}${isDefault ? " (default)" : ""}`);
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
case "remove": {
|
|
178
|
-
const name = args[2];
|
|
179
|
-
if (!name) { console.error("Usage: adk proxy remove <name>"); process.exit(1); }
|
|
180
|
-
const removed = await adk.proxy.remove(name);
|
|
181
|
-
console.log(removed ? `Removed: ${name}` : `Not found: ${name}`);
|
|
182
|
-
break;
|
|
183
|
-
}
|
|
184
|
-
case "list": {
|
|
185
|
-
const proxies = await adk.proxy.list();
|
|
186
|
-
if (proxies.length === 0) {
|
|
187
|
-
console.log("No proxies configured.");
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
console.log(`\n${proxies.length} proxy(s)\n`);
|
|
191
|
-
for (const p of proxies) {
|
|
192
|
-
const def = p.default ? " (default)" : "";
|
|
193
|
-
console.log(` ${p.name}${def}`);
|
|
194
|
-
console.log(` ${p.url} [${p.type}${p.agent ? ` → ${p.agent}` : ""}]`);
|
|
195
|
-
console.log();
|
|
196
|
-
}
|
|
197
|
-
break;
|
|
198
|
-
}
|
|
199
|
-
default:
|
|
200
|
-
console.error(`Unknown proxy operation: ${op}`);
|
|
201
|
-
console.error("Operations: add, remove, list");
|
|
202
|
-
process.exit(1);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
150
|
// ============================================
|
|
207
151
|
// Registry CLI
|
|
208
152
|
// ============================================
|
|
@@ -224,8 +168,40 @@ async function runRegistry() {
|
|
|
224
168
|
const auth = authType && authType !== "none"
|
|
225
169
|
? { type: authType as "bearer" | "api-key" }
|
|
226
170
|
: undefined;
|
|
227
|
-
|
|
228
|
-
|
|
171
|
+
// Proxy modifier: when set, ref ops for agents sourced from this
|
|
172
|
+
// registry are forwarded to a server-side adk-tools agent instead of
|
|
173
|
+
// running locally. Use for cloud-hosted registries that own OAuth
|
|
174
|
+
// client creds / user tokens (e.g. Twin).
|
|
175
|
+
//
|
|
176
|
+
// If the registry advertises `capabilities.registry.proxy` in its
|
|
177
|
+
// MCP initialize response, `adk.registry.add` auto-populates this
|
|
178
|
+
// field during probe — these flags are only for explicit opt-in or
|
|
179
|
+
// override.
|
|
180
|
+
const wantProxy = hasFlag("--proxy") || hasFlag("--proxy-required");
|
|
181
|
+
const proxyOptional = hasFlag("--proxy-optional");
|
|
182
|
+
const proxyAgent = getArg("--proxy-agent") ?? undefined;
|
|
183
|
+
const proxy = wantProxy || proxyOptional
|
|
184
|
+
? {
|
|
185
|
+
mode: (proxyOptional ? "optional" : "required") as "required" | "optional",
|
|
186
|
+
...(proxyAgent && { agent: proxyAgent }),
|
|
187
|
+
}
|
|
188
|
+
: undefined;
|
|
189
|
+
const displayName = name ?? new URL(url).hostname;
|
|
190
|
+
await adk.registry.add({
|
|
191
|
+
url,
|
|
192
|
+
name: displayName,
|
|
193
|
+
...(auth && { auth }),
|
|
194
|
+
...(proxy && { proxy }),
|
|
195
|
+
});
|
|
196
|
+
// Discovery on the add path may have auto-populated proxy; read back.
|
|
197
|
+
const stored = (await adk.registry.list()).find(
|
|
198
|
+
(r) => r.name === displayName || r.url === url,
|
|
199
|
+
);
|
|
200
|
+
const effectiveProxy = stored?.proxy ?? proxy;
|
|
201
|
+
const source = !proxy && stored?.proxy ? " (auto-detected)" : "";
|
|
202
|
+
console.log(
|
|
203
|
+
`Added registry: ${displayName}${effectiveProxy ? ` (proxy: ${effectiveProxy.mode} → ${effectiveProxy.agent ?? "@config"}${source})` : ""}`,
|
|
204
|
+
);
|
|
229
205
|
break;
|
|
230
206
|
}
|
|
231
207
|
case "remove": {
|
|
@@ -246,6 +222,7 @@ async function runRegistry() {
|
|
|
246
222
|
console.log(` ${r.name ?? r.url}`);
|
|
247
223
|
console.log(` ${r.url}`);
|
|
248
224
|
if (r.auth) console.log(` auth: ${r.auth.type}`);
|
|
225
|
+
if (r.proxy) console.log(` proxy: ${r.proxy.mode} → ${r.proxy.agent ?? "@config"}`);
|
|
249
226
|
console.log();
|
|
250
227
|
}
|
|
251
228
|
break;
|
|
@@ -555,9 +532,6 @@ switch (command) {
|
|
|
555
532
|
}
|
|
556
533
|
break;
|
|
557
534
|
}
|
|
558
|
-
case "proxy":
|
|
559
|
-
await runProxy();
|
|
560
|
-
break;
|
|
561
535
|
case "registry":
|
|
562
536
|
await runRegistry();
|
|
563
537
|
break;
|
package/src/config-store.test.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
|
+
createAdk,
|
|
4
|
+
createAdkTools,
|
|
3
5
|
createAgentRegistry,
|
|
4
6
|
createAgentServer,
|
|
5
|
-
createAdk,
|
|
6
7
|
defineAgent,
|
|
7
8
|
defineTool,
|
|
8
9
|
} from "./index";
|
|
@@ -458,3 +459,170 @@ describe("ADK ref.call() full auto-refresh flow", () => {
|
|
|
458
459
|
expect((result as any)?.result?.token).toBe("refreshed-token");
|
|
459
460
|
});
|
|
460
461
|
});
|
|
462
|
+
|
|
463
|
+
// ─── ADK Config Store: registry proxy routing ───────────────────
|
|
464
|
+
|
|
465
|
+
describe("ADK registry proxy routing", () => {
|
|
466
|
+
let proxyServer: AgentServer;
|
|
467
|
+
let proxyServerAdk: ReturnType<typeof createAdk>;
|
|
468
|
+
const PROXY_PORT = 19930;
|
|
469
|
+
|
|
470
|
+
beforeAll(async () => {
|
|
471
|
+
// Real server-side adk that the proxy agent operates on. When the
|
|
472
|
+
// local adk forwards ref ops, they land on this backing store via
|
|
473
|
+
// the real @config `ref` tool produced by createAdkTools — no mocks.
|
|
474
|
+
const proxyServerFs = createMemoryFs();
|
|
475
|
+
proxyServerAdk = createAdk(proxyServerFs);
|
|
476
|
+
await proxyServerAdk.writeConfig({
|
|
477
|
+
refs: [
|
|
478
|
+
{
|
|
479
|
+
ref: "@gmail",
|
|
480
|
+
scheme: "mcp",
|
|
481
|
+
url: "https://gmail.example.com/mcp",
|
|
482
|
+
},
|
|
483
|
+
],
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// Expose that adk via the same tool surface production uses.
|
|
487
|
+
const adkTools = createAdkTools({ resolveScope: () => proxyServerAdk });
|
|
488
|
+
const configAgent = defineAgent({
|
|
489
|
+
path: "@config",
|
|
490
|
+
entrypoint: "@config agent for proxy routing tests",
|
|
491
|
+
tools: adkTools,
|
|
492
|
+
visibility: "public",
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const proxyRegistry = createAgentRegistry();
|
|
496
|
+
proxyRegistry.register(configAgent);
|
|
497
|
+
proxyServer = createAgentServer(proxyRegistry, {
|
|
498
|
+
port: PROXY_PORT,
|
|
499
|
+
// Advertise proxy mode in the MCP initialize response so the
|
|
500
|
+
// "registry.add auto-detects proxy" test can verify discovery.
|
|
501
|
+
registry: { version: "1.0", proxy: { mode: "required" } },
|
|
502
|
+
});
|
|
503
|
+
await proxyServer.start();
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
afterAll(async () => {
|
|
507
|
+
await proxyServer.stop();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Seed the local consumer config with a ref sourced from the proxy
|
|
512
|
+
* registry. We bypass ref.add's reachability check because we're
|
|
513
|
+
* specifically testing how proxying routes around local state.
|
|
514
|
+
*/
|
|
515
|
+
async function seedLocalRefFromProxy(fs: FsStore, refName: string) {
|
|
516
|
+
const raw = (await fs.readFile("consumer-config.json")) ?? "{}";
|
|
517
|
+
const config = JSON.parse(raw);
|
|
518
|
+
config.refs = [
|
|
519
|
+
{
|
|
520
|
+
ref: refName,
|
|
521
|
+
scheme: "registry",
|
|
522
|
+
sourceRegistry: {
|
|
523
|
+
url: `http://localhost:${PROXY_PORT}`,
|
|
524
|
+
agentPath: refName,
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
];
|
|
528
|
+
await fs.writeFile("consumer-config.json", JSON.stringify(config));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
test("ref.authStatus forwards to the real @config on the proxy registry", async () => {
|
|
532
|
+
const fs = createMemoryFs();
|
|
533
|
+
const adk = createAdk(fs);
|
|
534
|
+
|
|
535
|
+
await adk.registry.add({
|
|
536
|
+
url: `http://localhost:${PROXY_PORT}`,
|
|
537
|
+
name: "cloud",
|
|
538
|
+
proxy: { mode: "required" },
|
|
539
|
+
});
|
|
540
|
+
await seedLocalRefFromProxy(fs, "@gmail");
|
|
541
|
+
|
|
542
|
+
// The proxy-side adk owns the @gmail ref (no security declared in
|
|
543
|
+
// its config above) so authStatus should report { complete: true }
|
|
544
|
+
// with security: null.
|
|
545
|
+
const status = await adk.ref.authStatus("@gmail");
|
|
546
|
+
expect(status).toBeDefined();
|
|
547
|
+
// The remote @config returned something — local adk never saw the ref,
|
|
548
|
+
// so this would throw "Ref not found" if proxying wasn't wired.
|
|
549
|
+
expect((status as { name?: string }).name ?? "@gmail").toBe("@gmail");
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("ref.auth forwards and returns the remote auth start result", async () => {
|
|
553
|
+
const fs = createMemoryFs();
|
|
554
|
+
const adk = createAdk(fs);
|
|
555
|
+
|
|
556
|
+
await adk.registry.add({
|
|
557
|
+
url: `http://localhost:${PROXY_PORT}`,
|
|
558
|
+
name: "cloud",
|
|
559
|
+
proxy: { mode: "required" },
|
|
560
|
+
});
|
|
561
|
+
await seedLocalRefFromProxy(fs, "@gmail");
|
|
562
|
+
|
|
563
|
+
// @gmail on the proxy side has no security schema, so the real
|
|
564
|
+
// @config.ref tool returns { type: 'none', complete: true }. The
|
|
565
|
+
// assertion here is that we got *something* back from the remote,
|
|
566
|
+
// proving the call made a round trip instead of throwing locally.
|
|
567
|
+
const result = await adk.ref.auth("@gmail");
|
|
568
|
+
expect(result).toBeDefined();
|
|
569
|
+
expect((result as { type?: string }).type).toBeDefined();
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test("registry.add auto-detects proxy from the server's handshake", async () => {
|
|
573
|
+
const fs = createMemoryFs();
|
|
574
|
+
const adk = createAdk(fs);
|
|
575
|
+
|
|
576
|
+
// Caller passes NO proxy config. The server advertises
|
|
577
|
+
// `capabilities.registry.proxy: { mode: 'required' }` in its MCP
|
|
578
|
+
// initialize response (see beforeAll), so registry.add should
|
|
579
|
+
// auto-populate the RegistryEntry at probe time.
|
|
580
|
+
await adk.registry.add({
|
|
581
|
+
url: `http://localhost:${PROXY_PORT}`,
|
|
582
|
+
name: "cloud-autodetect",
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
const list = await adk.registry.list();
|
|
586
|
+
const entry = list.find((r) => r.name === "cloud-autodetect");
|
|
587
|
+
expect(entry?.proxy?.mode).toBe("required");
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test("explicit proxy on registry.add is not overwritten by auto-detection", async () => {
|
|
591
|
+
const fs = createMemoryFs();
|
|
592
|
+
const adk = createAdk(fs);
|
|
593
|
+
|
|
594
|
+
// Server advertises required; caller explicitly sets optional. The
|
|
595
|
+
// caller's choice wins — discovery only fills in blanks.
|
|
596
|
+
await adk.registry.add({
|
|
597
|
+
url: `http://localhost:${PROXY_PORT}`,
|
|
598
|
+
name: "cloud-explicit",
|
|
599
|
+
proxy: { mode: "optional", agent: "@custom" },
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const entry = (await adk.registry.list()).find((r) => r.name === "cloud-explicit");
|
|
603
|
+
expect(entry?.proxy?.mode).toBe("optional");
|
|
604
|
+
expect(entry?.proxy?.agent).toBe("@custom");
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test("optional proxy honors preferLocal and skips forwarding", async () => {
|
|
608
|
+
const fs = createMemoryFs();
|
|
609
|
+
const adk = createAdk(fs);
|
|
610
|
+
|
|
611
|
+
await adk.registry.add({
|
|
612
|
+
url: `http://localhost:${PROXY_PORT}`,
|
|
613
|
+
name: "cloud",
|
|
614
|
+
proxy: { mode: "optional" },
|
|
615
|
+
});
|
|
616
|
+
await seedLocalRefFromProxy(fs, "@gmail");
|
|
617
|
+
|
|
618
|
+
// With preferLocal:true we should fall through to the local path.
|
|
619
|
+
// The local adk has no usable config for @gmail, so this path
|
|
620
|
+
// throws or returns an empty status — either way, we prove we
|
|
621
|
+
// stayed local by catching and confirming no exception bubbled
|
|
622
|
+
// with "authorizeUrl" (which only the proxy path returns).
|
|
623
|
+
const result = await adk.ref
|
|
624
|
+
.auth("@gmail", { preferLocal: true })
|
|
625
|
+
.catch((err: Error) => ({ _error: err.message }));
|
|
626
|
+
expect((result as { authorizeUrl?: string }).authorizeUrl).toBeUndefined();
|
|
627
|
+
});
|
|
628
|
+
});
|
package/src/config-store.ts
CHANGED
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
import type { FsStore } from "./agent-definitions/config.js";
|
|
21
21
|
import type {
|
|
22
22
|
ConsumerConfig,
|
|
23
|
-
ProxyEntry,
|
|
24
23
|
RefEntry,
|
|
25
24
|
RegistryEntry,
|
|
26
25
|
ResolvedRef,
|
|
@@ -242,6 +241,12 @@ export interface AdkRefApi {
|
|
|
242
241
|
stateContext?: Record<string, unknown>;
|
|
243
242
|
/** Additional scopes to request (e.g., optional scopes declared by the agent) */
|
|
244
243
|
scopes?: string[];
|
|
244
|
+
/**
|
|
245
|
+
* Opt out of proxy routing when the ref's source registry has
|
|
246
|
+
* `proxy: { mode: 'optional' }`. Ignored for `mode: 'required'`.
|
|
247
|
+
* Defaults to `false` — if a registry offers a proxy we use it.
|
|
248
|
+
*/
|
|
249
|
+
preferLocal?: boolean;
|
|
245
250
|
}): Promise<AuthStartResult>;
|
|
246
251
|
/**
|
|
247
252
|
* Run the full OAuth flow locally: start auth, spin up a callback
|
|
@@ -265,14 +270,7 @@ export interface AdkRefApi {
|
|
|
265
270
|
refreshToken(name: string): Promise<{ accessToken: string } | null>;
|
|
266
271
|
}
|
|
267
272
|
|
|
268
|
-
export interface AdkProxyApi {
|
|
269
|
-
add(entry: ProxyEntry): Promise<void>;
|
|
270
|
-
remove(name: string): Promise<boolean>;
|
|
271
|
-
list(): Promise<ProxyEntry[]>;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
273
|
export interface Adk {
|
|
275
|
-
proxy: AdkProxyApi;
|
|
276
274
|
registry: AdkRegistryApi;
|
|
277
275
|
ref: AdkRefApi;
|
|
278
276
|
readConfig(): Promise<ConsumerConfig>;
|
|
@@ -724,6 +722,82 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
724
722
|
return fallback;
|
|
725
723
|
}
|
|
726
724
|
|
|
725
|
+
// ==========================================
|
|
726
|
+
// Proxy Routing
|
|
727
|
+
// ==========================================
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Find the configured RegistryEntry for a ref, consulting `sourceRegistry`
|
|
731
|
+
* first and falling back to the first registry in config. Returns `null` when
|
|
732
|
+
* the ref is sourced from a raw URL (no registry), in which case proxy routing
|
|
733
|
+
* does not apply.
|
|
734
|
+
*/
|
|
735
|
+
async function findRegistryEntryForRef(entry: RefEntry): Promise<RegistryEntry | null> {
|
|
736
|
+
const sourceUrl = entry.sourceRegistry?.url;
|
|
737
|
+
if (!sourceUrl) return null;
|
|
738
|
+
const config = await readConfig();
|
|
739
|
+
const match = (config.registries ?? []).find((r) => {
|
|
740
|
+
if (typeof r === "string") return r === sourceUrl;
|
|
741
|
+
return r.url === sourceUrl;
|
|
742
|
+
});
|
|
743
|
+
if (!match || typeof match === "string") return null;
|
|
744
|
+
return match;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Returns the proxy settings for a ref when its source registry has
|
|
749
|
+
* `proxy` configured. `null` means "run locally".
|
|
750
|
+
*
|
|
751
|
+
* Callers pass `{ preferLocal: true }` to opt out of `mode: 'optional'`
|
|
752
|
+
* proxying when they already hold credentials locally. `mode: 'required'`
|
|
753
|
+
* cannot be bypassed — the registry owns auth server-side and there is
|
|
754
|
+
* nothing useful the local SDK can do.
|
|
755
|
+
*/
|
|
756
|
+
async function resolveProxyForRef(
|
|
757
|
+
entry: RefEntry,
|
|
758
|
+
opts?: { preferLocal?: boolean },
|
|
759
|
+
): Promise<{ reg: RegistryEntry; agent: string } | null> {
|
|
760
|
+
const reg = await findRegistryEntryForRef(entry);
|
|
761
|
+
if (!reg?.proxy) return null;
|
|
762
|
+
if (reg.proxy.mode === "optional" && opts?.preferLocal) return null;
|
|
763
|
+
return { reg, agent: reg.proxy.agent ?? "@config" };
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Forward an `@config ref` operation to the proxy agent on a remote registry.
|
|
768
|
+
*
|
|
769
|
+
* The remote side speaks the standard adk-tools surface, so the call shape is
|
|
770
|
+
* identical to what the local `ref` API would do — the only difference is
|
|
771
|
+
* that tokens and secrets live server-side. `callRegistry` returns the
|
|
772
|
+
* standard CallAgentResponse envelope: `{ success: true, result }` on
|
|
773
|
+
* success or `{ success: false, error }` on failure. We unwrap once and
|
|
774
|
+
* throw on error so callers get a result that matches the local signature.
|
|
775
|
+
*/
|
|
776
|
+
async function forwardRefOpToProxy<T>(
|
|
777
|
+
reg: RegistryEntry,
|
|
778
|
+
agent: string,
|
|
779
|
+
operation: string,
|
|
780
|
+
params: Record<string, unknown>,
|
|
781
|
+
): Promise<T> {
|
|
782
|
+
const consumer = await buildConsumerForRef({ ref: "", sourceRegistry: { url: reg.url, agentPath: agent } } as RefEntry);
|
|
783
|
+
const resolved = consumer.registries().find((r) => r.url === reg.url);
|
|
784
|
+
if (!resolved) throw new Error(`Registry ${reg.url} not resolvable for proxy forwarding`);
|
|
785
|
+
|
|
786
|
+
const response = await consumer.callRegistry(resolved, {
|
|
787
|
+
action: "execute_tool",
|
|
788
|
+
path: agent,
|
|
789
|
+
tool: "ref",
|
|
790
|
+
params: { operation, ...params },
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
if (!response.success) {
|
|
794
|
+
const errResponse = response as { success: false; error?: string; code?: string };
|
|
795
|
+
const msg = errResponse.error ?? `Proxy ${agent}.ref(${operation}) failed`;
|
|
796
|
+
throw new Error(msg);
|
|
797
|
+
}
|
|
798
|
+
return (response as { success: true; result: unknown }).result as T;
|
|
799
|
+
}
|
|
800
|
+
|
|
727
801
|
// ==========================================
|
|
728
802
|
// Registry API
|
|
729
803
|
// ==========================================
|
|
@@ -735,7 +809,37 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
735
809
|
const registries = (config.registries ?? []).filter(
|
|
736
810
|
(r) => registryDisplayName(r) !== alias,
|
|
737
811
|
);
|
|
738
|
-
|
|
812
|
+
|
|
813
|
+
// Probe the registry's MCP initialize response so we can auto-populate
|
|
814
|
+
// `proxy` when the server advertises it. Users who pass an explicit
|
|
815
|
+
// `proxy` on the entry always win — discovery only fills in blanks.
|
|
816
|
+
let final: RegistryEntry = entry;
|
|
817
|
+
if (!entry.proxy) {
|
|
818
|
+
try {
|
|
819
|
+
const probeConsumer = await createRegistryConsumer(
|
|
820
|
+
{ registries: [entry], refs: [] },
|
|
821
|
+
{ token: options.token, fetch: options.fetch },
|
|
822
|
+
);
|
|
823
|
+
const resolved = probeConsumer.registries()[0];
|
|
824
|
+
if (resolved) {
|
|
825
|
+
const discovered = await probeConsumer.discover(resolved.url);
|
|
826
|
+
if (discovered.proxy?.mode) {
|
|
827
|
+
final = {
|
|
828
|
+
...entry,
|
|
829
|
+
proxy: {
|
|
830
|
+
mode: discovered.proxy.mode,
|
|
831
|
+
...(discovered.proxy.agent && { agent: discovered.proxy.agent }),
|
|
832
|
+
},
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
} catch {
|
|
837
|
+
// Discovery is best-effort — offline, unreachable, or non-adk
|
|
838
|
+
// registries simply skip proxy auto-configuration.
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
registries.push(final);
|
|
739
843
|
await writeConfig({ ...config, registries });
|
|
740
844
|
},
|
|
741
845
|
|
|
@@ -778,6 +882,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
778
882
|
if (updates.name) existing.name = updates.name;
|
|
779
883
|
if (updates.auth) existing.auth = updates.auth;
|
|
780
884
|
if (updates.headers) existing.headers = { ...existing.headers, ...updates.headers };
|
|
885
|
+
if (updates.proxy !== undefined) existing.proxy = updates.proxy;
|
|
781
886
|
return existing;
|
|
782
887
|
});
|
|
783
888
|
if (!found) return false;
|
|
@@ -1103,6 +1208,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1103
1208
|
const entry = findRef(config.refs ?? [], name);
|
|
1104
1209
|
if (!entry) throw new Error(`Ref "${name}" not found`);
|
|
1105
1210
|
|
|
1211
|
+
// Registry-proxied refs: ask the remote @config for state (secrets live
|
|
1212
|
+
// server-side so local inspection would always return "missing").
|
|
1213
|
+
const proxy = await resolveProxyForRef(entry);
|
|
1214
|
+
if (proxy) {
|
|
1215
|
+
return forwardRefOpToProxy<RefAuthStatus>(proxy.reg, proxy.agent, "auth-status", { name });
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1106
1218
|
let security: SecuritySchemeSummary | null = null;
|
|
1107
1219
|
try {
|
|
1108
1220
|
const consumer = await buildConsumerForRef(entry);
|
|
@@ -1229,11 +1341,30 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1229
1341
|
stateContext?: Record<string, unknown>;
|
|
1230
1342
|
/** Additional scopes to request (e.g., optional scopes declared by the agent) */
|
|
1231
1343
|
scopes?: string[];
|
|
1344
|
+
/**
|
|
1345
|
+
* Opt out of proxy routing when the ref's source registry has
|
|
1346
|
+
* `proxy: { mode: 'optional' }`. Ignored for `mode: 'required'`.
|
|
1347
|
+
*/
|
|
1348
|
+
preferLocal?: boolean;
|
|
1232
1349
|
}): Promise<AuthStartResult> {
|
|
1233
1350
|
const config = await readConfig();
|
|
1234
1351
|
const entry = findRef(config.refs ?? [], name);
|
|
1235
1352
|
if (!entry) throw new Error(`Ref "${name}" not found`);
|
|
1236
1353
|
|
|
1354
|
+
// Registry-proxied auth: forward the start-of-flow to the remote @config
|
|
1355
|
+
// agent. The registry owns the client_id/secret and returns an authorize
|
|
1356
|
+
// URL pointing at the registry's OAuth callback domain, so the user
|
|
1357
|
+
// completes the flow against the registry instead of localhost.
|
|
1358
|
+
const proxy = await resolveProxyForRef(entry, { preferLocal: opts?.preferLocal });
|
|
1359
|
+
if (proxy) {
|
|
1360
|
+
const params: Record<string, unknown> = { name };
|
|
1361
|
+
if (opts?.apiKey !== undefined) params.apiKey = opts.apiKey;
|
|
1362
|
+
if (opts?.credentials) params.credentials = opts.credentials;
|
|
1363
|
+
if (opts?.scopes) params.scopes = opts.scopes;
|
|
1364
|
+
if (opts?.stateContext) params.stateContext = opts.stateContext;
|
|
1365
|
+
return forwardRefOpToProxy<AuthStartResult>(proxy.reg, proxy.agent, "auth", params);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1237
1368
|
const status = await ref.authStatus(name);
|
|
1238
1369
|
const security = status.security;
|
|
1239
1370
|
const resolve = options.resolveCredentials;
|
|
@@ -1488,16 +1619,34 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1488
1619
|
onAuthorizeUrl?: (url: string) => void;
|
|
1489
1620
|
timeoutMs?: number;
|
|
1490
1621
|
}): Promise<{ complete: boolean }> {
|
|
1622
|
+
// `ref.auth` is already proxy-aware — for proxied refs it returns
|
|
1623
|
+
// the authorizeUrl that the registry minted against its own
|
|
1624
|
+
// callback domain. Everything below is identical for local and
|
|
1625
|
+
// proxied refs except the last step (polling for the callback),
|
|
1626
|
+
// which only makes sense when we own the redirect URI.
|
|
1491
1627
|
const result = await ref.auth(name);
|
|
1492
|
-
|
|
1493
1628
|
if (result.complete) return { complete: true };
|
|
1494
1629
|
|
|
1630
|
+
const config = await readConfig();
|
|
1631
|
+
const entry = findRef(config.refs ?? [], name);
|
|
1632
|
+
const proxy = entry ? await resolveProxyForRef(entry) : null;
|
|
1633
|
+
|
|
1495
1634
|
const port = options.oauthCallbackPort ?? 8919;
|
|
1496
1635
|
const timeout = opts?.timeoutMs ?? 300_000;
|
|
1497
1636
|
const { createServer } = await import("node:http");
|
|
1498
1637
|
|
|
1499
|
-
// API key / HTTP auth —
|
|
1638
|
+
// API key / HTTP auth — local credential form.
|
|
1639
|
+
//
|
|
1640
|
+
// We refuse to serve the form for a proxied ref: the registry
|
|
1641
|
+
// owns the credential store, so the user needs to submit via
|
|
1642
|
+
// whatever UI the registry exposes. Supporting this through the
|
|
1643
|
+
// proxy would need a remote form endpoint — out of scope here.
|
|
1500
1644
|
if (result.fields && result.fields.length > 0 && result.type !== "oauth2") {
|
|
1645
|
+
if (proxy) {
|
|
1646
|
+
throw new Error(
|
|
1647
|
+
`Ref "${name}" is sourced from a proxied registry; submit credentials through ${proxy.agent} instead of a local form.`,
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1501
1650
|
return new Promise<{ complete: boolean }>((resolve, reject) => {
|
|
1502
1651
|
const server = createServer(async (req, res) => {
|
|
1503
1652
|
const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
@@ -1564,15 +1713,21 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1564
1713
|
});
|
|
1565
1714
|
}
|
|
1566
1715
|
|
|
1567
|
-
// OAuth2 —
|
|
1716
|
+
// OAuth2 — hand the authorize URL to the caller.
|
|
1568
1717
|
if (result.type !== "oauth2" || !result.authorizeUrl) {
|
|
1569
1718
|
throw new Error(`authLocal cannot handle auth type: ${result.type}`);
|
|
1570
1719
|
}
|
|
1571
|
-
|
|
1572
1720
|
if (opts?.onAuthorizeUrl) {
|
|
1573
1721
|
opts.onAuthorizeUrl(result.authorizeUrl);
|
|
1574
1722
|
}
|
|
1575
1723
|
|
|
1724
|
+
// Proxied refs: the registry owns the callback endpoint, so there's
|
|
1725
|
+
// nothing to poll here. Callers poll `ref.authStatus` on their own
|
|
1726
|
+
// schedule once the user finishes the remote consent screen.
|
|
1727
|
+
if (proxy) return { complete: false };
|
|
1728
|
+
|
|
1729
|
+
// Local refs: spin up the callback server on oauthCallbackPort and
|
|
1730
|
+
// block until the OAuth provider redirects back.
|
|
1576
1731
|
return new Promise<{ complete: boolean }>((resolve, reject) => {
|
|
1577
1732
|
const server = createServer(async (req, res) => {
|
|
1578
1733
|
const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
@@ -1614,6 +1769,20 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1614
1769
|
},
|
|
1615
1770
|
|
|
1616
1771
|
async refreshToken(name: string): Promise<{ accessToken: string } | null> {
|
|
1772
|
+
// Registry-proxied refs: the remote @config holds the refresh_token.
|
|
1773
|
+
const entryForProxy = await ref.get(name);
|
|
1774
|
+
if (entryForProxy) {
|
|
1775
|
+
const proxy = await resolveProxyForRef(entryForProxy);
|
|
1776
|
+
if (proxy) {
|
|
1777
|
+
return forwardRefOpToProxy<{ accessToken: string } | null>(
|
|
1778
|
+
proxy.reg,
|
|
1779
|
+
proxy.agent,
|
|
1780
|
+
"refresh-token",
|
|
1781
|
+
{ name },
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1617
1786
|
// Read stored refresh_token
|
|
1618
1787
|
const refreshToken = await readRefSecret(name, "refresh_token");
|
|
1619
1788
|
if (!refreshToken) return null;
|
|
@@ -1697,33 +1866,5 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1697
1866
|
return { refName: pending.refName, complete: true, stateContext };
|
|
1698
1867
|
}
|
|
1699
1868
|
|
|
1700
|
-
|
|
1701
|
-
// Proxy API
|
|
1702
|
-
// ==========================================
|
|
1703
|
-
|
|
1704
|
-
const proxy: AdkProxyApi = {
|
|
1705
|
-
async add(entry: ProxyEntry): Promise<void> {
|
|
1706
|
-
const config = await readConfig();
|
|
1707
|
-
const proxies = (config.proxies ?? []).filter((p) => p.name !== entry.name);
|
|
1708
|
-
proxies.push(entry);
|
|
1709
|
-
await writeConfig({ ...config, proxies });
|
|
1710
|
-
},
|
|
1711
|
-
|
|
1712
|
-
async remove(name: string): Promise<boolean> {
|
|
1713
|
-
const config = await readConfig();
|
|
1714
|
-
if (!config.proxies?.length) return false;
|
|
1715
|
-
const before = config.proxies.length;
|
|
1716
|
-
const proxies = config.proxies.filter((p) => p.name !== name);
|
|
1717
|
-
if (proxies.length === before) return false;
|
|
1718
|
-
await writeConfig({ ...config, proxies });
|
|
1719
|
-
return true;
|
|
1720
|
-
},
|
|
1721
|
-
|
|
1722
|
-
async list(): Promise<ProxyEntry[]> {
|
|
1723
|
-
const config = await readConfig();
|
|
1724
|
-
return config.proxies ?? [];
|
|
1725
|
-
},
|
|
1726
|
-
};
|
|
1727
|
-
|
|
1728
|
-
return { proxy, registry, ref, readConfig, writeConfig, handleCallback };
|
|
1869
|
+
return { registry, ref, readConfig, writeConfig, handleCallback };
|
|
1729
1870
|
}
|