@slashfi/agents-sdk 0.90.1 → 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.
@@ -238,7 +238,7 @@ function renderCredentialForm(name, fields, error) {
238
238
  <div class="field">
239
239
  <label for="${esc(f.name)}">${esc(f.label)}</label>
240
240
  ${f.description ? `<p class="desc">${esc(f.description)}</p>` : ""}
241
- <input id="${esc(f.name)}" name="${esc(f.name)}" type="${f.secret ? "password" : "text"}" required autocomplete="off" spellcheck="false" />
241
+ <input id="${esc(f.name)}" name="${esc(f.name)}" type="${f.secret ? "password" : "text"}" ${f.optional ? "" : "required"} autocomplete="off" spellcheck="false" />
242
242
  </div>`)
243
243
  .join("");
244
244
  const errorHtml = error ? `<div class="error">${esc(error)}</div>` : "";
@@ -480,6 +480,159 @@ function createAdk(fs, options = {}) {
480
480
  });
481
481
  };
482
482
  }
483
+ /**
484
+ * Call-time credential lookup: stored ref config first, then the host
485
+ * `resolveCredentials` callback. Does not persist resolved values.
486
+ */
487
+ async function resolveCallCredential(ctx, field) {
488
+ const stored = await readRefSecret(ctx.name, field);
489
+ if (stored)
490
+ return stored;
491
+ return makeTryResolve(ctx)(field);
492
+ }
493
+ const CALL_BEARER_FIELDS = ["access_token", "api_key", "token"];
494
+ function isBearerAuthField(field) {
495
+ return CALL_BEARER_FIELDS.includes(field);
496
+ }
497
+ /** Legacy cache entries may omit `outbound`; these are never call-time creds. */
498
+ const LEGACY_CONNECT_ONLY_FIELDS = new Set([
499
+ "client_id",
500
+ "client_secret",
501
+ "refresh_token",
502
+ ]);
503
+ function isCallOutboundAuthField(field, info) {
504
+ if (info.outbound === false)
505
+ return false;
506
+ if (info.outbound === true)
507
+ return true;
508
+ return !LEGACY_CONNECT_ONLY_FIELDS.has(field);
509
+ }
510
+ function readRegistryDeclaredAuthFields(security) {
511
+ if (!security || typeof security !== "object")
512
+ return undefined;
513
+ const raw = security.authFields;
514
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
515
+ return undefined;
516
+ const out = {};
517
+ for (const [field, meta] of Object.entries(raw)) {
518
+ if (!meta || typeof meta !== "object" || Array.isArray(meta))
519
+ continue;
520
+ const m = meta;
521
+ if (typeof m.required !== "boolean" || typeof m.automated !== "boolean") {
522
+ continue;
523
+ }
524
+ out[field] = { required: m.required, automated: m.automated };
525
+ if (typeof m.outbound === "boolean") {
526
+ out[field].outbound = m.outbound;
527
+ }
528
+ }
529
+ return Object.keys(out).length > 0 ? out : undefined;
530
+ }
531
+ async function mergeRegistryDeclaredAuthFields(fields, declared, canResolve, configKeys, refConfig) {
532
+ if (!declared)
533
+ return fields;
534
+ const next = {};
535
+ for (const [field, meta] of Object.entries(declared)) {
536
+ next[field] = {
537
+ required: meta.required,
538
+ automated: meta.automated,
539
+ present: configKeys.includes(field) || hasCredentialField(refConfig, field),
540
+ resolvable: await canResolve(field),
541
+ ...(meta.format && { format: meta.format }),
542
+ ...(meta.parts && { parts: meta.parts }),
543
+ ...(meta.outbound === false && { outbound: false }),
544
+ };
545
+ }
546
+ return next;
547
+ }
548
+ function bearerFieldSatisfied(accessToken, refConfig, field) {
549
+ if (accessToken)
550
+ return true;
551
+ return hasCredentialField(refConfig, field);
552
+ }
553
+ function fallbackCallAuthFields() {
554
+ return {
555
+ access_token: { required: true, automated: true },
556
+ api_key: { required: false, automated: true },
557
+ token: { required: false, automated: true },
558
+ };
559
+ }
560
+ function headerFieldSatisfied(headers, field) {
561
+ const wanted = normalizeCredentialKey(field);
562
+ return Object.keys(headers).some((key) => normalizeCredentialKey(key) === wanted);
563
+ }
564
+ function resolveHeaderNameForField(field, refConfig) {
565
+ const wanted = normalizeCredentialKey(field);
566
+ const configHeaders = refConfig.headers;
567
+ if (configHeaders &&
568
+ typeof configHeaders === "object" &&
569
+ !Array.isArray(configHeaders)) {
570
+ for (const key of Object.keys(configHeaders)) {
571
+ if (normalizeCredentialKey(key) === wanted)
572
+ return key;
573
+ }
574
+ }
575
+ // x_api_key → X-API-KEY (registry codegen declares the canonical name;
576
+ // env-resolved keys use the normalized storage field name).
577
+ return field
578
+ .split("_")
579
+ .filter(Boolean)
580
+ .map((part) => part.toUpperCase())
581
+ .join("-");
582
+ }
583
+ /**
584
+ * Supplement call-time credentials from `resolveCredentials` when they
585
+ * are not already present in consumer-config. Stored values and config
586
+ * headers win — this only fills gaps. Walks cached `authFields` as the
587
+ * source of truth (registry-declared when auth-status has run).
588
+ */
589
+ async function resolveAllCallCredentials(opts) {
590
+ if (!options.resolveCredentials) {
591
+ return {
592
+ accessToken: opts.accessToken,
593
+ resolvedHeaders: opts.resolvedHeaders,
594
+ };
595
+ }
596
+ let accessToken = opts.accessToken;
597
+ let resolvedHeaders = opts.resolvedHeaders;
598
+ const { ctx, refConfig } = opts;
599
+ const cache = await readRegistryCache();
600
+ const authFields = cache.refs[ctx.name]?.authFields ?? fallbackCallAuthFields();
601
+ for (const [field, info] of Object.entries(authFields)) {
602
+ if (!isCallOutboundAuthField(field, info))
603
+ continue;
604
+ if (!info.required && !info.automated)
605
+ continue;
606
+ if (isBearerAuthField(field)) {
607
+ if (bearerFieldSatisfied(accessToken, refConfig, field))
608
+ continue;
609
+ const value = await resolveCallCredential(ctx, field);
610
+ if (value)
611
+ accessToken = accessToken ?? value;
612
+ continue;
613
+ }
614
+ if (hasCredentialField(refConfig, field))
615
+ continue;
616
+ if (resolvedHeaders && headerFieldSatisfied(resolvedHeaders, field)) {
617
+ continue;
618
+ }
619
+ const value = await resolveCallCredential(ctx, field);
620
+ if (!value)
621
+ continue;
622
+ resolvedHeaders = resolvedHeaders ?? {};
623
+ if (!headerFieldSatisfied(resolvedHeaders, field)) {
624
+ resolvedHeaders[resolveHeaderNameForField(field, refConfig)] = value;
625
+ }
626
+ }
627
+ if (!accessToken) {
628
+ const username = await resolveCallCredential(ctx, "username");
629
+ const password = await resolveCallCredential(ctx, "password");
630
+ if (username && password) {
631
+ accessToken = btoa(`${username}:${password}`);
632
+ }
633
+ }
634
+ return { accessToken, resolvedHeaders };
635
+ }
483
636
  /**
484
637
  * Resolve OAuth client credentials (client_id + client_secret) for a
485
638
  * ref. Walks: `resolveCredentials` callback → per-ref VCS storage.
@@ -766,11 +919,15 @@ function createAdk(fs, options = {}) {
766
919
  return r;
767
920
  found = true;
768
921
  const existing = typeof r === "string" ? { url: r } : { ...r };
769
- mutate(existing);
770
922
  return existing;
771
923
  });
772
924
  if (!found)
773
925
  return false;
926
+ for (const r of registries) {
927
+ if (typeof r !== "string" && (registryDisplayName(r) === nameOrUrl || registryUrl(r) === nameOrUrl)) {
928
+ await mutate(r);
929
+ }
930
+ }
774
931
  await writeConfig({ ...config, registries });
775
932
  return true;
776
933
  }
@@ -865,6 +1022,7 @@ function createAdk(fs, options = {}) {
865
1022
  return;
866
1023
  const hasUsableAuth = entry.auth && entry.auth.type !== "none"
867
1024
  ? (entry.auth.type === "bearer" && !!entry.auth.token) ||
1025
+ (entry.auth.type === "basic" && !!entry.auth.username) ||
868
1026
  (entry.auth.type === "api-key" && !!entry.auth.key)
869
1027
  : false;
870
1028
  if (hasUsableAuth)
@@ -898,6 +1056,7 @@ function createAdk(fs, options = {}) {
898
1056
  let authRequirement;
899
1057
  const hasUsableAuth = entry.auth && entry.auth.type !== "none"
900
1058
  ? (entry.auth.type === "bearer" && !!entry.auth.token) ||
1059
+ (entry.auth.type === "basic" && !!entry.auth.username) ||
901
1060
  (entry.auth.type === "api-key" && !!entry.auth.key)
902
1061
  : false;
903
1062
  if (!hasUsableAuth) {
@@ -994,6 +1153,7 @@ function createAdk(fs, options = {}) {
994
1153
  if (typeof r !== "string" && r.authRequirement) {
995
1154
  const hasUsableAuth = r.auth && r.auth.type !== "none"
996
1155
  ? (r.auth.type === "bearer" && !!r.auth.token) ||
1156
+ (r.auth.type === "basic" && !!r.auth.username) ||
997
1157
  (r.auth.type === "api-key" && !!r.auth.key)
998
1158
  : false;
999
1159
  if (!hasUsableAuth) {
@@ -1030,23 +1190,29 @@ function createAdk(fs, options = {}) {
1030
1190
  });
1031
1191
  },
1032
1192
  async auth(nameOrUrl, credential) {
1033
- // Encrypt the secret value up-front so the write path is uniform;
1034
- // `buildConsumer` decrypts on the read side via `decryptConfigSecrets`.
1035
- const protectedValue = "token" in credential
1036
- ? await protectSecret(credential.token)
1037
- : await protectSecret(credential.apiKey);
1038
- const updated = await updateRegistryEntry(nameOrUrl, (existing) => {
1193
+ // Encrypt secret values before writing. `buildConsumer` decrypts on the
1194
+ // read side via `decryptConfigSecrets`.
1195
+ const updated = await updateRegistryEntry(nameOrUrl, async (existing) => {
1039
1196
  if ("token" in credential) {
1040
1197
  existing.auth = {
1041
1198
  type: "bearer",
1042
- token: protectedValue,
1199
+ token: await protectSecret(credential.token),
1043
1200
  ...(credential.tokenUrl && { tokenUrl: credential.tokenUrl }),
1044
1201
  };
1045
1202
  }
1203
+ else if ("username" in credential) {
1204
+ existing.auth = {
1205
+ type: "basic",
1206
+ username: await protectSecret(credential.username),
1207
+ ...(credential.password && {
1208
+ password: await protectSecret(credential.password),
1209
+ }),
1210
+ };
1211
+ }
1046
1212
  else {
1047
1213
  existing.auth = {
1048
1214
  type: "api-key",
1049
- key: protectedValue,
1215
+ key: await protectSecret(credential.apiKey),
1050
1216
  ...(credential.header && { header: credential.header }),
1051
1217
  };
1052
1218
  }
@@ -1097,6 +1263,7 @@ function createAdk(fs, options = {}) {
1097
1263
  // Already authenticated — nothing to do (unless forced above).
1098
1264
  const hasUsableAuth = target.auth && target.auth.type !== "none"
1099
1265
  ? (target.auth.type === "bearer" && !!target.auth.token) ||
1266
+ (target.auth.type === "basic" && !!target.auth.username) ||
1100
1267
  (target.auth.type === "api-key" && !!target.auth.key)
1101
1268
  : false;
1102
1269
  if (hasUsableAuth && !target.authRequirement) {
@@ -1528,7 +1695,7 @@ function createAdk(fs, options = {}) {
1528
1695
  const entry = findRef(config.refs ?? [], name);
1529
1696
  if (!entry)
1530
1697
  throw new Error(`Ref "${name}" not found`);
1531
- const accessToken = (await readRefSecret(name, "access_token")) ??
1698
+ let accessToken = (await readRefSecret(name, "access_token")) ??
1532
1699
  (await readRefSecret(name, "api_key")) ??
1533
1700
  (await readRefSecret(name, "token"));
1534
1701
  // Resolve custom headers from config (e.g. { "X-API-Key": "secret:..." })
@@ -1575,6 +1742,16 @@ function createAdk(fs, options = {}) {
1575
1742
  }
1576
1743
  }
1577
1744
  }
1745
+ if (options.resolveCredentials) {
1746
+ const supplemented = await resolveAllCallCredentials({
1747
+ ctx: { name, entry, security: null },
1748
+ refConfig,
1749
+ accessToken,
1750
+ resolvedHeaders,
1751
+ });
1752
+ accessToken = supplemented.accessToken;
1753
+ resolvedHeaders = supplemented.resolvedHeaders;
1754
+ }
1578
1755
  const doCall = async (token) => {
1579
1756
  // Direct MCP only for redirect/proxy agents with an MCP upstream.
1580
1757
  // API-mode agents must go through the registry (it does REST translation).
@@ -1686,7 +1863,7 @@ function createAdk(fs, options = {}) {
1686
1863
  async function canResolve(field, oauthMetadata) {
1687
1864
  return (await tryResolveField(field, oauthMetadata)) !== null;
1688
1865
  }
1689
- const fields = {};
1866
+ let fields = {};
1690
1867
  if (security.type === "oauth2") {
1691
1868
  const securityExt = security;
1692
1869
  const hasRegistration = !!securityExt.dynamicRegistration;
@@ -1720,6 +1897,7 @@ function createAdk(fs, options = {}) {
1720
1897
  automated: hasRegistration,
1721
1898
  present: configKeys.includes("client_id"),
1722
1899
  resolvable: await canResolve("client_id", oauthMetadata),
1900
+ outbound: false,
1723
1901
  };
1724
1902
  if (needsSecret) {
1725
1903
  fields.client_secret = {
@@ -1727,13 +1905,14 @@ function createAdk(fs, options = {}) {
1727
1905
  automated: hasRegistration,
1728
1906
  present: configKeys.includes("client_secret"),
1729
1907
  resolvable: await canResolve("client_secret", oauthMetadata),
1908
+ outbound: false,
1730
1909
  };
1731
1910
  }
1732
1911
  fields.access_token = {
1733
1912
  required: true,
1734
1913
  automated: accessTokenAutomated,
1735
1914
  present: configKeys.includes("access_token"),
1736
- resolvable: false,
1915
+ resolvable: await canResolve("access_token"),
1737
1916
  };
1738
1917
  }
1739
1918
  else if (security.type === "apiKey") {
@@ -1776,11 +1955,23 @@ function createAdk(fs, options = {}) {
1776
1955
  }
1777
1956
  }
1778
1957
  else if (security.type === "http") {
1958
+ const httpSec = security;
1959
+ const isBasic = httpSec.scheme === "basic";
1779
1960
  fields.token = {
1780
1961
  required: true,
1781
1962
  automated: false,
1782
1963
  present: configKeys.includes("token"),
1783
- resolvable: await canResolve("token"),
1964
+ resolvable: isBasic
1965
+ ? (await canResolve("username")) &&
1966
+ (await tryResolveField("password")) !== null
1967
+ : await canResolve("token"),
1968
+ ...(isBasic && {
1969
+ format: "basic",
1970
+ parts: [
1971
+ { name: "username", label: "Username", secret: false },
1972
+ { name: "password", label: "Password", secret: true, optional: true },
1973
+ ],
1974
+ }),
1784
1975
  };
1785
1976
  }
1786
1977
  else if (security.type === "form") {
@@ -1799,7 +1990,8 @@ function createAdk(fs, options = {}) {
1799
1990
  resolvable: await canResolve("access_token"),
1800
1991
  };
1801
1992
  }
1802
- const complete = Object.values(fields).every((f) => !f.required || f.present || f.resolvable);
1993
+ fields = await mergeRegistryDeclaredAuthFields(fields, readRegistryDeclaredAuthFields(security), canResolve, configKeys, (entry.config ?? {}));
1994
+ const complete = Object.values(fields).every((f) => !f.required || f.automated || f.present || f.resolvable);
1803
1995
  // Persist the slim {required, automated} per-field shape into the
1804
1996
  // registry cache so `isRefAuthComplete` can answer subsequent
1805
1997
  // host-side "is this ref ready?" checks without re-fetching the
@@ -1811,6 +2003,9 @@ function createAdk(fs, options = {}) {
1811
2003
  authFields[field] = {
1812
2004
  required: info.required,
1813
2005
  automated: info.automated,
2006
+ ...(info.format && { format: info.format }),
2007
+ ...(info.parts && { parts: info.parts }),
2008
+ ...(info.outbound === false && { outbound: false }),
1814
2009
  };
1815
2010
  }
1816
2011
  await upsertRegistryCacheAuthFields(name, entry.ref, authFields);
@@ -1908,24 +2103,23 @@ function createAdk(fs, options = {}) {
1908
2103
  const isBasic = httpSec.scheme === "basic";
1909
2104
  if (isBasic) {
1910
2105
  const username = opts?.credentials?.["username"] ?? (await tryResolve("username"));
1911
- const password = opts?.credentials?.["password"] ?? (await tryResolve("password"));
1912
- if (!username || !password) {
1913
- const missingFields = [];
1914
- if (!username)
1915
- missingFields.push({
1916
- name: "username",
1917
- label: "Username",
1918
- secret: false,
1919
- });
1920
- if (!password)
1921
- missingFields.push({
1922
- name: "password",
1923
- label: "Password",
1924
- secret: true,
1925
- });
1926
- return { type: "http", complete: false, fields: missingFields };
2106
+ const password = opts?.credentials?.["password"] ?? (await tryResolve("password")) ?? "";
2107
+ const hasUsername = username !== undefined && username !== null && username !== "";
2108
+ if (!hasUsername) {
2109
+ return {
2110
+ type: "http",
2111
+ complete: false,
2112
+ fields: [
2113
+ {
2114
+ name: "username",
2115
+ label: "Username",
2116
+ secret: false,
2117
+ },
2118
+ ],
2119
+ };
1927
2120
  }
1928
- // Store as base64 encoded basic auth token
2121
+ // Store as base64 encoded basic auth token. Password may be blank
2122
+ // for APIs that use the Basic username slot as an API key.
1929
2123
  const token = btoa(`${username}:${password}`);
1930
2124
  await storeRefSecret(name, "token", token);
1931
2125
  return { type: "http", complete: true };
@@ -2101,7 +2295,7 @@ function createAdk(fs, options = {}) {
2101
2295
  const credentials = {};
2102
2296
  for (const field of result.fields) {
2103
2297
  const val = params.get(field.name);
2104
- if (val)
2298
+ if (val !== null)
2105
2299
  credentials[field.name] = val;
2106
2300
  }
2107
2301
  try {