@slashfi/agents-sdk 0.40.0 → 0.41.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 (44) hide show
  1. package/dist/adk-tools.d.ts +31 -0
  2. package/dist/adk-tools.d.ts.map +1 -0
  3. package/dist/adk-tools.js +140 -0
  4. package/dist/adk-tools.js.map +1 -0
  5. package/dist/adk.js +75 -4
  6. package/dist/adk.js.map +1 -1
  7. package/dist/call-agent-schema.d.ts +96 -96
  8. package/dist/cjs/adk-tools.js +143 -0
  9. package/dist/cjs/adk-tools.js.map +1 -0
  10. package/dist/cjs/config-store.js +124 -45
  11. package/dist/cjs/config-store.js.map +1 -1
  12. package/dist/cjs/define-config.js.map +1 -1
  13. package/dist/cjs/index.js +3 -1
  14. package/dist/cjs/index.js.map +1 -1
  15. package/dist/cjs/registry-consumer.js +1 -0
  16. package/dist/cjs/registry-consumer.js.map +1 -1
  17. package/dist/cjs/registry.js +3 -0
  18. package/dist/cjs/registry.js.map +1 -1
  19. package/dist/config-store.d.ts +46 -7
  20. package/dist/config-store.d.ts.map +1 -1
  21. package/dist/config-store.js +124 -45
  22. package/dist/config-store.js.map +1 -1
  23. package/dist/define-config.d.ts +17 -0
  24. package/dist/define-config.d.ts.map +1 -1
  25. package/dist/define-config.js.map +1 -1
  26. package/dist/index.d.ts +4 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/registry-consumer.d.ts +2 -0
  31. package/dist/registry-consumer.d.ts.map +1 -1
  32. package/dist/registry-consumer.js +1 -0
  33. package/dist/registry-consumer.js.map +1 -1
  34. package/dist/registry.d.ts.map +1 -1
  35. package/dist/registry.js +3 -0
  36. package/dist/registry.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/adk-tools.ts +156 -0
  39. package/src/adk.ts +73 -2
  40. package/src/config-store.ts +191 -54
  41. package/src/define-config.ts +23 -0
  42. package/src/index.ts +7 -0
  43. package/src/registry-consumer.ts +4 -0
  44. package/src/registry.ts +3 -0
