@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.
- package/dist/adk.js +61 -11
- package/dist/adk.js.map +1 -1
- package/dist/cjs/config-store.js +227 -33
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/cjs/define-config.js.map +1 -1
- package/dist/cjs/registry-consumer.js +10 -1
- package/dist/cjs/registry-consumer.js.map +1 -1
- package/dist/config-store.d.ts +36 -0
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +227 -33
- package/dist/config-store.js.map +1 -1
- package/dist/define-config.d.ts +4 -0
- package/dist/define-config.d.ts.map +1 -1
- package/dist/define-config.js.map +1 -1
- package/dist/registry-consumer.d.ts.map +1 -1
- package/dist/registry-consumer.js +10 -1
- package/dist/registry-consumer.js.map +1 -1
- package/package.json +1 -1
- package/src/adk.ts +61 -13
- package/src/config-store.test.ts +350 -0
- package/src/config-store.ts +316 -36
- package/src/consumer.test.ts +54 -0
- package/src/define-config.ts +1 -0
- package/src/registry-consumer.ts +10 -1
package/src/config-store.ts
CHANGED
|
@@ -86,9 +86,36 @@ export interface RegistryCacheToolSummary {
|
|
|
86
86
|
* client registration). Doesn't need to be `present`
|
|
87
87
|
* in the user's config to count as satisfied.
|
|
88
88
|
*/
|
|
89
|
+
export type CompositeCredentialFormat = "basic";
|
|
90
|
+
|
|
91
|
+
export interface RegistryCacheAuthFieldPart {
|
|
92
|
+
/** Field key callers should collect (for example "username") */
|
|
93
|
+
name: string;
|
|
94
|
+
/** Human-readable label for UI rendering */
|
|
95
|
+
label: string;
|
|
96
|
+
/** Whether this part is secret and should be masked */
|
|
97
|
+
secret: boolean;
|
|
98
|
+
/** Whether this part may be intentionally blank */
|
|
99
|
+
optional?: boolean;
|
|
100
|
+
/** Optional description / help text */
|
|
101
|
+
description?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
89
104
|
export interface RegistryCacheAuthField {
|
|
90
105
|
required: boolean;
|
|
91
106
|
automated: boolean;
|
|
107
|
+
/** How `parts` compose into this canonical stored credential. */
|
|
108
|
+
format?: CompositeCredentialFormat;
|
|
109
|
+
/**
|
|
110
|
+
* Optional structured inputs that compose this canonical stored credential.
|
|
111
|
+
* For example HTTP Basic stores one `token` but asks UI for username/password.
|
|
112
|
+
*/
|
|
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;
|
|
92
119
|
}
|
|
93
120
|
|
|
94
121
|
/**
|
|
@@ -329,6 +356,7 @@ export interface AdkRegistryApi {
|
|
|
329
356
|
nameOrUrl: string,
|
|
330
357
|
credential:
|
|
331
358
|
| { token: string; tokenUrl?: string }
|
|
359
|
+
| { username: string; password?: string }
|
|
332
360
|
| { apiKey: string; header?: string },
|
|
333
361
|
): Promise<boolean>;
|
|
334
362
|
|
|
@@ -364,6 +392,12 @@ export interface CredentialField {
|
|
|
364
392
|
present: boolean;
|
|
365
393
|
/** Available via resolveCredentials callback */
|
|
366
394
|
resolvable: boolean;
|
|
395
|
+
/** How `parts` compose into this canonical stored credential */
|
|
396
|
+
format?: CompositeCredentialFormat;
|
|
397
|
+
/** Structured inputs that compose this canonical stored credential */
|
|
398
|
+
parts?: AuthChallengeField[];
|
|
399
|
+
/** Connect/refresh only — not forwarded on ref.call. */
|
|
400
|
+
outbound?: boolean;
|
|
367
401
|
}
|
|
368
402
|
|
|
369
403
|
/** Describes what auth a ref needs and what's already provided */
|
|
@@ -391,6 +425,8 @@ export interface AuthChallengeField {
|
|
|
391
425
|
label: string;
|
|
392
426
|
/** Whether this is a secret value (should be masked in UI) */
|
|
393
427
|
secret: boolean;
|
|
428
|
+
/** Whether this field may be intentionally blank */
|
|
429
|
+
optional?: boolean;
|
|
394
430
|
/** Optional description / help text */
|
|
395
431
|
description?: string;
|
|
396
432
|
}
|
|
@@ -667,7 +703,7 @@ function renderCredentialForm(
|
|
|
667
703
|
<div class="field">
|
|
668
704
|
<label for="${esc(f.name)}">${esc(f.label)}</label>
|
|
669
705
|
${f.description ? `<p class="desc">${esc(f.description)}</p>` : ""}
|
|
670
|
-
<input id="${esc(f.name)}" name="${esc(f.name)}" type="${f.secret ? "password" : "text"}" required autocomplete="off" spellcheck="false" />
|
|
706
|
+
<input id="${esc(f.name)}" name="${esc(f.name)}" type="${f.secret ? "password" : "text"}" ${f.optional ? "" : "required"} autocomplete="off" spellcheck="false" />
|
|
671
707
|
</div>`,
|
|
672
708
|
)
|
|
673
709
|
.join("");
|
|
@@ -962,6 +998,203 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
962
998
|
};
|
|
963
999
|
}
|
|
964
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
|
+
|
|
965
1198
|
/**
|
|
966
1199
|
* Resolve OAuth client credentials (client_id + client_secret) for a
|
|
967
1200
|
* ref. Walks: `resolveCredentials` callback → per-ref VCS storage.
|
|
@@ -1320,7 +1553,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1320
1553
|
*/
|
|
1321
1554
|
async function updateRegistryEntry(
|
|
1322
1555
|
nameOrUrl: string,
|
|
1323
|
-
mutate: (entry: RegistryEntry) => void
|
|
1556
|
+
mutate: (entry: RegistryEntry) => void | Promise<void>,
|
|
1324
1557
|
): Promise<boolean> {
|
|
1325
1558
|
const config = await readConfig();
|
|
1326
1559
|
if (!config.registries?.length) return false;
|
|
@@ -1331,10 +1564,14 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1331
1564
|
found = true;
|
|
1332
1565
|
const existing: RegistryEntry =
|
|
1333
1566
|
typeof r === "string" ? { url: r } : { ...r };
|
|
1334
|
-
mutate(existing);
|
|
1335
1567
|
return existing;
|
|
1336
1568
|
});
|
|
1337
1569
|
if (!found) return false;
|
|
1570
|
+
for (const r of registries) {
|
|
1571
|
+
if (typeof r !== "string" && (registryDisplayName(r) === nameOrUrl || registryUrl(r) === nameOrUrl)) {
|
|
1572
|
+
await mutate(r);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1338
1575
|
await writeConfig({ ...config, registries });
|
|
1339
1576
|
return true;
|
|
1340
1577
|
}
|
|
@@ -1433,6 +1670,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1433
1670
|
const hasUsableAuth =
|
|
1434
1671
|
entry.auth && entry.auth.type !== "none"
|
|
1435
1672
|
? (entry.auth.type === "bearer" && !!entry.auth.token) ||
|
|
1673
|
+
(entry.auth.type === "basic" && !!entry.auth.username) ||
|
|
1436
1674
|
(entry.auth.type === "api-key" && !!entry.auth.key)
|
|
1437
1675
|
: false;
|
|
1438
1676
|
if (hasUsableAuth) return;
|
|
@@ -1474,6 +1712,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1474
1712
|
const hasUsableAuth =
|
|
1475
1713
|
entry.auth && entry.auth.type !== "none"
|
|
1476
1714
|
? (entry.auth.type === "bearer" && !!entry.auth.token) ||
|
|
1715
|
+
(entry.auth.type === "basic" && !!entry.auth.username) ||
|
|
1477
1716
|
(entry.auth.type === "api-key" && !!entry.auth.key)
|
|
1478
1717
|
: false;
|
|
1479
1718
|
|
|
@@ -1584,6 +1823,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1584
1823
|
const hasUsableAuth =
|
|
1585
1824
|
r.auth && r.auth.type !== "none"
|
|
1586
1825
|
? (r.auth.type === "bearer" && !!r.auth.token) ||
|
|
1826
|
+
(r.auth.type === "basic" && !!r.auth.username) ||
|
|
1587
1827
|
(r.auth.type === "api-key" && !!r.auth.key)
|
|
1588
1828
|
: false;
|
|
1589
1829
|
if (!hasUsableAuth) {
|
|
@@ -1627,26 +1867,30 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1627
1867
|
nameOrUrl: string,
|
|
1628
1868
|
credential:
|
|
1629
1869
|
| { token: string; tokenUrl?: string }
|
|
1870
|
+
| { username: string; password?: string }
|
|
1630
1871
|
| { apiKey: string; header?: string },
|
|
1631
1872
|
): Promise<boolean> {
|
|
1632
|
-
// Encrypt
|
|
1633
|
-
//
|
|
1634
|
-
const
|
|
1635
|
-
"token" in credential
|
|
1636
|
-
? await protectSecret(credential.token)
|
|
1637
|
-
: await protectSecret(credential.apiKey);
|
|
1638
|
-
|
|
1639
|
-
const updated = await updateRegistryEntry(nameOrUrl, (existing) => {
|
|
1873
|
+
// Encrypt secret values before writing. `buildConsumer` decrypts on the
|
|
1874
|
+
// read side via `decryptConfigSecrets`.
|
|
1875
|
+
const updated = await updateRegistryEntry(nameOrUrl, async (existing) => {
|
|
1640
1876
|
if ("token" in credential) {
|
|
1641
1877
|
existing.auth = {
|
|
1642
1878
|
type: "bearer",
|
|
1643
|
-
token:
|
|
1879
|
+
token: await protectSecret(credential.token),
|
|
1644
1880
|
...(credential.tokenUrl && { tokenUrl: credential.tokenUrl }),
|
|
1645
1881
|
};
|
|
1882
|
+
} else if ("username" in credential) {
|
|
1883
|
+
existing.auth = {
|
|
1884
|
+
type: "basic",
|
|
1885
|
+
username: await protectSecret(credential.username),
|
|
1886
|
+
...(credential.password && {
|
|
1887
|
+
password: await protectSecret(credential.password),
|
|
1888
|
+
}),
|
|
1889
|
+
};
|
|
1646
1890
|
} else {
|
|
1647
1891
|
existing.auth = {
|
|
1648
1892
|
type: "api-key",
|
|
1649
|
-
key:
|
|
1893
|
+
key: await protectSecret(credential.apiKey),
|
|
1650
1894
|
...(credential.header && { header: credential.header }),
|
|
1651
1895
|
};
|
|
1652
1896
|
}
|
|
@@ -1710,6 +1954,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1710
1954
|
const hasUsableAuth =
|
|
1711
1955
|
target.auth && target.auth.type !== "none"
|
|
1712
1956
|
? (target.auth.type === "bearer" && !!target.auth.token) ||
|
|
1957
|
+
(target.auth.type === "basic" && !!target.auth.username) ||
|
|
1713
1958
|
(target.auth.type === "api-key" && !!target.auth.key)
|
|
1714
1959
|
: false;
|
|
1715
1960
|
if (hasUsableAuth && !target.authRequirement) {
|
|
@@ -2213,7 +2458,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2213
2458
|
const entry = findRef(config.refs ?? [], name);
|
|
2214
2459
|
if (!entry) throw new Error(`Ref "${name}" not found`);
|
|
2215
2460
|
|
|
2216
|
-
|
|
2461
|
+
let accessToken =
|
|
2217
2462
|
(await readRefSecret(name, "access_token")) ??
|
|
2218
2463
|
(await readRefSecret(name, "api_key")) ??
|
|
2219
2464
|
(await readRefSecret(name, "token"));
|
|
@@ -2265,6 +2510,17 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2265
2510
|
}
|
|
2266
2511
|
}
|
|
2267
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
|
+
|
|
2268
2524
|
const doCall = async (token: string | null) => {
|
|
2269
2525
|
// Direct MCP only for redirect/proxy agents with an MCP upstream.
|
|
2270
2526
|
// API-mode agents must go through the registry (it does REST translation).
|
|
@@ -2419,7 +2675,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2419
2675
|
return (await tryResolveField(field, oauthMetadata)) !== null;
|
|
2420
2676
|
}
|
|
2421
2677
|
|
|
2422
|
-
|
|
2678
|
+
let fields: Record<string, CredentialField> = {};
|
|
2423
2679
|
|
|
2424
2680
|
if (security.type === "oauth2") {
|
|
2425
2681
|
const securityExt = security as {
|
|
@@ -2465,6 +2721,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2465
2721
|
automated: hasRegistration,
|
|
2466
2722
|
present: configKeys.includes("client_id"),
|
|
2467
2723
|
resolvable: await canResolve("client_id", oauthMetadata),
|
|
2724
|
+
outbound: false,
|
|
2468
2725
|
};
|
|
2469
2726
|
if (needsSecret) {
|
|
2470
2727
|
fields.client_secret = {
|
|
@@ -2472,13 +2729,14 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2472
2729
|
automated: hasRegistration,
|
|
2473
2730
|
present: configKeys.includes("client_secret"),
|
|
2474
2731
|
resolvable: await canResolve("client_secret", oauthMetadata),
|
|
2732
|
+
outbound: false,
|
|
2475
2733
|
};
|
|
2476
2734
|
}
|
|
2477
2735
|
fields.access_token = {
|
|
2478
2736
|
required: true,
|
|
2479
2737
|
automated: accessTokenAutomated,
|
|
2480
2738
|
present: configKeys.includes("access_token"),
|
|
2481
|
-
resolvable:
|
|
2739
|
+
resolvable: await canResolve("access_token"),
|
|
2482
2740
|
};
|
|
2483
2741
|
} else if (security.type === "apiKey") {
|
|
2484
2742
|
const apiKeySec = security as {
|
|
@@ -2531,11 +2789,23 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2531
2789
|
};
|
|
2532
2790
|
}
|
|
2533
2791
|
} else if (security.type === "http") {
|
|
2792
|
+
const httpSec = security as { scheme?: string };
|
|
2793
|
+
const isBasic = httpSec.scheme === "basic";
|
|
2534
2794
|
fields.token = {
|
|
2535
2795
|
required: true,
|
|
2536
2796
|
automated: false,
|
|
2537
2797
|
present: configKeys.includes("token"),
|
|
2538
|
-
resolvable:
|
|
2798
|
+
resolvable: isBasic
|
|
2799
|
+
? (await canResolve("username")) &&
|
|
2800
|
+
(await tryResolveField("password")) !== null
|
|
2801
|
+
: await canResolve("token"),
|
|
2802
|
+
...(isBasic && {
|
|
2803
|
+
format: "basic" as const,
|
|
2804
|
+
parts: [
|
|
2805
|
+
{ name: "username", label: "Username", secret: false },
|
|
2806
|
+
{ name: "password", label: "Password", secret: true, optional: true },
|
|
2807
|
+
],
|
|
2808
|
+
}),
|
|
2539
2809
|
};
|
|
2540
2810
|
} else if (security.type === "form") {
|
|
2541
2811
|
// Form-based refs collect structured user input at connect time
|
|
@@ -2554,8 +2824,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2554
2824
|
};
|
|
2555
2825
|
}
|
|
2556
2826
|
|
|
2827
|
+
fields = await mergeRegistryDeclaredAuthFields(
|
|
2828
|
+
fields,
|
|
2829
|
+
readRegistryDeclaredAuthFields(security),
|
|
2830
|
+
canResolve,
|
|
2831
|
+
configKeys,
|
|
2832
|
+
(entry.config ?? {}) as Record<string, unknown>,
|
|
2833
|
+
);
|
|
2834
|
+
|
|
2557
2835
|
const complete = Object.values(fields).every(
|
|
2558
|
-
(f) => !f.required || f.present || f.resolvable,
|
|
2836
|
+
(f) => !f.required || f.automated || f.present || f.resolvable,
|
|
2559
2837
|
);
|
|
2560
2838
|
|
|
2561
2839
|
// Persist the slim {required, automated} per-field shape into the
|
|
@@ -2569,6 +2847,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2569
2847
|
authFields[field] = {
|
|
2570
2848
|
required: info.required,
|
|
2571
2849
|
automated: info.automated,
|
|
2850
|
+
...(info.format && { format: info.format }),
|
|
2851
|
+
...(info.parts && { parts: info.parts }),
|
|
2852
|
+
...(info.outbound === false && { outbound: false }),
|
|
2572
2853
|
};
|
|
2573
2854
|
}
|
|
2574
2855
|
await upsertRegistryCacheAuthFields(name, entry.ref, authFields);
|
|
@@ -2706,24 +2987,23 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2706
2987
|
const username =
|
|
2707
2988
|
opts?.credentials?.["username"] ?? (await tryResolve("username"));
|
|
2708
2989
|
const password =
|
|
2709
|
-
opts?.credentials?.["password"] ?? (await tryResolve("password"));
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
});
|
|
2724
|
-
return { type: "http", complete: false, fields: missingFields };
|
|
2990
|
+
opts?.credentials?.["password"] ?? (await tryResolve("password")) ?? "";
|
|
2991
|
+
const hasUsername = username !== undefined && username !== null && username !== "";
|
|
2992
|
+
if (!hasUsername) {
|
|
2993
|
+
return {
|
|
2994
|
+
type: "http",
|
|
2995
|
+
complete: false,
|
|
2996
|
+
fields: [
|
|
2997
|
+
{
|
|
2998
|
+
name: "username",
|
|
2999
|
+
label: "Username",
|
|
3000
|
+
secret: false,
|
|
3001
|
+
},
|
|
3002
|
+
],
|
|
3003
|
+
};
|
|
2725
3004
|
}
|
|
2726
|
-
// Store as base64 encoded basic auth token
|
|
3005
|
+
// Store as base64 encoded basic auth token. Password may be blank
|
|
3006
|
+
// for APIs that use the Basic username slot as an API key.
|
|
2727
3007
|
const token = btoa(`${username}:${password}`);
|
|
2728
3008
|
await storeRefSecret(name, "token", token);
|
|
2729
3009
|
return { type: "http", complete: true };
|
|
@@ -2951,7 +3231,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2951
3231
|
const credentials: Record<string, string> = {};
|
|
2952
3232
|
for (const field of result.fields!) {
|
|
2953
3233
|
const val = params.get(field.name);
|
|
2954
|
-
if (val) credentials[field.name] = val;
|
|
3234
|
+
if (val !== null) credentials[field.name] = val;
|
|
2955
3235
|
}
|
|
2956
3236
|
|
|
2957
3237
|
try {
|
package/src/consumer.test.ts
CHANGED
|
@@ -538,6 +538,60 @@ describe("Secret URI resolution", () => {
|
|
|
538
538
|
});
|
|
539
539
|
});
|
|
540
540
|
|
|
541
|
+
// ─── Basic Auth Tests ────────────────────────────────────────────
|
|
542
|
+
|
|
543
|
+
describe("Registry Consumer — Basic Auth", () => {
|
|
544
|
+
let server: AgentServer;
|
|
545
|
+
const PORT = 19895;
|
|
546
|
+
const USERNAME = "ashby-api-key";
|
|
547
|
+
const PASSWORD = "";
|
|
548
|
+
|
|
549
|
+
beforeAll(async () => {
|
|
550
|
+
const registry = createAgentRegistry();
|
|
551
|
+
registry.register(mathAgent);
|
|
552
|
+
registry.register(echoAgent);
|
|
553
|
+
|
|
554
|
+
server = createAgentServer(registry, {
|
|
555
|
+
port: PORT,
|
|
556
|
+
resolveAuth: async (req) => {
|
|
557
|
+
const auth = req.headers.get("authorization");
|
|
558
|
+
const expected = `Basic ${Buffer.from(`${USERNAME}:${PASSWORD}`, "utf8").toString("base64")}`;
|
|
559
|
+
if (auth === expected) {
|
|
560
|
+
return {
|
|
561
|
+
callerId: "basic-auth-user",
|
|
562
|
+
callerType: "system" as const,
|
|
563
|
+
scopes: ["*"],
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
return null;
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
await server.start();
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
afterAll(async () => {
|
|
573
|
+
await server.stop();
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("consumer with basic auth type can list agents", async () => {
|
|
577
|
+
const consumer = await createRegistryConsumer({
|
|
578
|
+
registries: [
|
|
579
|
+
{
|
|
580
|
+
url: `http://localhost:${PORT}`,
|
|
581
|
+
auth: { type: "basic", username: USERNAME, password: PASSWORD },
|
|
582
|
+
},
|
|
583
|
+
],
|
|
584
|
+
refs: [{ ref: "@math" }],
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
const agents = await consumer.list();
|
|
588
|
+
expect(agents.length).toBeGreaterThanOrEqual(2);
|
|
589
|
+
const paths = agents.map((a) => a.path);
|
|
590
|
+
expect(paths).toContain("@math");
|
|
591
|
+
expect(paths).toContain("@echo");
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
541
595
|
// ─── API Key Auth Tests ──────────────────────────────────────────
|
|
542
596
|
|
|
543
597
|
describe("Registry Consumer — API Key Auth", () => {
|
package/src/define-config.ts
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
export type RegistryAuth =
|
|
32
32
|
| { type: "none" }
|
|
33
33
|
| { type: "bearer"; token?: string; tokenUrl?: string }
|
|
34
|
+
| { type: "basic"; username?: string; password?: string }
|
|
34
35
|
| { type: "api-key"; key?: string; header?: string }
|
|
35
36
|
| { type: "jwt"; issuer?: string };
|
|
36
37
|
|
package/src/registry-consumer.ts
CHANGED
|
@@ -144,7 +144,7 @@ function expandEnvVars(value: string): string {
|
|
|
144
144
|
|
|
145
145
|
/**
|
|
146
146
|
* Build auth headers for a registry based on its auth config and custom headers.
|
|
147
|
-
* Merges typed auth (bearer, api-key) with arbitrary custom headers.
|
|
147
|
+
* Merges typed auth (bearer, basic, api-key) with arbitrary custom headers.
|
|
148
148
|
* Environment variable references ($VAR or ${VAR}) in header values are expanded.
|
|
149
149
|
*/
|
|
150
150
|
function buildRegistryAuthHeaders(
|
|
@@ -162,6 +162,15 @@ function buildRegistryAuthHeaders(
|
|
|
162
162
|
}
|
|
163
163
|
break;
|
|
164
164
|
}
|
|
165
|
+
case "basic": {
|
|
166
|
+
if ("username" in registry.auth && registry.auth.username) {
|
|
167
|
+
const password = ("password" in registry.auth ? registry.auth.password : undefined) ?? "";
|
|
168
|
+
const credentials = `${registry.auth.username}:${password}`;
|
|
169
|
+
const encoded = Buffer.from(credentials, "utf8").toString("base64");
|
|
170
|
+
headers.Authorization = `Basic ${encoded}`;
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
165
174
|
case "api-key": {
|
|
166
175
|
if ("key" in registry.auth && registry.auth.key) {
|
|
167
176
|
const headerName = ("header" in registry.auth ? registry.auth.header : undefined) ?? "x-api-key";
|