@slashfi/agents-sdk 0.74.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.
Files changed (41) hide show
  1. package/dist/adk.js +35 -71
  2. package/dist/adk.js.map +1 -1
  3. package/dist/cjs/config-store.js +154 -30
  4. package/dist/cjs/config-store.js.map +1 -1
  5. package/dist/cjs/define-config.js.map +1 -1
  6. package/dist/cjs/index.js.map +1 -1
  7. package/dist/cjs/init.js +1 -1
  8. package/dist/cjs/init.js.map +1 -1
  9. package/dist/cjs/registry-consumer.js +22 -13
  10. package/dist/cjs/registry-consumer.js.map +1 -1
  11. package/dist/cjs/server.js +8 -0
  12. package/dist/cjs/server.js.map +1 -1
  13. package/dist/config-store.d.ts +10 -10
  14. package/dist/config-store.d.ts.map +1 -1
  15. package/dist/config-store.js +154 -30
  16. package/dist/config-store.js.map +1 -1
  17. package/dist/define-config.d.ts +29 -17
  18. package/dist/define-config.d.ts.map +1 -1
  19. package/dist/define-config.js.map +1 -1
  20. package/dist/index.d.ts +3 -3
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/init.js +1 -1
  24. package/dist/init.js.map +1 -1
  25. package/dist/registry-consumer.d.ts +51 -20
  26. package/dist/registry-consumer.d.ts.map +1 -1
  27. package/dist/registry-consumer.js +22 -13
  28. package/dist/registry-consumer.js.map +1 -1
  29. package/dist/server.d.ts +11 -0
  30. package/dist/server.d.ts.map +1 -1
  31. package/dist/server.js +8 -0
  32. package/dist/server.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/adk.ts +38 -64
  35. package/src/config-store.test.ts +169 -1
  36. package/src/config-store.ts +189 -47
  37. package/src/define-config.ts +31 -23
  38. package/src/index.ts +3 -2
  39. package/src/init.ts +1 -1
  40. package/src/registry-consumer.ts +95 -44
  41. package/src/server.ts +19 -0
@@ -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,
@@ -31,7 +30,8 @@ import type { FetchFn } from "./fetch-types.js";
31
30
  import type { Logger } from "./logger.js";
32
31
  import { createRegistryConsumer } from "./registry-consumer.js";
33
32
  import type {
34
- AgentListing,
33
+ AgentListEntry,
34
+ AgentInspection,
35
35
  RegistryConfiguration,
36
36
  RegistryConsumer,
37
37
  } from "./registry-consumer.js";
@@ -129,7 +129,7 @@ export interface AdkRegistryApi {
129
129
  list(): Promise<RegistryEntry[]>;
130
130
  get(name: string): Promise<RegistryEntry | null>;
131
131
  update(name: string, updates: Partial<RegistryEntry>): Promise<boolean>;
132
- browse(name: string, query?: string): Promise<AgentListing[]>;
132
+ browse(name: string, query?: string): Promise<AgentListEntry[]>;
133
133
  inspect(name: string): Promise<RegistryConfiguration>;
134
134
  test(name?: string): Promise<RegistryTestResult[]>;
135
135
  }
@@ -217,7 +217,7 @@ export interface AdkRefApi {
217
217
  list(): Promise<ResolvedRef[]>;
218
218
  get(name: string): Promise<RefEntry | null>;
219
219
  update(name: string, updates: Partial<RefEntry>): Promise<boolean>;
220
- inspect(name: string, options?: { full?: boolean }): Promise<AgentListing | null>;
220
+ inspect(name: string, options?: { full?: boolean }): Promise<AgentInspection | null>;
221
221
  call: AdkRefCallFn;
222
222
  resources(name: string): Promise<CallAgentResponse>;
223
223
  read(name: string, uris: string[]): Promise<CallAgentResponse>;
@@ -241,6 +241,12 @@ export interface AdkRefApi {
241
241
  stateContext?: Record<string, unknown>;
242
242
  /** Additional scopes to request (e.g., optional scopes declared by the agent) */
243
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;
244
250
  }): Promise<AuthStartResult>;
