@slashfi/agents-sdk 0.81.0 → 0.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adk.js +3 -28
- package/dist/adk.js.map +1 -1
- package/dist/cjs/config-store.js +6 -205
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/cjs/define-config.js.map +1 -1
- package/dist/cjs/materialize.js +34 -13
- package/dist/cjs/materialize.js.map +1 -1
- package/dist/cjs/registry-consumer.js +0 -8
- package/dist/cjs/registry-consumer.js.map +1 -1
- package/dist/cjs/server.js +0 -8
- package/dist/cjs/server.js.map +1 -1
- package/dist/config-store.d.ts +0 -14
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +6 -205
- package/dist/config-store.js.map +1 -1
- package/dist/define-config.d.ts +0 -29
- package/dist/define-config.d.ts.map +1 -1
- package/dist/define-config.js.map +1 -1
- package/dist/materialize.d.ts.map +1 -1
- package/dist/materialize.js +34 -13
- package/dist/materialize.js.map +1 -1
- package/dist/registry-consumer.d.ts +0 -10
- package/dist/registry-consumer.d.ts.map +1 -1
- package/dist/registry-consumer.js +0 -8
- package/dist/registry-consumer.js.map +1 -1
- package/dist/server.d.ts +0 -11
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +0 -8
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/src/adk.ts +3 -31
- package/src/config-store.test.ts +0 -185
- package/src/config-store.ts +6 -254
- package/src/define-config.ts +0 -31
- package/src/materialize.ts +37 -13
- package/src/registry-consumer.ts +0 -27
- package/src/server.ts +0 -19
package/src/config-store.test.ts
CHANGED
|
@@ -488,175 +488,6 @@ describe("ADK ref.call() full auto-refresh flow", () => {
|
|
|
488
488
|
});
|
|
489
489
|
});
|
|
490
490
|
|
|
491
|
-
// ─── ADK Config Store: registry proxy routing ───────────────────
|
|
492
|
-
|
|
493
|
-
describe("ADK registry proxy routing", () => {
|
|
494
|
-
let proxyServer: AgentServer;
|
|
495
|
-
let proxyServerAdk: ReturnType<typeof createAdk>;
|
|
496
|
-
const PROXY_PORT = 19930;
|
|
497
|
-
|
|
498
|
-
beforeAll(async () => {
|
|
499
|
-
// Real server-side adk that the proxy agent operates on. When the
|
|
500
|
-
// local adk forwards ref ops, they land on this backing store via
|
|
501
|
-
// the real @config `ref` tool produced by createAdkTools — no mocks.
|
|
502
|
-
const proxyServerFs = createMemoryFs();
|
|
503
|
-
proxyServerAdk = createAdk(proxyServerFs);
|
|
504
|
-
await proxyServerAdk.writeConfig({
|
|
505
|
-
refs: [
|
|
506
|
-
{
|
|
507
|
-
ref: "@gmail",
|
|
508
|
-
scheme: "mcp",
|
|
509
|
-
url: "https://gmail.example.com/mcp",
|
|
510
|
-
},
|
|
511
|
-
],
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
// Expose that adk via the same tool surface production uses.
|
|
515
|
-
const adkTools = createAdkTools({ resolveScope: () => proxyServerAdk });
|
|
516
|
-
const configAgent = defineAgent({
|
|
517
|
-
path: "@config",
|
|
518
|
-
entrypoint: "@config agent for proxy routing tests",
|
|
519
|
-
tools: adkTools,
|
|
520
|
-
visibility: "public",
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
const proxyRegistry = createAgentRegistry();
|
|
524
|
-
proxyRegistry.register(configAgent);
|
|
525
|
-
proxyServer = createAgentServer(proxyRegistry, {
|
|
526
|
-
port: PROXY_PORT,
|
|
527
|
-
// Advertise proxy mode in the MCP initialize response so the
|
|
528
|
-
// "registry.add auto-detects proxy" test can verify discovery.
|
|
529
|
-
registry: { version: "1.0", proxy: { mode: "required" } },
|
|
530
|
-
});
|
|
531
|
-
await proxyServer.start();
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
afterAll(async () => {
|
|
535
|
-
await proxyServer.stop();
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* Seed the local consumer config with a ref sourced from the proxy
|
|
540
|
-
* registry. We bypass ref.add's reachability check because we're
|
|
541
|
-
* specifically testing how proxying routes around local state.
|
|
542
|
-
*/
|
|
543
|
-
async function seedLocalRefFromProxy(fs: FsStore, refName: string) {
|
|
544
|
-
const raw = (await fs.readFile("consumer-config.json")) ?? "{}";
|
|
545
|
-
const config = JSON.parse(raw);
|
|
546
|
-
config.refs = [
|
|
547
|
-
{
|
|
548
|
-
ref: refName,
|
|
549
|
-
scheme: "registry",
|
|
550
|
-
sourceRegistry: {
|
|
551
|
-
url: `http://localhost:${PROXY_PORT}`,
|
|
552
|
-
agentPath: refName,
|
|
553
|
-
},
|
|
554
|
-
},
|
|
555
|
-
];
|
|
556
|
-
await fs.writeFile("consumer-config.json", JSON.stringify(config));
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
test("ref.authStatus forwards to the real @config on the proxy registry", async () => {
|
|
560
|
-
const fs = createMemoryFs();
|
|
561
|
-
const adk = createAdk(fs);
|
|
562
|
-
|
|
563
|
-
await adk.registry.add({
|
|
564
|
-
url: `http://localhost:${PROXY_PORT}`,
|
|
565
|
-
name: "cloud",
|
|
566
|
-
proxy: { mode: "required" },
|
|
567
|
-
});
|
|
568
|
-
await seedLocalRefFromProxy(fs, "@gmail");
|
|
569
|
-
|
|
570
|
-
// The proxy-side adk owns the @gmail ref (no security declared in
|
|
571
|
-
// its config above) so authStatus should report { complete: true }
|
|
572
|
-
// with security: null.
|
|
573
|
-
const status = await adk.ref.authStatus("@gmail");
|
|
574
|
-
expect(status).toBeDefined();
|
|
575
|
-
// The remote @config returned something — local adk never saw the ref,
|
|
576
|
-
// so this would throw "Ref not found" if proxying wasn't wired.
|
|
577
|
-
expect((status as { name?: string }).name ?? "@gmail").toBe("@gmail");
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
test("ref.auth forwards and returns the remote auth start result", async () => {
|
|
581
|
-
const fs = createMemoryFs();
|
|
582
|
-
const adk = createAdk(fs);
|
|
583
|
-
|
|
584
|
-
await adk.registry.add({
|
|
585
|
-
url: `http://localhost:${PROXY_PORT}`,
|
|
586
|
-
name: "cloud",
|
|
587
|
-
proxy: { mode: "required" },
|
|
588
|
-
});
|
|
589
|
-
await seedLocalRefFromProxy(fs, "@gmail");
|
|
590
|
-
|
|
591
|
-
// @gmail on the proxy side has no security schema, so the real
|
|
592
|
-
// @config.ref tool returns { type: 'none', complete: true }. The
|
|
593
|
-
// assertion here is that we got *something* back from the remote,
|
|
594
|
-
// proving the call made a round trip instead of throwing locally.
|
|
595
|
-
const result = await adk.ref.auth("@gmail");
|
|
596
|
-
expect(result).toBeDefined();
|
|
597
|
-
expect((result as { type?: string }).type).toBeDefined();
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
test("registry.add auto-detects proxy from the server's handshake", async () => {
|
|
601
|
-
const fs = createMemoryFs();
|
|
602
|
-
const adk = createAdk(fs);
|
|
603
|
-
|
|
604
|
-
// Caller passes NO proxy config. The server advertises
|
|
605
|
-
// `capabilities.registry.proxy: { mode: 'required' }` in its MCP
|
|
606
|
-
// initialize response (see beforeAll), so registry.add should
|
|
607
|
-
// auto-populate the RegistryEntry at probe time.
|
|
608
|
-
await adk.registry.add({
|
|
609
|
-
url: `http://localhost:${PROXY_PORT}`,
|
|
610
|
-
name: "cloud-autodetect",
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
const list = await adk.registry.list();
|
|
614
|
-
const entry = list.find((r) => r.name === "cloud-autodetect");
|
|
615
|
-
expect(entry?.proxy?.mode).toBe("required");
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
test("explicit proxy on registry.add is not overwritten by auto-detection", async () => {
|
|
619
|
-
const fs = createMemoryFs();
|
|
620
|
-
const adk = createAdk(fs);
|
|
621
|
-
|
|
622
|
-
// Server advertises required; caller explicitly sets optional. The
|
|
623
|
-
// caller's choice wins — discovery only fills in blanks.
|
|
624
|
-
await adk.registry.add({
|
|
625
|
-
url: `http://localhost:${PROXY_PORT}`,
|
|
626
|
-
name: "cloud-explicit",
|
|
627
|
-
proxy: { mode: "optional", agent: "@custom" },
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
const entry = (await adk.registry.list()).find(
|
|
631
|
-
(r) => r.name === "cloud-explicit",
|
|
632
|
-
);
|
|
633
|
-
expect(entry?.proxy?.mode).toBe("optional");
|
|
634
|
-
expect(entry?.proxy?.agent).toBe("@custom");
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
test("optional proxy honors preferLocal and skips forwarding", async () => {
|
|
638
|
-
const fs = createMemoryFs();
|
|
639
|
-
const adk = createAdk(fs);
|
|
640
|
-
|
|
641
|
-
await adk.registry.add({
|
|
642
|
-
url: `http://localhost:${PROXY_PORT}`,
|
|
643
|
-
name: "cloud",
|
|
644
|
-
proxy: { mode: "optional" },
|
|
645
|
-
});
|
|
646
|
-
await seedLocalRefFromProxy(fs, "@gmail");
|
|
647
|
-
|
|
648
|
-
// With preferLocal:true we should fall through to the local path.
|
|
649
|
-
// The local adk has no usable config for @gmail, so this path
|
|
650
|
-
// throws or returns an empty status — either way, we prove we
|
|
651
|
-
// stayed local by catching and confirming no exception bubbled
|
|
652
|
-
// with "authorizeUrl" (which only the proxy path returns).
|
|
653
|
-
const result = await adk.ref
|
|
654
|
-
.auth("@gmail", { preferLocal: true })
|
|
655
|
-
.catch((err: Error) => ({ _error: err.message }));
|
|
656
|
-
expect((result as { authorizeUrl?: string }).authorizeUrl).toBeUndefined();
|
|
657
|
-
});
|
|
658
|
-
});
|
|
659
|
-
|
|
660
491
|
// ─── Registry auth lifecycle ─────────────────────────────────────
|
|
661
492
|
|
|
662
493
|
describe("ADK registry auth lifecycle", () => {
|
|
@@ -1190,22 +1021,6 @@ describe("isRefAuthComplete + cached authFields", () => {
|
|
|
1190
1021
|
expect(result).toBeNull();
|
|
1191
1022
|
});
|
|
1192
1023
|
|
|
1193
|
-
test("proxy mode short-circuits to true regardless of cache", async () => {
|
|
1194
|
-
const { isRefAuthComplete } = await import("./config-store");
|
|
1195
|
-
const result = isRefAuthComplete(
|
|
1196
|
-
{
|
|
1197
|
-
ref: "slash",
|
|
1198
|
-
name: "slash",
|
|
1199
|
-
scheme: "registry",
|
|
1200
|
-
// proxy mode set by ref.add when registry inspection includes it.
|
|
1201
|
-
// biome-ignore lint/suspicious/noExplicitAny: mode isn't on the public type
|
|
1202
|
-
mode: "proxy",
|
|
1203
|
-
} as any,
|
|
1204
|
-
undefined,
|
|
1205
|
-
);
|
|
1206
|
-
expect(result).toBe(true);
|
|
1207
|
-
});
|
|
1208
|
-
|
|
1209
1024
|
test("required field present → true", async () => {
|
|
1210
1025
|
const { isRefAuthComplete } = await import("./config-store");
|
|
1211
1026
|
const result = isRefAuthComplete(
|
package/src/config-store.ts
CHANGED
|
@@ -103,12 +103,6 @@ export interface RegistryCacheEntry {
|
|
|
103
103
|
* entry's `config` — no network round-trip needed. Absent when the
|
|
104
104
|
* scheme couldn't be fetched (e.g. registry was offline at add
|
|
105
105
|
* time); fall back to whatever heuristic the caller chooses.
|
|
106
|
-
*
|
|
107
|
-
* Note on proxy refs: when the entry is in `proxy` mode the
|
|
108
|
-
* security scheme is exposed by the *proxy*, and the answer to
|
|
109
|
-
* "is this callable?" lives server-side — `authFields` is omitted
|
|
110
|
-
* locally and hosts should treat proxy refs as authoritative
|
|
111
|
-
* regardless of entry-side fields.
|
|
112
106
|
*/
|
|
113
107
|
authFields?: Record<string, RegistryCacheAuthField>;
|
|
114
108
|
/** ISO timestamp of the most recent registry round-trip that wrote this. */
|
|
@@ -151,8 +145,6 @@ export interface RefAuthCompleteOptions {
|
|
|
151
145
|
* we evaluate satisfaction against the entry's current `config`.
|
|
152
146
|
*
|
|
153
147
|
* Behavior:
|
|
154
|
-
* - `mode: 'proxy'` refs → always true. Auth lives server-side; the
|
|
155
|
-
* proxy is the source of truth, no entry-side fields involved.
|
|
156
148
|
* - Cache miss (no `authFields` for this ref yet) → returns `null`,
|
|
157
149
|
* signaling "I don't know — caller should fall back to its own
|
|
158
150
|
* heuristic or call `auth-status` to populate the cache".
|
|
@@ -173,7 +165,6 @@ export function isRefAuthComplete(
|
|
|
173
165
|
opts?: RefAuthCompleteOptions,
|
|
174
166
|
): boolean | null {
|
|
175
167
|
if (typeof entry === "string") return false;
|
|
176
|
-
if ((entry as { mode?: unknown }).mode === "proxy") return true;
|
|
177
168
|
const authFields = cacheEntry?.authFields;
|
|
178
169
|
if (!authFields) return null;
|
|
179
170
|
const config = entry.config ?? {};
|
|
@@ -432,12 +423,6 @@ export interface AdkRefApi {
|
|
|
432
423
|
stateContext?: Record<string, unknown>;
|
|
433
424
|
/** Additional scopes to request (e.g., optional scopes declared by the agent) */
|
|
434
425
|
scopes?: string[];
|
|
435
|
-
/**
|
|
436
|
-
* Opt out of proxy routing when the ref's source registry has
|
|
437
|
-
* `proxy: { mode: 'optional' }`. Ignored for `mode: 'required'`.
|
|
438
|
-
* Defaults to `false` — if a registry offers a proxy we use it.
|
|
439
|
-
*/
|
|
440
|
-
preferLocal?: boolean;
|
|
441
426
|
},
|
|
442
427
|
): Promise<AuthStartResult>;
|
|
443
428
|
/**
|
|
@@ -1188,96 +1173,6 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1188
1173
|
return fallback;
|
|
1189
1174
|
}
|
|
1190
1175
|
|
|
1191
|
-
// ==========================================
|
|
1192
|
-
// Proxy Routing
|
|
1193
|
-
// ==========================================
|
|
1194
|
-
|
|
1195
|
-
/**
|
|
1196
|
-
* Find the configured RegistryEntry for a ref, consulting `sourceRegistry`
|
|
1197
|
-
* first and falling back to the first registry in config. Returns `null` when
|
|
1198
|
-
* the ref is sourced from a raw URL (no registry), in which case proxy routing
|
|
1199
|
-
* does not apply.
|
|
1200
|
-
*/
|
|
1201
|
-
async function findRegistryEntryForRef(
|
|
1202
|
-
entry: RefEntry,
|
|
1203
|
-
): Promise<RegistryEntry | null> {
|
|
1204
|
-
const sourceUrl = entry.sourceRegistry?.url;
|
|
1205
|
-
if (!sourceUrl) return null;
|
|
1206
|
-
const config = await readConfig();
|
|
1207
|
-
const match = (config.registries ?? []).find((r) => {
|
|
1208
|
-
if (typeof r === "string") return r === sourceUrl;
|
|
1209
|
-
return r.url === sourceUrl;
|
|
1210
|
-
});
|
|
1211
|
-
if (!match || typeof match === "string") return null;
|
|
1212
|
-
return match;
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
/**
|
|
1216
|
-
* Returns the proxy settings for a ref when its source registry has
|
|
1217
|
-
* `proxy` configured. `null` means "run locally".
|
|
1218
|
-
*
|
|
1219
|
-
* Callers pass `{ preferLocal: true }` to opt out of `mode: 'optional'`
|
|
1220
|
-
* proxying when they already hold credentials locally. `mode: 'required'`
|
|
1221
|
-
* cannot be bypassed — the registry owns auth server-side and there is
|
|
1222
|
-
* nothing useful the local SDK can do.
|
|
1223
|
-
*/
|
|
1224
|
-
async function resolveProxyForRef(
|
|
1225
|
-
entry: RefEntry,
|
|
1226
|
-
opts?: { preferLocal?: boolean },
|
|
1227
|
-
): Promise<{ reg: RegistryEntry; agent: string } | null> {
|
|
1228
|
-
const reg = await findRegistryEntryForRef(entry);
|
|
1229
|
-
if (!reg?.proxy) return null;
|
|
1230
|
-
if (reg.proxy.mode === "optional" && opts?.preferLocal) return null;
|
|
1231
|
-
return { reg, agent: reg.proxy.agent ?? "@config" };
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
/**
|
|
1235
|
-
* Forward an `@config ref` operation to the proxy agent on a remote registry.
|
|
1236
|
-
*
|
|
1237
|
-
* The remote side speaks the standard adk-tools surface, so the call shape is
|
|
1238
|
-
* identical to what the local `ref` API would do — the only difference is
|
|
1239
|
-
* that tokens and secrets live server-side. `callRegistry` returns the
|
|
1240
|
-
* standard CallAgentResponse envelope: `{ success: true, result }` on
|
|
1241
|
-
* success or `{ success: false, error }` on failure. We unwrap once and
|
|
1242
|
-
* throw on error so callers get a result that matches the local signature.
|
|
1243
|
-
*/
|
|
1244
|
-
async function forwardRefOpToProxy<T>(
|
|
1245
|
-
reg: RegistryEntry,
|
|
1246
|
-
agent: string,
|
|
1247
|
-
operation: string,
|
|
1248
|
-
params: Record<string, unknown>,
|
|
1249
|
-
): Promise<T> {
|
|
1250
|
-
const consumer = await buildConsumerForRef({
|
|
1251
|
-
ref: "",
|
|
1252
|
-
name: "",
|
|
1253
|
-
sourceRegistry: { url: reg.url, agentPath: agent },
|
|
1254
|
-
});
|
|
1255
|
-
const resolved = consumer.registries().find((r) => r.url === reg.url);
|
|
1256
|
-
if (!resolved)
|
|
1257
|
-
throw new Error(
|
|
1258
|
-
`Registry ${reg.url} not resolvable for proxy forwarding`,
|
|
1259
|
-
);
|
|
1260
|
-
|
|
1261
|
-
const response = await consumer.callRegistry(resolved, {
|
|
1262
|
-
action: "execute_tool",
|
|
1263
|
-
path: agent,
|
|
1264
|
-
tool: "ref",
|
|
1265
|
-
params: { operation, ...params },
|
|
1266
|
-
});
|
|
1267
|
-
|
|
1268
|
-
if (!response.success) {
|
|
1269
|
-
const errResponse = response as {
|
|
1270
|
-
success: false;
|
|
1271
|
-
error?: string;
|
|
1272
|
-
code?: string;
|
|
1273
|
-
};
|
|
1274
|
-
const msg =
|
|
1275
|
-
errResponse.error ?? `Proxy ${agent}.ref(${operation}) failed`;
|
|
1276
|
-
throw new Error(msg);
|
|
1277
|
-
}
|
|
1278
|
-
return (response as { success: true; result: unknown }).result as T;
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
1176
|
// ==========================================
|
|
1282
1177
|
// Registry API
|
|
1283
1178
|
// ==========================================
|
|
@@ -1292,39 +1187,6 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1292
1187
|
return `${SECRET_PREFIX}${await encryptSecret(value, options.encryptionKey)}`;
|
|
1293
1188
|
}
|
|
1294
1189
|
|
|
1295
|
-
/**
|
|
1296
|
-
* Re-probe a registry with the current stored credentials to see whether it
|
|
1297
|
-
* advertises `capabilities.registry.proxy` in its MCP `initialize` response,
|
|
1298
|
-
* and persist the proxy config when it does. Safe to call after a successful
|
|
1299
|
-
* `auth()` / `authLocal()` — on the add path we skip the proxy probe when
|
|
1300
|
-
* auth is required, so this is the second chance to back-fill it.
|
|
1301
|
-
*
|
|
1302
|
-
* Respects explicit user config: if `proxy` is already set, we leave it
|
|
1303
|
-
* alone. Any discovery failure is swallowed — proxy is an optimization,
|
|
1304
|
-
* not a correctness requirement.
|
|
1305
|
-
*/
|
|
1306
|
-
async function discoverProxyAfterAuth(nameOrUrl: string): Promise<void> {
|
|
1307
|
-
const config = await readConfig();
|
|
1308
|
-
const target = findRegistry(config.registries ?? [], nameOrUrl);
|
|
1309
|
-
if (!target || typeof target === "string") return;
|
|
1310
|
-
if (target.proxy) return;
|
|
1311
|
-
|
|
1312
|
-
try {
|
|
1313
|
-
const consumer = await buildConsumer(nameOrUrl);
|
|
1314
|
-
const discovered = await consumer.discover(target.url);
|
|
1315
|
-
if (!discovered.proxy?.mode) return;
|
|
1316
|
-
await updateRegistryEntry(nameOrUrl, (existing) => {
|
|
1317
|
-
if (existing.proxy) return;
|
|
1318
|
-
existing.proxy = {
|
|
1319
|
-
mode: discovered.proxy!.mode,
|
|
1320
|
-
...(discovered.proxy!.agent && { agent: discovered.proxy!.agent }),
|
|
1321
|
-
};
|
|
1322
|
-
});
|
|
1323
|
-
} catch {
|
|
1324
|
-
// Proxy probe is best-effort — auth itself already succeeded.
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
1190
|
/**
|
|
1329
1191
|
* Atomic read-modify-write on a registry entry by name or URL. Used by
|
|
1330
1192
|
* `authLocal` to persist both `auth` and `oauth` together, which `auth()`
|
|
@@ -1476,14 +1338,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1476
1338
|
(r) => registryDisplayName(r) !== alias,
|
|
1477
1339
|
);
|
|
1478
1340
|
|
|
1479
|
-
// Probe the registry before saving.
|
|
1480
|
-
//
|
|
1481
|
-
//
|
|
1482
|
-
//
|
|
1483
|
-
// 2. Proxy capability — the MCP `initialize` response may advertise
|
|
1484
|
-
// `capabilities.registry.proxy`, which auto-populates `proxy`.
|
|
1485
|
-
// Users who set `proxy` or `auth` explicitly on the entry always win:
|
|
1486
|
-
// discovery only fills in blanks.
|
|
1341
|
+
// Probe the registry before saving. If it returns 401 with a
|
|
1342
|
+
// WWW-Authenticate / RFC 9728 resource metadata pointer, persist
|
|
1343
|
+
// that on `authRequirement` so subsequent ops can refuse early
|
|
1344
|
+
// with a friendly message.
|
|
1487
1345
|
let final: RegistryEntry = entry;
|
|
1488
1346
|
let authRequirement: RegistryAuthRequirement | undefined;
|
|
1489
1347
|
|
|
@@ -1502,33 +1360,6 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1502
1360
|
}
|
|
1503
1361
|
}
|
|
1504
1362
|
|
|
1505
|
-
if (!entry.proxy && !authRequirement) {
|
|
1506
|
-
try {
|
|
1507
|
-
const probeConsumer = await createRegistryConsumer(
|
|
1508
|
-
{ registries: [entry], refs: [] },
|
|
1509
|
-
{ token: options.token, fetch: options.fetch },
|
|
1510
|
-
);
|
|
1511
|
-
const resolved = probeConsumer.registries()[0];
|
|
1512
|
-
if (resolved) {
|
|
1513
|
-
const discovered = await probeConsumer.discover(resolved.url);
|
|
1514
|
-
if (discovered.proxy?.mode) {
|
|
1515
|
-
final = {
|
|
1516
|
-
...final,
|
|
1517
|
-
proxy: {
|
|
1518
|
-
mode: discovered.proxy.mode,
|
|
1519
|
-
...(discovered.proxy.agent && {
|
|
1520
|
-
agent: discovered.proxy.agent,
|
|
1521
|
-
}),
|
|
1522
|
-
},
|
|
1523
|
-
};
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
} catch {
|
|
1527
|
-
// Discovery is best-effort — offline, unreachable, or non-adk
|
|
1528
|
-
// registries simply skip proxy auto-configuration.
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
1363
|
registries.push(final);
|
|
1533
1364
|
await writeConfig({ ...config, registries });
|
|
1534
1365
|
return authRequirement ? { authRequirement } : {};
|
|
@@ -1579,7 +1410,6 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1579
1410
|
if (updates.auth) existing.auth = updates.auth;
|
|
1580
1411
|
if (updates.headers)
|
|
1581
1412
|
existing.headers = { ...existing.headers, ...updates.headers };
|
|
1582
|
-
if (updates.proxy !== undefined) existing.proxy = updates.proxy;
|
|
1583
1413
|
return existing;
|
|
1584
1414
|
});
|
|
1585
1415
|
if (!found) return false;
|
|
@@ -1696,7 +1526,6 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1696
1526
|
}
|
|
1697
1527
|
delete existing.authRequirement;
|
|
1698
1528
|
});
|
|
1699
|
-
if (updated) await discoverProxyAfterAuth(nameOrUrl);
|
|
1700
1529
|
return updated;
|
|
1701
1530
|
},
|
|
1702
1531
|
|
|
@@ -1877,7 +1706,6 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1877
1706
|
};
|
|
1878
1707
|
delete existing.authRequirement;
|
|
1879
1708
|
});
|
|
1880
|
-
await discoverProxyAfterAuth(displayName);
|
|
1881
1709
|
resOut.writeHead(200, { "Content-Type": "text/html" });
|
|
1882
1710
|
resOut.end(renderAuthSuccess(displayName));
|
|
1883
1711
|
server.close();
|
|
@@ -2324,18 +2152,6 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2324
2152
|
const entry = findRef(config.refs ?? [], name);
|
|
2325
2153
|
if (!entry) throw new Error(`Ref "${name}" not found`);
|
|
2326
2154
|
|
|
2327
|
-
// Registry-proxied refs: ask the remote @config for state (secrets live
|
|
2328
|
-
// server-side so local inspection would always return "missing").
|
|
2329
|
-
const proxy = await resolveProxyForRef(entry);
|
|
2330
|
-
if (proxy) {
|
|
2331
|
-
return forwardRefOpToProxy<RefAuthStatus>(
|
|
2332
|
-
proxy.reg,
|
|
2333
|
-
proxy.agent,
|
|
2334
|
-
"auth-status",
|
|
2335
|
-
{ name },
|
|
2336
|
-
);
|
|
2337
|
-
}
|
|
2338
|
-
|
|
2339
2155
|
let security: SecuritySchemeSummary | null = null;
|
|
2340
2156
|
try {
|
|
2341
2157
|
const consumer = await buildConsumerForRef(entry);
|
|
@@ -2492,38 +2308,12 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2492
2308
|
stateContext?: Record<string, unknown>;
|
|
2493
2309
|
/** Additional scopes to request (e.g., optional scopes declared by the agent) */
|
|
2494
2310
|
scopes?: string[];
|
|
2495
|
-
/**
|
|
2496
|
-
* Opt out of proxy routing when the ref's source registry has
|
|
2497
|
-
* `proxy: { mode: 'optional' }`. Ignored for `mode: 'required'`.
|
|
2498
|
-
*/
|
|
2499
|
-
preferLocal?: boolean;
|
|
2500
2311
|
},
|
|
2501
2312
|
): Promise<AuthStartResult> {
|
|
2502
2313
|
const config = await readConfig();
|
|
2503
2314
|
const entry = findRef(config.refs ?? [], name);
|
|
2504
2315
|
if (!entry) throw new Error(`Ref "${name}" not found`);
|
|
2505
2316
|
|
|
2506
|
-
// Registry-proxied auth: forward the start-of-flow to the remote @config
|
|
2507
|
-
// agent. The registry owns the client_id/secret and returns an authorize
|
|
2508
|
-
// URL pointing at the registry's OAuth callback domain, so the user
|
|
2509
|
-
// completes the flow against the registry instead of localhost.
|
|
2510
|
-
const proxy = await resolveProxyForRef(entry, {
|
|
2511
|
-
preferLocal: opts?.preferLocal,
|
|
2512
|
-
});
|
|
2513
|
-
if (proxy) {
|
|
2514
|
-
const params: Record<string, unknown> = { name };
|
|
2515
|
-
if (opts?.apiKey !== undefined) params.apiKey = opts.apiKey;
|
|
2516
|
-
if (opts?.credentials) params.credentials = opts.credentials;
|
|
2517
|
-
if (opts?.scopes) params.scopes = opts.scopes;
|
|
2518
|
-
if (opts?.stateContext) params.stateContext = opts.stateContext;
|
|
2519
|
-
return forwardRefOpToProxy<AuthStartResult>(
|
|
2520
|
-
proxy.reg,
|
|
2521
|
-
proxy.agent,
|
|
2522
|
-
"auth",
|
|
2523
|
-
params,
|
|
2524
|
-
);
|
|
2525
|
-
}
|
|
2526
|
-
|
|
2527
2317
|
const status = await ref.authStatus(name);
|
|
2528
2318
|
const security = status.security;
|
|
2529
2319
|
const tryResolve = makeTryResolve({ name, entry, security });
|
|
@@ -2852,38 +2642,19 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2852
2642
|
timeoutMs?: number;
|
|
2853
2643
|
},
|
|
2854
2644
|
): Promise<{ complete: boolean }> {
|
|
2855
|
-
// `ref.auth` is already proxy-aware — for proxied refs it returns
|
|
2856
|
-
// the authorizeUrl that the registry minted against its own
|
|
2857
|
-
// callback domain. Everything below is identical for local and
|
|
2858
|
-
// proxied refs except the last step (polling for the callback),
|
|
2859
|
-
// which only makes sense when we own the redirect URI.
|
|
2860
2645
|
const result = await ref.auth(name);
|
|
2861
2646
|
if (result.complete) return { complete: true };
|
|
2862
2647
|
|
|
2863
|
-
const config = await readConfig();
|
|
2864
|
-
const entry = findRef(config.refs ?? [], name);
|
|
2865
|
-
const proxy = entry ? await resolveProxyForRef(entry) : null;
|
|
2866
|
-
|
|
2867
2648
|
const port = options.oauthCallbackPort ?? 8919;
|
|
2868
2649
|
const timeout = opts?.timeoutMs ?? 300_000;
|
|
2869
2650
|
const { createServer } = await import("node:http");
|
|
2870
2651
|
|
|
2871
2652
|
// API key / HTTP auth — local credential form.
|
|
2872
|
-
//
|
|
2873
|
-
// We refuse to serve the form for a proxied ref: the registry
|
|
2874
|
-
// owns the credential store, so the user needs to submit via
|
|
2875
|
-
// whatever UI the registry exposes. Supporting this through the
|
|
2876
|
-
// proxy would need a remote form endpoint — out of scope here.
|
|
2877
2653
|
if (
|
|
2878
2654
|
result.fields &&
|
|
2879
2655
|
result.fields.length > 0 &&
|
|
2880
2656
|
result.type !== "oauth2"
|
|
2881
2657
|
) {
|
|
2882
|
-
if (proxy) {
|
|
2883
|
-
throw new Error(
|
|
2884
|
-
`Ref "${name}" is sourced from a proxied registry; submit credentials through ${proxy.agent} instead of a local form.`,
|
|
2885
|
-
);
|
|
2886
|
-
}
|
|
2887
2658
|
return new Promise<{ complete: boolean }>((resolve, reject) => {
|
|
2888
2659
|
const server = createServer(async (req, res) => {
|
|
2889
2660
|
const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
@@ -2962,13 +2733,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2962
2733
|
opts.onAuthorizeUrl(result.authorizeUrl);
|
|
2963
2734
|
}
|
|
2964
2735
|
|
|
2965
|
-
//
|
|
2966
|
-
//
|
|
2967
|
-
// schedule once the user finishes the remote consent screen.
|
|
2968
|
-
if (proxy) return { complete: false };
|
|
2969
|
-
|
|
2970
|
-
// Local refs: spin up the callback server on oauthCallbackPort and
|
|
2971
|
-
// block until the OAuth provider redirects back.
|
|
2736
|
+
// Spin up the callback server on oauthCallbackPort and block
|
|
2737
|
+
// until the OAuth provider redirects back.
|
|
2972
2738
|
return new Promise<{ complete: boolean }>((resolve, reject) => {
|
|
2973
2739
|
const server = createServer(async (req, res) => {
|
|
2974
2740
|
const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
@@ -3013,20 +2779,6 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
3013
2779
|
},
|
|
3014
2780
|
|
|
3015
2781
|
async refreshToken(name: string): Promise<{ accessToken: string } | null> {
|
|
3016
|
-
// Registry-proxied refs: the remote @config holds the refresh_token.
|
|
3017
|
-
const entryForProxy = await ref.get(name);
|
|
3018
|
-
if (entryForProxy) {
|
|
3019
|
-
const proxy = await resolveProxyForRef(entryForProxy);
|
|
3020
|
-
if (proxy) {
|
|
3021
|
-
return forwardRefOpToProxy<{ accessToken: string } | null>(
|
|
3022
|
-
proxy.reg,
|
|
3023
|
-
proxy.agent,
|
|
3024
|
-
"refresh-token",
|
|
3025
|
-
{ name },
|
|
3026
|
-
);
|
|
3027
|
-
}
|
|
3028
|
-
}
|
|
3029
|
-
|
|
3030
2782
|
// Read stored refresh_token
|
|
3031
2783
|
const refreshToken = await readRefSecret(name, "refresh_token");
|
|
3032
2784
|
if (!refreshToken) return null;
|
package/src/define-config.ts
CHANGED
|
@@ -34,30 +34,6 @@ export type RegistryAuth =
|
|
|
34
34
|
| { type: "api-key"; key?: string; header?: string }
|
|
35
35
|
| { type: "jwt"; issuer?: string };
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
* Proxy configuration for a registry.
|
|
39
|
-
*
|
|
40
|
-
* When set, ref operations (`auth`, `auth-status`, `call`, `inspect`,
|
|
41
|
-
* `resources`, `read`, `refresh-token`) for refs sourced from this
|
|
42
|
-
* registry are forwarded to a server-side agent that implements the
|
|
43
|
-
* adk-tools surface. Use this for cloud-hosted registries that own
|
|
44
|
-
* OAuth client credentials and/or user tokens on behalf of consumers
|
|
45
|
-
* (e.g. `api.twin.slash.com/mcp`).
|
|
46
|
-
*
|
|
47
|
-
* - `mode: 'required'` — all ref ops MUST route through the proxy agent.
|
|
48
|
-
* Local handshake (`ref.authLocal`) is refused for refs from this
|
|
49
|
-
* registry because the local environment has no way to build an
|
|
50
|
-
* authorize URL without the server's client credentials.
|
|
51
|
-
* - `mode: 'optional'` — proxy is the default; callers may opt out via
|
|
52
|
-
* `{ preferLocal: true }` on a per-op basis when they already hold
|
|
53
|
-
* local credentials.
|
|
54
|
-
*/
|
|
55
|
-
export interface RegistryProxy {
|
|
56
|
-
mode: "required" | "optional";
|
|
57
|
-
/** Agent path to forward to. Defaults to `@config`. */
|
|
58
|
-
agent?: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
37
|
/**
|
|
62
38
|
* OAuth state captured after `adk registry auth` completes a dynamic-client
|
|
63
39
|
* registration + authorization-code flow against a registry. Stored alongside
|
|
@@ -121,13 +97,6 @@ export interface RegistryEntry {
|
|
|
121
97
|
/** Connection status — set by validation/test, used to filter active entries */
|
|
122
98
|
status?: "active" | "inactive" | "error";
|
|
123
99
|
|
|
124
|
-
/**
|
|
125
|
-
* If set, ref ops for refs sourced from this registry are forwarded
|
|
126
|
-
* to a server-side adk-tools agent (default `@config`) instead of
|
|
127
|
-
* running locally. See {@link RegistryProxy}.
|
|
128
|
-
*/
|
|
129
|
-
proxy?: RegistryProxy;
|
|
130
|
-
|
|
131
100
|
/**
|
|
132
101
|
* Populated by `adk registry add` when the probe returned 401. Cleared
|
|
133
102
|
* by `adk registry auth`. Registry ops refuse to run while this is set
|