@slashfi/agents-sdk 0.90.2 → 0.90.4

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.
@@ -111,6 +111,11 @@ export interface RegistryCacheAuthField {
111
111
  * For example HTTP Basic stores one `token` but asks UI for username/password.
112
112
  */
113
113
  parts?: RegistryCacheAuthFieldPart[];
114
+ /**
115
+ * When `false`, connect/refresh bookkeeping only — not forwarded on
116
+ * `ref.call`. Omitted or `true` for bearer, header, and call-time creds.
117
+ */
118
+ outbound?: boolean;
114
119
  }
115
120
 
116
121
  /**
@@ -391,6 +396,8 @@ export interface CredentialField {
391
396
  format?: CompositeCredentialFormat;
392
397
  /** Structured inputs that compose this canonical stored credential */
393
398
  parts?: AuthChallengeField[];
399
+ /** Connect/refresh only — not forwarded on ref.call. */
400
+ outbound?: boolean;
394
401
  }
395
402
 
396
403
  /** Describes what auth a ref needs and what's already provided */
@@ -991,6 +998,203 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
991
998
  };
992
999
  }
993
1000
 
1001
+ /**
1002
+ * Call-time credential lookup: stored ref config first, then the host
1003
+ * `resolveCredentials` callback. Does not persist resolved values.
1004
+ */
1005
+ async function resolveCallCredential(
1006
+ ctx: CredentialResolverContext,
1007
+ field: string,
1008
+ ): Promise<string | null> {
1009
+ const stored = await readRefSecret(ctx.name, field);
1010
+ if (stored) return stored;
1011
+ return makeTryResolve(ctx)(field);
1012
+ }
1013
+
1014
+ const CALL_BEARER_FIELDS = ["access_token", "api_key", "token"] as const;
1015
+
1016
+ function isBearerAuthField(field: string): boolean {
1017
+ return (CALL_BEARER_FIELDS as readonly string[]).includes(field);
1018
+ }
1019
+
1020
+ /** Legacy cache entries may omit `outbound`; these are never call-time creds. */
1021
+ const LEGACY_CONNECT_ONLY_FIELDS = new Set([
1022
+ "client_id",
1023
+ "client_secret",
1024
+ "refresh_token",
1025
+ ]);
1026
+
1027
+ function isCallOutboundAuthField(
1028
+ field: string,
1029
+ info: RegistryCacheAuthField,
1030
+ ): boolean {
1031
+ if (info.outbound === false) return false;
1032
+ if (info.outbound === true) return true;
1033
+ return !LEGACY_CONNECT_ONLY_FIELDS.has(field);
1034
+ }
1035
+
1036
+ function readRegistryDeclaredAuthFields(
1037
+ security: SecuritySchemeSummary | null,
1038
+ ): Record<string, RegistryCacheAuthField> | undefined {
1039
+ if (!security || typeof security !== "object") return undefined;
1040
+ const raw = (security as { authFields?: unknown }).authFields;
1041
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
1042
+ const out: Record<string, RegistryCacheAuthField> = {};
1043
+ for (const [field, meta] of Object.entries(raw as Record<string, unknown>)) {
1044
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) continue;
1045
+ const m = meta as Record<string, unknown>;
1046
+ if (typeof m.required !== "boolean" || typeof m.automated !== "boolean") {
1047
+ continue;
1048
+ }
1049
+ out[field] = { required: m.required, automated: m.automated };
1050
+ if (typeof m.outbound === "boolean") {
1051
+ out[field].outbound = m.outbound;
1052
+ }
1053
+ }
1054
+ return Object.keys(out).length > 0 ? out : undefined;
1055
+ }
1056
+
1057
+ async function mergeRegistryDeclaredAuthFields(
1058
+ fields: Record<string, CredentialField>,
1059
+ declared: Record<string, RegistryCacheAuthField> | undefined,
1060
+ canResolve: (field: string) => Promise<boolean>,
1061
+ configKeys: string[],
1062
+ refConfig: Record<string, unknown>,
1063
+ ): Promise<Record<string, CredentialField>> {
1064
+ if (!declared) return fields;
1065
+ const next: Record<string, CredentialField> = {};
1066
+ for (const [field, meta] of Object.entries(declared)) {
1067
+ next[field] = {
1068
+ required: meta.required,
1069
+ automated: meta.automated,
1070
+ present:
1071
+ configKeys.includes(field) || hasCredentialField(refConfig, field),
1072
+ resolvable: await canResolve(field),
1073
+ ...(meta.format && { format: meta.format }),
1074
+ ...(meta.parts && { parts: meta.parts }),
1075
+ ...(meta.outbound === false && { outbound: false }),
1076
+ };
1077
+ }
1078
+ return next;
1079
+ }
1080
+
1081
+ function bearerFieldSatisfied(
1082
+ accessToken: string | null,
1083
+ refConfig: Record<string, unknown>,
1084
+ field: string,
1085
+ ): boolean {
1086
+ if (accessToken) return true;
1087
+ return hasCredentialField(refConfig, field);
1088
+ }
1089
+
1090
+ function fallbackCallAuthFields(): Record<string, RegistryCacheAuthField> {
1091
+ return {
1092
+ access_token: { required: true, automated: true },
1093
+ api_key: { required: false, automated: true },
1094
+ token: { required: false, automated: true },
1095
+ };
1096
+ }
1097
+
1098
+ function headerFieldSatisfied(
1099
+ headers: Record<string, string>,
1100
+ field: string,
1101
+ ): boolean {
1102
+ const wanted = normalizeCredentialKey(field);
1103
+ return Object.keys(headers).some(
1104
+ (key) => normalizeCredentialKey(key) === wanted,
1105
+ );
1106
+ }
1107
+
1108
+ function resolveHeaderNameForField(
1109
+ field: string,
1110
+ refConfig: Record<string, unknown>,
1111
+ ): string {
1112
+ const wanted = normalizeCredentialKey(field);
1113
+ const configHeaders = refConfig.headers;
1114
+ if (
1115
+ configHeaders &&
1116
+ typeof configHeaders === "object" &&
1117
+ !Array.isArray(configHeaders)
1118
+ ) {
1119
+ for (const key of Object.keys(configHeaders as Record<string, unknown>)) {
1120
+ if (normalizeCredentialKey(key) === wanted) return key;
1121
+ }
1122
+ }
1123
+ // x_api_key → X-API-KEY (registry codegen declares the canonical name;
1124
+ // env-resolved keys use the normalized storage field name).
1125
+ return field
1126
+ .split("_")
1127
+ .filter(Boolean)
1128
+ .map((part) => part.toUpperCase())
1129
+ .join("-");
1130
+ }
1131
+
1132
+ /**
1133
+ * Supplement call-time credentials from `resolveCredentials` when they
1134
+ * are not already present in consumer-config. Stored values and config
1135
+ * headers win — this only fills gaps. Walks cached `authFields` as the
1136
+ * source of truth (registry-declared when auth-status has run).
1137
+ */
1138
+ async function resolveAllCallCredentials(opts: {
1139
+ ctx: CredentialResolverContext;
1140
+ refConfig: Record<string, unknown>;
1141
+ accessToken: string | null;
1142
+ resolvedHeaders: Record<string, string> | undefined;
1143
+ }): Promise<{
1144
+ accessToken: string | null;
1145
+ resolvedHeaders: Record<string, string> | undefined;
1146
+ }> {
1147
+ if (!options.resolveCredentials) {
1148
+ return {
1149
+ accessToken: opts.accessToken,
1150
+ resolvedHeaders: opts.resolvedHeaders,
1151
+ };
1152
+ }
1153
+
1154
+ let accessToken = opts.accessToken;
1155
+ let resolvedHeaders = opts.resolvedHeaders;
1156
+ const { ctx, refConfig } = opts;
1157
+
1158
+ const cache = await readRegistryCache();
1159
+ const authFields =
1160
+ cache.refs[ctx.name]?.authFields ?? fallbackCallAuthFields();
1161
+
1162
+ for (const [field, info] of Object.entries(authFields)) {
1163
+ if (!isCallOutboundAuthField(field, info)) continue;
1164
+ if (!info.required && !info.automated) continue;
1165
+
1166
+ if (isBearerAuthField(field)) {
1167
+ if (bearerFieldSatisfied(accessToken, refConfig, field)) continue;
1168
+ const value = await resolveCallCredential(ctx, field);
1169
+ if (value) accessToken = accessToken ?? value;
1170
+ continue;
1171
+ }
1172
+
1173
+ if (hasCredentialField(refConfig, field)) continue;
1174
+ if (resolvedHeaders && headerFieldSatisfied(resolvedHeaders, field)) {
1175
+ continue;
1176
+ }
1177
+
1178
+ const value = await resolveCallCredential(ctx, field);
1179
+ if (!value) continue;
1180
+
1181
+ resolvedHeaders = resolvedHeaders ?? {};
1182
+ if (!headerFieldSatisfied(resolvedHeaders, field)) {
1183
+ resolvedHeaders[resolveHeaderNameForField(field, refConfig)] = value;
1184
+ }
1185
+ }
1186
+
1187
+ if (!accessToken) {
1188
+ const username = await resolveCallCredential(ctx, "username");
1189
+ const password = await resolveCallCredential(ctx, "password");
1190
+ if (username && password) {
1191
+ accessToken = btoa(`${username}:${password}`);
1192
+ }
1193
+ }
1194
+
1195
+ return { accessToken, resolvedHeaders };
1196
+ }
1197
+
994
1198
  /**
995
1199
  * Resolve OAuth client credentials (client_id + client_secret) for a
996
1200
  * ref. Walks: `resolveCredentials` callback → per-ref VCS storage.
@@ -2254,7 +2458,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2254
2458
  const entry = findRef(config.refs ?? [], name);
2255
2459
  if (!entry) throw new Error(`Ref "${name}" not found`);
2256
2460
 
2257
- const accessToken =
2461
+ let accessToken =
2258
2462
  (await readRefSecret(name, "access_token")) ??
2259
2463
  (await readRefSecret(name, "api_key")) ??
2260
2464
  (await readRefSecret(name, "token"));
@@ -2306,6 +2510,17 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2306
2510
  }
2307
2511
  }
2308
2512
 
2513
+ if (options.resolveCredentials) {
2514
+ const supplemented = await resolveAllCallCredentials({
2515
+ ctx: { name, entry, security: null },
2516
+ refConfig,
2517
+ accessToken,
2518
+ resolvedHeaders,
2519
+ });
2520
+ accessToken = supplemented.accessToken;
2521
+ resolvedHeaders = supplemented.resolvedHeaders;
2522
+ }
2523
+
2309
2524
  const doCall = async (token: string | null) => {
2310
2525
  // Direct MCP only for redirect/proxy agents with an MCP upstream.
2311
2526
  // API-mode agents must go through the registry (it does REST translation).
@@ -2460,7 +2675,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2460
2675
  return (await tryResolveField(field, oauthMetadata)) !== null;
2461
2676
  }
2462
2677
 
2463
- const fields: Record<string, CredentialField> = {};
2678
+ let fields: Record<string, CredentialField> = {};
2464
2679
 
2465
2680
  if (security.type === "oauth2") {
2466
2681
  const securityExt = security as {
@@ -2506,6 +2721,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2506
2721
  automated: hasRegistration,
2507
2722
  present: configKeys.includes("client_id"),
2508
2723
  resolvable: await canResolve("client_id", oauthMetadata),
2724
+ outbound: false,
2509
2725
  };
2510
2726
  if (needsSecret) {
2511
2727
  fields.client_secret = {
@@ -2513,13 +2729,14 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2513
2729
  automated: hasRegistration,
2514
2730
  present: configKeys.includes("client_secret"),
2515
2731
  resolvable: await canResolve("client_secret", oauthMetadata),
2732
+ outbound: false,
2516
2733
  };
2517
2734
  }
2518
2735
  fields.access_token = {
2519
2736
  required: true,
2520
2737
  automated: accessTokenAutomated,
2521
2738
  present: configKeys.includes("access_token"),
2522
- resolvable: false,
2739
+ resolvable: await canResolve("access_token"),
2523
2740
  };
2524
2741
  } else if (security.type === "apiKey") {
2525
2742
  const apiKeySec = security as {
@@ -2607,8 +2824,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2607
2824
  };
2608
2825
  }
2609
2826
 
2827
+ fields = await mergeRegistryDeclaredAuthFields(
2828
+ fields,
2829
+ readRegistryDeclaredAuthFields(security),
2830
+ canResolve,
2831
+ configKeys,
2832
+ (entry.config ?? {}) as Record<string, unknown>,
2833
+ );
2834
+
2610
2835
  const complete = Object.values(fields).every(
2611
- (f) => !f.required || f.present || f.resolvable,
2836
+ (f) => !f.required || f.automated || f.present || f.resolvable,
2612
2837
  );
2613
2838
 
2614
2839
  // Persist the slim {required, automated} per-field shape into the
@@ -2624,6 +2849,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2624
2849
  automated: info.automated,
2625
2850
  ...(info.format && { format: info.format }),
2626
2851
  ...(info.parts && { parts: info.parts }),
2852
+ ...(info.outbound === false && { outbound: false }),
2627
2853
  };
2628
2854
  }
2629
2855
  await upsertRegistryCacheAuthFields(name, entry.ref, authFields);