245
251
  /**
246
252
  * Run the full OAuth flow locally: start auth, spin up a callback
@@ -264,14 +270,7 @@ export interface AdkRefApi {
264
270
  refreshToken(name: string): Promise<{ accessToken: string } | null>;
265
271
  }
266
272
 
267
- export interface AdkProxyApi {
268
- add(entry: ProxyEntry): Promise<void>;
269
- remove(name: string): Promise<boolean>;
270
- list(): Promise<ProxyEntry[]>;
271
- }
272
-
273
273
  export interface Adk {
274
- proxy: AdkProxyApi;
275
274
  registry: AdkRegistryApi;
276
275
  ref: AdkRefApi;
277
276
  readConfig(): Promise<ConsumerConfig>;
@@ -723,6 +722,82 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
723
722
  return fallback;
724
723
  }
725
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
+
726
801
  // ==========================================
727
802
  // Registry API
728
803
  // ==========================================
@@ -734,7 +809,37 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
734
809
  const registries = (config.registries ?? []).filter(
735
810
  (r) => registryDisplayName(r) !== alias,
736
811
  );
737
- 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);
738
843
  await writeConfig({ ...config, registries });
739
844
  },
740
845
 
@@ -777,6 +882,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
777
882
  if (updates.name) existing.name = updates.name;
778
883
  if (updates.auth) existing.auth = updates.auth;
779
884
  if (updates.headers) existing.headers = { ...existing.headers, ...updates.headers };
885
+ if (updates.proxy !== undefined) existing.proxy = updates.proxy;
780
886
  return existing;
781
887
  });
782
888
  if (!found) return false;
@@ -784,7 +890,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
784
890
  return true;
785
891
  },
786
892
 
787
- async browse(name: string, query?: string): Promise<AgentListing[]> {
893
+ async browse(name: string, query?: string): Promise<AgentListEntry[]> {
788
894
  const consumer = await buildConsumer(name);
789
895
  const config = await readConfig();
790
896
  const target = findRegistry(config.registries ?? [], name);
@@ -995,7 +1101,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
995
1101
  return true;
996
1102
  },
997
1103
 
998
- async inspect(name: string, opts?: { full?: boolean }): Promise<AgentListing | null> {
1104
+ async inspect(name: string, opts?: { full?: boolean }): Promise<AgentInspection | null> {
999
1105
  const config = await readConfig();
1000
1106
  const entry = findRef(config.refs ?? [], name);
1001
1107
  if (!entry) throw new Error(`Ref "${name}" not found`);
@@ -1102,6 +1208,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1102
1208
  const entry = findRef(config.refs ?? [], name);
1103
1209
  if (!entry) throw new Error(`Ref "${name}" not found`);
1104
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
+
1105
1218
  let security: SecuritySchemeSummary | null = null;
1106
1219
  try {
1107
1220
  const consumer = await buildConsumerForRef(entry);
@@ -1228,11 +1341,30 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1228
1341
  stateContext?: Record<string, unknown>;
1229
1342
  /** Additional scopes to request (e.g., optional scopes declared by the agent) */
