@slashfi/agents-sdk 0.77.3 → 0.78.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/call-agent-schema.d.ts +12 -12
- package/dist/cjs/config-store.js +265 -68
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/cjs/define-config.js.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/config-store.d.ts +34 -4
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +265 -68
- package/dist/config-store.js.map +1 -1
- package/dist/define-config.d.ts +28 -4
- package/dist/define-config.d.ts.map +1 -1
- package/dist/define-config.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/config-store.test.ts +345 -28
- package/src/config-store.ts +735 -250
- package/src/define-config.ts +47 -21
- package/src/index.ts +16 -13
package/src/config-store.ts
CHANGED
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
* ```
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { AdkError } from "./adk-error.js";
|
|
20
21
|
import type { FsStore } from "./agent-definitions/config.js";
|
|
22
|
+
import { decryptSecret, encryptSecret } from "./crypto.js";
|
|
21
23
|
import type {
|
|
22
24
|
ConsumerConfig,
|
|
23
25
|
RefAddInput,
|
|
@@ -27,31 +29,68 @@ import type {
|
|
|
27
29
|
ResolvedRegistry,
|
|
28
30
|
} from "./define-config.js";
|
|
29
31
|
import { normalizeRef } from "./define-config.js";
|
|
32
|
+
import type { RegistryAuthRequirement } from "./define-config.js";
|
|
30
33
|
import type { FetchFn } from "./fetch-types.js";
|
|
31
34
|
import type { Logger } from "./logger.js";
|
|
32
|
-
import { createRegistryConsumer } from "./registry-consumer.js";
|
|
33
|
-
import type {
|
|
34
|
-
AgentListEntry,
|
|
35
|
-
AgentInspection,
|
|
36
|
-
RegistryConfiguration,
|
|
37
|
-
RegistryConsumer,
|
|
38
|
-
} from "./registry-consumer.js";
|
|
39
|
-
import type { CallAgentResponse, SecuritySchemeSummary } from "./types.js";
|
|
40
|
-
import { decryptSecret, encryptSecret } from "./crypto.js";
|
|
41
|
-
import { AdkError } from "./adk-error.js";
|
|
42
35
|
import {
|
|
36
|
+
buildOAuthAuthorizeUrl,
|
|
43
37
|
discoverOAuthMetadata,
|
|
44
38
|
dynamicClientRegistration,
|
|
45
|
-
buildOAuthAuthorizeUrl,
|
|
46
39
|
exchangeCodeForTokens,
|
|
47
40
|
probeRegistryAuth,
|
|
48
41
|
refreshAccessToken,
|
|
49
42
|
} from "./mcp-client.js";
|
|
50
|
-
import
|
|
43
|
+
import { createRegistryConsumer } from "./registry-consumer.js";
|
|
44
|
+
import type {
|
|
45
|
+
AgentInspection,
|
|
46
|
+
AgentListEntry,
|
|
47
|
+
RegistryConfiguration,
|
|
48
|
+
RegistryConsumer,
|
|
49
|
+
} from "./registry-consumer.js";
|
|
50
|
+
import type { CallAgentResponse, SecuritySchemeSummary } from "./types.js";
|
|
51
51
|
|
|
52
52
|
const CONFIG_PATH = "consumer-config.json";
|
|
53
|
+
const REGISTRY_CACHE_PATH = "registry-cache.json";
|
|
53
54
|
const SECRET_PREFIX = "secret:";
|
|
54
55
|
|
|
56
|
+
// ============================================
|
|
57
|
+
// Registry cache types
|
|
58
|
+
// ============================================
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Slim tool summary stored in the registry cache. Mirrors the shape returned
|
|
62
|
+
* by `consumer.inspect()` (sans `inputSchema` and `fullTokens`) so the LLM
|
|
63
|
+
* can discover an agent's surface without a network round-trip.
|
|
64
|
+
*/
|
|
65
|
+
export interface RegistryCacheToolSummary {
|
|
66
|
+
name: string;
|
|
67
|
+
description?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Per-ref cache entry. Updated as a side-effect of `ref.add()` and
|
|
72
|
+
* `ref.inspect()` whenever the registry response carries description or tool
|
|
73
|
+
* information. Identity-relative (lives next to the consumer-config that
|
|
74
|
+
* issued the registry call), so permission-filtered views stay consistent.
|
|
75
|
+
*/
|
|
76
|
+
export interface RegistryCacheEntry {
|
|
77
|
+
/** Canonical agent path (e.g. `notion`). Stored for sanity/debug. */
|
|
78
|
+
ref: string;
|
|
79
|
+
description?: string;
|
|
80
|
+
tools?: RegistryCacheToolSummary[];
|
|
81
|
+
/** ISO timestamp of the most recent registry round-trip that wrote this. */
|
|
82
|
+
fetchedAt: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* On-disk shape of `registry-cache.json`. Keyed by `RefEntry.name` (local
|
|
87
|
+
* identifier) — the same key consumer-config uses, so hydration is a 1:1
|
|
88
|
+
* lookup.
|
|
89
|
+
*/
|
|
90
|
+
export interface RegistryCache {
|
|
91
|
+
refs: Record<string, RegistryCacheEntry>;
|
|
92
|
+
}
|
|
93
|
+
|
|
55
94
|
// ============================================
|
|
56
95
|
// Types
|
|
57
96
|
// ============================================
|
|
@@ -128,7 +167,9 @@ export interface RegistryTestResult {
|
|
|
128
167
|
}
|
|
129
168
|
|
|
130
169
|
export interface AdkRegistryApi {
|
|
131
|
-
add(
|
|
170
|
+
add(
|
|
171
|
+
entry: RegistryEntry,
|
|
172
|
+
): Promise<{ authRequirement?: RegistryAuthRequirement }>;
|
|
132
173
|
remove(nameOrUrl: string): Promise<boolean>;
|
|
133
174
|
list(): Promise<RegistryEntry[]>;
|
|
134
175
|
get(name: string): Promise<RegistryEntry | null>;
|
|
@@ -230,7 +271,7 @@ export interface AuthStartResult {
|
|
|
230
271
|
* When populated, call() rejects unknown agent paths and tool names at compile time.
|
|
231
272
|
*/
|
|
232
273
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
233
|
-
export
|
|
274
|
+
export type AdkAgentRegistry = {};
|
|
234
275
|
|
|
235
276
|
/** @internal Helper types for conditional call() signature */
|
|
236
277
|
type AgentPath = keyof AdkAgentRegistry;
|
|
@@ -244,9 +285,17 @@ type ParamsOf<
|
|
|
244
285
|
|
|
245
286
|
type AdkRefCallFn = keyof AdkAgentRegistry extends never
|
|
246
287
|
? // No registry — loose fallback
|
|
247
|
-
(
|
|
288
|
+
(
|
|
289
|
+
name: string,
|
|
290
|
+
tool: string,
|
|
291
|
+
params?: Record<string, unknown>,
|
|
292
|
+
) => Promise<CallAgentResponse>
|
|
248
293
|
: // Registry populated — strict typed overload
|
|
249
|
-
<A extends AgentPath, T extends ToolsOf<A>>(
|
|
294
|
+
<A extends AgentPath, T extends ToolsOf<A>>(
|
|
295
|
+
name: A,
|
|
296
|
+
tool: T,
|
|
297
|
+
params: ParamsOf<A, T>,
|
|
298
|
+
) => Promise<CallAgentResponse>;
|
|
250
299
|
|
|
251
300
|
export interface AdkRefApi {
|
|
252
301
|
add(entry: RefAddInput): Promise<{ security: SecuritySchemeSummary | null }>;
|
|
@@ -254,7 +303,10 @@ export interface AdkRefApi {
|
|
|
254
303
|
list(): Promise<ResolvedRef[]>;
|
|
255
304
|
get(name: string): Promise<ResolvedRef | null>;
|
|
256
305
|
update(name: string, updates: Partial<RefEntry>): Promise<boolean>;
|
|
257
|
-
inspect(
|
|
306
|
+
inspect(
|
|
307
|
+
name: string,
|
|
308
|
+
options?: { full?: boolean },
|
|
309
|
+
): Promise<AgentInspection | null>;
|
|
258
310
|
call: AdkRefCallFn;
|
|
259
311
|
resources(name: string): Promise<CallAgentResponse>;
|
|
260
312
|
read(name: string, uris: string[]): Promise<CallAgentResponse>;
|
|
@@ -265,37 +317,43 @@ export interface AdkRefApi {
|
|
|
265
317
|
* Call adk.handleCallback() when the callback arrives, or use
|
|
266
318
|
* adk.ref.authLocal() to spin up a local server and block.
|
|
267
319
|
*/
|
|
268
|
-
auth(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
320
|
+
auth(
|
|
321
|
+
name: string,
|
|
322
|
+
opts?: {
|
|
323
|
+
/** For API key / bearer auth: the key/token value (single-key shorthand) */
|
|
324
|
+
apiKey?: string;
|
|
325
|
+
/**
|
|
326
|
+
* Credentials map for multi-field auth. Keys match the `name` field
|
|
327
|
+
* from AuthChallengeField (e.g. { "api_key": "xxx", "app_key": "yyy" }).
|
|
328
|
+
* For single-key apiKey or http bearer, `apiKey` shorthand also works.
|
|
329
|
+
*/
|
|
330
|
+
credentials?: Record<string, string>;
|
|
331
|
+
/** Extra context to encode in the OAuth state (e.g., tenant/user IDs for multi-tenant callbacks) */
|
|
332
|
+
stateContext?: Record<string, unknown>;
|
|
333
|
+
/** Additional scopes to request (e.g., optional scopes declared by the agent) */
|
|
334
|
+
scopes?: string[];
|
|
335
|
+
/**
|
|
336
|
+
* Opt out of proxy routing when the ref's source registry has
|
|
337
|
+
* `proxy: { mode: 'optional' }`. Ignored for `mode: 'required'`.
|
|
338
|
+
* Defaults to `false` — if a registry offers a proxy we use it.
|
|
339
|
+
*/
|
|
340
|
+
preferLocal?: boolean;
|
|
341
|
+
},
|
|
342
|
+
): Promise<AuthStartResult>;
|
|
288
343
|
/**
|
|
289
344
|
* Run the full OAuth flow locally: start auth, spin up a callback
|
|
290
345
|
* server, open the browser, wait for the redirect, exchange tokens.
|
|
291
346
|
* Resolves when auth is complete or times out.
|
|
292
347
|
*/
|
|
293
|
-
authLocal(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
348
|
+
authLocal(
|
|
349
|
+
name: string,
|
|
350
|
+
opts?: {
|
|
351
|
+
/** Called with the authorize URL (e.g. to open in browser) */
|
|
352
|
+
onAuthorizeUrl?: (url: string) => void;
|
|
353
|
+
/** Timeout in ms (default 300_000 = 5 min) */
|
|
354
|
+
timeoutMs?: number;
|
|
355
|
+
},
|
|
356
|
+
): Promise<{ complete: boolean }>;
|
|
299
357
|
/**
|
|
300
358
|
* Refresh an OAuth access token using a stored refresh_token.
|
|
301
359
|
* Returns the new access_token, or null if refresh is not possible
|
|
@@ -317,7 +375,11 @@ export interface Adk {
|
|
|
317
375
|
* Parse the callback query params and pass them here.
|
|
318
376
|
* @returns the ref name and whether auth is complete
|
|
319
377
|
*/
|
|
320
|
-
handleCallback(params: { code: string; state: string }): Promise<{
|
|
378
|
+
handleCallback(params: { code: string; state: string }): Promise<{
|
|
379
|
+
refName: string;
|
|
380
|
+
complete: boolean;
|
|
381
|
+
stateContext?: Record<string, unknown>;
|
|
382
|
+
}>;
|
|
321
383
|
}
|
|
322
384
|
|
|
323
385
|
// ============================================
|
|
@@ -379,9 +441,19 @@ async function decryptConfigSecrets(
|
|
|
379
441
|
const result: Record<string, unknown> = {};
|
|
380
442
|
for (const [key, value] of Object.entries(obj)) {
|
|
381
443
|
if (typeof value === "string" && value.startsWith(SECRET_PREFIX)) {
|
|
382
|
-
result[key] = await decryptSecret(
|
|
383
|
-
|
|
384
|
-
|
|
444
|
+
result[key] = await decryptSecret(
|
|
445
|
+
value.slice(SECRET_PREFIX.length),
|
|
446
|
+
encryptionKey,
|
|
447
|
+
);
|
|
448
|
+
} else if (
|
|
449
|
+
value !== null &&
|
|
450
|
+
typeof value === "object" &&
|
|
451
|
+
!Array.isArray(value)
|
|
452
|
+
) {
|
|
453
|
+
result[key] = await decryptConfigSecrets(
|
|
454
|
+
value as Record<string, unknown>,
|
|
455
|
+
encryptionKey,
|
|
456
|
+
);
|
|
385
457
|
} else {
|
|
386
458
|
result[key] = value;
|
|
387
459
|
}
|
|
@@ -399,7 +471,7 @@ async function decryptConfigSecrets(
|
|
|
399
471
|
* Fallback: _httpStatus from tool result body
|
|
400
472
|
*/
|
|
401
473
|
function isUnauthorized(result: unknown): boolean {
|
|
402
|
-
if (!result || typeof result !==
|
|
474
|
+
if (!result || typeof result !== "object") return false;
|
|
403
475
|
const r = result as Record<string, unknown>;
|
|
404
476
|
// Primary: HTTP status forwarded by the registry and set by callRegistry
|
|
405
477
|
if (r.httpStatus === 401) return true;
|
|
@@ -414,19 +486,29 @@ function isUnauthorized(result: unknown): boolean {
|
|
|
414
486
|
// ============================================
|
|
415
487
|
|
|
416
488
|
const esc = (s: string) =>
|
|
417
|
-
s
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
489
|
+
s
|
|
490
|
+
.replace(/&/g, "&")
|
|
491
|
+
.replace(/</g, "<")
|
|
492
|
+
.replace(/>/g, ">")
|
|
493
|
+
.replace(/"/g, """);
|
|
494
|
+
|
|
495
|
+
function renderCredentialForm(
|
|
496
|
+
name: string,
|
|
497
|
+
fields: AuthChallengeField[],
|
|
498
|
+
error?: string,
|
|
499
|
+
): string {
|
|
500
|
+
const fieldHtml = fields
|
|
501
|
+
.map(
|
|
502
|
+
(f) => `
|
|
421
503
|
<div class="field">
|
|
422
504
|
<label for="${esc(f.name)}">${esc(f.label)}</label>
|
|
423
505
|
${f.description ? `<p class="desc">${esc(f.description)}</p>` : ""}
|
|
424
506
|
<input id="${esc(f.name)}" name="${esc(f.name)}" type="${f.secret ? "password" : "text"}" required autocomplete="off" spellcheck="false" />
|
|
425
|
-
</div
|
|
507
|
+
</div>`,
|
|
508
|
+
)
|
|
509
|
+
.join("");
|
|
426
510
|
|
|
427
|
-
const errorHtml = error
|
|
428
|
-
? `<div class="error">${esc(error)}</div>`
|
|
429
|
-
: "";
|
|
511
|
+
const errorHtml = error ? `<div class="error">${esc(error)}</div>` : "";
|
|
430
512
|
|
|
431
513
|
return `<!DOCTYPE html>
|
|
432
514
|
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
@@ -478,7 +560,6 @@ p{font-size:14px;color:#a3a3a3}
|
|
|
478
560
|
}
|
|
479
561
|
|
|
480
562
|
export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
481
|
-
|
|
482
563
|
async function readConfig(): Promise<ConsumerConfig> {
|
|
483
564
|
const content = await fs.readFile(CONFIG_PATH);
|
|
484
565
|
if (!content) return {};
|
|
@@ -493,11 +574,115 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
493
574
|
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
494
575
|
}
|
|
495
576
|
|
|
577
|
+
// -------------------------------------------------------------------------
|
|
578
|
+
// Registry cache helpers
|
|
579
|
+
//
|
|
580
|
+
// The cache is purely an internal optimization for the adk's read paths
|
|
581
|
+
// (`ref.list()`, `ref.get()`). Writes happen as side-effects of methods
|
|
582
|
+
// that already call the registry (`ref.add()`, `ref.inspect()`); the
|
|
583
|
+
// public surface never grows new methods. Cache failures (missing file,
|
|
584
|
+
// malformed JSON, fs errors during write) are swallowed so the registry
|
|
585
|
+
// cache can never break a registry operation.
|
|
586
|
+
// -------------------------------------------------------------------------
|
|
587
|
+
|
|
588
|
+
async function readRegistryCache(): Promise<RegistryCache> {
|
|
589
|
+
try {
|
|
590
|
+
const content = await fs.readFile(REGISTRY_CACHE_PATH);
|
|
591
|
+
if (!content) return { refs: {} };
|
|
592
|
+
const parsed = JSON.parse(content) as RegistryCache;
|
|
593
|
+
return { refs: parsed.refs ?? {} };
|
|
594
|
+
} catch {
|
|
595
|
+
return { refs: {} };
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async function writeRegistryCache(cache: RegistryCache): Promise<void> {
|
|
600
|
+
try {
|
|
601
|
+
await fs.writeFile(REGISTRY_CACHE_PATH, JSON.stringify(cache, null, 2));
|
|
602
|
+
} catch {
|
|
603
|
+
// Best-effort. A failed cache write should never break the operation
|
|
604
|
+
// that triggered it.
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Project an inspect/list response into the slim shape we cache. Drops
|
|
610
|
+
* `inputSchema` (too large) and `fullTokens` (registry-internal). Returns
|
|
611
|
+
* undefined if the response carries nothing worth caching.
|
|
612
|
+
*/
|
|
613
|
+
function buildCacheEntry(
|
|
614
|
+
ref: string,
|
|
615
|
+
info:
|
|
616
|
+
| {
|
|
617
|
+
description?: string;
|
|
618
|
+
tools?: Array<{ name: string; description?: string }>;
|
|
619
|
+
toolSummaries?: Array<{ name: string; description?: string }>;
|
|
620
|
+
}
|
|
621
|
+
| null
|
|
622
|
+
| undefined,
|
|
623
|
+
): RegistryCacheEntry | undefined {
|
|
624
|
+
if (!info) return undefined;
|
|
625
|
+
const toolSource = info.tools ?? info.toolSummaries;
|
|
626
|
+
const tools = toolSource?.map((t) => {
|
|
627
|
+
const slim: RegistryCacheToolSummary = { name: t.name };
|
|
628
|
+
if (t.description !== undefined) slim.description = t.description;
|
|
629
|
+
return slim;
|
|
630
|
+
});
|
|
631
|
+
if (info.description === undefined && (!tools || tools.length === 0)) {
|
|
632
|
+
return undefined;
|
|
633
|
+
}
|
|
634
|
+
const entry: RegistryCacheEntry = {
|
|
635
|
+
ref,
|
|
636
|
+
fetchedAt: new Date().toISOString(),
|
|
637
|
+
};
|
|
638
|
+
if (info.description !== undefined) entry.description = info.description;
|
|
639
|
+
if (tools && tools.length > 0) entry.tools = tools;
|
|
640
|
+
return entry;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async function upsertRegistryCacheEntry(
|
|
644
|
+
name: string,
|
|
645
|
+
entry: RegistryCacheEntry | undefined,
|
|
646
|
+
): Promise<void> {
|
|
647
|
+
if (!entry) return;
|
|
648
|
+
const cache = await readRegistryCache();
|
|
649
|
+
cache.refs[name] = entry;
|
|
650
|
+
await writeRegistryCache(cache);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function removeRegistryCacheEntry(name: string): Promise<void> {
|
|
654
|
+
const cache = await readRegistryCache();
|
|
655
|
+
if (!(name in cache.refs)) return;
|
|
656
|
+
delete cache.refs[name];
|
|
657
|
+
await writeRegistryCache(cache);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Hydrate a `ResolvedRef` with cached registry metadata when available.
|
|
662
|
+
* Pure: never mutates input. Leaves `description` / `tools` undefined when
|
|
663
|
+
* the cache has no entry, so callers can apply their own UX fallback.
|
|
664
|
+
*/
|
|
665
|
+
function hydrateFromCache(
|
|
666
|
+
ref: ResolvedRef,
|
|
667
|
+
cache: RegistryCache,
|
|
668
|
+
): ResolvedRef {
|
|
669
|
+
const cached = cache.refs[ref.name];
|
|
670
|
+
if (!cached) return ref;
|
|
671
|
+
const next: ResolvedRef = { ...ref };
|
|
672
|
+
if (cached.description !== undefined) next.description = cached.description;
|
|
673
|
+
if (cached.tools !== undefined) next.tools = cached.tools;
|
|
674
|
+
return next;
|
|
675
|
+
}
|
|
676
|
+
|
|
496
677
|
/**
|
|
497
678
|
* Store a secret value in a ref's config, encrypted if encryptionKey is set.
|
|
498
679
|
* The value is stored inline as "secret:<encrypted>" in consumer-config.json.
|
|
499
680
|
*/
|
|
500
|
-
async function storeRefSecret(
|
|
681
|
+
async function storeRefSecret(
|
|
682
|
+
name: string,
|
|
683
|
+
key: string,
|
|
684
|
+
value: string,
|
|
685
|
+
): Promise<void> {
|
|
501
686
|
const stored = options.encryptionKey
|
|
502
687
|
? `${SECRET_PREFIX}${await encryptSecret(value, options.encryptionKey)}`
|
|
503
688
|
: value;
|
|
@@ -510,13 +695,19 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
510
695
|
await writeConfig({ ...config, refs });
|
|
511
696
|
}
|
|
512
697
|
|
|
513
|
-
async function readRefSecret(
|
|
698
|
+
async function readRefSecret(
|
|
699
|
+
name: string,
|
|
700
|
+
key: string,
|
|
701
|
+
): Promise<string | null> {
|
|
514
702
|
const config = await readConfig();
|
|
515
703
|
const entry = findRef(config.refs ?? [], name);
|
|
516
704
|
const value = entry?.config?.[key];
|
|
517
705
|
if (typeof value !== "string") return null;
|
|
518
706
|
if (value.startsWith(SECRET_PREFIX) && options.encryptionKey) {
|
|
519
|
-
return decryptSecret(
|
|
707
|
+
return decryptSecret(
|
|
708
|
+
value.slice(SECRET_PREFIX.length),
|
|
709
|
+
options.encryptionKey,
|
|
710
|
+
);
|
|
520
711
|
}
|
|
521
712
|
return value;
|
|
522
713
|
}
|
|
@@ -601,23 +792,36 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
601
792
|
createdAt: number;
|
|
602
793
|
}
|
|
603
794
|
|
|
604
|
-
async function readPendingOAuth(): Promise<
|
|
795
|
+
async function readPendingOAuth(): Promise<
|
|
796
|
+
Record<string, PendingOAuthState>
|
|
797
|
+
> {
|
|
605
798
|
const content = await fs.readFile(PENDING_OAUTH_PATH);
|
|
606
799
|
if (!content) return {};
|
|
607
|
-
try {
|
|
800
|
+
try {
|
|
801
|
+
return JSON.parse(content);
|
|
802
|
+
} catch {
|
|
803
|
+
return {};
|
|
804
|
+
}
|
|
608
805
|
}
|
|
609
806
|
|
|
610
|
-
async function writePendingOAuth(
|
|
807
|
+
async function writePendingOAuth(
|
|
808
|
+
pending: Record<string, PendingOAuthState>,
|
|
809
|
+
): Promise<void> {
|
|
611
810
|
await fs.writeFile(PENDING_OAUTH_PATH, JSON.stringify(pending, null, 2));
|
|
612
811
|
}
|
|
613
812
|
|
|
614
|
-
async function storePendingOAuth(
|
|
813
|
+
async function storePendingOAuth(
|
|
814
|
+
state: string,
|
|
815
|
+
data: PendingOAuthState,
|
|
816
|
+
): Promise<void> {
|
|
615
817
|
const pending = await readPendingOAuth();
|
|
616
818
|
pending[state] = data;
|
|
617
819
|
await writePendingOAuth(pending);
|
|
618
820
|
}
|
|
619
821
|
|
|
620
|
-
async function consumePendingOAuth(
|
|
822
|
+
async function consumePendingOAuth(
|
|
823
|
+
state: string,
|
|
824
|
+
): Promise<PendingOAuthState | null> {
|
|
621
825
|
const pending = await readPendingOAuth();
|
|
622
826
|
const data = pending[state] ?? null;
|
|
623
827
|
if (data) {
|
|
@@ -646,7 +850,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
646
850
|
let reqId = 0;
|
|
647
851
|
let sessionId: string | undefined;
|
|
648
852
|
async function rpc(method: string, rpcParams?: Record<string, unknown>) {
|
|
649
|
-
const reqHeaders = {
|
|
853
|
+
const reqHeaders = {
|
|
854
|
+
...headers,
|
|
855
|
+
...(sessionId ? { "Mcp-Session-Id": sessionId } : {}),
|
|
856
|
+
};
|
|
650
857
|
const res = await globalThis.fetch(url, {
|
|
651
858
|
method: "POST",
|
|
652
859
|
headers: reqHeaders,
|
|
@@ -658,7 +865,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
658
865
|
}),
|
|
659
866
|
});
|
|
660
867
|
if (!res.ok) {
|
|
661
|
-
throw new Error(
|
|
868
|
+
throw new Error(
|
|
869
|
+
`MCP ${method} failed (${res.status}): ${await res.text().catch(() => "unknown")}`,
|
|
870
|
+
);
|
|
662
871
|
}
|
|
663
872
|
|
|
664
873
|
const contentType = res.headers.get("content-type") ?? "";
|
|
@@ -676,18 +885,23 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
676
885
|
try {
|
|
677
886
|
const json = JSON.parse(line.slice(6));
|
|
678
887
|
if (json.id === reqId) {
|
|
679
|
-
if (json.error)
|
|
888
|
+
if (json.error)
|
|
889
|
+
throw new Error(`MCP RPC error: ${json.error.message}`);
|
|
680
890
|
return json.result;
|
|
681
891
|
}
|
|
682
892
|
} catch (e) {
|
|
683
|
-
if (e instanceof Error && e.message.startsWith("MCP RPC"))
|
|
893
|
+
if (e instanceof Error && e.message.startsWith("MCP RPC"))
|
|
894
|
+
throw e;
|
|
684
895
|
}
|
|
685
896
|
}
|
|
686
897
|
}
|
|
687
898
|
return undefined;
|
|
688
899
|
}
|
|
689
900
|
|
|
690
|
-
const json = await res.json() as {
|
|
901
|
+
const json = (await res.json()) as {
|
|
902
|
+
result?: unknown;
|
|
903
|
+
error?: { message: string };
|
|
904
|
+
};
|
|
691
905
|
if (json.error) throw new Error(`MCP RPC error: ${json.error.message}`);
|
|
692
906
|
return json.result;
|
|
693
907
|
}
|
|
@@ -700,17 +914,34 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
700
914
|
});
|
|
701
915
|
await rpc("notifications/initialized").catch(() => {});
|
|
702
916
|
|
|
703
|
-
const result = await rpc("tools/call", {
|
|
704
|
-
|
|
917
|
+
const result = (await rpc("tools/call", {
|
|
918
|
+
name: toolName,
|
|
919
|
+
arguments: params,
|
|
920
|
+
})) as {
|
|
921
|
+
content?: Array<{ type: string; text?: string }>;
|
|
922
|
+
isError?: boolean;
|
|
923
|
+
};
|
|
705
924
|
|
|
706
925
|
const textContent = result?.content?.find((c) => c.type === "text");
|
|
707
926
|
if (textContent?.text) {
|
|
708
|
-
try {
|
|
709
|
-
|
|
927
|
+
try {
|
|
928
|
+
return {
|
|
929
|
+
success: true,
|
|
930
|
+
result: JSON.parse(textContent.text),
|
|
931
|
+
} as CallAgentResponse;
|
|
932
|
+
} catch {
|
|
933
|
+
return {
|
|
934
|
+
success: true,
|
|
935
|
+
result: textContent.text,
|
|
936
|
+
} as CallAgentResponse;
|
|
937
|
+
}
|
|
710
938
|
}
|
|
711
939
|
return { success: true, result } as CallAgentResponse;
|
|
712
940
|
} catch (err) {
|
|
713
|
-
return {
|
|
941
|
+
return {
|
|
942
|
+
success: false,
|
|
943
|
+
error: err instanceof Error ? err.message : String(err),
|
|
944
|
+
} as CallAgentResponse;
|
|
714
945
|
}
|
|
715
946
|
}
|
|
716
947
|
|
|
@@ -720,11 +951,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
720
951
|
}
|
|
721
952
|
|
|
722
953
|
/** Try fetching a URL directly as OAuth metadata (it may already be a discovery URL). */
|
|
723
|
-
async function tryFetchOAuthMetadata(
|
|
954
|
+
async function tryFetchOAuthMetadata(
|
|
955
|
+
url: string,
|
|
956
|
+
): Promise<import("./mcp-client.js").OAuthServerMetadata | null> {
|
|
724
957
|
try {
|
|
725
958
|
const res = await globalThis.fetch(url);
|
|
726
959
|
if (!res.ok) return null;
|
|
727
|
-
const data = await res.json() as Record<string, unknown>;
|
|
960
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
728
961
|
if (data.authorization_endpoint && data.token_endpoint) {
|
|
729
962
|
return data as unknown as import("./mcp-client.js").OAuthServerMetadata;
|
|
730
963
|
}
|
|
@@ -778,7 +1011,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
778
1011
|
* Build a consumer that includes the ref's sourceRegistry if present.
|
|
779
1012
|
* This ensures calls/inspect route to the correct registry endpoint.
|
|
780
1013
|
*/
|
|
781
|
-
async function buildConsumerForRef(
|
|
1014
|
+
async function buildConsumerForRef(
|
|
1015
|
+
entry: RefEntry,
|
|
1016
|
+
): Promise<RegistryConsumer> {
|
|
782
1017
|
const config = await readConfig();
|
|
783
1018
|
let registries = config.registries ?? [];
|
|
784
1019
|
|
|
@@ -816,7 +1051,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
816
1051
|
* Resolve the correct registry for a ref.
|
|
817
1052
|
* If the ref has a sourceRegistry, use that; otherwise fall back to the first registry.
|
|
818
1053
|
*/
|
|
819
|
-
function resolveRegistryForRef(
|
|
1054
|
+
function resolveRegistryForRef(
|
|
1055
|
+
consumer: RegistryConsumer,
|
|
1056
|
+
entry: RefEntry,
|
|
1057
|
+
): ResolvedRegistry {
|
|
820
1058
|
const regs = consumer.registries();
|
|
821
1059
|
if (entry.sourceRegistry?.url) {
|
|
822
1060
|
const match = regs.find((r) => r.url === entry.sourceRegistry!.url);
|
|
@@ -837,7 +1075,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
837
1075
|
* the ref is sourced from a raw URL (no registry), in which case proxy routing
|
|
838
1076
|
* does not apply.
|
|
839
1077
|
*/
|
|
840
|
-
async function findRegistryEntryForRef(
|
|
1078
|
+
async function findRegistryEntryForRef(
|
|
1079
|
+
entry: RefEntry,
|
|
1080
|
+
): Promise<RegistryEntry | null> {
|
|
841
1081
|
const sourceUrl = entry.sourceRegistry?.url;
|
|
842
1082
|
if (!sourceUrl) return null;
|
|
843
1083
|
const config = await readConfig();
|
|
@@ -890,7 +1130,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
890
1130
|
sourceRegistry: { url: reg.url, agentPath: agent },
|
|
891
1131
|
});
|
|
892
1132
|
const resolved = consumer.registries().find((r) => r.url === reg.url);
|
|
893
|
-
if (!resolved)
|
|
1133
|
+
if (!resolved)
|
|
1134
|
+
throw new Error(
|
|
1135
|
+
`Registry ${reg.url} not resolvable for proxy forwarding`,
|
|
1136
|
+
);
|
|
894
1137
|
|
|
895
1138
|
const response = await consumer.callRegistry(resolved, {
|
|
896
1139
|
action: "execute_tool",
|
|
@@ -900,8 +1143,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
900
1143
|
});
|
|
901
1144
|
|
|
902
1145
|
if (!response.success) {
|
|
903
|
-
const errResponse = response as {
|
|
904
|
-
|
|
1146
|
+
const errResponse = response as {
|
|
1147
|
+
success: false;
|
|
1148
|
+
error?: string;
|
|
1149
|
+
code?: string;
|
|
1150
|
+
};
|
|
1151
|
+
const msg =
|
|
1152
|
+
errResponse.error ?? `Proxy ${agent}.ref(${operation}) failed`;
|
|
905
1153
|
throw new Error(msg);
|
|
906
1154
|
}
|
|
907
1155
|
return (response as { success: true; result: unknown }).result as T;
|
|
@@ -970,7 +1218,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
970
1218
|
const rName = registryDisplayName(r);
|
|
971
1219
|
if (rName !== nameOrUrl && registryUrl(r) !== nameOrUrl) return r;
|
|
972
1220
|
found = true;
|
|
973
|
-
const existing: RegistryEntry =
|
|
1221
|
+
const existing: RegistryEntry =
|
|
1222
|
+
typeof r === "string" ? { url: r } : { ...r };
|
|
974
1223
|
mutate(existing);
|
|
975
1224
|
return existing;
|
|
976
1225
|
});
|
|
@@ -983,11 +1232,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
983
1232
|
* Decrypt a `secret:`-prefixed value if we hold the encryption key. Plaintext
|
|
984
1233
|
* values pass through unchanged so dev configs keep working.
|
|
985
1234
|
*/
|
|
986
|
-
async function revealSecret(
|
|
1235
|
+
async function revealSecret(
|
|
1236
|
+
value: string | undefined,
|
|
1237
|
+
): Promise<string | undefined> {
|
|
987
1238
|
if (!value) return value;
|
|
988
1239
|
if (!value.startsWith(SECRET_PREFIX)) return value;
|
|
989
1240
|
if (!options.encryptionKey) return undefined;
|
|
990
|
-
return decryptSecret(
|
|
1241
|
+
return decryptSecret(
|
|
1242
|
+
value.slice(SECRET_PREFIX.length),
|
|
1243
|
+
options.encryptionKey,
|
|
1244
|
+
);
|
|
991
1245
|
}
|
|
992
1246
|
|
|
993
1247
|
/**
|
|
@@ -1002,7 +1256,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1002
1256
|
const target = findRegistry(config.registries ?? [], nameOrUrl);
|
|
1003
1257
|
if (!target || typeof target === "string") return false;
|
|
1004
1258
|
const oauth = target.oauth;
|
|
1005
|
-
if (!oauth?.refreshToken || !oauth.tokenEndpoint || !oauth.clientId)
|
|
1259
|
+
if (!oauth?.refreshToken || !oauth.tokenEndpoint || !oauth.clientId)
|
|
1260
|
+
return false;
|
|
1006
1261
|
|
|
1007
1262
|
const refreshToken = await revealSecret(oauth.refreshToken);
|
|
1008
1263
|
const clientSecret = await revealSecret(oauth.clientSecret);
|
|
@@ -1044,7 +1299,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1044
1299
|
try {
|
|
1045
1300
|
return await fn();
|
|
1046
1301
|
} catch (err) {
|
|
1047
|
-
if (!(err instanceof AdkError) || err.code !== "registry_auth_required")
|
|
1302
|
+
if (!(err instanceof AdkError) || err.code !== "registry_auth_required")
|
|
1303
|
+
throw err;
|
|
1048
1304
|
let refreshed = false;
|
|
1049
1305
|
try {
|
|
1050
1306
|
refreshed = await refreshRegistryToken(nameOrUrl);
|
|
@@ -1088,7 +1344,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1088
1344
|
}
|
|
1089
1345
|
|
|
1090
1346
|
const registry: AdkRegistryApi = {
|
|
1091
|
-
async add(
|
|
1347
|
+
async add(
|
|
1348
|
+
entry: RegistryEntry,
|
|
1349
|
+
): Promise<{ authRequirement?: RegistryAuthRequirement }> {
|
|
1092
1350
|
const config = await readConfig();
|
|
1093
1351
|
const alias = entry.name ?? entry.url;
|
|
1094
1352
|
const registries = (config.registries ?? []).filter(
|
|
@@ -1135,7 +1393,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1135
1393
|
...final,
|
|
1136
1394
|
proxy: {
|
|
1137
1395
|
mode: discovered.proxy.mode,
|
|
1138
|
-
...(discovered.proxy.agent && {
|
|
1396
|
+
...(discovered.proxy.agent && {
|
|
1397
|
+
agent: discovered.proxy.agent,
|
|
1398
|
+
}),
|
|
1139
1399
|
},
|
|
1140
1400
|
};
|
|
1141
1401
|
}
|
|
@@ -1156,7 +1416,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1156
1416
|
if (!config.registries?.length) return false;
|
|
1157
1417
|
const before = config.registries.length;
|
|
1158
1418
|
const registries = config.registries.filter(
|
|
1159
|
-
(r) =>
|
|
1419
|
+
(r) =>
|
|
1420
|
+
registryDisplayName(r) !== nameOrUrl && registryUrl(r) !== nameOrUrl,
|
|
1160
1421
|
);
|
|
1161
1422
|
if (registries.length === before) return false;
|
|
1162
1423
|
await writeConfig({ ...config, registries });
|
|
@@ -1177,7 +1438,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1177
1438
|
return typeof target === "string" ? { url: target } : target;
|
|
1178
1439
|
},
|
|
1179
1440
|
|
|
1180
|
-
async update(
|
|
1441
|
+
async update(
|
|
1442
|
+
name: string,
|
|
1443
|
+
updates: Partial<RegistryEntry>,
|
|
1444
|
+
): Promise<boolean> {
|
|
1181
1445
|
const config = await readConfig();
|
|
1182
1446
|
if (!config.registries?.length) return false;
|
|
1183
1447
|
let found = false;
|
|
@@ -1185,11 +1449,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1185
1449
|
const rName = registryDisplayName(r);
|
|
1186
1450
|
if (rName !== name && registryUrl(r) !== name) return r;
|
|
1187
1451
|
found = true;
|
|
1188
|
-
const existing: RegistryEntry =
|
|
1452
|
+
const existing: RegistryEntry =
|
|
1453
|
+
typeof r === "string" ? { url: r } : { ...r };
|
|
1189
1454
|
if (updates.url) existing.url = updates.url;
|
|
1190
1455
|
if (updates.name) existing.name = updates.name;
|
|
1191
1456
|
if (updates.auth) existing.auth = updates.auth;
|
|
1192
|
-
if (updates.headers)
|
|
1457
|
+
if (updates.headers)
|
|
1458
|
+
existing.headers = { ...existing.headers, ...updates.headers };
|
|
1193
1459
|
if (updates.proxy !== undefined) existing.proxy = updates.proxy;
|
|
1194
1460
|
return existing;
|
|
1195
1461
|
});
|
|
@@ -1201,7 +1467,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1201
1467
|
async browse(name: string, query?: string): Promise<AgentListEntry[]> {
|
|
1202
1468
|
const config = await readConfig();
|
|
1203
1469
|
const target = findRegistry(config.registries ?? [], name);
|
|
1204
|
-
if (target && typeof target !== "string")
|
|
1470
|
+
if (target && typeof target !== "string")
|
|
1471
|
+
assertRegistryAuthorized(target);
|
|
1205
1472
|
return callWithRefresh(name, async () => {
|
|
1206
1473
|
const consumer = await buildConsumer(name);
|
|
1207
1474
|
const url = target ? registryUrl(target) : name;
|
|
@@ -1212,7 +1479,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1212
1479
|
async inspect(name: string): Promise<RegistryConfiguration> {
|
|
1213
1480
|
const config = await readConfig();
|
|
1214
1481
|
const target = findRegistry(config.registries ?? [], name);
|
|
1215
|
-
if (target && typeof target !== "string")
|
|
1482
|
+
if (target && typeof target !== "string")
|
|
1483
|
+
assertRegistryAuthorized(target);
|
|
1216
1484
|
return callWithRefresh(name, async () => {
|
|
1217
1485
|
const consumer = await buildConsumer(name);
|
|
1218
1486
|
const url = target ? registryUrl(target) : name;
|
|
@@ -1224,7 +1492,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1224
1492
|
const config = await readConfig();
|
|
1225
1493
|
const registries = config.registries ?? [];
|
|
1226
1494
|
const targets = name
|
|
1227
|
-
? registries.filter(
|
|
1495
|
+
? registries.filter(
|
|
1496
|
+
(r) => registryDisplayName(r) === name || registryUrl(r) === name,
|
|
1497
|
+
)
|
|
1228
1498
|
: registries;
|
|
1229
1499
|
|
|
1230
1500
|
const results = await Promise.allSettled(
|
|
@@ -1265,7 +1535,12 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1265
1535
|
return results.map((r) =>
|
|
1266
1536
|
r.status === "fulfilled"
|
|
1267
1537
|
? r.value
|
|
1268
|
-
: {
|
|
1538
|
+
: {
|
|
1539
|
+
name: "unknown",
|
|
1540
|
+
url: "unknown",
|
|
1541
|
+
status: "error" as const,
|
|
1542
|
+
error: "unknown",
|
|
1543
|
+
},
|
|
1269
1544
|
);
|
|
1270
1545
|
},
|
|
1271
1546
|
|
|
@@ -1337,7 +1612,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1337
1612
|
});
|
|
1338
1613
|
// Re-read so the flow below sees the fresh requirement.
|
|
1339
1614
|
const refreshed = await readConfig();
|
|
1340
|
-
const refreshedTarget = findRegistry(
|
|
1615
|
+
const refreshedTarget = findRegistry(
|
|
1616
|
+
refreshed.registries ?? [],
|
|
1617
|
+
nameOrUrl,
|
|
1618
|
+
);
|
|
1341
1619
|
if (refreshedTarget && typeof refreshedTarget !== "string") {
|
|
1342
1620
|
Object.assign(target, refreshedTarget);
|
|
1343
1621
|
}
|
|
@@ -1403,17 +1681,21 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1403
1681
|
);
|
|
1404
1682
|
|
|
1405
1683
|
const state = crypto.randomUUID();
|
|
1406
|
-
const { url: authorizeUrl, codeVerifier } =
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1684
|
+
const { url: authorizeUrl, codeVerifier } =
|
|
1685
|
+
await buildOAuthAuthorizeUrl({
|
|
1686
|
+
authorizationEndpoint: metadata.authorization_endpoint,
|
|
1687
|
+
clientId: registration.clientId,
|
|
1688
|
+
redirectUri,
|
|
1689
|
+
scopes: req.scopes,
|
|
1690
|
+
state,
|
|
1691
|
+
});
|
|
1413
1692
|
|
|
1414
1693
|
return new Promise<{ complete: boolean }>((resolve, reject) => {
|
|
1415
1694
|
const server = createServer(async (reqIn, resOut) => {
|
|
1416
|
-
const reqUrl = new URL(
|
|
1695
|
+
const reqUrl = new URL(
|
|
1696
|
+
reqIn.url ?? "/",
|
|
1697
|
+
`http://localhost:${port}`,
|
|
1698
|
+
);
|
|
1417
1699
|
if (reqUrl.pathname !== "/callback") {
|
|
1418
1700
|
resOut.writeHead(404);
|
|
1419
1701
|
resOut.end();
|
|
@@ -1423,7 +1705,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1423
1705
|
const code = reqUrl.searchParams.get("code");
|
|
1424
1706
|
const returnedState = reqUrl.searchParams.get("state");
|
|
1425
1707
|
if (!code || returnedState !== state) {
|
|
1426
|
-
const error =
|
|
1708
|
+
const error =
|
|
1709
|
+
reqUrl.searchParams.get("error") ?? "missing code/state";
|
|
1427
1710
|
resOut.writeHead(400, { "Content-Type": "text/html" });
|
|
1428
1711
|
resOut.end(`<h1>Error</h1><p>${esc(error)}</p>`);
|
|
1429
1712
|
server.close();
|
|
@@ -1439,13 +1722,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1439
1722
|
}
|
|
1440
1723
|
|
|
1441
1724
|
try {
|
|
1442
|
-
const tokens = await exchangeCodeForTokens(
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1725
|
+
const tokens = await exchangeCodeForTokens(
|
|
1726
|
+
metadata.token_endpoint,
|
|
1727
|
+
{
|
|
1728
|
+
code,
|
|
1729
|
+
codeVerifier,
|
|
1730
|
+
clientId: registration.clientId,
|
|
1731
|
+
clientSecret: registration.clientSecret,
|
|
1732
|
+
redirectUri,
|
|
1733
|
+
},
|
|
1734
|
+
);
|
|
1449
1735
|
const expiresAt = tokens.expiresIn
|
|
1450
1736
|
? new Date(Date.now() + tokens.expiresIn * 1000).toISOString()
|
|
1451
1737
|
: undefined;
|
|
@@ -1527,7 +1813,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1527
1813
|
const token = params.get("token");
|
|
1528
1814
|
if (!token) {
|
|
1529
1815
|
resOut.writeHead(200, { "Content-Type": "text/html" });
|
|
1530
|
-
resOut.end(
|
|
1816
|
+
resOut.end(
|
|
1817
|
+
renderCredentialForm(displayName, fields, "Token is required."),
|
|
1818
|
+
);
|
|
1531
1819
|
return;
|
|
1532
1820
|
}
|
|
1533
1821
|
try {
|
|
@@ -1571,7 +1859,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1571
1859
|
// ==========================================
|
|
1572
1860
|
|
|
1573
1861
|
const ref: AdkRefApi = {
|
|
1574
|
-
async add(
|
|
1862
|
+
async add(
|
|
1863
|
+
entryInput: RefAddInput,
|
|
1864
|
+
): Promise<{ security: SecuritySchemeSummary | null }> {
|
|
1575
1865
|
let security: SecuritySchemeSummary | null = null;
|
|
1576
1866
|
|
|
1577
1867
|
const config = await readConfig();
|
|
@@ -1593,7 +1883,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1593
1883
|
if (entry.sourceRegistry?.url) {
|
|
1594
1884
|
entry = { ...entry, scheme: "registry" };
|
|
1595
1885
|
} else if (entry.url) {
|
|
1596
|
-
entry = {
|
|
1886
|
+
entry = {
|
|
1887
|
+
...entry,
|
|
1888
|
+
scheme: entry.url.startsWith("http") ? "https" : "mcp",
|
|
1889
|
+
};
|
|
1597
1890
|
} else {
|
|
1598
1891
|
throw new AdkError({
|
|
1599
1892
|
code: "REF_INVALID",
|
|
@@ -1623,6 +1916,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1623
1916
|
});
|
|
1624
1917
|
}
|
|
1625
1918
|
|
|
1919
|
+
let cacheEntry: RegistryCacheEntry | undefined;
|
|
1626
1920
|
if (hasRegistries || entry.sourceRegistry?.url) {
|
|
1627
1921
|
try {
|
|
1628
1922
|
const consumer = await buildConsumerForRef(entry);
|
|
@@ -1631,11 +1925,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1631
1925
|
|
|
1632
1926
|
const requiresValidation = !!entry.sourceRegistry;
|
|
1633
1927
|
if (requiresValidation) {
|
|
1634
|
-
const hasContent =
|
|
1635
|
-
info
|
|
1636
|
-
(info.
|
|
1637
|
-
|
|
1638
|
-
|
|
1928
|
+
const hasContent =
|
|
1929
|
+
info &&
|
|
1930
|
+
(info.description ||
|
|
1931
|
+
(info.tools && info.tools.length > 0) ||
|
|
1932
|
+
(info.toolSummaries && info.toolSummaries.length > 0));
|
|
1639
1933
|
if (!hasContent) {
|
|
1640
1934
|
// Inspect returned empty — fall back to browse to check if agent exists
|
|
1641
1935
|
const registryUrl = entry.sourceRegistry?.url;
|
|
@@ -1644,7 +1938,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1644
1938
|
try {
|
|
1645
1939
|
const agents = await consumer.browse(registryUrl);
|
|
1646
1940
|
const stripAt = (s: string) => s.replace(/^@/, "");
|
|
1647
|
-
const refKey = stripAt(
|
|
1941
|
+
const refKey = stripAt(
|
|
1942
|
+
entry.sourceRegistry?.agentPath ?? entry.ref,
|
|
1943
|
+
);
|
|
1648
1944
|
foundInBrowse = agents.some(
|
|
1649
1945
|
(a) => a.path === entry.ref || stripAt(a.path) === refKey,
|
|
1650
1946
|
);
|
|
@@ -1658,7 +1954,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1658
1954
|
code: "REF_NOT_FOUND",
|
|
1659
1955
|
message: `Agent "${entry.ref}" not found on ${registryHint}`,
|
|
1660
1956
|
hint: "Check available agents with: adk registry browse",
|
|
1661
|
-
details: {
|
|
1957
|
+
details: {
|
|
1958
|
+
ref: entry.ref,
|
|
1959
|
+
sourceRegistry: entry.sourceRegistry,
|
|
1960
|
+
scheme: entry.scheme,
|
|
1961
|
+
},
|
|
1662
1962
|
});
|
|
1663
1963
|
}
|
|
1664
1964
|
}
|
|
@@ -1667,17 +1967,22 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1667
1967
|
if (info?.security) security = info.security;
|
|
1668
1968
|
const agentMode = (info as any)?.mode;
|
|
1669
1969
|
if (agentMode) (entry as any).mode = agentMode;
|
|
1670
|
-
if (info?.upstream && !entry.url && agentMode !==
|
|
1970
|
+
if (info?.upstream && !entry.url && agentMode !== "api") {
|
|
1671
1971
|
entry.url = info.upstream as string;
|
|
1672
1972
|
entry.scheme = entry.scheme ?? "mcp";
|
|
1673
1973
|
}
|
|
1974
|
+
|
|
1975
|
+
cacheEntry = buildCacheEntry(entry.ref, info);
|
|
1674
1976
|
} catch (err) {
|
|
1675
1977
|
if (err instanceof AdkError) throw err;
|
|
1676
1978
|
throw new AdkError({
|
|
1677
1979
|
code: "REGISTRY_UNREACHABLE",
|
|
1678
1980
|
message: `Could not reach registry to validate "${entry.ref}"`,
|
|
1679
1981
|
hint: "Check your registry connection with: adk registry test",
|
|
1680
|
-
details: {
|
|
1982
|
+
details: {
|
|
1983
|
+
ref: entry.ref,
|
|
1984
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1985
|
+
},
|
|
1681
1986
|
cause: err,
|
|
1682
1987
|
});
|
|
1683
1988
|
}
|
|
@@ -1685,6 +1990,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1685
1990
|
|
|
1686
1991
|
const refs = [...(config.refs ?? []), entry];
|
|
1687
1992
|
await writeConfig({ ...config, refs });
|
|
1993
|
+
await upsertRegistryCacheEntry(name, cacheEntry);
|
|
1688
1994
|
|
|
1689
1995
|
return { security };
|
|
1690
1996
|
},
|
|
@@ -1696,17 +2002,28 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1696
2002
|
const refs = config.refs.filter((r) => !refNameMatches(r, name));
|
|
1697
2003
|
if (refs.length === before) return false;
|
|
1698
2004
|
await writeConfig({ ...config, refs });
|
|
2005
|
+
await removeRegistryCacheEntry(name);
|
|
1699
2006
|
return true;
|
|
1700
2007
|
},
|
|
1701
2008
|
|
|
1702
2009
|
async list(): Promise<ResolvedRef[]> {
|
|
1703
|
-
const config = await
|
|
1704
|
-
|
|
2010
|
+
const [config, cache] = await Promise.all([
|
|
2011
|
+
readConfig(),
|
|
2012
|
+
readRegistryCache(),
|
|
2013
|
+
]);
|
|
2014
|
+
return (config.refs ?? [])
|
|
2015
|
+
.map(normalizeRef)
|
|
2016
|
+
.map((r) => hydrateFromCache(r, cache));
|
|
1705
2017
|
},
|
|
1706
2018
|
|
|
1707
2019
|
async get(name: string): Promise<ResolvedRef | null> {
|
|
1708
|
-
const config = await
|
|
1709
|
-
|
|
2020
|
+
const [config, cache] = await Promise.all([
|
|
2021
|
+
readConfig(),
|
|
2022
|
+
readRegistryCache(),
|
|
2023
|
+
]);
|
|
2024
|
+
const found = findRef(config.refs ?? [], name);
|
|
2025
|
+
if (!found) return null;
|
|
2026
|
+
return hydrateFromCache(found, cache);
|
|
1710
2027
|
},
|
|
1711
2028
|
|
|
1712
2029
|
async update(name: string, updates: Partial<RefEntry>): Promise<boolean> {
|
|
@@ -1735,8 +2052,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1735
2052
|
updated.name = updates.name;
|
|
1736
2053
|
}
|
|
1737
2054
|
if (updates.scheme) updated.scheme = updates.scheme;
|
|
1738
|
-
if (updates.config)
|
|
1739
|
-
|
|
2055
|
+
if (updates.config)
|
|
2056
|
+
updated.config = { ...updated.config, ...updates.config };
|
|
2057
|
+
if (updates.sourceRegistry)
|
|
2058
|
+
updated.sourceRegistry = updates.sourceRegistry;
|
|
1740
2059
|
return updated;
|
|
1741
2060
|
});
|
|
1742
2061
|
if (!found) return false;
|
|
@@ -1744,38 +2063,63 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1744
2063
|
return true;
|
|
1745
2064
|
},
|
|
1746
2065
|
|
|
1747
|
-
async inspect(
|
|
2066
|
+
async inspect(
|
|
2067
|
+
name: string,
|
|
2068
|
+
opts?: { full?: boolean },
|
|
2069
|
+
): Promise<AgentInspection | null> {
|
|
1748
2070
|
const config = await readConfig();
|
|
1749
2071
|
const entry = findRef(config.refs ?? [], name);
|
|
1750
2072
|
if (!entry) throw new Error(`Ref "${name}" not found`);
|
|
1751
2073
|
|
|
1752
2074
|
const consumer = await buildConsumerForRef(entry);
|
|
1753
|
-
|
|
2075
|
+
const result = await consumer.inspect(
|
|
1754
2076
|
entry.sourceRegistry?.agentPath ?? entry.ref,
|
|
1755
2077
|
entry.sourceRegistry?.url,
|
|
1756
2078
|
opts,
|
|
1757
2079
|
);
|
|
2080
|
+
|
|
2081
|
+
// Side-effect: refresh the registry cache so subsequent ref.list()
|
|
2082
|
+
// / ref.get() calls see the latest description and tool summaries
|
|
2083
|
+
// without another network round-trip. Strips inputSchema (caller's
|
|
2084
|
+
// `result` is unaffected — it still carries the full data).
|
|
2085
|
+
await upsertRegistryCacheEntry(name, buildCacheEntry(entry.ref, result));
|
|
2086
|
+
|
|
2087
|
+
return result;
|
|
1758
2088
|
},
|
|
1759
2089
|
|
|
1760
|
-
async call(
|
|
2090
|
+
async call(
|
|
2091
|
+
name: string,
|
|
2092
|
+
tool: string,
|
|
2093
|
+
params?: Record<string, unknown>,
|
|
2094
|
+
): Promise<CallAgentResponse> {
|
|
1761
2095
|
const config = await readConfig();
|
|
1762
2096
|
const entry = findRef(config.refs ?? [], name);
|
|
1763
2097
|
if (!entry) throw new Error(`Ref "${name}" not found`);
|
|
1764
2098
|
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
2099
|
+
const accessToken =
|
|
2100
|
+
(await readRefSecret(name, "access_token")) ??
|
|
2101
|
+
(await readRefSecret(name, "api_key")) ??
|
|
2102
|
+
(await readRefSecret(name, "token"));
|
|
1768
2103
|
|
|
1769
2104
|
// Resolve custom headers from config (e.g. { "X-API-Key": "secret:..." })
|
|
1770
2105
|
const refConfig = (entry.config ?? {}) as Record<string, unknown>;
|
|
1771
|
-
const rawHeaders = refConfig.headers as
|
|
2106
|
+
const rawHeaders = refConfig.headers as
|
|
2107
|
+
| Record<string, string>
|
|
2108
|
+
| undefined;
|
|
1772
2109
|
let resolvedHeaders: Record<string, string> | undefined;
|
|
1773
|
-
if (rawHeaders && typeof rawHeaders ===
|
|
2110
|
+
if (rawHeaders && typeof rawHeaders === "object") {
|
|
1774
2111
|
resolvedHeaders = {};
|
|
1775
2112
|
for (const [k, v] of Object.entries(rawHeaders)) {
|
|
1776
|
-
if (
|
|
1777
|
-
|
|
1778
|
-
|
|
2113
|
+
if (
|
|
2114
|
+
typeof v === "string" &&
|
|
2115
|
+
v.startsWith(SECRET_PREFIX) &&
|
|
2116
|
+
options.encryptionKey
|
|
2117
|
+
) {
|
|
2118
|
+
resolvedHeaders[k] = await decryptSecret(
|
|
2119
|
+
v.slice(SECRET_PREFIX.length),
|
|
2120
|
+
options.encryptionKey,
|
|
2121
|
+
);
|
|
2122
|
+
} else if (typeof v === "string") {
|
|
1779
2123
|
resolvedHeaders[k] = v;
|
|
1780
2124
|
}
|
|
1781
2125
|
}
|
|
@@ -1784,9 +2128,15 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1784
2128
|
const doCall = async (token: string | null) => {
|
|
1785
2129
|
// Direct MCP only for redirect/proxy agents with an MCP upstream.
|
|
1786
2130
|
// API-mode agents must go through the registry (it does REST translation).
|
|
1787
|
-
const agentMode = (entry as any).mode ??
|
|
1788
|
-
if (token && entry.url && agentMode !==
|
|
1789
|
-
return callMcpDirect(
|
|
2131
|
+
const agentMode = (entry as any).mode ?? "redirect";
|
|
2132
|
+
if (token && entry.url && agentMode !== "api") {
|
|
2133
|
+
return callMcpDirect(
|
|
2134
|
+
entry.url,
|
|
2135
|
+
tool,
|
|
2136
|
+
params ?? {},
|
|
2137
|
+
token,
|
|
2138
|
+
resolvedHeaders,
|
|
2139
|
+
);
|
|
1790
2140
|
}
|
|
1791
2141
|
|
|
1792
2142
|
const consumer = await buildConsumerForRef(entry);
|
|
@@ -1855,13 +2205,20 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1855
2205
|
// server-side so local inspection would always return "missing").
|
|
1856
2206
|
const proxy = await resolveProxyForRef(entry);
|
|
1857
2207
|
if (proxy) {
|
|
1858
|
-
return forwardRefOpToProxy<RefAuthStatus>(
|
|
2208
|
+
return forwardRefOpToProxy<RefAuthStatus>(
|
|
2209
|
+
proxy.reg,
|
|
2210
|
+
proxy.agent,
|
|
2211
|
+
"auth-status",
|
|
2212
|
+
{ name },
|
|
2213
|
+
);
|
|
1859
2214
|
}
|
|
1860
2215
|
|
|
1861
2216
|
let security: SecuritySchemeSummary | null = null;
|
|
1862
2217
|
try {
|
|
1863
2218
|
const consumer = await buildConsumerForRef(entry);
|
|
1864
|
-
const info = await consumer.inspect(
|
|
2219
|
+
const info = await consumer.inspect(
|
|
2220
|
+
entry.sourceRegistry?.agentPath ?? entry.ref,
|
|
2221
|
+
);
|
|
1865
2222
|
if (info?.security) security = info.security;
|
|
1866
2223
|
} catch {
|
|
1867
2224
|
// Can't reach registry
|
|
@@ -1889,12 +2246,15 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1889
2246
|
};
|
|
1890
2247
|
const hasRegistration = !!securityExt.dynamicRegistration;
|
|
1891
2248
|
|
|
1892
|
-
let oauthMetadata:
|
|
2249
|
+
let oauthMetadata:
|
|
2250
|
+
| import("./mcp-client.js").OAuthServerMetadata
|
|
2251
|
+
| null = null;
|
|
1893
2252
|
let needsSecret = false;
|
|
1894
2253
|
if (securityExt.discoveryUrl) {
|
|
1895
2254
|
oauthMetadata = await tryFetchOAuthMetadata(securityExt.discoveryUrl);
|
|
1896
2255
|
if (oauthMetadata) {
|
|
1897
|
-
const authMethods =
|
|
2256
|
+
const authMethods =
|
|
2257
|
+
oauthMetadata.token_endpoint_auth_methods_supported ?? [];
|
|
1898
2258
|
needsSecret = !authMethods.includes("none");
|
|
1899
2259
|
}
|
|
1900
2260
|
}
|
|
@@ -1921,15 +2281,22 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1921
2281
|
};
|
|
1922
2282
|
} else if (security.type === "apiKey") {
|
|
1923
2283
|
const apiKeySec = security as {
|
|
1924
|
-
name?: string;
|
|
2284
|
+
name?: string;
|
|
2285
|
+
headers?: Record<string, { description?: string }>;
|
|
1925
2286
|
};
|
|
1926
2287
|
const toStorageKey = (headerName: string) =>
|
|
1927
|
-
headerName
|
|
2288
|
+
headerName
|
|
2289
|
+
.toLowerCase()
|
|
2290
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
2291
|
+
.replace(/^_|_$/g, "");
|
|
1928
2292
|
|
|
1929
2293
|
// config.headers: { "Header-Name": "value" } — check by header name (case-insensitive)
|
|
1930
|
-
const configHeaders = (
|
|
1931
|
-
Record<string, unknown> | undefined
|
|
1932
|
-
|
|
2294
|
+
const configHeaders = (
|
|
2295
|
+
entry?.config as Record<string, unknown> | undefined
|
|
2296
|
+
)?.headers as Record<string, unknown> | undefined;
|
|
2297
|
+
const configHeaderKeys = configHeaders
|
|
2298
|
+
? Object.keys(configHeaders)
|
|
2299
|
+
: [];
|
|
1933
2300
|
const hasConfigHeader = (name: string) =>
|
|
1934
2301
|
configHeaderKeys.some((k) => k.toLowerCase() === name.toLowerCase());
|
|
1935
2302
|
|
|
@@ -1943,7 +2310,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1943
2310
|
for (const headerName of declaredHeaders) {
|
|
1944
2311
|
const storageKey = toStorageKey(headerName);
|
|
1945
2312
|
const inConfigHeaders = hasConfigHeader(headerName);
|
|
1946
|
-
const inLegacyKeys =
|
|
2313
|
+
const inLegacyKeys =
|
|
2314
|
+
configKeys.includes(storageKey) || configKeys.includes("api_key");
|
|
1947
2315
|
fields[storageKey] = {
|
|
1948
2316
|
required: true,
|
|
1949
2317
|
automated: false,
|
|
@@ -1977,19 +2345,22 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1977
2345
|
return { name, security, complete, fields };
|
|
1978
2346
|
},
|
|
1979
2347
|
|
|
1980
|
-
async auth(
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
2348
|
+
async auth(
|
|
2349
|
+
name: string,
|
|
2350
|
+
opts?: {
|
|
2351
|
+
apiKey?: string;
|
|
2352
|
+
credentials?: Record<string, string>;
|
|
2353
|
+
/** Extra context to encode in the OAuth state (e.g., tenant/user IDs for multi-tenant callbacks) */
|
|
2354
|
+
stateContext?: Record<string, unknown>;
|
|
2355
|
+
/** Additional scopes to request (e.g., optional scopes declared by the agent) */
|
|
2356
|
+
scopes?: string[];
|
|
2357
|
+
/**
|
|
2358
|
+
* Opt out of proxy routing when the ref's source registry has
|
|
2359
|
+
* `proxy: { mode: 'optional' }`. Ignored for `mode: 'required'`.
|
|
2360
|
+
*/
|
|
2361
|
+
preferLocal?: boolean;
|
|
2362
|
+
},
|
|
2363
|
+
): Promise<AuthStartResult> {
|
|
1993
2364
|
const config = await readConfig();
|
|
1994
2365
|
const entry = findRef(config.refs ?? [], name);
|
|
1995
2366
|
if (!entry) throw new Error(`Ref "${name}" not found`);
|
|
@@ -1998,14 +2369,21 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1998
2369
|
// agent. The registry owns the client_id/secret and returns an authorize
|
|
1999
2370
|
// URL pointing at the registry's OAuth callback domain, so the user
|
|
2000
2371
|
// completes the flow against the registry instead of localhost.
|
|
2001
|
-
const proxy = await resolveProxyForRef(entry, {
|
|
2372
|
+
const proxy = await resolveProxyForRef(entry, {
|
|
2373
|
+
preferLocal: opts?.preferLocal,
|
|
2374
|
+
});
|
|
2002
2375
|
if (proxy) {
|
|
2003
2376
|
const params: Record<string, unknown> = { name };
|
|
2004
2377
|
if (opts?.apiKey !== undefined) params.apiKey = opts.apiKey;
|
|
2005
2378
|
if (opts?.credentials) params.credentials = opts.credentials;
|
|
2006
2379
|
if (opts?.scopes) params.scopes = opts.scopes;
|
|
2007
2380
|
if (opts?.stateContext) params.stateContext = opts.stateContext;
|
|
2008
|
-
return forwardRefOpToProxy<AuthStartResult>(
|
|
2381
|
+
return forwardRefOpToProxy<AuthStartResult>(
|
|
2382
|
+
proxy.reg,
|
|
2383
|
+
proxy.agent,
|
|
2384
|
+
"auth",
|
|
2385
|
+
params,
|
|
2386
|
+
);
|
|
2009
2387
|
}
|
|
2010
2388
|
|
|
2011
2389
|
const status = await ref.authStatus(name);
|
|
@@ -2018,20 +2396,31 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2018
2396
|
|
|
2019
2397
|
if (security.type === "apiKey") {
|
|
2020
2398
|
const apiKeySec = security as {
|
|
2021
|
-
name?: string;
|
|
2399
|
+
name?: string;
|
|
2400
|
+
prefix?: string;
|
|
2022
2401
|
headers?: Record<string, { description?: string }>;
|
|
2023
2402
|
};
|
|
2024
2403
|
|
|
2025
2404
|
const toStorageKey = (headerName: string) =>
|
|
2026
|
-
headerName
|
|
2405
|
+
headerName
|
|
2406
|
+
.toLowerCase()
|
|
2407
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
2408
|
+
.replace(/^_|_$/g, "");
|
|
2027
2409
|
|
|
2028
2410
|
// Check existing config.headers
|
|
2029
|
-
const existingHeaders = (
|
|
2030
|
-
Record<string,
|
|
2411
|
+
const existingHeaders = (
|
|
2412
|
+
(entry.config ?? {}) as Record<string, unknown>
|
|
2413
|
+
).headers as Record<string, string> | undefined;
|
|
2031
2414
|
|
|
2032
2415
|
// Collect declared headers: from security.headers or security.name
|
|
2033
|
-
const declaredHeaders: Array<{
|
|
2034
|
-
|
|
2416
|
+
const declaredHeaders: Array<{
|
|
2417
|
+
headerName: string;
|
|
2418
|
+
description?: string;
|
|
2419
|
+
}> = apiKeySec.headers
|
|
2420
|
+
? Object.entries(apiKeySec.headers).map(([h, meta]) => ({
|
|
2421
|
+
headerName: h,
|
|
2422
|
+
description: meta.description,
|
|
2423
|
+
}))
|
|
2035
2424
|
: apiKeySec.name
|
|
2036
2425
|
? [{ headerName: apiKeySec.name }]
|
|
2037
2426
|
: [];
|
|
@@ -2043,12 +2432,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2043
2432
|
for (const { headerName, description } of declaredHeaders) {
|
|
2044
2433
|
const storageKey = toStorageKey(headerName);
|
|
2045
2434
|
// Check: credentials param → existing config.headers → legacy config key → resolve callback
|
|
2046
|
-
const value =
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2435
|
+
const value =
|
|
2436
|
+
opts?.credentials?.[storageKey] ??
|
|
2437
|
+
opts?.credentials?.[headerName] ??
|
|
2438
|
+
(existingHeaders &&
|
|
2439
|
+
Object.entries(existingHeaders).find(
|
|
2440
|
+
([k]) => k.toLowerCase() === headerName.toLowerCase(),
|
|
2441
|
+
)?.[1]) ??
|
|
2442
|
+
opts?.apiKey ??
|
|
2443
|
+
(await readRefSecret(name, storageKey)) ??
|
|
2444
|
+
(await tryResolve(storageKey));
|
|
2052
2445
|
|
|
2053
2446
|
if (value) {
|
|
2054
2447
|
resolvedHeaders[headerName] = value;
|
|
@@ -2070,23 +2463,30 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2070
2463
|
const encKey = options.encryptionKey;
|
|
2071
2464
|
const headersToStore: Record<string, string> = {};
|
|
2072
2465
|
for (const [h, v] of Object.entries(resolvedHeaders)) {
|
|
2073
|
-
headersToStore[h] = encKey
|
|
2466
|
+
headersToStore[h] = encKey
|
|
2467
|
+
? `${SECRET_PREFIX}${await encryptSecret(v, encKey)}`
|
|
2468
|
+
: v;
|
|
2074
2469
|
}
|
|
2075
2470
|
await ref.update(name, { config: { headers: headersToStore } });
|
|
2076
2471
|
return { type: "apiKey", complete: true };
|
|
2077
2472
|
}
|
|
2078
2473
|
|
|
2079
2474
|
// Fallback: no headers declared → generic api_key
|
|
2080
|
-
const key =
|
|
2475
|
+
const key =
|
|
2476
|
+
opts?.credentials?.["api_key"] ??
|
|
2477
|
+
opts?.apiKey ??
|
|
2478
|
+
(await tryResolve("api_key"));
|
|
2081
2479
|
if (!key) {
|
|
2082
2480
|
return {
|
|
2083
2481
|
type: "apiKey",
|
|
2084
2482
|
complete: false,
|
|
2085
|
-
fields: [
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2483
|
+
fields: [
|
|
2484
|
+
{
|
|
2485
|
+
name: "api_key",
|
|
2486
|
+
label: "API Key",
|
|
2487
|
+
secret: true,
|
|
2488
|
+
},
|
|
2489
|
+
],
|
|
2090
2490
|
};
|
|
2091
2491
|
}
|
|
2092
2492
|
await storeRefSecret(name, "api_key", key);
|
|
@@ -2098,12 +2498,24 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2098
2498
|
const isBasic = httpSec.scheme === "basic";
|
|
2099
2499
|
|
|
2100
2500
|
if (isBasic) {
|
|
2101
|
-
const username =
|
|
2102
|
-
|
|
2501
|
+
const username =
|
|
2502
|
+
opts?.credentials?.["username"] ?? (await tryResolve("username"));
|
|
2503
|
+
const password =
|
|
2504
|
+
opts?.credentials?.["password"] ?? (await tryResolve("password"));
|
|
2103
2505
|
if (!username || !password) {
|
|
2104
2506
|
const missingFields: AuthChallengeField[] = [];
|
|
2105
|
-
if (!username)
|
|
2106
|
-
|
|
2507
|
+
if (!username)
|
|
2508
|
+
missingFields.push({
|
|
2509
|
+
name: "username",
|
|
2510
|
+
label: "Username",
|
|
2511
|
+
secret: false,
|
|
2512
|
+
});
|
|
2513
|
+
if (!password)
|
|
2514
|
+
missingFields.push({
|
|
2515
|
+
name: "password",
|
|
2516
|
+
label: "Password",
|
|
2517
|
+
secret: true,
|
|
2518
|
+
});
|
|
2107
2519
|
return { type: "http", complete: false, fields: missingFields };
|
|
2108
2520
|
}
|
|
2109
2521
|
// Store as base64 encoded basic auth token
|
|
@@ -2113,7 +2525,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2113
2525
|
}
|
|
2114
2526
|
|
|
2115
2527
|
// Bearer token
|
|
2116
|
-
const token =
|
|
2528
|
+
const token =
|
|
2529
|
+
opts?.credentials?.["token"] ??
|
|
2530
|
+
opts?.apiKey ??
|
|
2531
|
+
(await tryResolve("token"));
|
|
2117
2532
|
if (!token) {
|
|
2118
2533
|
return {
|
|
2119
2534
|
type: "http",
|
|
@@ -2126,7 +2541,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2126
2541
|
}
|
|
2127
2542
|
|
|
2128
2543
|
if (security.type === "oauth2") {
|
|
2129
|
-
const flows = (
|
|
2544
|
+
const flows = (
|
|
2545
|
+
security as {
|
|
2546
|
+
flows?: {
|
|
2547
|
+
authorizationCode?: {
|
|
2548
|
+
authorizationUrl?: string;
|
|
2549
|
+
tokenUrl?: string;
|
|
2550
|
+
};
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
).flows;
|
|
2130
2554
|
const authCodeFlow = flows?.authorizationCode;
|
|
2131
2555
|
if (!authCodeFlow?.authorizationUrl) {
|
|
2132
2556
|
return {
|
|
@@ -2147,7 +2571,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2147
2571
|
}
|
|
2148
2572
|
// Fallback: construct metadata from the security scheme's explicit URLs
|
|
2149
2573
|
if (!metadata && authCodeFlow.tokenUrl) {
|
|
2150
|
-
const flowScopes = (authCodeFlow as Record<string, unknown>).scopes as
|
|
2574
|
+
const flowScopes = (authCodeFlow as Record<string, unknown>).scopes as
|
|
2575
|
+
| Record<string, string>
|
|
2576
|
+
| undefined;
|
|
2151
2577
|
metadata = {
|
|
2152
2578
|
issuer: new URL(authUrl).origin,
|
|
2153
2579
|
authorization_endpoint: authUrl,
|
|
@@ -2172,18 +2598,24 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2172
2598
|
let clientSecret: string | undefined = fromHelper?.clientSecret;
|
|
2173
2599
|
|
|
2174
2600
|
if (!clientId && metadata.registration_endpoint) {
|
|
2175
|
-
const supportedAuthMethods =
|
|
2601
|
+
const supportedAuthMethods =
|
|
2602
|
+
metadata.token_endpoint_auth_methods_supported ?? ["none"];
|
|
2176
2603
|
const preferredMethod = supportedAuthMethods.includes("none")
|
|
2177
2604
|
? "none"
|
|
2178
|
-
: supportedAuthMethods[0] ?? "client_secret_post";
|
|
2179
|
-
|
|
2180
|
-
const securityClientName = (security as { clientName?: string })
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2605
|
+
: (supportedAuthMethods[0] ?? "client_secret_post");
|
|
2606
|
+
|
|
2607
|
+
const securityClientName = (security as { clientName?: string })
|
|
2608
|
+
.clientName;
|
|
2609
|
+
const reg = await dynamicClientRegistration(
|
|
2610
|
+
metadata.registration_endpoint,
|
|
2611
|
+
{
|
|
2612
|
+
clientName:
|
|
2613
|
+
securityClientName ?? options.oauthClientName ?? "adk",
|
|
2614
|
+
redirectUris: [redirectUri],
|
|
2615
|
+
grantTypes: ["authorization_code"],
|
|
2616
|
+
tokenEndpointAuthMethod: preferredMethod,
|
|
2617
|
+
},
|
|
2618
|
+
);
|
|
2187
2619
|
clientId = reg.clientId;
|
|
2188
2620
|
clientSecret = reg.clientSecret;
|
|
2189
2621
|
await storeRefSecret(name, "client_id", clientId);
|
|
@@ -2196,10 +2628,18 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2196
2628
|
// Return fields telling the caller what OAuth credentials to provide
|
|
2197
2629
|
const missingFields: AuthChallengeField[] = [];
|
|
2198
2630
|
if (!clientId) {
|
|
2199
|
-
missingFields.push({
|
|
2631
|
+
missingFields.push({
|
|
2632
|
+
name: "client_id",
|
|
2633
|
+
label: "Client ID",
|
|
2634
|
+
secret: false,
|
|
2635
|
+
});
|
|
2200
2636
|
}
|
|
2201
2637
|
// Always ask for client_secret alongside client_id — most providers need it
|
|
2202
|
-
missingFields.push({
|
|
2638
|
+
missingFields.push({
|
|
2639
|
+
name: "client_secret",
|
|
2640
|
+
label: "Client Secret",
|
|
2641
|
+
secret: true,
|
|
2642
|
+
});
|
|
2203
2643
|
return { type: "oauth2", complete: false, fields: missingFields };
|
|
2204
2644
|
}
|
|
2205
2645
|
|
|
@@ -2213,32 +2653,42 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2213
2653
|
};
|
|
2214
2654
|
const state = btoa(JSON.stringify(statePayload));
|
|
2215
2655
|
|
|
2216
|
-
const securityExt2 = security as {
|
|
2217
|
-
|
|
2656
|
+
const securityExt2 = security as {
|
|
2657
|
+
requiredScopes?: string[];
|
|
2658
|
+
optionalScopes?: string[];
|
|
2659
|
+
authorizationParams?: Record<string, string>;
|
|
2660
|
+
};
|
|
2661
|
+
const flowScopes = (authCodeFlow as Record<string, unknown>).scopes as
|
|
2662
|
+
| Record<string, string>
|
|
2663
|
+
| undefined;
|
|
2218
2664
|
const agentScopes = [
|
|
2219
2665
|
...(securityExt2.requiredScopes ?? []),
|
|
2220
2666
|
...(flowScopes ? Object.keys(flowScopes) : []),
|
|
2221
2667
|
...(opts?.scopes ?? []),
|
|
2222
2668
|
].filter((v, i, a) => a.indexOf(v) === i);
|
|
2223
|
-
const scopes =
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2669
|
+
const scopes =
|
|
2670
|
+
agentScopes.length > 0
|
|
2671
|
+
? [
|
|
2672
|
+
...agentScopes,
|
|
2673
|
+
...(metadata.scopes_supported?.includes("openid")
|
|
2674
|
+
? ["openid"]
|
|
2675
|
+
: []),
|
|
2676
|
+
]
|
|
2677
|
+
: metadata.scopes_supported;
|
|
2229
2678
|
|
|
2230
2679
|
// Read provider-specific authorization params from the agent's security section
|
|
2231
2680
|
// (e.g., { access_type: 'offline', prompt: 'consent' } for Google)
|
|
2232
2681
|
const authorizationParams = securityExt2.authorizationParams;
|
|
2233
2682
|
|
|
2234
|
-
const { url: authorizeUrl, codeVerifier } =
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2683
|
+
const { url: authorizeUrl, codeVerifier } =
|
|
2684
|
+
await buildOAuthAuthorizeUrl({
|
|
2685
|
+
authorizationEndpoint: metadata.authorization_endpoint,
|
|
2686
|
+
clientId,
|
|
2687
|
+
redirectUri,
|
|
2688
|
+
scopes,
|
|
2689
|
+
state,
|
|
2690
|
+
extraParams: authorizationParams,
|
|
2691
|
+
});
|
|
2242
2692
|
|
|
2243
2693
|
// Persist pending state so handleCallback works across processes
|
|
2244
2694
|
await storePendingOAuth(state, {
|
|
@@ -2257,10 +2707,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2257
2707
|
return { type: security.type, complete: false };
|
|
2258
2708
|
},
|
|
2259
2709
|
|
|
2260
|
-
async authLocal(
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2710
|
+
async authLocal(
|
|
2711
|
+
name: string,
|
|
2712
|
+
opts?: {
|
|
2713
|
+
onAuthorizeUrl?: (url: string) => void;
|
|
2714
|
+
timeoutMs?: number;
|
|
2715
|
+
},
|
|
2716
|
+
): Promise<{ complete: boolean }> {
|
|
2264
2717
|
// `ref.auth` is already proxy-aware — for proxied refs it returns
|
|
2265
2718
|
// the authorizeUrl that the registry minted against its own
|
|
2266
2719
|
// callback domain. Everything below is identical for local and
|
|
@@ -2283,7 +2736,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2283
2736
|
// owns the credential store, so the user needs to submit via
|
|
2284
2737
|
// whatever UI the registry exposes. Supporting this through the
|
|
2285
2738
|
// proxy would need a remote form endpoint — out of scope here.
|
|
2286
|
-
if (
|
|
2739
|
+
if (
|
|
2740
|
+
result.fields &&
|
|
2741
|
+
result.fields.length > 0 &&
|
|
2742
|
+
result.type !== "oauth2"
|
|
2743
|
+
) {
|
|
2287
2744
|
if (proxy) {
|
|
2288
2745
|
throw new Error(
|
|
2289
2746
|
`Ref "${name}" is sourced from a proxied registry; submit credentials through ${proxy.agent} instead of a local form.`,
|
|
@@ -2320,19 +2777,23 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2320
2777
|
resolve({ complete: true });
|
|
2321
2778
|
} else {
|
|
2322
2779
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2323
|
-
res.end(
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2780
|
+
res.end(
|
|
2781
|
+
renderCredentialForm(
|
|
2782
|
+
name,
|
|
2783
|
+
authResult.fields ?? result.fields!,
|
|
2784
|
+
"Some credentials were missing or invalid.",
|
|
2785
|
+
),
|
|
2786
|
+
);
|
|
2328
2787
|
}
|
|
2329
2788
|
} catch (err) {
|
|
2330
2789
|
res.writeHead(500, { "Content-Type": "text/html" });
|
|
2331
|
-
res.end(
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2790
|
+
res.end(
|
|
2791
|
+
renderCredentialForm(
|
|
2792
|
+
name,
|
|
2793
|
+
result.fields!,
|
|
2794
|
+
err instanceof Error ? err.message : String(err),
|
|
2795
|
+
),
|
|
2796
|
+
);
|
|
2336
2797
|
}
|
|
2337
2798
|
return;
|
|
2338
2799
|
}
|
|
@@ -2379,7 +2840,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2379
2840
|
const state = reqUrl.searchParams.get("state");
|
|
2380
2841
|
|
|
2381
2842
|
if (!code || !state) {
|
|
2382
|
-
const error =
|
|
2843
|
+
const error =
|
|
2844
|
+
reqUrl.searchParams.get("error") ?? "missing code/state";
|
|
2383
2845
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
2384
2846
|
res.end(`<h1>Error</h1><p>${error}</p>`);
|
|
2385
2847
|
server.close();
|
|
@@ -2395,7 +2857,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2395
2857
|
resolve({ complete: cbResult.complete });
|
|
2396
2858
|
} catch (err) {
|
|
2397
2859
|
res.writeHead(500, { "Content-Type": "text/html" });
|
|
2398
|
-
res.end(
|
|
2860
|
+
res.end(
|
|
2861
|
+
`<h1>Error</h1><p>${err instanceof Error ? err.message : String(err)}</p>`,
|
|
2862
|
+
);
|
|
2399
2863
|
server.close();
|
|
2400
2864
|
reject(err);
|
|
2401
2865
|
}
|
|
@@ -2440,9 +2904,17 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2440
2904
|
|
|
2441
2905
|
const status = await ref.authStatus(name);
|
|
2442
2906
|
const security = status.security;
|
|
2443
|
-
const flows =
|
|
2444
|
-
|
|
2445
|
-
|
|
2907
|
+
const flows =
|
|
2908
|
+
security && "flows" in security
|
|
2909
|
+
? (
|
|
2910
|
+
security as {
|
|
2911
|
+
flows?: Record<
|
|
2912
|
+
string,
|
|
2913
|
+
{ tokenUrl?: string; refreshUrl?: string }
|
|
2914
|
+
>;
|
|
2915
|
+
}
|
|
2916
|
+
).flows
|
|
2917
|
+
: undefined;
|
|
2446
2918
|
const authCodeFlow = flows?.authorizationCode;
|
|
2447
2919
|
const tokenUrl = authCodeFlow?.refreshUrl ?? authCodeFlow?.tokenUrl;
|
|
2448
2920
|
if (!tokenUrl) return null;
|
|
@@ -2469,7 +2941,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2469
2941
|
|
|
2470
2942
|
if (!res.ok) return null;
|
|
2471
2943
|
|
|
2472
|
-
const data = await res.json() as Record<string, unknown>;
|
|
2944
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
2473
2945
|
const newAccessToken = data.access_token as string | undefined;
|
|
2474
2946
|
if (!newAccessToken) return null;
|
|
2475
2947
|
|
|
@@ -2487,7 +2959,14 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2487
2959
|
// Top-level callback handler
|
|
2488
2960
|
// ==========================================
|
|
2489
2961
|
|
|
2490
|
-
async function handleCallback(params: {
|
|
2962
|
+
async function handleCallback(params: {
|
|
2963
|
+
code: string;
|
|
2964
|
+
state: string;
|
|
2965
|
+
}): Promise<{
|
|
2966
|
+
refName: string;
|
|
2967
|
+
complete: boolean;
|
|
2968
|
+
stateContext?: Record<string, unknown>;
|
|
2969
|
+
}> {
|
|
2491
2970
|
const pending = await consumePendingOAuth(params.state);
|
|
2492
2971
|
if (!pending) {
|
|
2493
2972
|
throw new Error(`No pending OAuth flow for state "${params.state}".`);
|
|
@@ -2503,13 +2982,19 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2503
2982
|
|
|
2504
2983
|
await storeRefSecret(pending.refName, "access_token", tokens.accessToken);
|
|
2505
2984
|
if (tokens.refreshToken) {
|
|
2506
|
-
await storeRefSecret(
|
|
2985
|
+
await storeRefSecret(
|
|
2986
|
+
pending.refName,
|
|
2987
|
+
"refresh_token",
|
|
2988
|
+
tokens.refreshToken,
|
|
2989
|
+
);
|
|
2507
2990
|
}
|
|
2508
2991
|
|
|
2509
2992
|
let stateContext: Record<string, unknown> | undefined;
|
|
2510
2993
|
try {
|
|
2511
2994
|
stateContext = JSON.parse(atob(params.state));
|
|
2512
|
-
} catch {
|
|
2995
|
+
} catch {
|
|
2996
|
+
/* state wasn't base64 JSON — legacy format */
|
|
2997
|
+
}
|
|
2513
2998
|
|
|
2514
2999
|
return { refName: pending.refName, complete: true, stateContext };
|
|
2515
3000
|
}
|