@@ -0,0 +1,156 @@
1
+ /**
2
+ * ADK Tools — expose an Adk instance as MCP ToolDefinitions.
3
+ *
4
+ * Used by atlas-environments (@config agent) and atlas-runtime
5
+ * to register adk ref/registry operations as MCP tools.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // Standalone — no scopes
10
+ * const tools = createAdkTools({ resolveScope: () => adk });
11
+ *
12
+ * // Atlas — user/tenant scopes
13
+ * const tools = createAdkTools({
14
+ * resolveScope: (scope) => scope === 'tenant' ? tenantAdk : userAdk,
15
+ * scopes: ['user', 'tenant'],
16
+ * });
17
+ * ```
18
+ */
19
+
20
+ import { defineTool } from "./define.js";
21
+ import type { ToolDefinition, ToolContext } from "./types.js";
22
+ import type { Adk } from "./config-store.js";
23
+ import type { RefEntry, RegistryEntry } from "./define-config.js";
24
+
25
+ export interface CreateAdkToolsOptions<TCtx extends ToolContext = ToolContext> {
26
+ /**
27
+ * Resolve an Adk instance from the scope string and tool context.
28
+ * Called per-operation. TCtx is your environment's context type.
29
+ */
30
+ resolveScope: (scope: string | undefined, ctx: TCtx) => Adk | Promise<Adk>;
31
+ /** Allowed scope values. If set, added as enum to the JSON schema. */
32
+ scopes?: string[];
33
+ }
34
+
35
+ export function createAdkTools<TCtx extends ToolContext = ToolContext>(opts: CreateAdkToolsOptions<TCtx>): ToolDefinition<TCtx>[] {
36
+ const { resolveScope, scopes } = opts;
37
+
38
+ const scopeSchema = scopes
39
+ ? { type: "string" as const, enum: scopes, description: "Config scope to operate on" }
40
+ : { type: "string" as const, description: "Config scope (optional)" };
41
+
42
+ const refTool = defineTool({
43
+ name: "ref",
44
+ description:
45
+ "Manage agent refs. Operations: add, remove, list, get, update, inspect, call, auth, auth-status, resources, read.",
46
+ inputSchema: {
47
+ type: "object" as const,
48
+ properties: {
49
+ operation: {
50
+ type: "string",
51
+ enum: ["add", "remove", "list", "get", "update", "inspect", "call", "auth", "auth-status", "resources", "read"],
52
+ },
53
+ scope: scopeSchema,
54
+ ref: { type: "string" },
55
+ name: { type: "string" },
56
+ scheme: { type: "string" },
57
+ url: { type: "string" },
58
+ as: { type: "string" },
59
+ sourceRegistry: { type: "object", properties: { url: { type: "string" }, agentPath: { type: "string" } } },
60
+ config: { type: "object" },
61
+ tool: { type: "string" },
62
+ params: { type: "object" },
63
+ full: { type: "boolean" },
64
+ uris: { type: "array", items: { type: "string" } },
65
+ apiKey: { type: "string" },
66
+ },
67
+ required: ["operation"],
68
+ },
69
+ execute: async (input: Record<string, unknown>, ctx) => {
70
+ const adk = await resolveScope(input.scope as string | undefined, ctx as TCtx);
71
+ const op = input.operation as string;
72
+
73
+ switch (op) {
74
+ case "add": {
75
+ const entry: Record<string, unknown> = { ref: input.ref };
76
+ if (input.scheme) entry.scheme = input.scheme;
77
+ if (input.url) entry.url = input.url;
78
+ if (input.as) entry.as = input.as;
79
+ if (input.sourceRegistry) entry.sourceRegistry = input.sourceRegistry;
80
+ if (input.config) entry.config = input.config;
81
+ return adk.ref.add(entry as unknown as RefEntry);
82
+ }
83
+ case "remove":
84
+ return { removed: await adk.ref.remove(input.name as string) };
85
+ case "list":
86
+ return { refs: await adk.ref.list() };
87
+ case "get":
88
+ return await adk.ref.get(input.name as string);
89
+ case "update":
90
+ return { updated: await adk.ref.update(input.name as string, input as unknown as Partial<RefEntry>) };
91
+ case "inspect":
92
+ return await adk.ref.inspect(input.name as string, { full: input.full as boolean });
93
+ case "call":
94
+ return await adk.ref.call(input.name as string, input.tool as string, input.params as Record<string, unknown>);
95
+ case "auth":
96
+ return await adk.ref.auth(input.name as string, { apiKey: input.apiKey as string });
97
+ case "auth-status":
98
+ return await adk.ref.authStatus(input.name as string);
99
+ case "resources":
100
+ return await adk.ref.resources(input.name as string);
101
+ case "read":
102
+ return await adk.ref.read(input.name as string, input.uris as string[]);
103
+ default:
104
+ throw new Error(`Unknown ref operation: ${op}`);
105
+ }
106
+ },
107
+ }) as ToolDefinition<TCtx>;
108
+
109
+ const registryTool = defineTool({
110
+ name: "registry",
111
+ description:
112
+ "Manage registry connections. Operations: add, remove, list, browse, inspect, test.",
113
+ inputSchema: {
114
+ type: "object" as const,
115
+ properties: {
116
+ operation: {
117
+ type: "string",
118
+ enum: ["add", "remove", "list", "browse", "inspect", "test"],
119
+ },
120
+ scope: scopeSchema,
121
+ url: { type: "string" },
122
+ name: { type: "string" },
123
+ query: { type: "string" },
124
+ auth: { type: "object" },
125
+ },
126
+ required: ["operation"],
127
+ },
128
+ execute: async (input: Record<string, unknown>, ctx) => {
129
+ const adk = await resolveScope(input.scope as string | undefined, ctx as TCtx);
130
+ const op = input.operation as string;
131
+
132
+ switch (op) {
133
+ case "add": {
134
+ const entry: Record<string, unknown> = { url: input.url, name: input.name };
135
+ if (input.auth) entry.auth = input.auth;
136
+ await adk.registry.add(entry as unknown as RegistryEntry);
137
+ return { added: true, name: input.name, url: input.url };
138
+ }
139
+ case "remove":
140
+ return { removed: await adk.registry.remove(input.name as string) };
141
+ case "list":
142
+ return { registries: await adk.registry.list() };
143
+ case "browse":
144
+ return { agents: await adk.registry.browse(input.name as string, input.query as string) };
145
+ case "inspect":
146
+ return await adk.registry.inspect(input.name as string);
147
+ case "test":
148
+ return { results: await adk.registry.test(input.name as string) };
149
+ default:
150
+ throw new Error(`Unknown registry operation: ${op}`);
151
+ }
152
+ },
153
+ });
154
+
155
+ return [refTool, registryTool as unknown as ToolDefinition<TCtx>];
156
+ }
package/src/adk.ts CHANGED
@@ -78,6 +78,7 @@ function printUsage() {
78
78
  adk — Agent Development Kit
79
79
 
80
80
  Usage:
81
+ adk proxy <op> [options] Manage remote adk proxies
81
82
  adk registry <op> [options] Manage registry connections
82
83
  adk ref <op> [options] Manage agent refs
83
84
  adk codegen [options] Generate agent from MCP server
@@ -87,6 +88,11 @@ Usage:
87
88
  adk use <agent> [options] Execute a tool on a generated agent
88
89
  adk list List all generated agents
89
90
 
91
+ Proxy operations:
92
+ adk proxy add <url> --name <name> [--type mcp|registry] [--agent @config] [--default]
93
+ adk proxy remove <name>
94
+ adk proxy list
95
+
90
96
  Registry operations:
91
97
  adk registry add <url> --name <name> [--auth-type bearer|api-key|none]
92
98
  adk registry remove <name>
@@ -368,6 +374,59 @@ function runList() {
368
374
  // Registry CLI
369
375
  // ============================================
370
376
 
377
+ // ============================================
378
+ // Proxy CLI
379
+ // ============================================
380
+
381
+ async function runProxy() {
382
+ const op = args[1];
383
+ const adk = getAdk();
384
+
385
+ switch (op) {
386
+ case "add": {
387
+ const url = args[2];
388
+ const name = getArg("--name");
389
+ if (!url || !name) { console.error("Usage: adk proxy add <url> --name <name> [--type mcp|registry] [--agent @config] [--default]"); process.exit(1); }
390
+ const type = (getArg("--type") ?? (url.startsWith("http") ? "mcp" : "registry")) as "mcp" | "registry";
391
+ const agent = getArg("--agent");
392
+ const isDefault = hasFlag("--default");
393
+ await adk.proxy.add({ name, url, type, ...(agent && { agent }), ...(isDefault && { default: true }) });
394
+ console.log(`Added proxy: ${name} → ${url}${isDefault ? " (default)" : ""}`);
395
+ break;
396
+ }
397
+ case "remove": {
398
+ const name = args[2];
399
+ if (!name) { console.error("Usage: adk proxy remove <name>"); process.exit(1); }
400
+ const removed = await adk.proxy.remove(name);
401
+ console.log(removed ? `Removed: ${name}` : `Not found: ${name}`);
402
+ break;
403
+ }
404
+ case "list": {
405
+ const proxies = await adk.proxy.list();
406
+ if (proxies.length === 0) {
407
+ console.log("No proxies configured.");
408
+ break;
409
+ }
410
+ console.log(`\n${proxies.length} proxy(s)\n`);
411
+ for (const p of proxies) {
412
+ const def = p.default ? " (default)" : "";
413
+ console.log(` ${p.name}${def}`);
414
+ console.log(` ${p.url} [${p.type}${p.agent ? ` → ${p.agent}` : ""}]`);
415
+ console.log();
416
+ }
417
+ break;
418
+ }
419
+ default:
420
+ console.error(`Unknown proxy operation: ${op}`);
421
+ console.error("Operations: add, remove, list");
422
+ process.exit(1);
423
+ }
424
+ }
425
+
426
+ // ============================================
427
+ // Registry CLI
428
+ // ============================================
429
+
371
430
  async function runRegistry() {
372
431
  const op = args[1];
373
432
  const adk = getAdk();
@@ -561,8 +620,17 @@ async function runRef() {
561
620
  console.log(`\n${icon} ${status.name}`);
562
621
  console.log(` auth type: ${status.security?.type ?? "none"}`);
563
622
  console.log(` complete: ${status.complete}`);
564
- if (status.present.length > 0) console.log(` stored: ${status.present.join(", ")}`);
565
- if (status.missing.length > 0) console.log(` missing: ${status.missing.join(", ")}`);
623
+ for (const [field, info] of Object.entries(status.fields)) {
624
+ const fieldIcon = info.present ? "\x1b[32m\u2713\x1b[0m"
625
+ : info.resolvable ? "\x1b[36m\u2192\x1b[0m"
626
+ : info.automated ? "\x1b[33m~\x1b[0m"
627
+ : "\x1b[31m\u2717\x1b[0m";
628
+ const source = info.present ? "stored"
629
+ : info.resolvable ? "resolvable"
630
+ : info.automated ? "automated"
631
+ : "missing";
632
+ console.log(` ${fieldIcon} ${field}: ${source}${info.required ? "" : " (optional)"}`);
633
+ }
566
634
  console.log();
567
635
  break;
568
636
  }
@@ -622,6 +690,9 @@ async function runRef() {
622
690
  // ============================================
623
691
 
624
692
  switch (command) {
693
+ case "proxy":
694
+ await runProxy();
695
+ break;
625
696
  case "registry":
626
697
  await runRegistry();
627
698
  break;
@@ -20,6 +20,7 @@
20
20
  import type { FsStore } from "./agent-definitions/config.js";
21
21
  import type {
22
22
  ConsumerConfig,
23
+ ProxyEntry,
23
24
  RefEntry,
24
25
  RegistryEntry,
25
26
  ResolvedRef,
@@ -47,6 +48,29 @@ const SECRET_PREFIX = "secret:";
47
48
  // Types
48
49
  // ============================================
49
50
 
51
+ /** Context passed to the resolveCredentials callback */
52
+ export interface ResolveCredentialsContext {
53
+ /** Ref name */
54
+ ref: string;
55
+ /** Credential field being resolved (e.g. "client_id", "client_secret", "api_key") */
56
+ field: string;
57
+ /** The full ref entry from config */
58
+ entry: RefEntry;
59
+ /** Security scheme from the registry */
60
+ security: SecuritySchemeSummary | null;
61
+ /** OAuth metadata if available (from discovery) */
62
+ oauthMetadata?: import("./mcp-client.js").OAuthServerMetadata | null;
63
+ }
64
+
65
+ /**
66
+ * Resolve a credential field for a ref.
67
+ * Called during auth() when the adk needs a credential it can't auto-obtain.
68
+ * Return the value, or null to indicate it's not available.
69
+ */
70
+ export type ResolveCredentials = (
71
+ ctx: ResolveCredentialsContext,
72
+ ) => Promise<string | null>;
73
+
50
74
  export interface AdkOptions {
51
75
  /** Passphrase for encrypting/decrypting secret: values */
52
76
  encryptionKey?: string;
@@ -60,8 +84,14 @@ export interface AdkOptions {
60
84
  oauthCallbackUrl?: string;
61
85
  /** Port for local OAuth callback server (default 8919) */
62
86
  oauthCallbackPort?: number;
63
- /** Client name for OAuth dynamic client registration (default: "Claude Code") */
87
+ /** Client name for OAuth dynamic client registration (default: "adk") */
64
88
  oauthClientName?: string;
89
+ /**
90
+ * Resolve preconfigured credentials for a ref.
91
+ * Used by atlas to inject platform-level or tenant-level credentials
92
+ * (e.g. client_id/client_secret) before the user auth flow runs.
93
+ */
94
+ resolveCredentials?: ResolveCredentials;
65
95
  }
66
96
 
67
97
  export interface RegistryTestResult {
@@ -83,16 +113,25 @@ export interface AdkRegistryApi {
83
113
  test(name?: string): Promise<RegistryTestResult[]>;
84
114
  }
85
115
 
116
+ /** Describes a single credential field requirement */
117
+ export interface CredentialField {
118
+ required: boolean;
119
+ /** Can be obtained automatically (dynamic registration, OAuth flow) */
120
+ automated: boolean;
121
+ /** Already present in the ref's config */
122
+ present: boolean;
123
+ /** Available via resolveCredentials callback */
124
+ resolvable: boolean;
125
+ }
126
+
86
127
  /** Describes what auth a ref needs and what's already provided */
87
128
  export interface RefAuthStatus {
88
129
  name: string;
89
130
  security: SecuritySchemeSummary | null;
90
- /** All required secret fields are present (may be untested) */
131
+ /** All required fields are either present, resolvable, or automated */
91
132
  complete: boolean;
92
- /** Fields that still need to be provided */
93
- missing: string[];
94
- /** Fields already stored */
95
- present: string[];
133
+ /** Per-field breakdown */
134
+ fields: Record<string, CredentialField>;
96
135
  }
97
136
 
98
137
  export interface OAuthResult {
@@ -143,7 +182,14 @@ export interface AdkRefApi {
143
182
  }): Promise<{ complete: boolean }>;
144
183
  }
145
184
 
185
+ export interface AdkProxyApi {
186
+ add(entry: ProxyEntry): Promise<void>;
187
+ remove(name: string): Promise<boolean>;
188
+ list(): Promise<ProxyEntry[]>;
189
+ }
190
+
146
191
  export interface Adk {
192
+ proxy: AdkProxyApi;
147
193
  registry: AdkRegistryApi;
148
194
  ref: AdkRefApi;
149
195
  readConfig(): Promise<ConsumerConfig>;
@@ -547,21 +593,26 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
547
593
 
548
594
  const ref: AdkRefApi = {
549
595
  async add(entry: RefEntry): Promise<{ security: SecuritySchemeSummary | null }> {
550
- const config = await readConfig();
551
- const name = refName(entry);
552
- const refs = (config.refs ?? []).filter((r) => refName(r) !== name);
553
- refs.push(entry);
554
- await writeConfig({ ...config, refs });
555
-
556
- // Check security requirements
596
+ // Enrich ref with upstream info from the registry
557
597
  let security: SecuritySchemeSummary | null = null;
558
598
  try {
559
599
  const consumer = await buildConsumer();
560
600
  const info = await consumer.inspect(entry.ref);
561
601
  if (info?.security) security = info.security;
602
+ if (info?.upstream && !entry.url) {
603
+ entry.url = info.upstream as string;
604
+ entry.scheme = entry.scheme ?? "mcp";
605
+ }
562
606
  } catch {
563
607
  // Non-fatal — registry might be unreachable
564
608
  }
609
+
610
+ const config = await readConfig();
611
+ const name = refName(entry);
612
+ const refs = (config.refs ?? []).filter((r) => refName(r) !== name);
613
+ refs.push(entry);
614
+ await writeConfig({ ...config, refs });
615
+
565
616
  return { security };
566
617
  },
567
618
 
@@ -623,15 +674,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
623
674
 
624
675
  // If we have a direct access_token from OAuth, call the agent's MCP server
625
676
  // directly instead of going through the registry
626
- if (accessToken) {
627
- const info = await ref.inspect(name);
628
- const security = info?.security as { type: string; flows?: { authorizationCode?: { authorizationUrl?: string } } } | undefined;
629
- const authUrl = security?.flows?.authorizationCode?.authorizationUrl;
630
- if (authUrl) {
631
- // MCP endpoint is at /mcp under the OAuth origin
632
- const mcpUrl = `${new URL(authUrl).origin}/mcp`;
633
- return callMcpDirect(mcpUrl, tool, params ?? {}, accessToken);
634
- }
677
+ if (accessToken && entry.url) {
678
+ return callMcpDirect(entry.url, tool, params ?? {}, accessToken);
635
679
  }
636
680
 
637
681
  const consumer = await buildConsumer();
@@ -691,46 +735,112 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
691
735
  // Can't reach registry
692
736
  }
693
737
 
738
+ if (!security || security.type === "none") {
739
+ return { name, security, complete: true, fields: {} };
740
+ }
741
+
694
742
  const configKeys = Object.keys(entry.config ?? {});
743
+ const resolve = options.resolveCredentials;
695
744
 
696
- if (!security || security.type === "none") {
697
- return { name, security, complete: true, missing: [], present: configKeys };
745
+ async function canResolve(field: string, oauthMetadata?: import("./mcp-client.js").OAuthServerMetadata | null): Promise<boolean> {
746
+ if (!resolve || !entry) return false;
747
+ const val = await resolve({ ref: name, field, entry, security, oauthMetadata });
748
+ return val !== null;
698
749
  }
699
750
 
700
- const requiredFields = (() => {
701
- switch (security.type) {
702
- case "oauth2": return ["access_token"];
703
- case "apiKey": return ["api_key"];
704
- case "http": return ["token"];
705
- default: return [];
751
+ const fields: Record<string, CredentialField> = {};
752
+
753
+ if (security.type === "oauth2") {
754
+ const securityExt = security as {
755
+ dynamicRegistration?: boolean;
756
+ discoveryUrl?: string;
757
+ };
758
+ const hasRegistration = !!securityExt.dynamicRegistration;
759
+
760
+ let oauthMetadata: import("./mcp-client.js").OAuthServerMetadata | null = null;
761
+ let needsSecret = false;
762
+ if (securityExt.discoveryUrl) {
763
+ oauthMetadata = await tryFetchOAuthMetadata(securityExt.discoveryUrl);
764
+ if (oauthMetadata) {
765
+ const authMethods = oauthMetadata.token_endpoint_auth_methods_supported ?? [];
766
+ needsSecret = !authMethods.includes("none");
767
+ }
768
+ }
769
+
770
+ fields.client_id = {
771
+ required: true,
772
+ automated: hasRegistration,
773
+ present: configKeys.includes("client_id"),
774
+ resolvable: await canResolve("client_id", oauthMetadata),
775
+ };
776
+ if (needsSecret) {
777
+ fields.client_secret = {
778
+ required: true,
779
+ automated: hasRegistration,
780
+ present: configKeys.includes("client_secret"),
781
+ resolvable: await canResolve("client_secret", oauthMetadata),
782
+ };
706
783
  }
707
- })();
784
+ fields.access_token = {
785
+ required: true,
786
+ automated: true,
787
+ present: configKeys.includes("access_token"),
788
+ resolvable: false,
789
+ };
790
+ } else if (security.type === "apiKey") {
791
+ fields.api_key = {
792
+ required: true,
793
+ automated: false,
794
+ present: configKeys.includes("api_key"),
795
+ resolvable: await canResolve("api_key"),
796
+ };
797
+ } else if (security.type === "http") {
798
+ fields.token = {
799
+ required: true,
800
+ automated: false,
801
+ present: configKeys.includes("token"),
802
+ resolvable: await canResolve("token"),
803
+ };
804
+ }
708
805
 
709
- const missing = requiredFields.filter((f) => !configKeys.includes(f));
710
- const present = requiredFields.filter((f) => configKeys.includes(f));
806
+ const complete = Object.values(fields).every(
807
+ (f) => !f.required || f.present || f.resolvable || f.automated,
808
+ );
711
809
 
712
- return { name, security, complete: missing.length === 0, missing, present };
810
+ return { name, security, complete, fields };
713
811
  },
714
812
 
715
813
  async auth(name: string, opts?: {
716
814
  apiKey?: string;
717
815
  }): Promise<AuthStartResult> {
816
+ const config = await readConfig();
817
+ const entry = (config.refs ?? []).find((r) => refName(r) === name);
818
+ if (!entry) throw new Error(`Ref "${name}" not found`);
819
+
718
820
  const status = await ref.authStatus(name);
719
821
  const security = status.security;
822
+ const resolve = options.resolveCredentials;
823
+
824
+ async function tryResolve(field: string, oauthMetadata?: import("./mcp-client.js").OAuthServerMetadata | null): Promise<string | null> {
825
+ if (!resolve) return null;
826
+ return resolve({ ref: name, field, entry: entry!, security, oauthMetadata });
827
+ }
720
828
 
721
829
  if (!security || security.type === "none") {
722
830
  return { type: "none", complete: true };
723
831
  }
724
832
 
725
833
  if (security.type === "apiKey") {
726
- if (!opts?.apiKey) return { type: "apiKey", complete: false };
727
- await storeRefSecret(name, "api_key", opts.apiKey);
834
+ const key = opts?.apiKey ?? await tryResolve("api_key");
835
+ if (!key) return { type: "apiKey", complete: false };
836
+ await storeRefSecret(name, "api_key", key);
728
837
  return { type: "apiKey", complete: true };
729
838
  }
730
839
 
731
840
  if (security.type === "http") {
732
- if (!opts?.apiKey) return { type: "http", complete: false };
733
- await storeRefSecret(name, "token", opts.apiKey);
841
+ const token = opts?.apiKey ?? await tryResolve("token");
842
+ if (!token) return { type: "http", complete: false };
843
+ await storeRefSecret(name, "token", token);
734
844
  return { type: "http", complete: true };
735
845
  }
736
846
 
@@ -741,11 +851,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
741
851
  return { type: "oauth2", complete: false };
742
852
  }
743
853
 
744
- // The authorizationUrl might be the discovery URL itself or a base URL
745
854
  const authUrl = authCodeFlow.authorizationUrl;
746
855
  let metadata = await tryFetchOAuthMetadata(authUrl);
747
856
  if (!metadata) {
748
- // Try base origin
749
857
  const origin = new URL(authUrl).origin;
750
858
  metadata = await discoverOAuthMetadata(origin);
751
859
  }
@@ -755,10 +863,14 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
755
863
 
756
864
  const redirectUri = callbackUrl();
757
865
 
758
- // Dynamic client registration if supported
759
- let clientId: string;
760
- let clientSecret: string | undefined;
761
- if (metadata.registration_endpoint) {
866
+ // Resolve client credentials: callback → stored → dynamic registration
867
+ let clientId = await tryResolve("client_id", metadata)
868
+ ?? await readRefSecret(name, "client_id");
869
+ let clientSecret = await tryResolve("client_secret", metadata)
870
+ ?? await readRefSecret(name, "client_secret")
871
+ ?? undefined;
872
+
873
+ if (!clientId && metadata.registration_endpoint) {
762
874
  const supportedAuthMethods = metadata.token_endpoint_auth_methods_supported ?? ["none"];
763
875
  const preferredMethod = supportedAuthMethods.includes("none")
764
876
  ? "none"
@@ -777,15 +889,12 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
777
889
  if (clientSecret) {
778
890
  await storeRefSecret(name, "client_secret", clientSecret);
779
891
  }
780
- } else {
781
- const stored = await readRefSecret(name, "client_id");
782
- if (!stored) {
783
- throw new Error(
784
- "OAuth server doesn't support dynamic client registration. " +
785
- "Store a client_id first.",
786
- );
787
- }
788
- clientId = stored;
892
+ }
893
+
894
+ if (!clientId) {
895
+ throw new Error(
896
+ "Could not obtain client_id. Provide via resolveCredentials callback or store manually.",
897
+ );
789
898
  }
790
899
 
791
900
  // State ties the callback back to this ref
@@ -903,5 +1012,33 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
903
1012
  return { refName: pending.refName, complete: true };
904
1013
  }
905
1014
 
906
- return { registry, ref, readConfig, writeConfig, handleCallback };
1015
+ // ==========================================
1016
+ // Proxy API
1017
+ // ==========================================
1018
+
1019
+ const proxy: AdkProxyApi = {
1020
+ async add(entry: ProxyEntry): Promise<void> {
1021
+ const config = await readConfig();
1022
+ const proxies = (config.proxies ?? []).filter((p) => p.name !== entry.name);
1023
+ proxies.push(entry);
1024
+ await writeConfig({ ...config, proxies });
1025
+ },
1026
+
1027
+ async remove(name: string): Promise<boolean> {
1028
+ const config = await readConfig();
1029
+ if (!config.proxies?.length) return false;
1030
+ const before = config.proxies.length;
1031
+ const proxies = config.proxies.filter((p) => p.name !== name);
1032
+ if (proxies.length === before) return false;
1033
+ await writeConfig({ ...config, proxies });
1034
+ return true;
1035
+ },
1036
+
1037
+ async list(): Promise<ProxyEntry[]> {
1038
+ const config = await readConfig();
1039
+ return config.proxies ?? [];
1040
+ },
1041
+ };
1042
+
1043
+ return { proxy, registry, ref, readConfig, writeConfig, handleCallback };
907
1044
  }
@@ -86,12 +86,35 @@ export type RefEntry = {
86
86
  status?: 'active' | 'inactive' | 'error';
87
87
  };
88
88
 
89
+ // ============================================
90
+ // Proxy Config
91
+ // ============================================
92
+
93
+ /** A proxy target — remote adk server that handles ref/registry operations */
94
+ export interface ProxyEntry {
95
+ /** Human-readable name */
96
+ name: string;
97
+ /** URL of the remote server */
98
+ url: string;
99
+ /** Connection type: 'mcp' (direct MCP server) or 'registry' (agent on a registry) */
100
+ type: 'mcp' | 'registry';
101
+ /** For type 'registry': the agent path that implements adk tools (e.g. '@config') */
102
+ agent?: string;
103
+ /** Auth for connecting to the proxy */
104
+ auth?: RegistryAuth;
105
+ /** Whether this is the default proxy when no local refs/registries exist */
106
+ default?: boolean;
107
+ }
108
+
89
109
  // ============================================
90
110
  // Consumer Config
91
111
  // ============================================
92
112
 
93
113
  /** The full consumer configuration */
94
114
  export interface ConsumerConfig {
115
+ /** Remote adk proxies — forward operations to a remote server */
116
+ proxies?: ProxyEntry[];
117
+
95
118
  /** Registries to connect to, in resolution order */
96
119
  registries?: (string | RegistryEntry)[];
97
120