1230
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;
1231
1349
  }): Promise<AuthStartResult> {
1232
1350
  const config = await readConfig();
1233
1351
  const entry = findRef(config.refs ?? [], name);
1234
1352
  if (!entry) throw new Error(`Ref "${name}" not found`);
1235
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
+
1236
1368
  const status = await ref.authStatus(name);
1237
1369
  const security = status.security;
1238
1370
  const resolve = options.resolveCredentials;
@@ -1487,16 +1619,34 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1487
1619
  onAuthorizeUrl?: (url: string) => void;
1488
1620
  timeoutMs?: number;
1489
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.
1490
1627
  const result = await ref.auth(name);
1491
-
1492
1628
  if (result.complete) return { complete: true };
1493
1629
 
1630
+ const config = await readConfig();
1631
+ const entry = findRef(config.refs ?? [], name);
1632
+ const proxy = entry ? await resolveProxyForRef(entry) : null;
1633
+
1494
1634
  const port = options.oauthCallbackPort ?? 8919;
1495
1635
  const timeout = opts?.timeoutMs ?? 300_000;
1496
1636
  const { createServer } = await import("node:http");
1497
1637
 
1498
- // 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.
1499
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
+ }
1500
1650
  return new Promise<{ complete: boolean }>((resolve, reject) => {
1501
1651
  const server = createServer(async (req, res) => {
1502
1652
  const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
@@ -1563,15 +1713,21 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1563
1713
  });
1564
1714
  }
1565
1715
 
1566
- // OAuth2 — open authorize URL and wait for callback
1716
+ // OAuth2 — hand the authorize URL to the caller.
1567
1717
  if (result.type !== "oauth2" || !result.authorizeUrl) {
1568
1718
  throw new Error(`authLocal cannot handle auth type: ${result.type}`);
1569
1719
  }
1570
-
1571
1720
  if (opts?.onAuthorizeUrl) {
1572
1721
  opts.onAuthorizeUrl(result.authorizeUrl);
1573
1722
  }
1574
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.
1575
1731
  return new Promise<{ complete: boolean }>((resolve, reject) => {
1576
1732
  const server = createServer(async (req, res) => {
1577
1733
  const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
@@ -1613,6 +1769,20 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1613
1769
  },
1614
1770
 
1615
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
+
1616
1786
  // Read stored refresh_token
1617
1787
  const refreshToken = await readRefSecret(name, "refresh_token");
1618
1788
  if (!refreshToken) return null;
@@ -1696,33 +1866,5 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1696
1866
  return { refName: pending.refName, complete: true, stateContext };
1697
1867
  }
1698
1868
 
1699
- // ==========================================
1700
- // Proxy API
1701
- // ==========================================
1702
-
1703
- const proxy: AdkProxyApi = {
1704
- async add(entry: ProxyEntry): Promise<void> {
1705
- const config = await readConfig();
1706
- const proxies = (config.proxies ?? []).filter((p) => p.name !== entry.name);
1707
- proxies.push(entry);
1708
- await writeConfig({ ...config, proxies });
1709
- },
1710
-
1711
- async remove(name: string): Promise<boolean> {
1712
- const config = await readConfig();
1713
- if (!config.proxies?.length) return false;
1714
- const before = config.proxies.length;
1715
- const proxies = config.proxies.filter((p) => p.name !== name);
1716
- if (proxies.length === before) return false;
1717
- await writeConfig({ ...config, proxies });
1718
- return true;
1719
- },
1720
-
1721
- async list(): Promise<ProxyEntry[]> {
1722
- const config = await readConfig();
1723
- return config.proxies ?? [];
1724
- },
1725
- };
1726
-
1727
- return { proxy, registry, ref, readConfig, writeConfig, handleCallback };
1869
+ return { registry, ref, readConfig, writeConfig, handleCallback };
1728
1870
  }
@@ -34,6 +34,30 @@ export type RegistryAuth =
34
34
  | { type: "api-key"; key?: string; header?: string }
35
35
  | { type: "jwt"; issuer?: string };
36
36
 
37
+ /**
38
+ * Proxy configuration for a registry.
39
+ *
40
+ * When set, ref operations (`auth`, `auth-status`, `call`, `inspect`,
41
+ * `resources`, `read`, `refresh-token`) for refs sourced from this
42
+ * registry are forwarded to a server-side agent that implements the
43
+ * adk-tools surface. Use this for cloud-hosted registries that own
44
+ * OAuth client credentials and/or user tokens on behalf of consumers
45
+ * (e.g. `api.twin.slash.com/mcp`).
46
+ *
47
+ * - `mode: 'required'` — all ref ops MUST route through the proxy agent.
48
+ * Local handshake (`ref.authLocal`) is refused for refs from this
49
+ * registry because the local environment has no way to build an
50
+ * authorize URL without the server's client credentials.
51
+ * - `mode: 'optional'` — proxy is the default; callers may opt out via
52
+ * `{ preferLocal: true }` on a per-op basis when they already hold
53
+ * local credentials.
54
+ */
55
+ export interface RegistryProxy {
56
+ mode: 'required' | 'optional';
57
+ /** Agent path to forward to. Defaults to `@config`. */
58
+ agent?: string;
59
+ }
60
+
37
61
  /** A registry endpoint the consumer connects to */
38
62
  export interface RegistryEntry {
39
63
  /** Registry URL (e.g., 'https://registry.slash.com') */
@@ -53,6 +77,13 @@ export interface RegistryEntry {
53
77
 
54
78
  /** Connection status — set by validation/test, used to filter active entries */
55
79
  status?: 'active' | 'inactive' | 'error';
80
+
81
+ /**
82
+ * If set, ref ops for refs sourced from this registry are forwarded
83
+ * to a server-side adk-tools agent (default `@config`) instead of
84
+ * running locally. See {@link RegistryProxy}.
85
+ */
86
+ proxy?: RegistryProxy;
56
87
  }
57
88
 
58
89
  // ============================================
@@ -100,35 +131,12 @@ export type RefEntry = {
100
131
  status?: 'active' | 'inactive' | 'error';
101
132
  };
102
133
 
103
- // ============================================
104
- // Proxy Config
105
- // ============================================
106
-
107
- /** A proxy target — remote adk server that handles ref/registry operations */
108
- export interface ProxyEntry {
109
- /** Human-readable name */
110
- name: string;
111
- /** URL of the remote server */
112
- url: string;
113
- /** Connection type: 'mcp' (direct MCP server) or 'registry' (agent on a registry) */
114
- type: 'mcp' | 'registry';
115
- /** For type 'registry': the agent path that implements adk tools (e.g. '@config') */
116
- agent?: string;
117
- /** Auth for connecting to the proxy */
118
- auth?: RegistryAuth;
119
- /** Whether this is the default proxy when no local refs/registries exist */
120
- default?: boolean;
121
- }
122
-
123
134
  // ============================================
124
135
  // Consumer Config
125
136
  // ============================================
126
137
 
127
138
  /** The full consumer configuration */
128
139
  export interface ConsumerConfig {
129
- /** Remote adk proxies — forward operations to a remote server */
130
- proxies?: ProxyEntry[];
131
-
132
140
  /** Registries to connect to, in resolution order */
133
141
  registries?: (string | RegistryEntry)[];
134
142
 
package/src/index.ts CHANGED
@@ -306,7 +306,6 @@ export {
306
306
  export type {
307
307
  RegistryAuth,
308
308
  RegistryEntry,
309
- ProxyEntry,
310
309
  RefConfig,
311
310
  RefEntry,
312
311
  ConsumerConfig,
@@ -324,6 +323,9 @@ export type {
324
323
  RegistryConsumer,
325
324
  RegistryConsumerOptions,
326
325
  RegistryConfiguration,
326
+ AgentBase,
327
+ AgentListEntry,
328
+ AgentInspection,
327
329
  AgentListing,
328
330
  SecretResolver,
329
331
  } from "./registry-consumer.js";
@@ -424,7 +426,6 @@ export { createAdk } from "./config-store.js";
424
426
  export type {
425
427
  Adk,
426
428
  AdkOptions,
427
- AdkProxyApi,
428
429
  AdkRegistryApi,
429
430
  AdkRefApi,
430
431
  AdkAgentRegistry,
package/src/init.ts CHANGED
@@ -185,7 +185,7 @@ export async function runInit(adk: Adk, targets: SkillTarget[]): Promise<void> {
185
185
  if (agents.length > 0) {
186
186
  console.log(`\n${agents.length} agent(s) available on ${DEFAULT_REGISTRY_URL}:\n`);
187
187
  for (const a of agents) {
188
- const toolCount = a.tools?.length ?? 0;
188
+ const toolCount = a.toolCount ?? 0;
189
189
  console.log(` ${a.path} (${toolCount} tools)`);
190
190
  if (a.description) console.log(` ${a.description.slice(0, 120)}`);
191
191
  console.log();