@slashfi/agents-sdk 0.40.1 → 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.
@@ -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,18 +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
- // Use upstream URL from registry if available, fall back to deriving from OAuth URL
629
- const upstream = (info as { upstream?: string } | null)?.upstream;
630
- const security = info?.security as { type: string; flows?: { authorizationCode?: { authorizationUrl?: string } } } | undefined;
631
- const mcpUrl = upstream
632
- ?? (security?.flows?.authorizationCode?.authorizationUrl
633
- ? `${new URL(security.flows.authorizationCode.authorizationUrl).origin}/mcp`
634
- : null);
635
- if (mcpUrl) {
636
- return callMcpDirect(mcpUrl, tool, params ?? {}, accessToken);
637
- }
677
+ if (accessToken && entry.url) {
678
+ return callMcpDirect(entry.url, tool, params ?? {}, accessToken);
638
679
  }
639
680
 
640
681
  const consumer = await buildConsumer();
@@ -694,46 +735,112 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
694
735
  // Can't reach registry
695
736
  }
696
737
 
738
+ if (!security || security.type === "none") {
739
+ return { name, security, complete: true, fields: {} };
740
+ }
741
+
697
742
  const configKeys = Object.keys(entry.config ?? {});
743
+ const resolve = options.resolveCredentials;
698
744
 
699
- if (!security || security.type === "none") {
700
- 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;
701
749
  }
702
750
 
703
- const requiredFields = (() => {
704
- switch (security.type) {
705
- case "oauth2": return ["access_token"];
706
- case "apiKey": return ["api_key"];
707
- case "http": return ["token"];
708
- 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
+ };
709
783
  }
710
- })();
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
+ }
711
805
 
712
- const missing = requiredFields.filter((f) => !configKeys.includes(f));
713
- 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
+ );
714
809
 
715
- return { name, security, complete: missing.length === 0, missing, present };
810
+ return { name, security, complete, fields };
716
811
  },
717
812
 
718
813
  async auth(name: string, opts?: {
719
814
  apiKey?: string;
720
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
+
721
820
  const status = await ref.authStatus(name);
722
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
+ }
723
828
 
724
829
  if (!security || security.type === "none") {
725
830
  return { type: "none", complete: true };
726
831
  }
727
832
 
728
833
  if (security.type === "apiKey") {
729
- if (!opts?.apiKey) return { type: "apiKey", complete: false };
730
- 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);
731
837
  return { type: "apiKey", complete: true };
732
838
  }
733
839
 
734
840
  if (security.type === "http") {
735
- if (!opts?.apiKey) return { type: "http", complete: false };
736
- 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);
737
844
  return { type: "http", complete: true };
738
845
  }
739
846
 
@@ -744,11 +851,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
744
851
  return { type: "oauth2", complete: false };
745
852
  }
746
853
 
747
- // The authorizationUrl might be the discovery URL itself or a base URL
748
854
  const authUrl = authCodeFlow.authorizationUrl;
749
855
  let metadata = await tryFetchOAuthMetadata(authUrl);
750
856
  if (!metadata) {
751
- // Try base origin
752
857
  const origin = new URL(authUrl).origin;
753
858
  metadata = await discoverOAuthMetadata(origin);
754
859
  }
@@ -758,10 +863,14 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
758
863
 
759
864
  const redirectUri = callbackUrl();
760
865
 
761
- // Dynamic client registration if supported
762
- let clientId: string;
763
- let clientSecret: string | undefined;
764
- 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) {
765
874
  const supportedAuthMethods = metadata.token_endpoint_auth_methods_supported ?? ["none"];
766
875
  const preferredMethod = supportedAuthMethods.includes("none")
767
876
  ? "none"
@@ -780,15 +889,12 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
780
889
  if (clientSecret) {
781
890
  await storeRefSecret(name, "client_secret", clientSecret);
782
891
  }
783
- } else {
784
- const stored = await readRefSecret(name, "client_id");
785
- if (!stored) {
786
- throw new Error(
787
- "OAuth server doesn't support dynamic client registration. " +
788
- "Store a client_id first.",
789
- );
790
- }
791
- 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
+ );
792
898
  }
793
899
 
794
900
  // State ties the callback back to this ref
@@ -906,5 +1012,33 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
906
1012
  return { refName: pending.refName, complete: true };
907
1013
  }
908
1014
 
909
- 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 };
910
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
 
package/src/index.ts CHANGED
@@ -292,6 +292,7 @@ export {
292
292
  export type {
293
293
  RegistryAuth,
294
294
  RegistryEntry,
295
+ ProxyEntry,
295
296
  RefConfig,
296
297
  RefEntry,
297
298
  ConsumerConfig,
@@ -453,11 +454,17 @@ export { createAdk } from "./config-store.js";
453
454
  export type {
454
455
  Adk,
455
456
  AdkOptions,
457
+ AdkProxyApi,
456
458
  AdkRegistryApi,
457
459
  AdkRefApi,
458
460
  RefAuthStatus,
461
+ CredentialField,
459
462
  AuthStartResult,
460
463
  OAuthResult,
464
+ ResolveCredentials,
465
+ ResolveCredentialsContext,
461
466
  RegistryTestResult,
462
467
  } from "./config-store.js";
463
468
  export { createLocalFsStore, getLocalEncryptionKey } from "./local-fs.js";
469
+ export { createAdkTools } from "./adk-tools.js";
470
+ export type { CreateAdkToolsOptions } from "./adk-tools.js";
@@ -224,6 +224,8 @@ export interface AgentListing {
224
224
  }>;
225
225
  /** Context hint from describe_tools (e.g., "Use full: true for complete schemas") */
226
226
  context?: string;
227
+ /** Upstream MCP/API URL for direct connections */
228
+ upstream?: string;
227
229
  /** Integration config if applicable */
228
230
  integration?: {
229
231
  provider: string;
@@ -928,6 +930,7 @@ export async function createRegistryConsumer(
928
930
  security?: SecuritySchemeSummary;
929
931
  resources?: Array<{ uri: string; name?: string; mimeType?: string }>;
930
932
  context?: string;
933
+ upstream?: string;
931
934
  } | null;
932
935
  if (!data) return null;
933
936
  return {
@@ -939,6 +942,7 @@ export async function createRegistryConsumer(
939
942
  security: data.security,
940
943
  resources: data.resources,
941
944
  context: data.context,
945
+ ...(data.upstream && { upstream: data.upstream }),
942
946
  } as AgentListing;
943
947
  }),
944
948
  );