@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/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
- await adk.registry.add({ url, name: name ?? new URL(url).hostname, ...(auth && { auth }) });
228
- console.log(`Added registry: ${name ?? url}`);
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;
@@ -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
+ });
@@ -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
- registries.push(entry);
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 — serve a local credential form
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 — open authorize URL and wait for callback
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
  }