@slashfi/agents-sdk 0.76.0 → 0.77.1
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-tools.d.ts +2 -2
- package/dist/adk-tools.d.ts.map +1 -1
- package/dist/adk-tools.js +9 -18
- package/dist/adk-tools.js.map +1 -1
- package/dist/adk.js +190 -120
- package/dist/adk.js.map +1 -1
- package/dist/agent-definitions/config.d.ts.map +1 -1
- package/dist/agent-definitions/config.js +12 -14
- package/dist/agent-definitions/config.js.map +1 -1
- package/dist/cjs/adk-tools.js +9 -18
- package/dist/cjs/adk-tools.js.map +1 -1
- package/dist/cjs/agent-definitions/config.js +12 -14
- package/dist/cjs/agent-definitions/config.js.map +1 -1
- package/dist/cjs/config-store.js +527 -30
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/cjs/define-config.js +5 -7
- package/dist/cjs/define-config.js.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/materialize.js +1 -1
- package/dist/cjs/materialize.js.map +1 -1
- package/dist/cjs/mcp-client.js +98 -0
- package/dist/cjs/mcp-client.js.map +1 -1
- package/dist/cjs/registry-consumer.js +69 -11
- package/dist/cjs/registry-consumer.js.map +1 -1
- package/dist/config-store.d.ts +39 -4
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +528 -31
- package/dist/config-store.js.map +1 -1
- package/dist/define-config.d.ts +65 -18
- package/dist/define-config.d.ts.map +1 -1
- package/dist/define-config.js +5 -7
- package/dist/define-config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/materialize.js +1 -1
- package/dist/materialize.js.map +1 -1
- package/dist/mcp-client.d.ts +44 -0
- package/dist/mcp-client.d.ts.map +1 -1
- package/dist/mcp-client.js +95 -0
- package/dist/mcp-client.js.map +1 -1
- package/dist/registry-consumer.d.ts +1 -1
- package/dist/registry-consumer.d.ts.map +1 -1
- package/dist/registry-consumer.js +69 -11
- package/dist/registry-consumer.js.map +1 -1
- package/dist/validate.d.ts +8 -8
- package/package.json +1 -1
- package/src/adk-tools.ts +11 -18
- package/src/adk.ts +78 -11
- package/src/agent-definitions/config.ts +15 -16
- package/src/config-store.test.ts +212 -0
- package/src/config-store.ts +615 -37
- package/src/consumer.test.ts +7 -7
- package/src/define-config.ts +69 -20
- package/src/index.ts +1 -0
- package/src/materialize.ts +1 -1
- package/src/mcp-client.ts +121 -0
- package/src/ref-naming.test.ts +115 -90
- package/src/registry-consumer.ts +75 -13
package/src/config-store.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import type { FsStore } from "./agent-definitions/config.js";
|
|
21
21
|
import type {
|
|
22
22
|
ConsumerConfig,
|
|
23
|
+
RefAddInput,
|
|
23
24
|
RefEntry,
|
|
24
25
|
RegistryEntry,
|
|
25
26
|
ResolvedRef,
|
|
@@ -43,7 +44,10 @@ import {
|
|
|
43
44
|
dynamicClientRegistration,
|
|
44
45
|
buildOAuthAuthorizeUrl,
|
|
45
46
|
exchangeCodeForTokens,
|
|
47
|
+
probeRegistryAuth,
|
|
48
|
+
refreshAccessToken,
|
|
46
49
|
} from "./mcp-client.js";
|
|
50
|
+
import type { RegistryAuthRequirement } from "./define-config.js";
|
|
47
51
|
|
|
48
52
|
const CONFIG_PATH = "consumer-config.json";
|
|
49
53
|
const SECRET_PREFIX = "secret:";
|
|
@@ -124,7 +128,7 @@ export interface RegistryTestResult {
|
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
export interface AdkRegistryApi {
|
|
127
|
-
add(entry: RegistryEntry): Promise<
|
|
131
|
+
add(entry: RegistryEntry): Promise<{ authRequirement?: RegistryAuthRequirement }>;
|
|
128
132
|
remove(nameOrUrl: string): Promise<boolean>;
|
|
129
133
|
list(): Promise<RegistryEntry[]>;
|
|
130
134
|
get(name: string): Promise<RegistryEntry | null>;
|
|
@@ -132,6 +136,39 @@ export interface AdkRegistryApi {
|
|
|
132
136
|
browse(name: string, query?: string): Promise<AgentListEntry[]>;
|
|
133
137
|
inspect(name: string): Promise<RegistryConfiguration>;
|
|
134
138
|
test(name?: string): Promise<RegistryTestResult[]>;
|
|
139
|
+
/**
|
|
140
|
+
* Attach a credential to a registry that returned 401 during `add`. Clears
|
|
141
|
+
* `authRequirement` so subsequent ops stop throwing `registry_auth_required`.
|
|
142
|
+
* Accepts a pre-existing token / api-key when the caller already has one.
|
|
143
|
+
*/
|
|
144
|
+
auth(
|
|
145
|
+
nameOrUrl: string,
|
|
146
|
+
credential:
|
|
147
|
+
| { token: string; tokenUrl?: string }
|
|
148
|
+
| { apiKey: string; header?: string },
|
|
149
|
+
): Promise<boolean>;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Resolve auth for a registry the way `adk registry auth` does — runs the
|
|
153
|
+
* full OAuth flow (dynamic client registration + PKCE authorize + callback
|
|
154
|
+
* + token exchange) when the registry advertised authorization servers,
|
|
155
|
+
* or spins up a local HTTPS form for bearer-token entry otherwise.
|
|
156
|
+
*
|
|
157
|
+
* Returns `{ complete: true }` once the registry has usable credentials
|
|
158
|
+
* persisted. The `onAuthorizeUrl` callback fires with the URL the user
|
|
159
|
+
* should open (browser redirect URL for OAuth, or `http://localhost/auth`
|
|
160
|
+
* for the token-entry form). Pass `force: true` to skip the short-circuit
|
|
161
|
+
* when existing credentials look syntactically valid but may be stale
|
|
162
|
+
* server-side — the common case when the CLI command is invoked explicitly.
|
|
163
|
+
*/
|
|
164
|
+
authLocal(
|
|
165
|
+
nameOrUrl: string,
|
|
166
|
+
opts?: {
|
|
167
|
+
onAuthorizeUrl?: (url: string) => void;
|
|
168
|
+
timeoutMs?: number;
|
|
169
|
+
force?: boolean;
|
|
170
|
+
},
|
|
171
|
+
): Promise<{ complete: boolean }>;
|
|
135
172
|
}
|
|
136
173
|
|
|
137
174
|
/** Describes a single credential field requirement */
|
|
@@ -212,10 +249,10 @@ type AdkRefCallFn = keyof AdkAgentRegistry extends never
|
|
|
212
249
|
<A extends AgentPath, T extends ToolsOf<A>>(name: A, tool: T, params: ParamsOf<A, T>) => Promise<CallAgentResponse>;
|
|
213
250
|
|
|
214
251
|
export interface AdkRefApi {
|
|
215
|
-
add(entry:
|
|
252
|
+
add(entry: RefAddInput): Promise<{ security: SecuritySchemeSummary | null }>;
|
|
216
253
|
remove(name: string): Promise<boolean>;
|
|
217
254
|
list(): Promise<ResolvedRef[]>;
|
|
218
|
-
get(name: string): Promise<
|
|
255
|
+
get(name: string): Promise<ResolvedRef | null>;
|
|
219
256
|
update(name: string, updates: Partial<RefEntry>): Promise<boolean>;
|
|
220
257
|
inspect(name: string, options?: { full?: boolean }): Promise<AgentInspection | null>;
|
|
221
258
|
call: AdkRefCallFn;
|
|
@@ -296,11 +333,12 @@ function refName(entry: RefEntry): string {
|
|
|
296
333
|
* Refs may be stored as `@foo` or `foo` depending on how they were added;
|
|
297
334
|
* this ensures lookups work regardless of which form the caller uses.
|
|
298
335
|
*/
|
|
299
|
-
function findRef(refs: RefEntry[], name: string):
|
|
336
|
+
function findRef(refs: RefEntry[], name: string): ResolvedRef | undefined {
|
|
300
337
|
const match = refs.find((r) => refName(r) === name);
|
|
301
|
-
if (match) return match;
|
|
338
|
+
if (match) return normalizeRef(match);
|
|
302
339
|
const alt = name.startsWith("@") ? name.slice(1) : `@${name}`;
|
|
303
|
-
|
|
340
|
+
const altMatch = refs.find((r) => refName(r) === alt);
|
|
341
|
+
return altMatch ? normalizeRef(altMatch) : undefined;
|
|
304
342
|
}
|
|
305
343
|
|
|
306
344
|
/**
|
|
@@ -466,7 +504,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
466
504
|
const config = await readConfig();
|
|
467
505
|
const refs = (config.refs ?? []).map((r): RefEntry => {
|
|
468
506
|
if (refName(r) !== name) return r;
|
|
469
|
-
|
|
507
|
+
const normalized = normalizeRef(r);
|
|
508
|
+
return { ...normalized, config: { ...normalized.config, [key]: stored } };
|
|
470
509
|
});
|
|
471
510
|
await writeConfig({ ...config, refs });
|
|
472
511
|
}
|
|
@@ -779,7 +818,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
779
818
|
operation: string,
|
|
780
819
|
params: Record<string, unknown>,
|
|
781
820
|
): Promise<T> {
|
|
782
|
-
const consumer = await buildConsumerForRef({
|
|
821
|
+
const consumer = await buildConsumerForRef({
|
|
822
|
+
ref: "",
|
|
823
|
+
name: "",
|
|
824
|
+
sourceRegistry: { url: reg.url, agentPath: agent },
|
|
825
|
+
});
|
|
783
826
|
const resolved = consumer.registries().find((r) => r.url === reg.url);
|
|
784
827
|
if (!resolved) throw new Error(`Registry ${reg.url} not resolvable for proxy forwarding`);
|
|
785
828
|
|
|
@@ -802,19 +845,217 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
802
845
|
// Registry API
|
|
803
846
|
// ==========================================
|
|
804
847
|
|
|
848
|
+
/**
|
|
849
|
+
* Encrypt with `secret:` prefix when an encryption key is configured, so the
|
|
850
|
+
* value is readable by the existing `decryptConfigSecrets` path on the read
|
|
851
|
+
* side. Plaintext fallback preserves the "no key = dev mode" contract.
|
|
852
|
+
*/
|
|
853
|
+
async function protectSecret(value: string): Promise<string> {
|
|
854
|
+
if (!options.encryptionKey) return value;
|
|
855
|
+
return `${SECRET_PREFIX}${await encryptSecret(value, options.encryptionKey)}`;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Re-probe a registry with the current stored credentials to see whether it
|
|
860
|
+
* advertises `capabilities.registry.proxy` in its MCP `initialize` response,
|
|
861
|
+
* and persist the proxy config when it does. Safe to call after a successful
|
|
862
|
+
* `auth()` / `authLocal()` — on the add path we skip the proxy probe when
|
|
863
|
+
* auth is required, so this is the second chance to back-fill it.
|
|
864
|
+
*
|
|
865
|
+
* Respects explicit user config: if `proxy` is already set, we leave it
|
|
866
|
+
* alone. Any discovery failure is swallowed — proxy is an optimization,
|
|
867
|
+
* not a correctness requirement.
|
|
868
|
+
*/
|
|
869
|
+
async function discoverProxyAfterAuth(nameOrUrl: string): Promise<void> {
|
|
870
|
+
const config = await readConfig();
|
|
871
|
+
const target = findRegistry(config.registries ?? [], nameOrUrl);
|
|
872
|
+
if (!target || typeof target === "string") return;
|
|
873
|
+
if (target.proxy) return;
|
|
874
|
+
|
|
875
|
+
try {
|
|
876
|
+
const consumer = await buildConsumer(nameOrUrl);
|
|
877
|
+
const discovered = await consumer.discover(target.url);
|
|
878
|
+
if (!discovered.proxy?.mode) return;
|
|
879
|
+
await updateRegistryEntry(nameOrUrl, (existing) => {
|
|
880
|
+
if (existing.proxy) return;
|
|
881
|
+
existing.proxy = {
|
|
882
|
+
mode: discovered.proxy!.mode,
|
|
883
|
+
...(discovered.proxy!.agent && { agent: discovered.proxy!.agent }),
|
|
884
|
+
};
|
|
885
|
+
});
|
|
886
|
+
} catch {
|
|
887
|
+
// Proxy probe is best-effort — auth itself already succeeded.
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Atomic read-modify-write on a registry entry by name or URL. Used by
|
|
893
|
+
* `authLocal` to persist both `auth` and `oauth` together, which `auth()`
|
|
894
|
+
* alone can't express. Returns true when the entry was found and written.
|
|
895
|
+
*/
|
|
896
|
+
async function updateRegistryEntry(
|
|
897
|
+
nameOrUrl: string,
|
|
898
|
+
mutate: (entry: RegistryEntry) => void,
|
|
899
|
+
): Promise<boolean> {
|
|
900
|
+
const config = await readConfig();
|
|
901
|
+
if (!config.registries?.length) return false;
|
|
902
|
+
let found = false;
|
|
903
|
+
const registries = config.registries.map((r): string | RegistryEntry => {
|
|
904
|
+
const rName = registryDisplayName(r);
|
|
905
|
+
if (rName !== nameOrUrl && registryUrl(r) !== nameOrUrl) return r;
|
|
906
|
+
found = true;
|
|
907
|
+
const existing: RegistryEntry = typeof r === "string" ? { url: r } : { ...r };
|
|
908
|
+
mutate(existing);
|
|
909
|
+
return existing;
|
|
910
|
+
});
|
|
911
|
+
if (!found) return false;
|
|
912
|
+
await writeConfig({ ...config, registries });
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Decrypt a `secret:`-prefixed value if we hold the encryption key. Plaintext
|
|
918
|
+
* values pass through unchanged so dev configs keep working.
|
|
919
|
+
*/
|
|
920
|
+
async function revealSecret(value: string | undefined): Promise<string | undefined> {
|
|
921
|
+
if (!value) return value;
|
|
922
|
+
if (!value.startsWith(SECRET_PREFIX)) return value;
|
|
923
|
+
if (!options.encryptionKey) return undefined;
|
|
924
|
+
return decryptSecret(value.slice(SECRET_PREFIX.length), options.encryptionKey);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Refresh a registry's OAuth access token using the stored refresh token.
|
|
929
|
+
* Persists the new access token (encrypted) and updates `expiresAt`. If the
|
|
930
|
+
* provider rotates the refresh token, that's encrypted and stored too.
|
|
931
|
+
* Returns `true` when the refresh succeeded. Callers should catch and fall
|
|
932
|
+
* back to full re-auth on failure.
|
|
933
|
+
*/
|
|
934
|
+
async function refreshRegistryToken(nameOrUrl: string): Promise<boolean> {
|
|
935
|
+
const config = await readConfig();
|
|
936
|
+
const target = findRegistry(config.registries ?? [], nameOrUrl);
|
|
937
|
+
if (!target || typeof target === "string") return false;
|
|
938
|
+
const oauth = target.oauth;
|
|
939
|
+
if (!oauth?.refreshToken || !oauth.tokenEndpoint || !oauth.clientId) return false;
|
|
940
|
+
|
|
941
|
+
const refreshToken = await revealSecret(oauth.refreshToken);
|
|
942
|
+
const clientSecret = await revealSecret(oauth.clientSecret);
|
|
943
|
+
if (!refreshToken) return false;
|
|
944
|
+
|
|
945
|
+
const refreshed = await refreshAccessToken(oauth.tokenEndpoint, {
|
|
946
|
+
refreshToken,
|
|
947
|
+
clientId: oauth.clientId,
|
|
948
|
+
...(clientSecret && { clientSecret }),
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
const expiresAt = refreshed.expiresIn
|
|
952
|
+
? new Date(Date.now() + refreshed.expiresIn * 1000).toISOString()
|
|
953
|
+
: undefined;
|
|
954
|
+
const encAccess = await protectSecret(refreshed.accessToken);
|
|
955
|
+
const encRefresh = refreshed.refreshToken
|
|
956
|
+
? await protectSecret(refreshed.refreshToken)
|
|
957
|
+
: undefined;
|
|
958
|
+
|
|
959
|
+
await updateRegistryEntry(nameOrUrl, (existing) => {
|
|
960
|
+
existing.auth = { type: "bearer", token: encAccess };
|
|
961
|
+
if (!existing.oauth) return;
|
|
962
|
+
if (encRefresh) existing.oauth.refreshToken = encRefresh;
|
|
963
|
+
if (expiresAt) existing.oauth.expiresAt = expiresAt;
|
|
964
|
+
else delete existing.oauth.expiresAt;
|
|
965
|
+
});
|
|
966
|
+
return true;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Run a registry op once; on 401 (`registry_auth_required`), try to refresh
|
|
971
|
+
* via the stored refresh token and retry exactly once. Any other AdkError
|
|
972
|
+
* propagates as-is.
|
|
973
|
+
*/
|
|
974
|
+
async function callWithRefresh<T>(
|
|
975
|
+
nameOrUrl: string,
|
|
976
|
+
fn: () => Promise<T>,
|
|
977
|
+
): Promise<T> {
|
|
978
|
+
try {
|
|
979
|
+
return await fn();
|
|
980
|
+
} catch (err) {
|
|
981
|
+
if (!(err instanceof AdkError) || err.code !== "registry_auth_required") throw err;
|
|
982
|
+
let refreshed = false;
|
|
983
|
+
try {
|
|
984
|
+
refreshed = await refreshRegistryToken(nameOrUrl);
|
|
985
|
+
} catch {
|
|
986
|
+
// Refresh failed — surface the original 401 below.
|
|
987
|
+
}
|
|
988
|
+
if (!refreshed) throw err;
|
|
989
|
+
return fn();
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* Throw a typed error if the registry has a recorded auth challenge and
|
|
995
|
+
* no usable credentials on the entry. Callers should invoke this before
|
|
996
|
+
* running any op that talks to the registry.
|
|
997
|
+
*/
|
|
998
|
+
function assertRegistryAuthorized(entry: RegistryEntry): void {
|
|
999
|
+
if (!entry.authRequirement) return;
|
|
1000
|
+
const hasUsableAuth =
|
|
1001
|
+
entry.auth && entry.auth.type !== "none"
|
|
1002
|
+
? (entry.auth.type === "bearer" && !!entry.auth.token) ||
|
|
1003
|
+
(entry.auth.type === "api-key" && !!entry.auth.key)
|
|
1004
|
+
: false;
|
|
1005
|
+
if (hasUsableAuth) return;
|
|
1006
|
+
|
|
1007
|
+
const name = entry.name ?? entry.url;
|
|
1008
|
+
const scope = entry.authRequirement.scopes?.join(" ");
|
|
1009
|
+
throw new AdkError({
|
|
1010
|
+
code: "registry_auth_required",
|
|
1011
|
+
message: `Registry "${name}" requires authentication.`,
|
|
1012
|
+
hint: `Run: adk registry auth ${name} --token <token>${scope ? ` (scopes: ${scope})` : ""}`,
|
|
1013
|
+
details: {
|
|
1014
|
+
url: entry.url,
|
|
1015
|
+
scheme: entry.authRequirement.scheme,
|
|
1016
|
+
realm: entry.authRequirement.realm,
|
|
1017
|
+
authorizationServers: entry.authRequirement.authorizationServers,
|
|
1018
|
+
scopes: entry.authRequirement.scopes,
|
|
1019
|
+
resourceMetadataUrl: entry.authRequirement.resourceMetadataUrl,
|
|
1020
|
+
},
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
|
|
805
1024
|
const registry: AdkRegistryApi = {
|
|
806
|
-
async add(entry: RegistryEntry): Promise<
|
|
1025
|
+
async add(entry: RegistryEntry): Promise<{ authRequirement?: RegistryAuthRequirement }> {
|
|
807
1026
|
const config = await readConfig();
|
|
808
1027
|
const alias = entry.name ?? entry.url;
|
|
809
1028
|
const registries = (config.registries ?? []).filter(
|
|
810
1029
|
(r) => registryDisplayName(r) !== alias,
|
|
811
1030
|
);
|
|
812
1031
|
|
|
813
|
-
// Probe the registry
|
|
814
|
-
//
|
|
815
|
-
//
|
|
1032
|
+
// Probe the registry before saving. Two things fall out of the probe:
|
|
1033
|
+
// 1. Auth challenge — 401 + WWW-Authenticate points at RFC 9728
|
|
1034
|
+
// resource metadata; we persist it on `authRequirement` so
|
|
1035
|
+
// subsequent ops can refuse early with a friendly message.
|
|
1036
|
+
// 2. Proxy capability — the MCP `initialize` response may advertise
|
|
1037
|
+
// `capabilities.registry.proxy`, which auto-populates `proxy`.
|
|
1038
|
+
// Users who set `proxy` or `auth` explicitly on the entry always win:
|
|
1039
|
+
// discovery only fills in blanks.
|
|
816
1040
|
let final: RegistryEntry = entry;
|
|
817
|
-
|
|
1041
|
+
let authRequirement: RegistryAuthRequirement | undefined;
|
|
1042
|
+
|
|
1043
|
+
const hasUsableAuth =
|
|
1044
|
+
entry.auth && entry.auth.type !== "none"
|
|
1045
|
+
? (entry.auth.type === "bearer" && !!entry.auth.token) ||
|
|
1046
|
+
(entry.auth.type === "api-key" && !!entry.auth.key)
|
|
1047
|
+
: false;
|
|
1048
|
+
|
|
1049
|
+
if (!hasUsableAuth) {
|
|
1050
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
1051
|
+
const probe = await probeRegistryAuth(entry.url, fetchFn);
|
|
1052
|
+
if (probe.ok === false) {
|
|
1053
|
+
authRequirement = probe.requirement;
|
|
1054
|
+
final = { ...final, authRequirement };
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (!entry.proxy && !authRequirement) {
|
|
818
1059
|
try {
|
|
819
1060
|
const probeConsumer = await createRegistryConsumer(
|
|
820
1061
|
{ registries: [entry], refs: [] },
|
|
@@ -825,7 +1066,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
825
1066
|
const discovered = await probeConsumer.discover(resolved.url);
|
|
826
1067
|
if (discovered.proxy?.mode) {
|
|
827
1068
|
final = {
|
|
828
|
-
...
|
|
1069
|
+
...final,
|
|
829
1070
|
proxy: {
|
|
830
1071
|
mode: discovered.proxy.mode,
|
|
831
1072
|
...(discovered.proxy.agent && { agent: discovered.proxy.agent }),
|
|
@@ -841,6 +1082,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
841
1082
|
|
|
842
1083
|
registries.push(final);
|
|
843
1084
|
await writeConfig({ ...config, registries });
|
|
1085
|
+
return authRequirement ? { authRequirement } : {};
|
|
844
1086
|
},
|
|
845
1087
|
|
|
846
1088
|
async remove(nameOrUrl: string): Promise<boolean> {
|
|
@@ -891,19 +1133,25 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
891
1133
|
},
|
|
892
1134
|
|
|
893
1135
|
async browse(name: string, query?: string): Promise<AgentListEntry[]> {
|
|
894
|
-
const consumer = await buildConsumer(name);
|
|
895
1136
|
const config = await readConfig();
|
|
896
1137
|
const target = findRegistry(config.registries ?? [], name);
|
|
897
|
-
|
|
898
|
-
return
|
|
1138
|
+
if (target && typeof target !== "string") assertRegistryAuthorized(target);
|
|
1139
|
+
return callWithRefresh(name, async () => {
|
|
1140
|
+
const consumer = await buildConsumer(name);
|
|
1141
|
+
const url = target ? registryUrl(target) : name;
|
|
1142
|
+
return consumer.browse(url, query);
|
|
1143
|
+
});
|
|
899
1144
|
},
|
|
900
1145
|
|
|
901
1146
|
async inspect(name: string): Promise<RegistryConfiguration> {
|
|
902
|
-
const consumer = await buildConsumer(name);
|
|
903
1147
|
const config = await readConfig();
|
|
904
1148
|
const target = findRegistry(config.registries ?? [], name);
|
|
905
|
-
|
|
906
|
-
return
|
|
1149
|
+
if (target && typeof target !== "string") assertRegistryAuthorized(target);
|
|
1150
|
+
return callWithRefresh(name, async () => {
|
|
1151
|
+
const consumer = await buildConsumer(name);
|
|
1152
|
+
const url = target ? registryUrl(target) : name;
|
|
1153
|
+
return consumer.discover(url);
|
|
1154
|
+
});
|
|
907
1155
|
},
|
|
908
1156
|
|
|
909
1157
|
async test(name?: string): Promise<RegistryTestResult[]> {
|
|
@@ -917,12 +1165,29 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
917
1165
|
targets.map(async (r): Promise<RegistryTestResult> => {
|
|
918
1166
|
const url = registryUrl(r);
|
|
919
1167
|
const rName = registryDisplayName(r);
|
|
1168
|
+
if (typeof r !== "string" && r.authRequirement) {
|
|
1169
|
+
const hasUsableAuth =
|
|
1170
|
+
r.auth && r.auth.type !== "none"
|
|
1171
|
+
? (r.auth.type === "bearer" && !!r.auth.token) ||
|
|
1172
|
+
(r.auth.type === "api-key" && !!r.auth.key)
|
|
1173
|
+
: false;
|
|
1174
|
+
if (!hasUsableAuth) {
|
|
1175
|
+
return {
|
|
1176
|
+
name: rName,
|
|
1177
|
+
url,
|
|
1178
|
+
status: "error",
|
|
1179
|
+
error: `auth required — run: adk registry auth ${rName} --token <token>`,
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
920
1183
|
try {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
)
|
|
925
|
-
|
|
1184
|
+
// Route through buildConsumer so encrypted auth/headers get
|
|
1185
|
+
// decrypted, then use callWithRefresh so a 401 triggers the
|
|
1186
|
+
// stored refresh token before giving up.
|
|
1187
|
+
const disc = await callWithRefresh(rName, async () => {
|
|
1188
|
+
const consumer = await buildConsumer(rName);
|
|
1189
|
+
return consumer.discover(url);
|
|
1190
|
+
});
|
|
926
1191
|
return { name: rName, url, status: "active", issuer: disc.issuer };
|
|
927
1192
|
} catch (err: unknown) {
|
|
928
1193
|
const msg = err instanceof Error ? err.message : "unknown";
|
|
@@ -937,6 +1202,302 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
937
1202
|
: { name: "unknown", url: "unknown", status: "error" as const, error: "unknown" },
|
|
938
1203
|
);
|
|
939
1204
|
},
|
|
1205
|
+
|
|
1206
|
+
async auth(
|
|
1207
|
+
nameOrUrl: string,
|
|
1208
|
+
credential:
|
|
1209
|
+
| { token: string; tokenUrl?: string }
|
|
1210
|
+
| { apiKey: string; header?: string },
|
|
1211
|
+
): Promise<boolean> {
|
|
1212
|
+
// Encrypt the secret value up-front so the write path is uniform;
|
|
1213
|
+
// `buildConsumer` decrypts on the read side via `decryptConfigSecrets`.
|
|
1214
|
+
const protectedValue =
|
|
1215
|
+
"token" in credential
|
|
1216
|
+
? await protectSecret(credential.token)
|
|
1217
|
+
: await protectSecret(credential.apiKey);
|
|
1218
|
+
|
|
1219
|
+
const updated = await updateRegistryEntry(nameOrUrl, (existing) => {
|
|
1220
|
+
if ("token" in credential) {
|
|
1221
|
+
existing.auth = {
|
|
1222
|
+
type: "bearer",
|
|
1223
|
+
token: protectedValue,
|
|
1224
|
+
...(credential.tokenUrl && { tokenUrl: credential.tokenUrl }),
|
|
1225
|
+
};
|
|
1226
|
+
} else {
|
|
1227
|
+
existing.auth = {
|
|
1228
|
+
type: "api-key",
|
|
1229
|
+
key: protectedValue,
|
|
1230
|
+
...(credential.header && { header: credential.header }),
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
delete existing.authRequirement;
|
|
1234
|
+
});
|
|
1235
|
+
if (updated) await discoverProxyAfterAuth(nameOrUrl);
|
|
1236
|
+
return updated;
|
|
1237
|
+
},
|
|
1238
|
+
|
|
1239
|
+
async authLocal(
|
|
1240
|
+
nameOrUrl: string,
|
|
1241
|
+
opts?: {
|
|
1242
|
+
onAuthorizeUrl?: (url: string) => void;
|
|
1243
|
+
timeoutMs?: number;
|
|
1244
|
+
force?: boolean;
|
|
1245
|
+
},
|
|
1246
|
+
): Promise<{ complete: boolean }> {
|
|
1247
|
+
const config = await readConfig();
|
|
1248
|
+
const target = findRegistry(config.registries ?? [], nameOrUrl);
|
|
1249
|
+
if (!target || typeof target === "string") {
|
|
1250
|
+
throw new AdkError({
|
|
1251
|
+
code: "registry_not_found",
|
|
1252
|
+
message: `Registry not found: ${nameOrUrl}`,
|
|
1253
|
+
hint: "Run `adk registry list` to see configured registries.",
|
|
1254
|
+
details: { nameOrUrl },
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// When the caller forces re-auth, wipe the existing credentials and
|
|
1259
|
+
// re-probe so we know what scheme the registry wants now. Servers can
|
|
1260
|
+
// rotate auth server metadata between runs.
|
|
1261
|
+
if (opts?.force) {
|
|
1262
|
+
await updateRegistryEntry(nameOrUrl, (existing) => {
|
|
1263
|
+
delete existing.auth;
|
|
1264
|
+
delete existing.oauth;
|
|
1265
|
+
});
|
|
1266
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
1267
|
+
const probe = await probeRegistryAuth(target.url, fetchFn);
|
|
1268
|
+
if (probe.ok === false) {
|
|
1269
|
+
await updateRegistryEntry(nameOrUrl, (existing) => {
|
|
1270
|
+
existing.authRequirement = probe.requirement;
|
|
1271
|
+
});
|
|
1272
|
+
// Re-read so the flow below sees the fresh requirement.
|
|
1273
|
+
const refreshed = await readConfig();
|
|
1274
|
+
const refreshedTarget = findRegistry(refreshed.registries ?? [], nameOrUrl);
|
|
1275
|
+
if (refreshedTarget && typeof refreshedTarget !== "string") {
|
|
1276
|
+
Object.assign(target, refreshedTarget);
|
|
1277
|
+
}
|
|
1278
|
+
} else if (probe.ok === true) {
|
|
1279
|
+
// Registry no longer requires auth — nothing to do.
|
|
1280
|
+
await updateRegistryEntry(nameOrUrl, (existing) => {
|
|
1281
|
+
delete existing.authRequirement;
|
|
1282
|
+
});
|
|
1283
|
+
return { complete: true };
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// Already authenticated — nothing to do (unless forced above).
|
|
1288
|
+
const hasUsableAuth =
|
|
1289
|
+
target.auth && target.auth.type !== "none"
|
|
1290
|
+
? (target.auth.type === "bearer" && !!target.auth.token) ||
|
|
1291
|
+
(target.auth.type === "api-key" && !!target.auth.key)
|
|
1292
|
+
: false;
|
|
1293
|
+
if (hasUsableAuth && !target.authRequirement) {
|
|
1294
|
+
return { complete: true };
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
const req = target.authRequirement;
|
|
1298
|
+
const port = options.oauthCallbackPort ?? 8919;
|
|
1299
|
+
const timeout = opts?.timeoutMs ?? 300_000;
|
|
1300
|
+
const displayName = target.name ?? target.url;
|
|
1301
|
+
const { createServer } = await import("node:http");
|
|
1302
|
+
|
|
1303
|
+
// OAuth path — the registry advertised authorization servers via
|
|
1304
|
+
// RFC 9728 protected-resource metadata. Walk the full flow:
|
|
1305
|
+
// AS metadata → dynamic client registration → PKCE authorize →
|
|
1306
|
+
// local callback → token exchange → persist access token.
|
|
1307
|
+
if (req?.authorizationServers?.length) {
|
|
1308
|
+
const authServer = req.authorizationServers[0]!;
|
|
1309
|
+
const metadata =
|
|
1310
|
+
(await discoverOAuthMetadata(authServer)) ??
|
|
1311
|
+
(await tryFetchOAuthMetadata(authServer));
|
|
1312
|
+
if (!metadata) {
|
|
1313
|
+
throw new AdkError({
|
|
1314
|
+
code: "registry_oauth_discovery_failed",
|
|
1315
|
+
message: `Could not discover OAuth metadata at ${authServer}.`,
|
|
1316
|
+
hint: "The authorization server must expose /.well-known/oauth-authorization-server.",
|
|
1317
|
+
details: { authServer, registry: displayName },
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
if (!metadata.registration_endpoint) {
|
|
1321
|
+
throw new AdkError({
|
|
1322
|
+
code: "registry_oauth_no_registration",
|
|
1323
|
+
message: `Authorization server ${authServer} does not support dynamic client registration.`,
|
|
1324
|
+
hint: `Obtain a bearer token manually, then run: adk registry auth ${displayName} --token <token>`,
|
|
1325
|
+
details: { authServer, registry: displayName },
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
const redirectUri = `http://localhost:${port}/callback`;
|
|
1330
|
+
const registration = await dynamicClientRegistration(
|
|
1331
|
+
metadata.registration_endpoint,
|
|
1332
|
+
{
|
|
1333
|
+
clientName: options.oauthClientName ?? "adk",
|
|
1334
|
+
redirectUris: [redirectUri],
|
|
1335
|
+
grantTypes: ["authorization_code"],
|
|
1336
|
+
},
|
|
1337
|
+
);
|
|
1338
|
+
|
|
1339
|
+
const state = crypto.randomUUID();
|
|
1340
|
+
const { url: authorizeUrl, codeVerifier } = await buildOAuthAuthorizeUrl({
|
|
1341
|
+
authorizationEndpoint: metadata.authorization_endpoint,
|
|
1342
|
+
clientId: registration.clientId,
|
|
1343
|
+
redirectUri,
|
|
1344
|
+
scopes: req.scopes,
|
|
1345
|
+
state,
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
return new Promise<{ complete: boolean }>((resolve, reject) => {
|
|
1349
|
+
const server = createServer(async (reqIn, resOut) => {
|
|
1350
|
+
const reqUrl = new URL(reqIn.url ?? "/", `http://localhost:${port}`);
|
|
1351
|
+
if (reqUrl.pathname !== "/callback") {
|
|
1352
|
+
resOut.writeHead(404);
|
|
1353
|
+
resOut.end();
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
const code = reqUrl.searchParams.get("code");
|
|
1358
|
+
const returnedState = reqUrl.searchParams.get("state");
|
|
1359
|
+
if (!code || returnedState !== state) {
|
|
1360
|
+
const error = reqUrl.searchParams.get("error") ?? "missing code/state";
|
|
1361
|
+
resOut.writeHead(400, { "Content-Type": "text/html" });
|
|
1362
|
+
resOut.end(`<h1>Error</h1><p>${esc(error)}</p>`);
|
|
1363
|
+
server.close();
|
|
1364
|
+
reject(
|
|
1365
|
+
new AdkError({
|
|
1366
|
+
code: "registry_oauth_denied",
|
|
1367
|
+
message: `OAuth callback rejected: ${error}`,
|
|
1368
|
+
hint: "Retry `adk registry auth` and complete the browser consent.",
|
|
1369
|
+
details: { registry: displayName, error },
|
|
1370
|
+
}),
|
|
1371
|
+
);
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
try {
|
|
1376
|
+
const tokens = await exchangeCodeForTokens(metadata.token_endpoint, {
|
|
1377
|
+
code,
|
|
1378
|
+
codeVerifier,
|
|
1379
|
+
clientId: registration.clientId,
|
|
1380
|
+
clientSecret: registration.clientSecret,
|
|
1381
|
+
redirectUri,
|
|
1382
|
+
});
|
|
1383
|
+
const expiresAt = tokens.expiresIn
|
|
1384
|
+
? new Date(Date.now() + tokens.expiresIn * 1000).toISOString()
|
|
1385
|
+
: undefined;
|
|
1386
|
+
const encToken = await protectSecret(tokens.accessToken);
|
|
1387
|
+
const encRefresh = tokens.refreshToken
|
|
1388
|
+
? await protectSecret(tokens.refreshToken)
|
|
1389
|
+
: undefined;
|
|
1390
|
+
const encClientSecret = registration.clientSecret
|
|
1391
|
+
? await protectSecret(registration.clientSecret)
|
|
1392
|
+
: undefined;
|
|
1393
|
+
await updateRegistryEntry(displayName, (existing) => {
|
|
1394
|
+
existing.auth = { type: "bearer", token: encToken };
|
|
1395
|
+
existing.oauth = {
|
|
1396
|
+
tokenEndpoint: metadata.token_endpoint,
|
|
1397
|
+
clientId: registration.clientId,
|
|
1398
|
+
...(encClientSecret && { clientSecret: encClientSecret }),
|
|
1399
|
+
...(encRefresh && { refreshToken: encRefresh }),
|
|
1400
|
+
...(expiresAt && { expiresAt }),
|
|
1401
|
+
...(req.scopes?.length && { scopes: req.scopes }),
|
|
1402
|
+
};
|
|
1403
|
+
delete existing.authRequirement;
|
|
1404
|
+
});
|
|
1405
|
+
await discoverProxyAfterAuth(displayName);
|
|
1406
|
+
resOut.writeHead(200, { "Content-Type": "text/html" });
|
|
1407
|
+
resOut.end(renderAuthSuccess(displayName));
|
|
1408
|
+
server.close();
|
|
1409
|
+
resolve({ complete: true });
|
|
1410
|
+
} catch (err) {
|
|
1411
|
+
resOut.writeHead(500, { "Content-Type": "text/html" });
|
|
1412
|
+
resOut.end(
|
|
1413
|
+
`<h1>Error</h1><p>${esc(err instanceof Error ? err.message : String(err))}</p>`,
|
|
1414
|
+
);
|
|
1415
|
+
server.close();
|
|
1416
|
+
reject(err);
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
server.listen(port, () => {
|
|
1421
|
+
opts?.onAuthorizeUrl?.(authorizeUrl);
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
const timer = setTimeout(() => {
|
|
1425
|
+
server.close();
|
|
1426
|
+
reject(new Error("OAuth callback timed out"));
|
|
1427
|
+
}, timeout);
|
|
1428
|
+
server.on("close", () => clearTimeout(timer));
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// No OAuth metadata — serve a local HTTPS form asking for a token.
|
|
1433
|
+
// Used when the registry returned 401 without pointing at an AS, or
|
|
1434
|
+
// when the caller simply wants to paste a pre-issued token.
|
|
1435
|
+
const fields: AuthChallengeField[] = [
|
|
1436
|
+
{
|
|
1437
|
+
name: "token",
|
|
1438
|
+
label: "Bearer token",
|
|
1439
|
+
description: req?.realm
|
|
1440
|
+
? `Token for realm "${req.realm}"`
|
|
1441
|
+
: "Token sent as `Authorization: Bearer <token>`.",
|
|
1442
|
+
secret: true,
|
|
1443
|
+
},
|
|
1444
|
+
];
|
|
1445
|
+
|
|
1446
|
+
return new Promise<{ complete: boolean }>((resolve, reject) => {
|
|
1447
|
+
const server = createServer(async (reqIn, resOut) => {
|
|
1448
|
+
const reqUrl = new URL(reqIn.url ?? "/", `http://localhost:${port}`);
|
|
1449
|
+
|
|
1450
|
+
if (reqIn.method === "GET" && reqUrl.pathname === "/auth") {
|
|
1451
|
+
resOut.writeHead(200, { "Content-Type": "text/html" });
|
|
1452
|
+
resOut.end(renderCredentialForm(displayName, fields));
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
if (reqIn.method === "POST" && reqUrl.pathname === "/auth") {
|
|
1457
|
+
const chunks: Buffer[] = [];
|
|
1458
|
+
for await (const chunk of reqIn) chunks.push(chunk as Buffer);
|
|
1459
|
+
const body = Buffer.concat(chunks).toString();
|
|
1460
|
+
const params = new URLSearchParams(body);
|
|
1461
|
+
const token = params.get("token");
|
|
1462
|
+
if (!token) {
|
|
1463
|
+
resOut.writeHead(200, { "Content-Type": "text/html" });
|
|
1464
|
+
resOut.end(renderCredentialForm(displayName, fields, "Token is required."));
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
try {
|
|
1468
|
+
await registry.auth(displayName, { token });
|
|
1469
|
+
resOut.writeHead(200, { "Content-Type": "text/html" });
|
|
1470
|
+
resOut.end(renderAuthSuccess(displayName));
|
|
1471
|
+
server.close();
|
|
1472
|
+
resolve({ complete: true });
|
|
1473
|
+
} catch (err) {
|
|
1474
|
+
resOut.writeHead(500, { "Content-Type": "text/html" });
|
|
1475
|
+
resOut.end(
|
|
1476
|
+
renderCredentialForm(
|
|
1477
|
+
displayName,
|
|
1478
|
+
fields,
|
|
1479
|
+
err instanceof Error ? err.message : String(err),
|
|
1480
|
+
),
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
resOut.writeHead(404);
|
|
1487
|
+
resOut.end();
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
server.listen(port, () => {
|
|
1491
|
+
opts?.onAuthorizeUrl?.(`http://localhost:${port}/auth`);
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
const timer = setTimeout(() => {
|
|
1495
|
+
server.close();
|
|
1496
|
+
reject(new Error("Auth timed out"));
|
|
1497
|
+
}, timeout);
|
|
1498
|
+
server.on("close", () => clearTimeout(timer));
|
|
1499
|
+
});
|
|
1500
|
+
},
|
|
940
1501
|
};
|
|
941
1502
|
|
|
942
1503
|
// ==========================================
|
|
@@ -944,11 +1505,22 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
944
1505
|
// ==========================================
|
|
945
1506
|
|
|
946
1507
|
const ref: AdkRefApi = {
|
|
947
|
-
async add(
|
|
1508
|
+
async add(entryInput: RefAddInput): Promise<{ security: SecuritySchemeSummary | null }> {
|
|
948
1509
|
let security: SecuritySchemeSummary | null = null;
|
|
949
1510
|
|
|
950
1511
|
const config = await readConfig();
|
|
951
1512
|
const hasRegistries = (config.registries ?? []).length > 0;
|
|
1513
|
+
const name = entryInput.name ?? entryInput.ref;
|
|
1514
|
+
let entry: RefEntry = { ...entryInput, name };
|
|
1515
|
+
|
|
1516
|
+
if ((config.refs ?? []).some((r) => refNameMatches(r, name))) {
|
|
1517
|
+
throw new AdkError({
|
|
1518
|
+
code: "REF_INVALID",
|
|
1519
|
+
message: `Cannot add ref "${entry.ref}" as "${name}": a ref with that name already exists`,
|
|
1520
|
+
hint: "Choose a different name, or remove/update the existing ref first.",
|
|
1521
|
+
details: { ref: entry.ref, name },
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
952
1524
|
|
|
953
1525
|
// Auto-infer scheme from context
|
|
954
1526
|
if (!entry.scheme) {
|
|
@@ -1045,9 +1617,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1045
1617
|
}
|
|
1046
1618
|
}
|
|
1047
1619
|
|
|
1048
|
-
const
|
|
1049
|
-
const refs = (config.refs ?? []).filter((r) => refName(r) !== name);
|
|
1050
|
-
refs.push(entry);
|
|
1620
|
+
const refs = [...(config.refs ?? []), entry];
|
|
1051
1621
|
await writeConfig({ ...config, refs });
|
|
1052
1622
|
|
|
1053
1623
|
return { security };
|
|
@@ -1068,7 +1638,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1068
1638
|
return (config.refs ?? []).map(normalizeRef);
|
|
1069
1639
|
},
|
|
1070
1640
|
|
|
1071
|
-
async get(name: string): Promise<
|
|
1641
|
+
async get(name: string): Promise<ResolvedRef | null> {
|
|
1072
1642
|
const config = await readConfig();
|
|
1073
1643
|
return findRef(config.refs ?? [], name) ?? null;
|
|
1074
1644
|
},
|
|
@@ -1082,14 +1652,21 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1082
1652
|
found = true;
|
|
1083
1653
|
const updated = { ...r };
|
|
1084
1654
|
if (updates.url) updated.url = updates.url;
|
|
1085
|
-
// Rename: prefer `name`, fall back to legacy `as`. When the
|
|
1086
|
-
// caller passes `name`, clear the legacy `as` so the stored
|
|
1087
|
-
// entry has one source of truth.
|
|
1088
1655
|
if (updates.name !== undefined) {
|
|
1656
|
+
const duplicate = config.refs?.some(
|
|
1657
|
+
(candidate) =>
|
|
1658
|
+
!refNameMatches(candidate, name) &&
|
|
1659
|
+
refNameMatches(candidate, updates.name as string),
|
|
1660
|
+
);
|
|
1661
|
+
if (duplicate) {
|
|
1662
|
+
throw new AdkError({
|
|
1663
|
+
code: "REF_INVALID",
|
|
1664
|
+
message: `Cannot rename ref "${name}" to "${updates.name}": a ref with that name already exists`,
|
|
1665
|
+
hint: "Choose a different name, or remove/update the existing ref first.",
|
|
1666
|
+
details: { name, newName: updates.name },
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1089
1669
|
updated.name = updates.name;
|
|
1090
|
-
if (updated.as !== undefined) updated.as = undefined;
|
|
1091
|
-
} else if (updates.as !== undefined) {
|
|
1092
|
-
updated.as = updates.as;
|
|
1093
1670
|
}
|
|
1094
1671
|
if (updates.scheme) updated.scheme = updates.scheme;
|
|
1095
1672
|
if (updates.config) updated.config = { ...updated.config, ...updates.config };
|
|
@@ -1566,7 +2143,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1566
2143
|
// so callers can include extra context (tenant/user IDs).
|
|
1567
2144
|
const statePayload = {
|
|
1568
2145
|
...opts?.stateContext,
|
|
1569
|
-
ref:
|
|
2146
|
+
ref: entry.ref,
|
|
2147
|
+
name,
|
|
1570
2148
|
ts: Date.now(),
|
|
1571
2149
|
};
|
|
1572
2150
|
const state = btoa(JSON.stringify(statePayload));
|