@slashfi/agents-sdk 0.77.2 → 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/adk-tools.d.ts.map +1 -1
- package/dist/adk-tools.js +104 -253
- package/dist/adk-tools.js.map +1 -1
- package/dist/call-agent-schema.d.ts +12 -12
- package/dist/cjs/adk-tools.js +104 -253
- package/dist/cjs/adk-tools.js.map +1 -1
- package/dist/cjs/config-store.js +333 -94
- 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/cjs/registry.js +2 -33
- package/dist/cjs/registry.js.map +1 -1
- package/dist/cjs/types.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 +333 -94
- 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/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +2 -33
- package/dist/registry.js.map +1 -1
- package/dist/types.d.ts +3 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/adk-tools.ts +113 -282
- package/src/config-store.test.ts +345 -28
- package/src/config-store.ts +829 -274
- package/src/define-config.ts +47 -21
- package/src/index.ts +16 -13
- package/src/ref-naming.test.ts +1 -49
- package/src/registry.ts +2 -40
- package/src/types.ts +6 -21
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,18 +695,90 @@ 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
|
}
|
|
523
714
|
|
|
715
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
716
|
+
// Credential resolution helpers
|
|
717
|
+
//
|
|
718
|
+
// Three callsites used to inline a `tryResolve`/`canResolve` closure
|
|
719
|
+
// (auth, authStatus, refreshToken) and two of them duplicated the
|
|
720
|
+
// client_id/client_secret lookup chain verbatim. Those chains MUST stay
|
|
721
|
+
// symmetric — if `auth` accepts a credential source, `refreshToken` has
|
|
722
|
+
// to read from the same source or refresh silently no-ops on every ref
|
|
723
|
+
// `auth` succeeded against. Centralising it here removes that drift
|
|
724
|
+
// risk.
|
|
725
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
726
|
+
|
|
727
|
+
type OAuthServerMetadata = import("./mcp-client.js").OAuthServerMetadata;
|
|
728
|
+
|
|
729
|
+
interface CredentialResolverContext {
|
|
730
|
+
name: string;
|
|
731
|
+
entry: RefEntry;
|
|
732
|
+
security: SecuritySchemeSummary | null;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Build a `tryResolve(field, oauthMetadata?)` function bound to a
|
|
737
|
+
* specific ref + entry + security context. Wraps the host-injected
|
|
738
|
+
* `resolveCredentials` callback (e.g. atlas's env/static/tenant chain
|
|
739
|
+
* for first-party agents). Errors propagate to the caller.
|
|
740
|
+
*/
|
|
741
|
+
function makeTryResolve(ctx: CredentialResolverContext) {
|
|
742
|
+
return async (
|
|
743
|
+
field: string,
|
|
744
|
+
oauthMetadata?: OAuthServerMetadata | null,
|
|
745
|
+
): Promise<string | null> => {
|
|
746
|
+
const resolve = options.resolveCredentials;
|
|
747
|
+
if (!resolve) return null;
|
|
748
|
+
return resolve({
|
|
749
|
+
ref: ctx.name,
|
|
750
|
+
field,
|
|
751
|
+
entry: ctx.entry,
|
|
752
|
+
security: ctx.security,
|
|
753
|
+
oauthMetadata,
|
|
754
|
+
});
|
|
755
|
+
};
|
|
756
|
+
}
|
|
524
757
|
|
|
758
|
+
/**
|
|
759
|
+
* Resolve OAuth client credentials (client_id + client_secret) for a
|
|
760
|
+
* ref. Walks: `resolveCredentials` callback → per-ref VCS storage.
|
|
761
|
+
* Used by both `auth` (initial OAuth flow) and `refreshToken` (token
|
|
762
|
+
* refresh) — must be a single function so the two paths can never
|
|
763
|
+
* disagree about where credentials live.
|
|
764
|
+
*
|
|
765
|
+
* Returns null when no client_id is available anywhere; caller decides
|
|
766
|
+
* whether to attempt dynamic registration (`auth`) or bail (`refresh`).
|
|
767
|
+
*/
|
|
768
|
+
async function resolveOAuthClient(
|
|
769
|
+
ctx: CredentialResolverContext & { metadata?: OAuthServerMetadata | null },
|
|
770
|
+
): Promise<{ clientId: string; clientSecret?: string } | null> {
|
|
771
|
+
const tryResolve = makeTryResolve(ctx);
|
|
772
|
+
const clientId =
|
|
773
|
+
(await tryResolve("client_id", ctx.metadata)) ??
|
|
774
|
+
(await readRefSecret(ctx.name, "client_id"));
|
|
775
|
+
if (!clientId) return null;
|
|
776
|
+
const clientSecret =
|
|
777
|
+
(await tryResolve("client_secret", ctx.metadata)) ??
|
|
778
|
+
(await readRefSecret(ctx.name, "client_secret")) ??
|
|
779
|
+
undefined;
|
|
780
|
+
return { clientId, ...(clientSecret && { clientSecret }) };
|
|
781
|
+
}
|
|
525
782
|
|
|
526
783
|
const PENDING_OAUTH_PATH = "pending-oauth.json";
|
|
527
784
|
|
|
@@ -535,23 +792,36 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
535
792
|
createdAt: number;
|
|
536
793
|
}
|
|
537
794
|
|
|
538
|
-
async function readPendingOAuth(): Promise<
|
|
795
|
+
async function readPendingOAuth(): Promise<
|
|
796
|
+
Record<string, PendingOAuthState>
|
|
797
|
+
> {
|
|
539
798
|
const content = await fs.readFile(PENDING_OAUTH_PATH);
|
|
540
799
|
if (!content) return {};
|
|
541
|
-
try {
|
|
800
|
+
try {
|
|
801
|
+
return JSON.parse(content);
|
|
802
|
+
} catch {
|
|
803
|
+
return {};
|
|
804
|
+
}
|
|
542
805
|
}
|
|
543
806
|
|
|
544
|
-
async function writePendingOAuth(
|
|
807
|
+
async function writePendingOAuth(
|
|
808
|
+
pending: Record<string, PendingOAuthState>,
|
|
809
|
+
): Promise<void> {
|
|
545
810
|
await fs.writeFile(PENDING_OAUTH_PATH, JSON.stringify(pending, null, 2));
|
|
546
811
|
}
|
|
547
812
|
|
|
548
|
-
async function storePendingOAuth(
|
|
813
|
+
async function storePendingOAuth(
|
|
814
|
+
state: string,
|
|
815
|
+
data: PendingOAuthState,
|
|
816
|
+
): Promise<void> {
|
|
549
817
|
const pending = await readPendingOAuth();
|
|
550
818
|
pending[state] = data;
|
|
551
819
|
await writePendingOAuth(pending);
|
|
552
820
|
}
|
|
553
821
|
|
|
554
|
-
async function consumePendingOAuth(
|
|
822
|
+
async function consumePendingOAuth(
|
|
823
|
+
state: string,
|
|
824
|
+
): Promise<PendingOAuthState | null> {
|
|
555
825
|
const pending = await readPendingOAuth();
|
|
556
826
|
const data = pending[state] ?? null;
|
|
557
827
|
if (data) {
|
|
@@ -580,7 +850,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
580
850
|
let reqId = 0;
|
|
581
851
|
let sessionId: string | undefined;
|
|
582
852
|
async function rpc(method: string, rpcParams?: Record<string, unknown>) {
|
|
583
|
-
const reqHeaders = {
|
|
853
|
+
const reqHeaders = {
|
|
854
|
+
...headers,
|
|
855
|
+
...(sessionId ? { "Mcp-Session-Id": sessionId } : {}),
|
|
856
|
+
};
|
|
584
857
|
const res = await globalThis.fetch(url, {
|
|
585
858
|
method: "POST",
|
|
586
859
|
headers: reqHeaders,
|
|
@@ -592,7 +865,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
592
865
|
}),
|
|
593
866
|
});
|
|
594
867
|
if (!res.ok) {
|
|
595
|
-
throw new Error(
|
|
868
|
+
throw new Error(
|
|
869
|
+
`MCP ${method} failed (${res.status}): ${await res.text().catch(() => "unknown")}`,
|
|
870
|
+
);
|
|
596
871
|
}
|
|
597
872
|
|
|
598
873
|
const contentType = res.headers.get("content-type") ?? "";
|
|
@@ -610,18 +885,23 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
610
885
|
try {
|
|
611
886
|
const json = JSON.parse(line.slice(6));
|
|
612
887
|
if (json.id === reqId) {
|
|
613
|
-
if (json.error)
|
|
888
|
+
if (json.error)
|
|
889
|
+
throw new Error(`MCP RPC error: ${json.error.message}`);
|
|
614
890
|
return json.result;
|
|
615
891
|
}
|
|
616
892
|
} catch (e) {
|
|
617
|
-
if (e instanceof Error && e.message.startsWith("MCP RPC"))
|
|
893
|
+
if (e instanceof Error && e.message.startsWith("MCP RPC"))
|
|
894
|
+
throw e;
|
|
618
895
|
}
|
|
619
896
|
}
|
|
620
897
|
}
|
|
621
898
|
return undefined;
|
|
622
899
|
}
|
|
623
900
|
|
|
624
|
-
const json = await res.json() as {
|
|
901
|
+
const json = (await res.json()) as {
|
|
902
|
+
result?: unknown;
|
|
903
|
+
error?: { message: string };
|
|
904
|
+
};
|
|
625
905
|
if (json.error) throw new Error(`MCP RPC error: ${json.error.message}`);
|
|
626
906
|
return json.result;
|
|
627
907
|
}
|
|
@@ -634,17 +914,34 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
634
914
|
});
|
|
635
915
|
await rpc("notifications/initialized").catch(() => {});
|
|
636
916
|
|
|
637
|
-
const result = await rpc("tools/call", {
|
|
638
|
-
|
|
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
|
+
};
|
|
639
924
|
|
|
640
925
|
const textContent = result?.content?.find((c) => c.type === "text");
|
|
641
926
|
if (textContent?.text) {
|
|
642
|
-
try {
|
|
643
|
-
|
|
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
|
+
}
|
|
644
938
|
}
|
|
645
939
|
return { success: true, result } as CallAgentResponse;
|
|
646
940
|
} catch (err) {
|
|
647
|
-
return {
|
|
941
|
+
return {
|
|
942
|
+
success: false,
|
|
943
|
+
error: err instanceof Error ? err.message : String(err),
|
|
944
|
+
} as CallAgentResponse;
|
|
648
945
|
}
|
|
649
946
|
}
|
|
650
947
|
|
|
@@ -654,11 +951,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
654
951
|
}
|
|
655
952
|
|
|
656
953
|
/** Try fetching a URL directly as OAuth metadata (it may already be a discovery URL). */
|
|
657
|
-
async function tryFetchOAuthMetadata(
|
|
954
|
+
async function tryFetchOAuthMetadata(
|
|
955
|
+
url: string,
|
|
956
|
+
): Promise<import("./mcp-client.js").OAuthServerMetadata | null> {
|
|
658
957
|
try {
|
|
659
958
|
const res = await globalThis.fetch(url);
|
|
660
959
|
if (!res.ok) return null;
|
|
661
|
-
const data = await res.json() as Record<string, unknown>;
|
|
960
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
662
961
|
if (data.authorization_endpoint && data.token_endpoint) {
|
|
663
962
|
return data as unknown as import("./mcp-client.js").OAuthServerMetadata;
|
|
664
963
|
}
|
|
@@ -712,7 +1011,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
712
1011
|
* Build a consumer that includes the ref's sourceRegistry if present.
|
|
713
1012
|
* This ensures calls/inspect route to the correct registry endpoint.
|
|
714
1013
|
*/
|
|
715
|
-
async function buildConsumerForRef(
|
|
1014
|
+
async function buildConsumerForRef(
|
|
1015
|
+
entry: RefEntry,
|
|
1016
|
+
): Promise<RegistryConsumer> {
|
|
716
1017
|
const config = await readConfig();
|
|
717
1018
|
let registries = config.registries ?? [];
|
|
718
1019
|
|
|
@@ -750,7 +1051,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
750
1051
|
* Resolve the correct registry for a ref.
|
|
751
1052
|
* If the ref has a sourceRegistry, use that; otherwise fall back to the first registry.
|
|
752
1053
|
*/
|
|
753
|
-
function resolveRegistryForRef(
|
|
1054
|
+
function resolveRegistryForRef(
|
|
1055
|
+
consumer: RegistryConsumer,
|
|
1056
|
+
entry: RefEntry,
|
|
1057
|
+
): ResolvedRegistry {
|
|
754
1058
|
const regs = consumer.registries();
|
|
755
1059
|
if (entry.sourceRegistry?.url) {
|
|
756
1060
|
const match = regs.find((r) => r.url === entry.sourceRegistry!.url);
|
|
@@ -771,7 +1075,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
771
1075
|
* the ref is sourced from a raw URL (no registry), in which case proxy routing
|
|
772
1076
|
* does not apply.
|
|
773
1077
|
*/
|
|
774
|
-
async function findRegistryEntryForRef(
|
|
1078
|
+
async function findRegistryEntryForRef(
|
|
1079
|
+
entry: RefEntry,
|
|
1080
|
+
): Promise<RegistryEntry | null> {
|
|
775
1081
|
const sourceUrl = entry.sourceRegistry?.url;
|
|
776
1082
|
if (!sourceUrl) return null;
|
|
777
1083
|
const config = await readConfig();
|
|
@@ -824,7 +1130,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
824
1130
|
sourceRegistry: { url: reg.url, agentPath: agent },
|
|
825
1131
|
});
|
|
826
1132
|
const resolved = consumer.registries().find((r) => r.url === reg.url);
|
|
827
|
-
if (!resolved)
|
|
1133
|
+
if (!resolved)
|
|
1134
|
+
throw new Error(
|
|
1135
|
+
`Registry ${reg.url} not resolvable for proxy forwarding`,
|
|
1136
|
+
);
|
|
828
1137
|
|
|
829
1138
|
const response = await consumer.callRegistry(resolved, {
|
|
830
1139
|
action: "execute_tool",
|
|
@@ -834,8 +1143,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
834
1143
|
});
|
|
835
1144
|
|
|
836
1145
|
if (!response.success) {
|
|
837
|
-
const errResponse = response as {
|
|
838
|
-
|
|
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`;
|
|
839
1153
|
throw new Error(msg);
|
|
840
1154
|
}
|
|
841
1155
|
return (response as { success: true; result: unknown }).result as T;
|
|
@@ -904,7 +1218,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
904
1218
|
const rName = registryDisplayName(r);
|
|
905
1219
|
if (rName !== nameOrUrl && registryUrl(r) !== nameOrUrl) return r;
|
|
906
1220
|
found = true;
|
|
907
|
-
const existing: RegistryEntry =
|
|
1221
|
+
const existing: RegistryEntry =
|
|
1222
|
+
typeof r === "string" ? { url: r } : { ...r };
|
|
908
1223
|
mutate(existing);
|
|
909
1224
|
return existing;
|
|
910
1225
|
});
|
|
@@ -917,11 +1232,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
917
1232
|
* Decrypt a `secret:`-prefixed value if we hold the encryption key. Plaintext
|
|
918
1233
|
* values pass through unchanged so dev configs keep working.
|
|
919
1234
|
*/
|
|
920
|
-
async function revealSecret(
|
|
1235
|
+
async function revealSecret(
|
|
1236
|
+
value: string | undefined,
|
|
1237
|
+
): Promise<string | undefined> {
|
|
921
1238
|
if (!value) return value;
|
|
922
1239
|
if (!value.startsWith(SECRET_PREFIX)) return value;
|
|
923
1240
|
if (!options.encryptionKey) return undefined;
|
|
924
|
-
return decryptSecret(
|
|
1241
|
+
return decryptSecret(
|
|
1242
|
+
value.slice(SECRET_PREFIX.length),
|
|
1243
|
+
options.encryptionKey,
|
|
1244
|
+
);
|
|
925
1245
|
}
|
|
926
1246
|
|
|
927
1247
|
/**
|
|
@@ -936,7 +1256,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
936
1256
|
const target = findRegistry(config.registries ?? [], nameOrUrl);
|
|
937
1257
|
if (!target || typeof target === "string") return false;
|
|
938
1258
|
const oauth = target.oauth;
|
|
939
|
-
if (!oauth?.refreshToken || !oauth.tokenEndpoint || !oauth.clientId)
|
|
1259
|
+
if (!oauth?.refreshToken || !oauth.tokenEndpoint || !oauth.clientId)
|
|
1260
|
+
return false;
|
|
940
1261
|
|
|
941
1262
|
const refreshToken = await revealSecret(oauth.refreshToken);
|
|
942
1263
|
const clientSecret = await revealSecret(oauth.clientSecret);
|
|
@@ -978,7 +1299,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
978
1299
|
try {
|
|
979
1300
|
return await fn();
|
|
980
1301
|
} catch (err) {
|
|
981
|
-
if (!(err instanceof AdkError) || err.code !== "registry_auth_required")
|
|
1302
|
+
if (!(err instanceof AdkError) || err.code !== "registry_auth_required")
|
|
1303
|
+
throw err;
|
|
982
1304
|
let refreshed = false;
|
|
983
1305
|
try {
|
|
984
1306
|
refreshed = await refreshRegistryToken(nameOrUrl);
|
|
@@ -1022,7 +1344,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1022
1344
|
}
|
|
1023
1345
|
|
|
1024
1346
|
const registry: AdkRegistryApi = {
|
|
1025
|
-
async add(
|
|
1347
|
+
async add(
|
|
1348
|
+
entry: RegistryEntry,
|
|
1349
|
+
): Promise<{ authRequirement?: RegistryAuthRequirement }> {
|
|
1026
1350
|
const config = await readConfig();
|
|
1027
1351
|
const alias = entry.name ?? entry.url;
|
|
1028
1352
|
const registries = (config.registries ?? []).filter(
|
|
@@ -1069,7 +1393,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1069
1393
|
...final,
|
|
1070
1394
|
proxy: {
|
|
1071
1395
|
mode: discovered.proxy.mode,
|
|
1072
|
-
...(discovered.proxy.agent && {
|
|
1396
|
+
...(discovered.proxy.agent && {
|
|
1397
|
+
agent: discovered.proxy.agent,
|
|
1398
|
+
}),
|
|
1073
1399
|
},
|
|
1074
1400
|
};
|
|
1075
1401
|
}
|
|
@@ -1090,7 +1416,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1090
1416
|
if (!config.registries?.length) return false;
|
|
1091
1417
|
const before = config.registries.length;
|
|
1092
1418
|
const registries = config.registries.filter(
|
|
1093
|
-
(r) =>
|
|
1419
|
+
(r) =>
|
|
1420
|
+
registryDisplayName(r) !== nameOrUrl && registryUrl(r) !== nameOrUrl,
|
|
1094
1421
|
);
|
|
1095
1422
|
if (registries.length === before) return false;
|
|
1096
1423
|
await writeConfig({ ...config, registries });
|
|
@@ -1111,7 +1438,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1111
1438
|
return typeof target === "string" ? { url: target } : target;
|
|
1112
1439
|
},
|
|
1113
1440
|
|
|
1114
|
-
async update(
|
|
1441
|
+
async update(
|
|
1442
|
+
name: string,
|
|
1443
|
+
updates: Partial<RegistryEntry>,
|
|
1444
|
+
): Promise<boolean> {
|
|
1115
1445
|
const config = await readConfig();
|
|
1116
1446
|
if (!config.registries?.length) return false;
|
|
1117
1447
|
let found = false;
|
|
@@ -1119,11 +1449,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1119
1449
|
const rName = registryDisplayName(r);
|
|
1120
1450
|
if (rName !== name && registryUrl(r) !== name) return r;
|
|
1121
1451
|
found = true;
|
|
1122
|
-
const existing: RegistryEntry =
|
|
1452
|
+
const existing: RegistryEntry =
|
|
1453
|
+
typeof r === "string" ? { url: r } : { ...r };
|
|
1123
1454
|
if (updates.url) existing.url = updates.url;
|
|
1124
1455
|
if (updates.name) existing.name = updates.name;
|
|
1125
1456
|
if (updates.auth) existing.auth = updates.auth;
|
|
1126
|
-
if (updates.headers)
|
|
1457
|
+
if (updates.headers)
|
|
1458
|
+
existing.headers = { ...existing.headers, ...updates.headers };
|
|
1127
1459
|
if (updates.proxy !== undefined) existing.proxy = updates.proxy;
|
|
1128
1460
|
return existing;
|
|
1129
1461
|
});
|
|
@@ -1135,7 +1467,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1135
1467
|
async browse(name: string, query?: string): Promise<AgentListEntry[]> {
|
|
1136
1468
|
const config = await readConfig();
|
|
1137
1469
|
const target = findRegistry(config.registries ?? [], name);
|
|
1138
|
-
if (target && typeof target !== "string")
|
|
1470
|
+
if (target && typeof target !== "string")
|
|
1471
|
+
assertRegistryAuthorized(target);
|
|
1139
1472
|
return callWithRefresh(name, async () => {
|
|
1140
1473
|
const consumer = await buildConsumer(name);
|
|
1141
1474
|
const url = target ? registryUrl(target) : name;
|
|
@@ -1146,7 +1479,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1146
1479
|
async inspect(name: string): Promise<RegistryConfiguration> {
|
|
1147
1480
|
const config = await readConfig();
|
|
1148
1481
|
const target = findRegistry(config.registries ?? [], name);
|
|
1149
|
-
if (target && typeof target !== "string")
|
|
1482
|
+
if (target && typeof target !== "string")
|
|
1483
|
+
assertRegistryAuthorized(target);
|
|
1150
1484
|
return callWithRefresh(name, async () => {
|
|
1151
1485
|
const consumer = await buildConsumer(name);
|
|
1152
1486
|
const url = target ? registryUrl(target) : name;
|
|
@@ -1158,7 +1492,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1158
1492
|
const config = await readConfig();
|
|
1159
1493
|
const registries = config.registries ?? [];
|
|
1160
1494
|
const targets = name
|
|
1161
|
-
? registries.filter(
|
|
1495
|
+
? registries.filter(
|
|
1496
|
+
(r) => registryDisplayName(r) === name || registryUrl(r) === name,
|
|
1497
|
+
)
|
|
1162
1498
|
: registries;
|
|
1163
1499
|
|
|
1164
1500
|
const results = await Promise.allSettled(
|
|
@@ -1199,7 +1535,12 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1199
1535
|
return results.map((r) =>
|
|
1200
1536
|
r.status === "fulfilled"
|
|
1201
1537
|
? r.value
|
|
1202
|
-
: {
|
|
1538
|
+
: {
|
|
1539
|
+
name: "unknown",
|
|
1540
|
+
url: "unknown",
|
|
1541
|
+
status: "error" as const,
|
|
1542
|
+
error: "unknown",
|
|
1543
|
+
},
|
|
1203
1544
|
);
|
|
1204
1545
|
},
|
|
1205
1546
|
|
|
@@ -1271,7 +1612,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1271
1612
|
});
|
|
1272
1613
|
// Re-read so the flow below sees the fresh requirement.
|
|
1273
1614
|
const refreshed = await readConfig();
|
|
1274
|
-
const refreshedTarget = findRegistry(
|
|
1615
|
+
const refreshedTarget = findRegistry(
|
|
1616
|
+
refreshed.registries ?? [],
|
|
1617
|
+
nameOrUrl,
|
|
1618
|
+
);
|
|
1275
1619
|
if (refreshedTarget && typeof refreshedTarget !== "string") {
|
|
1276
1620
|
Object.assign(target, refreshedTarget);
|
|
1277
1621
|
}
|
|
@@ -1337,17 +1681,21 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1337
1681
|
);
|
|
1338
1682
|
|
|
1339
1683
|
const state = crypto.randomUUID();
|
|
1340
|
-
const { url: authorizeUrl, codeVerifier } =
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
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
|
+
});
|
|
1347
1692
|
|
|
1348
1693
|
return new Promise<{ complete: boolean }>((resolve, reject) => {
|
|
1349
1694
|
const server = createServer(async (reqIn, resOut) => {
|
|
1350
|
-
const reqUrl = new URL(
|
|
1695
|
+
const reqUrl = new URL(
|
|
1696
|
+
reqIn.url ?? "/",
|
|
1697
|
+
`http://localhost:${port}`,
|
|
1698
|
+
);
|
|
1351
1699
|
if (reqUrl.pathname !== "/callback") {
|
|
1352
1700
|
resOut.writeHead(404);
|
|
1353
1701
|
resOut.end();
|
|
@@ -1357,7 +1705,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1357
1705
|
const code = reqUrl.searchParams.get("code");
|
|
1358
1706
|
const returnedState = reqUrl.searchParams.get("state");
|
|
1359
1707
|
if (!code || returnedState !== state) {
|
|
1360
|
-
const error =
|
|
1708
|
+
const error =
|
|
1709
|
+
reqUrl.searchParams.get("error") ?? "missing code/state";
|
|
1361
1710
|
resOut.writeHead(400, { "Content-Type": "text/html" });
|
|
1362
1711
|
resOut.end(`<h1>Error</h1><p>${esc(error)}</p>`);
|
|
1363
1712
|
server.close();
|
|
@@ -1373,13 +1722,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1373
1722
|
}
|
|
1374
1723
|
|
|
1375
1724
|
try {
|
|
1376
|
-
const tokens = await exchangeCodeForTokens(
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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
|
+
);
|
|
1383
1735
|
const expiresAt = tokens.expiresIn
|
|
1384
1736
|
? new Date(Date.now() + tokens.expiresIn * 1000).toISOString()
|
|
1385
1737
|
: undefined;
|
|
@@ -1461,7 +1813,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1461
1813
|
const token = params.get("token");
|
|
1462
1814
|
if (!token) {
|
|
1463
1815
|
resOut.writeHead(200, { "Content-Type": "text/html" });
|
|
1464
|
-
resOut.end(
|
|
1816
|
+
resOut.end(
|
|
1817
|
+
renderCredentialForm(displayName, fields, "Token is required."),
|
|
1818
|
+
);
|
|
1465
1819
|
return;
|
|
1466
1820
|
}
|
|
1467
1821
|
try {
|
|
@@ -1505,7 +1859,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1505
1859
|
// ==========================================
|
|
1506
1860
|
|
|
1507
1861
|
const ref: AdkRefApi = {
|
|
1508
|
-
async add(
|
|
1862
|
+
async add(
|
|
1863
|
+
entryInput: RefAddInput,
|
|
1864
|
+
): Promise<{ security: SecuritySchemeSummary | null }> {
|
|
1509
1865
|
let security: SecuritySchemeSummary | null = null;
|
|
1510
1866
|
|
|
1511
1867
|
const config = await readConfig();
|
|
@@ -1527,7 +1883,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1527
1883
|
if (entry.sourceRegistry?.url) {
|
|
1528
1884
|
entry = { ...entry, scheme: "registry" };
|
|
1529
1885
|
} else if (entry.url) {
|
|
1530
|
-
entry = {
|
|
1886
|
+
entry = {
|
|
1887
|
+
...entry,
|
|
1888
|
+
scheme: entry.url.startsWith("http") ? "https" : "mcp",
|
|
1889
|
+
};
|
|
1531
1890
|
} else {
|
|
1532
1891
|
throw new AdkError({
|
|
1533
1892
|
code: "REF_INVALID",
|
|
@@ -1557,6 +1916,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1557
1916
|
});
|
|
1558
1917
|
}
|
|
1559
1918
|
|
|
1919
|
+
let cacheEntry: RegistryCacheEntry | undefined;
|
|
1560
1920
|
if (hasRegistries || entry.sourceRegistry?.url) {
|
|
1561
1921
|
try {
|
|
1562
1922
|
const consumer = await buildConsumerForRef(entry);
|
|
@@ -1565,11 +1925,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1565
1925
|
|
|
1566
1926
|
const requiresValidation = !!entry.sourceRegistry;
|
|
1567
1927
|
if (requiresValidation) {
|
|
1568
|
-
const hasContent =
|
|
1569
|
-
info
|
|
1570
|
-
(info.
|
|
1571
|
-
|
|
1572
|
-
|
|
1928
|
+
const hasContent =
|
|
1929
|
+
info &&
|
|
1930
|
+
(info.description ||
|
|
1931
|
+
(info.tools && info.tools.length > 0) ||
|
|
1932
|
+
(info.toolSummaries && info.toolSummaries.length > 0));
|
|
1573
1933
|
if (!hasContent) {
|
|
1574
1934
|
// Inspect returned empty — fall back to browse to check if agent exists
|
|
1575
1935
|
const registryUrl = entry.sourceRegistry?.url;
|
|
@@ -1578,7 +1938,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1578
1938
|
try {
|
|
1579
1939
|
const agents = await consumer.browse(registryUrl);
|
|
1580
1940
|
const stripAt = (s: string) => s.replace(/^@/, "");
|
|
1581
|
-
const refKey = stripAt(
|
|
1941
|
+
const refKey = stripAt(
|
|
1942
|
+
entry.sourceRegistry?.agentPath ?? entry.ref,
|
|
1943
|
+
);
|
|
1582
1944
|
foundInBrowse = agents.some(
|
|
1583
1945
|
(a) => a.path === entry.ref || stripAt(a.path) === refKey,
|
|
1584
1946
|
);
|
|
@@ -1592,7 +1954,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1592
1954
|
code: "REF_NOT_FOUND",
|
|
1593
1955
|
message: `Agent "${entry.ref}" not found on ${registryHint}`,
|
|
1594
1956
|
hint: "Check available agents with: adk registry browse",
|
|
1595
|
-
details: {
|
|
1957
|
+
details: {
|
|
1958
|
+
ref: entry.ref,
|
|
1959
|
+
sourceRegistry: entry.sourceRegistry,
|
|
1960
|
+
scheme: entry.scheme,
|
|
1961
|
+
},
|
|
1596
1962
|
});
|
|
1597
1963
|
}
|
|
1598
1964
|
}
|
|
@@ -1601,17 +1967,22 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1601
1967
|
if (info?.security) security = info.security;
|
|
1602
1968
|
const agentMode = (info as any)?.mode;
|
|
1603
1969
|
if (agentMode) (entry as any).mode = agentMode;
|
|
1604
|
-
if (info?.upstream && !entry.url && agentMode !==
|
|
1970
|
+
if (info?.upstream && !entry.url && agentMode !== "api") {
|
|
1605
1971
|
entry.url = info.upstream as string;
|
|
1606
1972
|
entry.scheme = entry.scheme ?? "mcp";
|
|
1607
1973
|
}
|
|
1974
|
+
|
|
1975
|
+
cacheEntry = buildCacheEntry(entry.ref, info);
|
|
1608
1976
|
} catch (err) {
|
|
1609
1977
|
if (err instanceof AdkError) throw err;
|
|
1610
1978
|
throw new AdkError({
|
|
1611
1979
|
code: "REGISTRY_UNREACHABLE",
|
|
1612
1980
|
message: `Could not reach registry to validate "${entry.ref}"`,
|
|
1613
1981
|
hint: "Check your registry connection with: adk registry test",
|
|
1614
|
-
details: {
|
|
1982
|
+
details: {
|
|
1983
|
+
ref: entry.ref,
|
|
1984
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1985
|
+
},
|
|
1615
1986
|
cause: err,
|
|
1616
1987
|
});
|
|
1617
1988
|
}
|
|
@@ -1619,6 +1990,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1619
1990
|
|
|
1620
1991
|
const refs = [...(config.refs ?? []), entry];
|
|
1621
1992
|
await writeConfig({ ...config, refs });
|
|
1993
|
+
await upsertRegistryCacheEntry(name, cacheEntry);
|
|
1622
1994
|
|
|
1623
1995
|
return { security };
|
|
1624
1996
|
},
|
|
@@ -1630,17 +2002,28 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1630
2002
|
const refs = config.refs.filter((r) => !refNameMatches(r, name));
|
|
1631
2003
|
if (refs.length === before) return false;
|
|
1632
2004
|
await writeConfig({ ...config, refs });
|
|
2005
|
+
await removeRegistryCacheEntry(name);
|
|
1633
2006
|
return true;
|
|
1634
2007
|
},
|
|
1635
2008
|
|
|
1636
2009
|
async list(): Promise<ResolvedRef[]> {
|
|
1637
|
-
const config = await
|
|
1638
|
-
|
|
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));
|
|
1639
2017
|
},
|
|
1640
2018
|
|
|
1641
2019
|
async get(name: string): Promise<ResolvedRef | null> {
|
|
1642
|
-
const config = await
|
|
1643
|
-
|
|
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);
|
|
1644
2027
|
},
|
|
1645
2028
|
|
|
1646
2029
|
async update(name: string, updates: Partial<RefEntry>): Promise<boolean> {
|
|
@@ -1669,8 +2052,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1669
2052
|
updated.name = updates.name;
|
|
1670
2053
|
}
|
|
1671
2054
|
if (updates.scheme) updated.scheme = updates.scheme;
|
|
1672
|
-
if (updates.config)
|
|
1673
|
-
|
|
2055
|
+
if (updates.config)
|
|
2056
|
+
updated.config = { ...updated.config, ...updates.config };
|
|
2057
|
+
if (updates.sourceRegistry)
|
|
2058
|
+
updated.sourceRegistry = updates.sourceRegistry;
|
|
1674
2059
|
return updated;
|
|
1675
2060
|
});
|
|
1676
2061
|
if (!found) return false;
|
|
@@ -1678,38 +2063,63 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1678
2063
|
return true;
|
|
1679
2064
|
},
|
|
1680
2065
|
|
|
1681
|
-
async inspect(
|
|
2066
|
+
async inspect(
|
|
2067
|
+
name: string,
|
|
2068
|
+
opts?: { full?: boolean },
|
|
2069
|
+
): Promise<AgentInspection | null> {
|
|
1682
2070
|
const config = await readConfig();
|
|
1683
2071
|
const entry = findRef(config.refs ?? [], name);
|
|
1684
2072
|
if (!entry) throw new Error(`Ref "${name}" not found`);
|
|
1685
2073
|
|
|
1686
2074
|
const consumer = await buildConsumerForRef(entry);
|
|
1687
|
-
|
|
2075
|
+
const result = await consumer.inspect(
|
|
1688
2076
|
entry.sourceRegistry?.agentPath ?? entry.ref,
|
|
1689
2077
|
entry.sourceRegistry?.url,
|
|
1690
2078
|
opts,
|
|
1691
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;
|
|
1692
2088
|
},
|
|
1693
2089
|
|
|
1694
|
-
async call(
|
|
2090
|
+
async call(
|
|
2091
|
+
name: string,
|
|
2092
|
+
tool: string,
|
|
2093
|
+
params?: Record<string, unknown>,
|
|
2094
|
+
): Promise<CallAgentResponse> {
|
|
1695
2095
|
const config = await readConfig();
|
|
1696
2096
|
const entry = findRef(config.refs ?? [], name);
|
|
1697
2097
|
if (!entry) throw new Error(`Ref "${name}" not found`);
|
|
1698
2098
|
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
2099
|
+
const accessToken =
|
|
2100
|
+
(await readRefSecret(name, "access_token")) ??
|
|
2101
|
+
(await readRefSecret(name, "api_key")) ??
|
|
2102
|
+
(await readRefSecret(name, "token"));
|
|
1702
2103
|
|
|
1703
2104
|
// Resolve custom headers from config (e.g. { "X-API-Key": "secret:..." })
|
|
1704
2105
|
const refConfig = (entry.config ?? {}) as Record<string, unknown>;
|
|
1705
|
-
const rawHeaders = refConfig.headers as
|
|
2106
|
+
const rawHeaders = refConfig.headers as
|
|
2107
|
+
| Record<string, string>
|
|
2108
|
+
| undefined;
|
|
1706
2109
|
let resolvedHeaders: Record<string, string> | undefined;
|
|
1707
|
-
if (rawHeaders && typeof rawHeaders ===
|
|
2110
|
+
if (rawHeaders && typeof rawHeaders === "object") {
|
|
1708
2111
|
resolvedHeaders = {};
|
|
1709
2112
|
for (const [k, v] of Object.entries(rawHeaders)) {
|
|
1710
|
-
if (
|
|
1711
|
-
|
|
1712
|
-
|
|
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") {
|
|
1713
2123
|
resolvedHeaders[k] = v;
|
|
1714
2124
|
}
|
|
1715
2125
|
}
|
|
@@ -1718,9 +2128,15 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1718
2128
|
const doCall = async (token: string | null) => {
|
|
1719
2129
|
// Direct MCP only for redirect/proxy agents with an MCP upstream.
|
|
1720
2130
|
// API-mode agents must go through the registry (it does REST translation).
|
|
1721
|
-
const agentMode = (entry as any).mode ??
|
|
1722
|
-
if (token && entry.url && agentMode !==
|
|
1723
|
-
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
|
+
);
|
|
1724
2140
|
}
|
|
1725
2141
|
|
|
1726
2142
|
const consumer = await buildConsumerForRef(entry);
|
|
@@ -1789,13 +2205,20 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1789
2205
|
// server-side so local inspection would always return "missing").
|
|
1790
2206
|
const proxy = await resolveProxyForRef(entry);
|
|
1791
2207
|
if (proxy) {
|
|
1792
|
-
return forwardRefOpToProxy<RefAuthStatus>(
|
|
2208
|
+
return forwardRefOpToProxy<RefAuthStatus>(
|
|
2209
|
+
proxy.reg,
|
|
2210
|
+
proxy.agent,
|
|
2211
|
+
"auth-status",
|
|
2212
|
+
{ name },
|
|
2213
|
+
);
|
|
1793
2214
|
}
|
|
1794
2215
|
|
|
1795
2216
|
let security: SecuritySchemeSummary | null = null;
|
|
1796
2217
|
try {
|
|
1797
2218
|
const consumer = await buildConsumerForRef(entry);
|
|
1798
|
-
const info = await consumer.inspect(
|
|
2219
|
+
const info = await consumer.inspect(
|
|
2220
|
+
entry.sourceRegistry?.agentPath ?? entry.ref,
|
|
2221
|
+
);
|
|
1799
2222
|
if (info?.security) security = info.security;
|
|
1800
2223
|
} catch {
|
|
1801
2224
|
// Can't reach registry
|
|
@@ -1806,12 +2229,12 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1806
2229
|
}
|
|
1807
2230
|
|
|
1808
2231
|
const configKeys = Object.keys(entry.config ?? {});
|
|
1809
|
-
const
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
return
|
|
2232
|
+
const tryResolveField = makeTryResolve({ name, entry, security });
|
|
2233
|
+
async function canResolve(
|
|
2234
|
+
field: string,
|
|
2235
|
+
oauthMetadata?: OAuthServerMetadata | null,
|
|
2236
|
+
): Promise<boolean> {
|
|
2237
|
+
return (await tryResolveField(field, oauthMetadata)) !== null;
|
|
1815
2238
|
}
|
|
1816
2239
|
|
|
1817
2240
|
const fields: Record<string, CredentialField> = {};
|
|
@@ -1823,12 +2246,15 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1823
2246
|
};
|
|
1824
2247
|
const hasRegistration = !!securityExt.dynamicRegistration;
|
|
1825
2248
|
|
|
1826
|
-
let oauthMetadata:
|
|
2249
|
+
let oauthMetadata:
|
|
2250
|
+
| import("./mcp-client.js").OAuthServerMetadata
|
|
2251
|
+
| null = null;
|
|
1827
2252
|
let needsSecret = false;
|
|
1828
2253
|
if (securityExt.discoveryUrl) {
|
|
1829
2254
|
oauthMetadata = await tryFetchOAuthMetadata(securityExt.discoveryUrl);
|
|
1830
2255
|
if (oauthMetadata) {
|
|
1831
|
-
const authMethods =
|
|
2256
|
+
const authMethods =
|
|
2257
|
+
oauthMetadata.token_endpoint_auth_methods_supported ?? [];
|
|
1832
2258
|
needsSecret = !authMethods.includes("none");
|
|
1833
2259
|
}
|
|
1834
2260
|
}
|
|
@@ -1855,15 +2281,22 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1855
2281
|
};
|
|
1856
2282
|
} else if (security.type === "apiKey") {
|
|
1857
2283
|
const apiKeySec = security as {
|
|
1858
|
-
name?: string;
|
|
2284
|
+
name?: string;
|
|
2285
|
+
headers?: Record<string, { description?: string }>;
|
|
1859
2286
|
};
|
|
1860
2287
|
const toStorageKey = (headerName: string) =>
|
|
1861
|
-
headerName
|
|
2288
|
+
headerName
|
|
2289
|
+
.toLowerCase()
|
|
2290
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
2291
|
+
.replace(/^_|_$/g, "");
|
|
1862
2292
|
|
|
1863
2293
|
// config.headers: { "Header-Name": "value" } — check by header name (case-insensitive)
|
|
1864
|
-
const configHeaders = (
|
|
1865
|
-
Record<string, unknown> | undefined
|
|
1866
|
-
|
|
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
|
+
: [];
|
|
1867
2300
|
const hasConfigHeader = (name: string) =>
|
|
1868
2301
|
configHeaderKeys.some((k) => k.toLowerCase() === name.toLowerCase());
|
|
1869
2302
|
|
|
@@ -1877,7 +2310,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1877
2310
|
for (const headerName of declaredHeaders) {
|
|
1878
2311
|
const storageKey = toStorageKey(headerName);
|
|
1879
2312
|
const inConfigHeaders = hasConfigHeader(headerName);
|
|
1880
|
-
const inLegacyKeys =
|
|
2313
|
+
const inLegacyKeys =
|
|
2314
|
+
configKeys.includes(storageKey) || configKeys.includes("api_key");
|
|
1881
2315
|
fields[storageKey] = {
|
|
1882
2316
|
required: true,
|
|
1883
2317
|
automated: false,
|
|
@@ -1911,19 +2345,22 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1911
2345
|
return { name, security, complete, fields };
|
|
1912
2346
|
},
|
|
1913
2347
|
|
|
1914
|
-
async auth(
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
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> {
|
|
1927
2364
|
const config = await readConfig();
|
|
1928
2365
|
const entry = findRef(config.refs ?? [], name);
|
|
1929
2366
|
if (!entry) throw new Error(`Ref "${name}" not found`);
|
|
@@ -1932,24 +2369,26 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1932
2369
|
// agent. The registry owns the client_id/secret and returns an authorize
|
|
1933
2370
|
// URL pointing at the registry's OAuth callback domain, so the user
|
|
1934
2371
|
// completes the flow against the registry instead of localhost.
|
|
1935
|
-
const proxy = await resolveProxyForRef(entry, {
|
|
2372
|
+
const proxy = await resolveProxyForRef(entry, {
|
|
2373
|
+
preferLocal: opts?.preferLocal,
|
|
2374
|
+
});
|
|
1936
2375
|
if (proxy) {
|
|
1937
2376
|
const params: Record<string, unknown> = { name };
|
|
1938
2377
|
if (opts?.apiKey !== undefined) params.apiKey = opts.apiKey;
|
|
1939
2378
|
if (opts?.credentials) params.credentials = opts.credentials;
|
|
1940
2379
|
if (opts?.scopes) params.scopes = opts.scopes;
|
|
1941
2380
|
if (opts?.stateContext) params.stateContext = opts.stateContext;
|
|
1942
|
-
return forwardRefOpToProxy<AuthStartResult>(
|
|
2381
|
+
return forwardRefOpToProxy<AuthStartResult>(
|
|
2382
|
+
proxy.reg,
|
|
2383
|
+
proxy.agent,
|
|
2384
|
+
"auth",
|
|
2385
|
+
params,
|
|
2386
|
+
);
|
|
1943
2387
|
}
|
|
1944
2388
|
|
|
1945
2389
|
const status = await ref.authStatus(name);
|
|
1946
2390
|
const security = status.security;
|
|
1947
|
-
const
|
|
1948
|
-
|
|
1949
|
-
async function tryResolve(field: string, oauthMetadata?: import("./mcp-client.js").OAuthServerMetadata | null): Promise<string | null> {
|
|
1950
|
-
if (!resolve) return null;
|
|
1951
|
-
return resolve({ ref: name, field, entry: entry!, security, oauthMetadata });
|
|
1952
|
-
}
|
|
2391
|
+
const tryResolve = makeTryResolve({ name, entry, security });
|
|
1953
2392
|
|
|
1954
2393
|
if (!security || security.type === "none") {
|
|
1955
2394
|
return { type: "none", complete: true };
|
|
@@ -1957,20 +2396,31 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1957
2396
|
|
|
1958
2397
|
if (security.type === "apiKey") {
|
|
1959
2398
|
const apiKeySec = security as {
|
|
1960
|
-
name?: string;
|
|
2399
|
+
name?: string;
|
|
2400
|
+
prefix?: string;
|
|
1961
2401
|
headers?: Record<string, { description?: string }>;
|
|
1962
2402
|
};
|
|
1963
2403
|
|
|
1964
2404
|
const toStorageKey = (headerName: string) =>
|
|
1965
|
-
headerName
|
|
2405
|
+
headerName
|
|
2406
|
+
.toLowerCase()
|
|
2407
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
2408
|
+
.replace(/^_|_$/g, "");
|
|
1966
2409
|
|
|
1967
2410
|
// Check existing config.headers
|
|
1968
|
-
const existingHeaders = (
|
|
1969
|
-
Record<string,
|
|
2411
|
+
const existingHeaders = (
|
|
2412
|
+
(entry.config ?? {}) as Record<string, unknown>
|
|
2413
|
+
).headers as Record<string, string> | undefined;
|
|
1970
2414
|
|
|
1971
2415
|
// Collect declared headers: from security.headers or security.name
|
|
1972
|
-
const declaredHeaders: Array<{
|
|
1973
|
-
|
|
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
|
+
}))
|
|
1974
2424
|
: apiKeySec.name
|
|
1975
2425
|
? [{ headerName: apiKeySec.name }]
|
|
1976
2426
|
: [];
|
|
@@ -1982,12 +2432,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1982
2432
|
for (const { headerName, description } of declaredHeaders) {
|
|
1983
2433
|
const storageKey = toStorageKey(headerName);
|
|
1984
2434
|
// Check: credentials param → existing config.headers → legacy config key → resolve callback
|
|
1985
|
-
const value =
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
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));
|
|
1991
2445
|
|
|
1992
2446
|
if (value) {
|
|
1993
2447
|
resolvedHeaders[headerName] = value;
|
|
@@ -2009,23 +2463,30 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2009
2463
|
const encKey = options.encryptionKey;
|
|
2010
2464
|
const headersToStore: Record<string, string> = {};
|
|
2011
2465
|
for (const [h, v] of Object.entries(resolvedHeaders)) {
|
|
2012
|
-
headersToStore[h] = encKey
|
|
2466
|
+
headersToStore[h] = encKey
|
|
2467
|
+
? `${SECRET_PREFIX}${await encryptSecret(v, encKey)}`
|
|
2468
|
+
: v;
|
|
2013
2469
|
}
|
|
2014
2470
|
await ref.update(name, { config: { headers: headersToStore } });
|
|
2015
2471
|
return { type: "apiKey", complete: true };
|
|
2016
2472
|
}
|
|
2017
2473
|
|
|
2018
2474
|
// Fallback: no headers declared → generic api_key
|
|
2019
|
-
const key =
|
|
2475
|
+
const key =
|
|
2476
|
+
opts?.credentials?.["api_key"] ??
|
|
2477
|
+
opts?.apiKey ??
|
|
2478
|
+
(await tryResolve("api_key"));
|
|
2020
2479
|
if (!key) {
|
|
2021
2480
|
return {
|
|
2022
2481
|
type: "apiKey",
|
|
2023
2482
|
complete: false,
|
|
2024
|
-
fields: [
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2483
|
+
fields: [
|
|
2484
|
+
{
|
|
2485
|
+
name: "api_key",
|
|
2486
|
+
label: "API Key",
|
|
2487
|
+
secret: true,
|
|
2488
|
+
},
|
|
2489
|
+
],
|
|
2029
2490
|
};
|
|
2030
2491
|
}
|
|
2031
2492
|
await storeRefSecret(name, "api_key", key);
|
|
@@ -2037,12 +2498,24 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2037
2498
|
const isBasic = httpSec.scheme === "basic";
|
|
2038
2499
|
|
|
2039
2500
|
if (isBasic) {
|
|
2040
|
-
const username =
|
|
2041
|
-
|
|
2501
|
+
const username =
|
|
2502
|
+
opts?.credentials?.["username"] ?? (await tryResolve("username"));
|
|
2503
|
+
const password =
|
|
2504
|
+
opts?.credentials?.["password"] ?? (await tryResolve("password"));
|
|
2042
2505
|
if (!username || !password) {
|
|
2043
2506
|
const missingFields: AuthChallengeField[] = [];
|
|
2044
|
-
if (!username)
|
|
2045
|
-
|
|
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
|
+
});
|
|
2046
2519
|
return { type: "http", complete: false, fields: missingFields };
|
|
2047
2520
|
}
|
|
2048
2521
|
// Store as base64 encoded basic auth token
|
|
@@ -2052,7 +2525,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2052
2525
|
}
|
|
2053
2526
|
|
|
2054
2527
|
// Bearer token
|
|
2055
|
-
const token =
|
|
2528
|
+
const token =
|
|
2529
|
+
opts?.credentials?.["token"] ??
|
|
2530
|
+
opts?.apiKey ??
|
|
2531
|
+
(await tryResolve("token"));
|
|
2056
2532
|
if (!token) {
|
|
2057
2533
|
return {
|
|
2058
2534
|
type: "http",
|
|
@@ -2065,7 +2541,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2065
2541
|
}
|
|
2066
2542
|
|
|
2067
2543
|
if (security.type === "oauth2") {
|
|
2068
|
-
const flows = (
|
|
2544
|
+
const flows = (
|
|
2545
|
+
security as {
|
|
2546
|
+
flows?: {
|
|
2547
|
+
authorizationCode?: {
|
|
2548
|
+
authorizationUrl?: string;
|
|
2549
|
+
tokenUrl?: string;
|
|
2550
|
+
};
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
).flows;
|
|
2069
2554
|
const authCodeFlow = flows?.authorizationCode;
|
|
2070
2555
|
if (!authCodeFlow?.authorizationUrl) {
|
|
2071
2556
|
return {
|
|
@@ -2086,7 +2571,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2086
2571
|
}
|
|
2087
2572
|
// Fallback: construct metadata from the security scheme's explicit URLs
|
|
2088
2573
|
if (!metadata && authCodeFlow.tokenUrl) {
|
|
2089
|
-
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;
|
|
2090
2577
|
metadata = {
|
|
2091
2578
|
issuer: new URL(authUrl).origin,
|
|
2092
2579
|
authorization_endpoint: authUrl,
|
|
@@ -2101,25 +2588,34 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2101
2588
|
const redirectUri = callbackUrl();
|
|
2102
2589
|
|
|
2103
2590
|
// Resolve client credentials: callback → stored → dynamic registration
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2591
|
+
const fromHelper = await resolveOAuthClient({
|
|
2592
|
+
name,
|
|
2593
|
+
entry,
|
|
2594
|
+
security,
|
|
2595
|
+
metadata,
|
|
2596
|
+
});
|
|
2597
|
+
let clientId: string | undefined = fromHelper?.clientId;
|
|
2598
|
+
let clientSecret: string | undefined = fromHelper?.clientSecret;
|
|
2109
2599
|
|
|
2110
2600
|
if (!clientId && metadata.registration_endpoint) {
|
|
2111
|
-
const supportedAuthMethods =
|
|
2601
|
+
const supportedAuthMethods =
|
|
2602
|
+
metadata.token_endpoint_auth_methods_supported ?? ["none"];
|
|
2112
2603
|
const preferredMethod = supportedAuthMethods.includes("none")
|
|
2113
2604
|
? "none"
|
|
2114
|
-
: supportedAuthMethods[0] ?? "client_secret_post";
|
|
2115
|
-
|
|
2116
|
-
const securityClientName = (security as { clientName?: string })
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
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
|
+
);
|
|
2123
2619
|
clientId = reg.clientId;
|
|
2124
2620
|
clientSecret = reg.clientSecret;
|
|
2125
2621
|
await storeRefSecret(name, "client_id", clientId);
|
|
@@ -2132,10 +2628,18 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2132
2628
|
// Return fields telling the caller what OAuth credentials to provide
|
|
2133
2629
|
const missingFields: AuthChallengeField[] = [];
|
|
2134
2630
|
if (!clientId) {
|
|
2135
|
-
missingFields.push({
|
|
2631
|
+
missingFields.push({
|
|
2632
|
+
name: "client_id",
|
|
2633
|
+
label: "Client ID",
|
|
2634
|
+
secret: false,
|
|
2635
|
+
});
|
|
2136
2636
|
}
|
|
2137
2637
|
// Always ask for client_secret alongside client_id — most providers need it
|
|
2138
|
-
missingFields.push({
|
|
2638
|
+
missingFields.push({
|
|
2639
|
+
name: "client_secret",
|
|
2640
|
+
label: "Client Secret",
|
|
2641
|
+
secret: true,
|
|
2642
|
+
});
|
|
2139
2643
|
return { type: "oauth2", complete: false, fields: missingFields };
|
|
2140
2644
|
}
|
|
2141
2645
|
|
|
@@ -2149,32 +2653,42 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2149
2653
|
};
|
|
2150
2654
|
const state = btoa(JSON.stringify(statePayload));
|
|
2151
2655
|
|
|
2152
|
-
const securityExt2 = security as {
|
|
2153
|
-
|
|
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;
|
|
2154
2664
|
const agentScopes = [
|
|
2155
2665
|
...(securityExt2.requiredScopes ?? []),
|
|
2156
2666
|
...(flowScopes ? Object.keys(flowScopes) : []),
|
|
2157
2667
|
...(opts?.scopes ?? []),
|
|
2158
2668
|
].filter((v, i, a) => a.indexOf(v) === i);
|
|
2159
|
-
const scopes =
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2669
|
+
const scopes =
|
|
2670
|
+
agentScopes.length > 0
|
|
2671
|
+
? [
|
|
2672
|
+
...agentScopes,
|
|
2673
|
+
...(metadata.scopes_supported?.includes("openid")
|
|
2674
|
+
? ["openid"]
|
|
2675
|
+
: []),
|
|
2676
|
+
]
|
|
2677
|
+
: metadata.scopes_supported;
|
|
2165
2678
|
|
|
2166
2679
|
// Read provider-specific authorization params from the agent's security section
|
|
2167
2680
|
// (e.g., { access_type: 'offline', prompt: 'consent' } for Google)
|
|
2168
2681
|
const authorizationParams = securityExt2.authorizationParams;
|
|
2169
2682
|
|
|
2170
|
-
const { url: authorizeUrl, codeVerifier } =
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
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
|
+
});
|
|
2178
2692
|
|
|
2179
2693
|
// Persist pending state so handleCallback works across processes
|
|
2180
2694
|
await storePendingOAuth(state, {
|
|
@@ -2193,10 +2707,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2193
2707
|
return { type: security.type, complete: false };
|
|
2194
2708
|
},
|
|
2195
2709
|
|
|
2196
|
-
async authLocal(
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2710
|
+
async authLocal(
|
|
2711
|
+
name: string,
|
|
2712
|
+
opts?: {
|
|
2713
|
+
onAuthorizeUrl?: (url: string) => void;
|
|
2714
|
+
timeoutMs?: number;
|
|
2715
|
+
},
|
|
2716
|
+
): Promise<{ complete: boolean }> {
|
|
2200
2717
|
// `ref.auth` is already proxy-aware — for proxied refs it returns
|
|
2201
2718
|
// the authorizeUrl that the registry minted against its own
|
|
2202
2719
|
// callback domain. Everything below is identical for local and
|
|
@@ -2219,7 +2736,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2219
2736
|
// owns the credential store, so the user needs to submit via
|
|
2220
2737
|
// whatever UI the registry exposes. Supporting this through the
|
|
2221
2738
|
// proxy would need a remote form endpoint — out of scope here.
|
|
2222
|
-
if (
|
|
2739
|
+
if (
|
|
2740
|
+
result.fields &&
|
|
2741
|
+
result.fields.length > 0 &&
|
|
2742
|
+
result.type !== "oauth2"
|
|
2743
|
+
) {
|
|
2223
2744
|
if (proxy) {
|
|
2224
2745
|
throw new Error(
|
|
2225
2746
|
`Ref "${name}" is sourced from a proxied registry; submit credentials through ${proxy.agent} instead of a local form.`,
|
|
@@ -2256,19 +2777,23 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2256
2777
|
resolve({ complete: true });
|
|
2257
2778
|
} else {
|
|
2258
2779
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2259
|
-
res.end(
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2780
|
+
res.end(
|
|
2781
|
+
renderCredentialForm(
|
|
2782
|
+
name,
|
|
2783
|
+
authResult.fields ?? result.fields!,
|
|
2784
|
+
"Some credentials were missing or invalid.",
|
|
2785
|
+
),
|
|
2786
|
+
);
|
|
2264
2787
|
}
|
|
2265
2788
|
} catch (err) {
|
|
2266
2789
|
res.writeHead(500, { "Content-Type": "text/html" });
|
|
2267
|
-
res.end(
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2790
|
+
res.end(
|
|
2791
|
+
renderCredentialForm(
|
|
2792
|
+
name,
|
|
2793
|
+
result.fields!,
|
|
2794
|
+
err instanceof Error ? err.message : String(err),
|
|
2795
|
+
),
|
|
2796
|
+
);
|
|
2272
2797
|
}
|
|
2273
2798
|
return;
|
|
2274
2799
|
}
|
|
@@ -2315,7 +2840,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2315
2840
|
const state = reqUrl.searchParams.get("state");
|
|
2316
2841
|
|
|
2317
2842
|
if (!code || !state) {
|
|
2318
|
-
const error =
|
|
2843
|
+
const error =
|
|
2844
|
+
reqUrl.searchParams.get("error") ?? "missing code/state";
|
|
2319
2845
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
2320
2846
|
res.end(`<h1>Error</h1><p>${error}</p>`);
|
|
2321
2847
|
server.close();
|
|
@@ -2331,7 +2857,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2331
2857
|
resolve({ complete: cbResult.complete });
|
|
2332
2858
|
} catch (err) {
|
|
2333
2859
|
res.writeHead(500, { "Content-Type": "text/html" });
|
|
2334
|
-
res.end(
|
|
2860
|
+
res.end(
|
|
2861
|
+
`<h1>Error</h1><p>${err instanceof Error ? err.message : String(err)}</p>`,
|
|
2862
|
+
);
|
|
2335
2863
|
server.close();
|
|
2336
2864
|
reject(err);
|
|
2337
2865
|
}
|
|
@@ -2365,22 +2893,36 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2365
2893
|
const refreshToken = await readRefSecret(name, "refresh_token");
|
|
2366
2894
|
if (!refreshToken) return null;
|
|
2367
2895
|
|
|
2368
|
-
//
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
//
|
|
2896
|
+
// Resolve token endpoint + OAuth client via the host's
|
|
2897
|
+
// `resolveCredentials` chain. Same chain `auth` uses (see
|
|
2898
|
+
// `resolveOAuthClient`) — kept symmetric so refresh works on every
|
|
2899
|
+
// ref `auth` works on, including first-party registry-hosted
|
|
2900
|
+
// clients whose creds live in env / tenant scope, not the user's
|
|
2901
|
+
// per-ref config.
|
|
2374
2902
|
const entry = await ref.get(name);
|
|
2375
2903
|
if (!entry) return null;
|
|
2376
2904
|
|
|
2377
|
-
const
|
|
2378
|
-
const security =
|
|
2379
|
-
const flows =
|
|
2905
|
+
const status = await ref.authStatus(name);
|
|
2906
|
+
const security = status.security;
|
|
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;
|
|
2380
2918
|
const authCodeFlow = flows?.authorizationCode;
|
|
2381
|
-
const tokenUrl =
|
|
2919
|
+
const tokenUrl = authCodeFlow?.refreshUrl ?? authCodeFlow?.tokenUrl;
|
|
2382
2920
|
if (!tokenUrl) return null;
|
|
2383
2921
|
|
|
2922
|
+
const oauthClient = await resolveOAuthClient({ name, entry, security });
|
|
2923
|
+
if (!oauthClient) return null;
|
|
2924
|
+
const { clientId, clientSecret } = oauthClient;
|
|
2925
|
+
|
|
2384
2926
|
// POST to the token endpoint with grant_type=refresh_token
|
|
2385
2927
|
const body = new URLSearchParams({
|
|
2386
2928
|
grant_type: "refresh_token",
|
|
@@ -2399,7 +2941,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2399
2941
|
|
|
2400
2942
|
if (!res.ok) return null;
|
|
2401
2943
|
|
|
2402
|
-
const data = await res.json() as Record<string, unknown>;
|
|
2944
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
2403
2945
|
const newAccessToken = data.access_token as string | undefined;
|
|
2404
2946
|
if (!newAccessToken) return null;
|
|
2405
2947
|
|
|
@@ -2417,7 +2959,14 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2417
2959
|
// Top-level callback handler
|
|
2418
2960
|
// ==========================================
|
|
2419
2961
|
|
|
2420
|
-
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
|
+
}> {
|
|
2421
2970
|
const pending = await consumePendingOAuth(params.state);
|
|
2422
2971
|
if (!pending) {
|
|
2423
2972
|
throw new Error(`No pending OAuth flow for state "${params.state}".`);
|
|
@@ -2433,13 +2982,19 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2433
2982
|
|
|
2434
2983
|
await storeRefSecret(pending.refName, "access_token", tokens.accessToken);
|
|
2435
2984
|
if (tokens.refreshToken) {
|
|
2436
|
-
await storeRefSecret(
|
|
2985
|
+
await storeRefSecret(
|
|
2986
|
+
pending.refName,
|
|
2987
|
+
"refresh_token",
|
|
2988
|
+
tokens.refreshToken,
|
|
2989
|
+
);
|
|
2437
2990
|
}
|
|
2438
2991
|
|
|
2439
2992
|
let stateContext: Record<string, unknown> | undefined;
|
|
2440
2993
|
try {
|
|
2441
2994
|
stateContext = JSON.parse(atob(params.state));
|
|
2442
|
-
} catch {
|
|
2995
|
+
} catch {
|
|
2996
|
+
/* state wasn't base64 JSON — legacy format */
|
|
2997
|
+
}
|
|
2443
2998
|
|
|
2444
2999
|
return { refName: pending.refName, complete: true, stateContext };
|
|
2445
3000
|
}
|