melchizedek-agents 1.1.0 → 1.3.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/.env.example +19 -0
- package/AGENT_SETUP.md +3 -1
- package/CHANGELOG.md +373 -0
- package/DOCUMENTATION.md +193 -21
- package/ONBOARDING.md +378 -0
- package/QUICKSTART.md +15 -2
- package/README.md +26 -3
- package/config/agents/examples/connectors.yaml +82 -0
- package/config/agents/examples/council.yaml +1 -1
- package/config/agents/examples/critic.yaml +8 -8
- package/config/agents/examples/image_production.yaml +4 -3
- package/config/agents/syndicate.schema.json +389 -33
- package/config/agents/syndicateSchema.yaml +18 -6
- package/config/agents/templates/intake_extractor.yaml +3 -2
- package/config/agents/templates/systems_operator.yaml +5 -2
- package/dist/lib/a2a/app.d.ts +12 -0
- package/dist/lib/a2a/app.js +25 -11
- package/dist/lib/a2a/executor.js +14 -6
- package/dist/lib/a2a/oauthSetup.d.ts +16 -0
- package/dist/lib/a2a/oauthSetup.js +77 -8
- package/dist/lib/agentDialect.d.ts +18 -8
- package/dist/lib/agentDialect.js +36 -19
- package/dist/lib/chatgpt/adapter.d.ts +50 -0
- package/dist/lib/chatgpt/adapter.js +107 -0
- package/dist/lib/chatgpt/cli.d.ts +23 -0
- package/dist/lib/chatgpt/cli.js +77 -0
- package/dist/lib/chatgpt/oauth.d.ts +103 -0
- package/dist/lib/chatgpt/oauth.js +448 -0
- package/dist/lib/chatgpt/state.d.ts +98 -0
- package/dist/lib/chatgpt/state.js +145 -0
- package/dist/lib/chatgpt/store.d.ts +24 -0
- package/dist/lib/chatgpt/store.js +94 -0
- package/dist/lib/compile.d.ts +38 -7
- package/dist/lib/compile.js +97 -62
- package/dist/lib/compileNative.d.ts +11 -3
- package/dist/lib/compileNative.js +18 -1
- package/dist/lib/dispatch.js +6 -2
- package/dist/lib/doctor.d.ts +107 -0
- package/dist/lib/doctor.js +217 -7
- package/dist/lib/loadSyndicate.d.ts +15 -0
- package/dist/lib/mcp/server.d.ts +195 -0
- package/dist/lib/mcp/server.js +564 -0
- package/dist/lib/memory/supabaseMemoryService.js +2 -2
- package/dist/lib/models/adapterResolver.js +6 -1
- package/dist/lib/models/capabilities.d.ts +9 -0
- package/dist/lib/models/capabilities.js +24 -0
- package/dist/lib/models/chatCompletionsAdapter.d.ts +11 -3
- package/dist/lib/models/chatCompletionsAdapter.js +19 -5
- package/dist/lib/models/claudeAdapter.js +4 -0
- package/dist/lib/models/contract.d.ts +5 -2
- package/dist/lib/models/gateway.d.ts +3 -2
- package/dist/lib/models/gateway.js +8 -4
- package/dist/lib/models/gatewayAdapter.d.ts +7 -1
- package/dist/lib/models/gatewayAdapter.js +8 -0
- package/dist/lib/models/geminiAdapter.js +8 -2
- package/dist/lib/models/genaiMapping.d.ts +8 -4
- package/dist/lib/models/genaiMapping.js +23 -9
- package/dist/lib/models/gptAdapter.d.ts +4 -2
- package/dist/lib/models/gptAdapter.js +11 -4
- package/dist/lib/models/grokAdapter.d.ts +3 -1
- package/dist/lib/models/grokAdapter.js +11 -3
- package/dist/lib/models/kimiAdapter.d.ts +2 -1
- package/dist/lib/models/kimiAdapter.js +3 -3
- package/dist/lib/models/reasoning.d.ts +30 -2
- package/dist/lib/models/reasoning.js +56 -5
- package/dist/lib/onboarding.d.ts +135 -0
- package/dist/lib/onboarding.js +578 -0
- package/dist/lib/runtime/approvals.d.ts +2 -0
- package/dist/lib/runtime/approvals.js +4 -3
- package/dist/lib/runtime/credentials.d.ts +6 -0
- package/dist/lib/runtime/native/agentLoop.d.ts +5 -2
- package/dist/lib/runtime/native/agentLoop.js +2 -0
- package/dist/lib/runtime/native/checkpoint.d.ts +12 -5
- package/dist/lib/runtime/native/checkpoint.js +13 -19
- package/dist/lib/runtime/native/delegate.d.ts +82 -1
- package/dist/lib/runtime/native/delegate.js +157 -2
- package/dist/lib/runtime/native/interrupts.d.ts +88 -5
- package/dist/lib/runtime/native/interrupts.js +220 -30
- package/dist/lib/runtime/syndicateTurn.d.ts +4 -2
- package/dist/lib/runtime/syndicateTurn.js +324 -77
- package/dist/lib/runtime/turnControl.d.ts +41 -0
- package/dist/lib/session/transcript.d.ts +9 -0
- package/dist/lib/session/transcript.js +20 -1
- package/dist/lib/skills.d.ts +18 -0
- package/dist/lib/skills.js +73 -1
- package/dist/lib/storage/postgres/index.d.ts +6 -1
- package/dist/lib/storage/postgres/index.js +1 -1
- package/dist/lib/storage/postgres/taskQueue.d.ts +7 -3
- package/dist/lib/storage/postgres/taskQueue.js +10 -3
- package/dist/lib/syndicateSchema.d.ts +184 -9
- package/dist/lib/syndicateSchema.js +143 -29
- package/dist/lib/tools/auth.d.ts +13 -1
- package/dist/lib/tools/auth.js +10 -2
- package/dist/lib/tools/credentialHosts.d.ts +88 -0
- package/dist/lib/tools/credentialHosts.js +165 -0
- package/dist/lib/tools/credentialUses.d.ts +98 -0
- package/dist/lib/tools/credentialUses.js +120 -0
- package/dist/lib/tools/generateImageTool.js +2 -1
- package/dist/lib/tools/inspectImageTool.js +1 -1
- package/dist/lib/tools/mcpToolFactory.d.ts +29 -1
- package/dist/lib/tools/mcpToolFactory.js +72 -26
- package/dist/lib/tools/mcpTransports.d.ts +12 -0
- package/dist/lib/tools/mcpTransports.js +11 -0
- package/dist/lib/tools/oauthConsent.d.ts +18 -3
- package/dist/lib/tools/oauthConsent.js +57 -15
- package/dist/lib/tools/oauthDiscovery.d.ts +202 -0
- package/dist/lib/tools/oauthDiscovery.js +616 -0
- package/dist/lib/tools/oauthHosts.d.ts +2 -0
- package/dist/lib/tools/oauthHosts.js +2 -2
- package/dist/lib/tools/oauthTools.d.ts +54 -13
- package/dist/lib/tools/oauthTools.js +106 -23
- package/dist/lib/tools/openapiTools.d.ts +7 -0
- package/dist/lib/tools/openapiTools.js +49 -3
- package/dist/lib/tools/taskTools.d.ts +32 -2
- package/dist/lib/tools/taskTools.js +93 -33
- package/dist/lib/tools/xApiSearchTool.js +2 -2
- package/dist/lib/workflow/turn.d.ts +22 -3
- package/dist/lib/workflow/turn.js +151 -31
- package/dist/lib/workflow.d.ts +10 -7
- package/dist/lib/workflow.js +10 -7
- package/dist/scripts/a2a_server.d.ts +2 -2
- package/dist/scripts/a2a_server.js +19 -2
- package/dist/scripts/assistant_worker.d.ts +2 -0
- package/dist/scripts/assistant_worker.js +24 -5
- package/dist/scripts/doctor.d.ts +4 -2
- package/dist/scripts/doctor.js +6 -4
- package/dist/scripts/mcp_server.d.ts +12 -0
- package/dist/scripts/mcp_server.js +345 -0
- package/dist/scripts/setup.d.ts +25 -0
- package/dist/scripts/setup.js +181 -0
- package/dist/scripts/skills_install.d.ts +3 -1
- package/dist/scripts/skills_install.js +17 -2
- package/dist/scripts/syndicate_chat.js +12 -3
- package/dist/scripts/yaml_codemod.d.ts +12 -2
- package/dist/scripts/yaml_codemod.js +115 -33
- package/package.json +8 -2
- package/skills/README.md +11 -2
- package/skills/briefs/_shared.md +1 -1
- package/skills/briefs/melchizedek-models.md +4 -2
- package/skills/briefs/skills-readme.md +3 -1
- package/skills/melchizedek-models/SKILL.md +6 -2
- package/skills/melchizedek-onboard/SKILL.md +71 -0
- package/skills/melchizedek-onboard-cloud/SKILL.md +38 -0
- package/skills/melchizedek-onboard-keys/SKILL.md +47 -0
- package/skills/melchizedek-onboard-local/SKILL.md +41 -0
- package/skills/melchizedek-onboard-serve/SKILL.md +43 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/tools/credentialUses.ts — every static credential variable a syndicate
|
|
3
|
+
* YAML sends somewhere, and where (ADR 0122).
|
|
4
|
+
*
|
|
5
|
+
* A YAML sends a static credential in three places: an OpenAPI entry's
|
|
6
|
+
* `auth.bearer_env` or `auth.api_key.env` (to the spec's server, or
|
|
7
|
+
* `base_url`), and an `oauth2` block's `client_secret_env` (to its
|
|
8
|
+
* `token_url`; the OpenAPI entry's or the MCP server's `mcp_auth`). The
|
|
9
|
+
* operator's credential host allowlist (lib/tools/credentialHosts.ts) is
|
|
10
|
+
* checked against them when a served syndicate loads
|
|
11
|
+
* (`syndicateCredentialHostProblems`), and the doctor and the server binary
|
|
12
|
+
* name each variable no allowlist binds (`unboundCredentialEnvs`).
|
|
13
|
+
*
|
|
14
|
+
* Names and hosts only: no value is read here.
|
|
15
|
+
*/
|
|
16
|
+
import type { CredentialHostAllowlist } from './credentialHosts.ts';
|
|
17
|
+
/** One credential variable a YAML sends, and to what. */
|
|
18
|
+
export interface CredentialUse {
|
|
19
|
+
/** The agent that holds the tools ("Parent › Child" inside a nested file). */
|
|
20
|
+
agent: string;
|
|
21
|
+
/** `openapi <spec>` or `mcp <url>`. */
|
|
22
|
+
tools: string;
|
|
23
|
+
/** The variable's name. */
|
|
24
|
+
env: string;
|
|
25
|
+
/** Where the YAML names it. */
|
|
26
|
+
role: 'bearer_env' | 'api_key' | 'client_secret_env';
|
|
27
|
+
/**
|
|
28
|
+
* The URLs its value goes to: the token endpoint for a client secret,
|
|
29
|
+
* `base_url` for an OpenAPI entry that sets one. Absent for an OpenAPI
|
|
30
|
+
* entry whose server is in its spec (read only when checked, by
|
|
31
|
+
* `syndicateCredentialHostProblems`).
|
|
32
|
+
*/
|
|
33
|
+
destinations?: string[];
|
|
34
|
+
/** The OpenAPI entry, for its spec's servers. */
|
|
35
|
+
openapi?: OpenApiEntryLike;
|
|
36
|
+
}
|
|
37
|
+
/** The parts of an `oauth2` block read here. */
|
|
38
|
+
interface OAuth2Like {
|
|
39
|
+
token_url?: string;
|
|
40
|
+
client_secret_env?: string;
|
|
41
|
+
}
|
|
42
|
+
/** The parts of an `openapi:` entry read here. */
|
|
43
|
+
interface OpenApiEntryLike {
|
|
44
|
+
spec?: string;
|
|
45
|
+
base_url?: string;
|
|
46
|
+
auth?: {
|
|
47
|
+
bearer_env?: string;
|
|
48
|
+
api_key?: {
|
|
49
|
+
env?: string;
|
|
50
|
+
};
|
|
51
|
+
oauth2?: OAuth2Like;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
interface AgentLike {
|
|
55
|
+
name?: string;
|
|
56
|
+
openapi?: OpenApiEntryLike[];
|
|
57
|
+
mcp_server_url?: string;
|
|
58
|
+
mcp_auth?: {
|
|
59
|
+
oauth2?: OAuth2Like;
|
|
60
|
+
};
|
|
61
|
+
mcp_servers?: Array<{
|
|
62
|
+
url?: string;
|
|
63
|
+
auth?: {
|
|
64
|
+
oauth2?: OAuth2Like;
|
|
65
|
+
};
|
|
66
|
+
}>;
|
|
67
|
+
yaml_reference?: string;
|
|
68
|
+
}
|
|
69
|
+
/** The parts of a syndicate config read here. */
|
|
70
|
+
export interface SyndicateLike {
|
|
71
|
+
orchestrator?: AgentLike;
|
|
72
|
+
subagents?: AgentLike[];
|
|
73
|
+
}
|
|
74
|
+
/** The credential variables one agent's tools send. */
|
|
75
|
+
export declare function agentCredentialUses(agent: AgentLike, prefix?: string): CredentialUse[];
|
|
76
|
+
/**
|
|
77
|
+
* Every credential variable these syndicates send, orchestrators and
|
|
78
|
+
* subagents, following a nested `yaml_reference:` through `load` when given
|
|
79
|
+
* (once per file; agent names then read "Parent › Child").
|
|
80
|
+
*/
|
|
81
|
+
export declare function syndicateCredentialUses(configs: readonly SyndicateLike[], load?: (ref: string) => SyndicateLike): CredentialUse[];
|
|
82
|
+
/**
|
|
83
|
+
* Why these syndicates may not be served here, one line per refusal
|
|
84
|
+
* (ADR 0122): each credential variable they send, against the operator's
|
|
85
|
+
* credential host allowlist. With none configured, nothing is refused. The
|
|
86
|
+
* A2A server checks every syndicate it loads to serve; the compile checks
|
|
87
|
+
* again, and each call once more.
|
|
88
|
+
*/
|
|
89
|
+
export declare function syndicateCredentialHostProblems(configs: readonly SyndicateLike[], options?: {
|
|
90
|
+
load?: (ref: string) => SyndicateLike;
|
|
91
|
+
allowlist?: CredentialHostAllowlist | null;
|
|
92
|
+
}): string[];
|
|
93
|
+
/**
|
|
94
|
+
* The credential variables these uses send that no allowlist binds, sorted
|
|
95
|
+
* and once each: every one of them when none is configured. Names only.
|
|
96
|
+
*/
|
|
97
|
+
export declare function unboundCredentialEnvs(uses: readonly CredentialUse[], allowlist: CredentialHostAllowlist | null | undefined): string[];
|
|
98
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/tools/credentialUses.ts — every static credential variable a syndicate
|
|
3
|
+
* YAML sends somewhere, and where (ADR 0122).
|
|
4
|
+
*
|
|
5
|
+
* A YAML sends a static credential in three places: an OpenAPI entry's
|
|
6
|
+
* `auth.bearer_env` or `auth.api_key.env` (to the spec's server, or
|
|
7
|
+
* `base_url`), and an `oauth2` block's `client_secret_env` (to its
|
|
8
|
+
* `token_url`; the OpenAPI entry's or the MCP server's `mcp_auth`). The
|
|
9
|
+
* operator's credential host allowlist (lib/tools/credentialHosts.ts) is
|
|
10
|
+
* checked against them when a served syndicate loads
|
|
11
|
+
* (`syndicateCredentialHostProblems`), and the doctor and the server binary
|
|
12
|
+
* name each variable no allowlist binds (`unboundCredentialEnvs`).
|
|
13
|
+
*
|
|
14
|
+
* Names and hosts only: no value is read here.
|
|
15
|
+
*/
|
|
16
|
+
import { resolve } from 'node:path';
|
|
17
|
+
import { credentialHostProblem, credentialHosts } from './credentialHosts.js';
|
|
18
|
+
import { openApiServers } from './openapiTools.js';
|
|
19
|
+
/** The token endpoint a client secret goes to; none named yet for a grant whose endpoints are discovered (ADR 0124). */
|
|
20
|
+
const tokenUrlOf = (o) => (o.token_url ? [o.token_url] : []);
|
|
21
|
+
/** The credential variables one agent's tools send. */
|
|
22
|
+
export function agentCredentialUses(agent, prefix = '') {
|
|
23
|
+
const name = `${prefix}${agent.name ?? ''}`;
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const entry of agent.openapi ?? []) {
|
|
26
|
+
const tools = `openapi ${entry?.spec ?? ''}`;
|
|
27
|
+
const auth = entry?.auth;
|
|
28
|
+
if (!auth)
|
|
29
|
+
continue;
|
|
30
|
+
const servers = entry.base_url ? { destinations: [entry.base_url] } : {};
|
|
31
|
+
if (auth.bearer_env)
|
|
32
|
+
out.push({ agent: name, tools, env: auth.bearer_env, role: 'bearer_env', ...servers, openapi: entry });
|
|
33
|
+
if (auth.api_key?.env)
|
|
34
|
+
out.push({ agent: name, tools, env: auth.api_key.env, role: 'api_key', ...servers, openapi: entry });
|
|
35
|
+
if (auth.oauth2?.client_secret_env)
|
|
36
|
+
out.push({ agent: name, tools, env: auth.oauth2.client_secret_env, role: 'client_secret_env', destinations: tokenUrlOf(auth.oauth2) });
|
|
37
|
+
}
|
|
38
|
+
const mcp = agent.mcp_auth?.oauth2;
|
|
39
|
+
if (mcp?.client_secret_env)
|
|
40
|
+
out.push({ agent: name, tools: `mcp ${agent.mcp_server_url ?? ''}`, env: mcp.client_secret_env, role: 'client_secret_env', destinations: tokenUrlOf(mcp) });
|
|
41
|
+
// Each mcp_servers entry's grant (ADR 0124), as the single server's.
|
|
42
|
+
for (const server of agent.mcp_servers ?? []) {
|
|
43
|
+
const o = server?.auth?.oauth2;
|
|
44
|
+
if (o?.client_secret_env)
|
|
45
|
+
out.push({ agent: name, tools: `mcp ${server.url ?? ''}`, env: o.client_secret_env, role: 'client_secret_env', destinations: tokenUrlOf(o) });
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Every credential variable these syndicates send, orchestrators and
|
|
51
|
+
* subagents, following a nested `yaml_reference:` through `load` when given
|
|
52
|
+
* (once per file; agent names then read "Parent › Child").
|
|
53
|
+
*/
|
|
54
|
+
export function syndicateCredentialUses(configs, load) {
|
|
55
|
+
const out = [];
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
const visit = (config, prefix) => {
|
|
58
|
+
const agents = [config.orchestrator, ...(config.subagents ?? [])].filter((a) => !!a);
|
|
59
|
+
for (const agent of agents) {
|
|
60
|
+
if (agent.yaml_reference) {
|
|
61
|
+
if (!load || seen.has(agent.yaml_reference))
|
|
62
|
+
continue;
|
|
63
|
+
seen.add(agent.yaml_reference);
|
|
64
|
+
visit(load(agent.yaml_reference), `${prefix}${agent.name ?? agent.yaml_reference} › `);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
out.push(...agentCredentialUses(agent, prefix));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
for (const config of configs)
|
|
71
|
+
visit(config, '');
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/** The URLs a use's value goes to; an OpenAPI spec that cannot be read gives none (its compile fails on its own). */
|
|
75
|
+
function destinationsOf(use) {
|
|
76
|
+
if (use.destinations)
|
|
77
|
+
return use.destinations;
|
|
78
|
+
if (!use.openapi?.spec)
|
|
79
|
+
return [];
|
|
80
|
+
try {
|
|
81
|
+
return openApiServers(use.openapi, resolve(process.cwd()));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Why these syndicates may not be served here, one line per refusal
|
|
89
|
+
* (ADR 0122): each credential variable they send, against the operator's
|
|
90
|
+
* credential host allowlist. With none configured, nothing is refused. The
|
|
91
|
+
* A2A server checks every syndicate it loads to serve; the compile checks
|
|
92
|
+
* again, and each call once more.
|
|
93
|
+
*/
|
|
94
|
+
export function syndicateCredentialHostProblems(configs, options = {}) {
|
|
95
|
+
const allowlist = options.allowlist === undefined ? credentialHosts() ?? null : options.allowlist;
|
|
96
|
+
if (!allowlist)
|
|
97
|
+
return [];
|
|
98
|
+
const out = new Set();
|
|
99
|
+
for (const use of syndicateCredentialUses(configs, options.load)) {
|
|
100
|
+
const role = use.role === 'client_secret_env' ? 'token_url' : 'server';
|
|
101
|
+
const where = `${use.agent} · ${use.tools} · ${use.role}`;
|
|
102
|
+
if (!Object.hasOwn(allowlist, use.env)) {
|
|
103
|
+
out.add(`${where}: ${credentialHostProblem(use.env, '', role, allowlist)}`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
for (const url of destinationsOf(use)) {
|
|
107
|
+
const problem = credentialHostProblem(use.env, url, role, allowlist);
|
|
108
|
+
if (problem)
|
|
109
|
+
out.add(`${where}: ${problem}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return [...out];
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The credential variables these uses send that no allowlist binds, sorted
|
|
116
|
+
* and once each: every one of them when none is configured. Names only.
|
|
117
|
+
*/
|
|
118
|
+
export function unboundCredentialEnvs(uses, allowlist) {
|
|
119
|
+
return [...new Set(uses.map((u) => u.env))].filter((name) => !allowlist || !Object.hasOwn(allowlist, name)).sort();
|
|
120
|
+
}
|
|
@@ -38,7 +38,8 @@ export const generateImageContract = defineTool({
|
|
|
38
38
|
console.error(`[ImageTool ERROR] Initialization failed: ${err.message}`);
|
|
39
39
|
throw err;
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
// Sizes and which options are set, never their values: the prompt carries the person's words.
|
|
42
|
+
console.log(`[ImageTool] Parameters - prompt ${prompt.length} chars; set: ${[style && 'style', aspect_ratio && 'aspect_ratio', color_palette && 'color_palette'].filter(Boolean).join(', ') || 'none'}`);
|
|
42
43
|
const ai = new GoogleGenAI({ apiKey });
|
|
43
44
|
// Build a rich prompt that bakes in the structured parameters.
|
|
44
45
|
const parts = [prompt];
|
|
@@ -46,7 +46,7 @@ export const inspectImageContract = defineTool({
|
|
|
46
46
|
image_path: z.string().describe('Path to the saved image, e.g. "outputs/image_1234.png".'),
|
|
47
47
|
}),
|
|
48
48
|
execute: async ({ image_path }) => {
|
|
49
|
-
console.log(`[InspectTool] Blind inventory requested
|
|
49
|
+
console.log(`[InspectTool] Blind inventory requested (path ${image_path.length} chars).`);
|
|
50
50
|
// Confine reads to the outputs/ directory — this tool inventories
|
|
51
51
|
// generated artifacts, not the filesystem. OUTPUTS_DIR env overrides
|
|
52
52
|
// the cwd default (must match generateImageTool's).
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ToolDeclaration } from '../models/contract.ts';
|
|
2
2
|
import type { RedirectPolicy } from '../net/redirects.ts';
|
|
3
3
|
import type { OAuth2AuthConfig } from './oauthTools.ts';
|
|
4
|
+
import { MCP_TRANSPORTS } from './mcpTransports.ts';
|
|
5
|
+
import type { McpTransportKind } from './mcpTransports.ts';
|
|
4
6
|
import type { Tool } from './tool.ts';
|
|
5
7
|
export declare function assertSafeMcpUrl(raw: string, env?: NodeJS.ProcessEnv): Promise<URL>;
|
|
6
8
|
export declare function mcpAuthHeaders(url: URL, env?: NodeJS.ProcessEnv): Record<string, string>;
|
|
@@ -23,6 +25,15 @@ export declare const mcpFetch: (input: string | URL | Request, init?: RequestIni
|
|
|
23
25
|
export declare const MAX_MCP_DESCRIPTION_CHARS = 1000;
|
|
24
26
|
/** Every tool's shared result limit (lib/tools/tool.ts). */
|
|
25
27
|
export declare const MAX_MCP_RESULT_CHARS = 20000;
|
|
28
|
+
export { MCP_TRANSPORTS };
|
|
29
|
+
export type { McpTransportKind };
|
|
30
|
+
/**
|
|
31
|
+
* True when a failed Streamable HTTP connect is the spec's signal that the
|
|
32
|
+
* server speaks only the older HTTP+SSE transport: the initialize POST
|
|
33
|
+
* answered with a 4xx (404, 405, 400 …). A 401 or 403 is the server refusing
|
|
34
|
+
* the credential, which another transport would not change.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isSseFallbackSignal(error: unknown): boolean;
|
|
26
37
|
/**
|
|
27
38
|
* Close every open MCP connection. A connection otherwise lives as long as
|
|
28
39
|
* the process (its tools are called for the life of the compiled agent), and
|
|
@@ -44,10 +55,27 @@ export declare function mcpToolParameters(inputSchema: {
|
|
|
44
55
|
} | undefined): ToolDeclaration['parameters'];
|
|
45
56
|
/** How an agent reaches its MCP server: which tools it may use, and the OAuth grant the server takes. */
|
|
46
57
|
export interface McpToolOptions {
|
|
47
|
-
/** `mcp_tools
|
|
58
|
+
/** `mcp_tools` (or an `mcp_servers` entry's `tools`): the names this agent may use. Required with an authorization_code grant. */
|
|
48
59
|
tools?: string[];
|
|
49
60
|
/** `mcp_auth.oauth2` (ADR 0112). Without it, MCP_BEARER_TOKENS applies. */
|
|
50
61
|
oauth2?: OAuth2AuthConfig;
|
|
62
|
+
/** `mcp_transport` (or an entry's `transport`): default `auto` (ADR 0124). */
|
|
63
|
+
transport?: McpTransportKind;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* One entry of an agent's `mcp_servers:` list (ADR 0124): a server, the
|
|
67
|
+
* tools of it the agent may use, the grant it takes and its transport.
|
|
68
|
+
*/
|
|
69
|
+
export interface McpServerConfig {
|
|
70
|
+
/** A short name for the server, unique on the agent: logs and errors name it. */
|
|
71
|
+
name: string;
|
|
72
|
+
url: string;
|
|
73
|
+
/** The server's tools this agent may use, by name. Required: names across servers are refused on collision at load. */
|
|
74
|
+
tools: string[];
|
|
75
|
+
auth?: {
|
|
76
|
+
oauth2: OAuth2AuthConfig;
|
|
77
|
+
};
|
|
78
|
+
transport?: McpTransportKind;
|
|
51
79
|
}
|
|
52
80
|
/** Per-user connections at most per authorization-code server; the least recently used is closed first. */
|
|
53
81
|
export declare const MAX_MCP_USER_CONNECTIONS = 64;
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* lib/tools/mcpToolFactory.ts — a remote MCP server's tools, as the
|
|
3
3
|
* engine's own Tools (lib/tools/tool.ts, ADR 0062).
|
|
4
4
|
*
|
|
5
|
-
* An agent's `mcp_server_url:`
|
|
6
|
-
*
|
|
5
|
+
* An agent's `mcp_server_url:` (or each entry of `mcp_servers:`, ADR 0124)
|
|
6
|
+
* connects here over Streamable HTTP, falling back to the legacy SSE
|
|
7
|
+
* transport when the server answers the initialize POST with the spec's
|
|
8
|
+
* fallback signal (a 4xx other than 401/403), or over the transport the YAML
|
|
9
|
+
* names. Every tool the server lists becomes an own Tool whose declaration is the server's
|
|
7
10
|
* description (bounded) and input schema, and whose execute calls the
|
|
8
11
|
* server and returns its text (bounded). loadMcpTools returns them;
|
|
9
12
|
* createMcpTools is the name the compiler calls.
|
|
@@ -20,11 +23,13 @@
|
|
|
20
23
|
*/
|
|
21
24
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
22
25
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
26
|
+
import { StreamableHTTPClientTransport, StreamableHTTPError } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
23
27
|
import { toContractJsonSchema } from '../models/schemaNormalize.js';
|
|
24
28
|
import { checkHost } from '../net/addressGuard.js';
|
|
25
29
|
import { fetchWithRedirectPolicy } from '../net/redirects.js';
|
|
26
30
|
import { ToolCredentialError } from './auth.js';
|
|
27
31
|
import { oauthCallProblem, oauthTokenSource } from './oauthTools.js';
|
|
32
|
+
import { MCP_TRANSPORTS } from './mcpTransports.js';
|
|
28
33
|
import { MAX_RESULT_CHARS } from './tool.js';
|
|
29
34
|
import { toGeminiSchema } from './toolContract.js';
|
|
30
35
|
// Security (SSRF): mcp_server_url can arrive from a registry-stored syndicate
|
|
@@ -116,6 +121,54 @@ export const MAX_MCP_RESULT_CHARS = MAX_RESULT_CHARS;
|
|
|
116
121
|
const bounded = (text, max, what) => text.length <= max ? text : `${text.slice(0, max)}… [${what} cut at ${max} characters]`;
|
|
117
122
|
/** Every MCP connection opened and still in use, so a shutdown can close them. */
|
|
118
123
|
const openTransports = new Set();
|
|
124
|
+
export { MCP_TRANSPORTS };
|
|
125
|
+
/**
|
|
126
|
+
* True when a failed Streamable HTTP connect is the spec's signal that the
|
|
127
|
+
* server speaks only the older HTTP+SSE transport: the initialize POST
|
|
128
|
+
* answered with a 4xx (404, 405, 400 …). A 401 or 403 is the server refusing
|
|
129
|
+
* the credential, which another transport would not change.
|
|
130
|
+
*/
|
|
131
|
+
export function isSseFallbackSignal(error) {
|
|
132
|
+
if (!(error instanceof StreamableHTTPError))
|
|
133
|
+
return false;
|
|
134
|
+
const code = error.code;
|
|
135
|
+
return typeof code === 'number' && code >= 400 && code < 500 && code !== 401 && code !== 403;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* A client connected to `url` over `kind`, every request through `fetchFn`
|
|
139
|
+
* (the redirect rule, and the credential when there is one). A transport
|
|
140
|
+
* that fails to connect is closed before the error leaves, so nothing keeps
|
|
141
|
+
* reconnecting; with `auto`, a failed Streamable HTTP attempt carrying the
|
|
142
|
+
* fallback signal is closed and the SSE transport tried once.
|
|
143
|
+
*/
|
|
144
|
+
async function connectMcp(url, kind, fetchFn, headers = {}) {
|
|
145
|
+
const attempt = async (which) => {
|
|
146
|
+
const requestInit = Object.keys(headers).length ? { requestInit: { headers } } : {};
|
|
147
|
+
const transport = which === 'sse' ? new SSEClientTransport(url, { ...requestInit, fetch: fetchFn }) : new StreamableHTTPClientTransport(url, { ...requestInit, fetch: fetchFn });
|
|
148
|
+
const client = new Client({ name: 'melchizedek-a2a-client', version: '1.0.0' }, { capabilities: {} });
|
|
149
|
+
try {
|
|
150
|
+
await client.connect(transport);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
await transport.close().catch(() => { });
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
openTransports.add(transport);
|
|
157
|
+
return { client, transport, kind: which };
|
|
158
|
+
};
|
|
159
|
+
if (kind === 'sse')
|
|
160
|
+
return attempt('sse');
|
|
161
|
+
if (kind === 'streamable_http')
|
|
162
|
+
return attempt('streamable_http');
|
|
163
|
+
try {
|
|
164
|
+
return await attempt('streamable_http');
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (!isSseFallbackSignal(error))
|
|
168
|
+
throw error;
|
|
169
|
+
return attempt('sse');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
119
172
|
/**
|
|
120
173
|
* Close every open MCP connection. A connection otherwise lives as long as
|
|
121
174
|
* the process (its tools are called for the life of the compiled agent), and
|
|
@@ -174,7 +227,7 @@ function bearerFetch(oauth2, token) {
|
|
|
174
227
|
// message endpoint the server names), before a token is attached (ADR 0114).
|
|
175
228
|
const destination = input instanceof Request ? input.url : String(input);
|
|
176
229
|
if (oauthCallProblem(oauth2, destination))
|
|
177
|
-
throw new ToolCredentialError('host_refused', oauth2.provider);
|
|
230
|
+
throw new ToolCredentialError('host_refused', oauth2.provider, 'oauth');
|
|
178
231
|
const headers = new Headers(init?.headers);
|
|
179
232
|
headers.set('Authorization', `Bearer ${await token(destination)}`);
|
|
180
233
|
return mcpFetch(input, { ...init, headers });
|
|
@@ -204,23 +257,17 @@ const userPools = new Set();
|
|
|
204
257
|
*/
|
|
205
258
|
export async function loadMcpTools(mcpServerUrl, options = {}) {
|
|
206
259
|
const oauth2 = options.oauth2;
|
|
260
|
+
const kind = options.transport ?? 'auto';
|
|
207
261
|
if (oauth2?.grant === 'authorization_code')
|
|
208
|
-
return userGrantMcpTools(mcpServerUrl, oauth2, options.tools);
|
|
262
|
+
return userGrantMcpTools(mcpServerUrl, oauth2, options.tools, kind);
|
|
209
263
|
const tokenSource = oauth2 ? oauthTokenSource(oauth2, `mcp ${mcpServerUrl}`, mcpServerUrl, { allowPrivate: process.env.ALLOW_PRIVATE_MCP === 'true' }) : undefined;
|
|
210
|
-
let
|
|
264
|
+
let conn;
|
|
211
265
|
try {
|
|
212
266
|
const url = await assertSafeMcpUrl(mcpServerUrl);
|
|
213
|
-
|
|
214
|
-
?
|
|
215
|
-
:
|
|
216
|
-
const client =
|
|
217
|
-
name: 'melchizedek-a2a-client',
|
|
218
|
-
version: '1.0.0'
|
|
219
|
-
}, {
|
|
220
|
-
capabilities: {}
|
|
221
|
-
});
|
|
222
|
-
await client.connect(transport);
|
|
223
|
-
openTransports.add(transport);
|
|
267
|
+
conn = tokenSource
|
|
268
|
+
? await connectMcp(url, kind, bearerFetch(oauth2, (destination) => tokenSource(undefined, destination)))
|
|
269
|
+
: await connectMcp(url, kind, mcpFetch, mcpAuthHeaders(url));
|
|
270
|
+
const client = conn.client;
|
|
224
271
|
// Fetch available tools from the MCP server
|
|
225
272
|
const toolsResponse = await client.listTools();
|
|
226
273
|
return toolsResponse.tools.map((tool) => {
|
|
@@ -244,16 +291,18 @@ export async function loadMcpTools(mcpServerUrl, options = {}) {
|
|
|
244
291
|
catch (error) {
|
|
245
292
|
// A token that cannot be had is reported by kind; the rest as before.
|
|
246
293
|
console.warn(`[MCP] Failed to connect or load tools from ${mcpServerUrl}`, error instanceof ToolCredentialError ? error.message : error);
|
|
247
|
-
// A failed connect
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
294
|
+
// A failed connect closed its own transport (connectMcp); a failed
|
|
295
|
+
// listing closes this one, or its stream keeps reconnecting for the
|
|
296
|
+
// process's life.
|
|
297
|
+
if (conn) {
|
|
298
|
+
openTransports.delete(conn.transport);
|
|
299
|
+
await conn.transport.close().catch(() => { });
|
|
300
|
+
}
|
|
252
301
|
return [];
|
|
253
302
|
}
|
|
254
303
|
}
|
|
255
304
|
/** The tools of a server whose calls carry each user's own OAuth token (authorization_code). */
|
|
256
|
-
function userGrantMcpTools(mcpServerUrl, oauth2, names) {
|
|
305
|
+
function userGrantMcpTools(mcpServerUrl, oauth2, names, kind) {
|
|
257
306
|
const where = `mcp ${mcpServerUrl}`;
|
|
258
307
|
if (!names?.length)
|
|
259
308
|
throw new Error(`${where}: an authorization_code mcp_auth needs mcp_tools, the names this agent may use (no user's grant exists to list them at startup)`);
|
|
@@ -286,11 +335,8 @@ function userGrantMcpTools(mcpServerUrl, oauth2, names) {
|
|
|
286
335
|
conn.holder = holder;
|
|
287
336
|
conn.ready = (async () => {
|
|
288
337
|
const url = await assertSafeMcpUrl(mcpServerUrl);
|
|
289
|
-
const transport =
|
|
338
|
+
const { client, transport } = await connectMcp(url, kind, bearerFetch(oauth2, async () => holder.token));
|
|
290
339
|
conn.transport = transport;
|
|
291
|
-
const client = new Client({ name: 'melchizedek-a2a-client', version: '1.0.0' }, { capabilities: {} });
|
|
292
|
-
await client.connect(transport);
|
|
293
|
-
openTransports.add(transport);
|
|
294
340
|
if (!listed) {
|
|
295
341
|
const offered = await client.listTools();
|
|
296
342
|
for (const tool of offered.tools)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/tools/mcpTransports.ts — the transports an agent may name for an MCP
|
|
3
|
+
* server (ADR 0124), in a module light enough for the YAML schema to import
|
|
4
|
+
* without loading the MCP SDK.
|
|
5
|
+
*
|
|
6
|
+
* `auto` (the default) posts the initialize request over Streamable HTTP, the
|
|
7
|
+
* spec's current transport, and falls back to the legacy HTTP+SSE transport
|
|
8
|
+
* on the spec's signal (lib/tools/mcpToolFactory.ts isSseFallbackSignal);
|
|
9
|
+
* `streamable_http` and `sse` use that one transport only.
|
|
10
|
+
*/
|
|
11
|
+
export declare const MCP_TRANSPORTS: readonly ['auto', 'streamable_http', 'sse'];
|
|
12
|
+
export type McpTransportKind = (typeof MCP_TRANSPORTS)[number];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/tools/mcpTransports.ts — the transports an agent may name for an MCP
|
|
3
|
+
* server (ADR 0124), in a module light enough for the YAML schema to import
|
|
4
|
+
* without loading the MCP SDK.
|
|
5
|
+
*
|
|
6
|
+
* `auto` (the default) posts the initialize request over Streamable HTTP, the
|
|
7
|
+
* spec's current transport, and falls back to the legacy HTTP+SSE transport
|
|
8
|
+
* on the spec's signal (lib/tools/mcpToolFactory.ts isSseFallbackSignal);
|
|
9
|
+
* `streamable_http` and `sse` use that one transport only.
|
|
10
|
+
*/
|
|
11
|
+
export const MCP_TRANSPORTS = ['auto', 'streamable_http', 'sse'];
|
|
@@ -50,10 +50,23 @@ export interface OAuthClientConfig {
|
|
|
50
50
|
scopes?: string[];
|
|
51
51
|
/** Extra authorization parameters the provider needs (`access_type=offline`, say). Never `state`, `redirect_uri` or the PKCE pair. */
|
|
52
52
|
authorizationParams?: Record<string, string>;
|
|
53
|
+
/**
|
|
54
|
+
* The protected resource the token is for (RFC 8707): sent as `resource`
|
|
55
|
+
* on the authorization request, the code exchange and each refresh. An MCP
|
|
56
|
+
* server's canonical URI, for a client registered by discovery (ADR 0124).
|
|
57
|
+
*/
|
|
58
|
+
resource?: string;
|
|
53
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* A client found when first needed rather than configured at boot: an MCP
|
|
62
|
+
* server's authorization server, discovered and registered at
|
|
63
|
+
* (lib/tools/oauthDiscovery.ts dynamicOAuthClient, ADR 0124). Throws when
|
|
64
|
+
* it cannot be had; the next call tries again.
|
|
65
|
+
*/
|
|
66
|
+
export type OAuthClientSource = () => Promise<OAuthClientConfig>;
|
|
54
67
|
export interface OAuthConsentOptions {
|
|
55
|
-
/** Each provider's client, by provider name (lowercase, as the credential store names it). */
|
|
56
|
-
providers: Record<string, OAuthClientConfig>;
|
|
68
|
+
/** Each provider's client, by provider name (lowercase, as the credential store names it): configured, or found when first needed. */
|
|
69
|
+
providers: Record<string, OAuthClientConfig | OAuthClientSource>;
|
|
57
70
|
/**
|
|
58
71
|
* The server's callback URL, exactly as registered at every provider
|
|
59
72
|
* (`https://agents.example.com/oauth/callback`). The A2A server mounts the
|
|
@@ -122,7 +135,9 @@ export type ConsentErrorCode =
|
|
|
122
135
|
/** The grant could not be stored. */
|
|
123
136
|
| 'store_failed'
|
|
124
137
|
/** The provider has no client configured. */
|
|
125
|
-
| 'unknown_provider'
|
|
138
|
+
| 'unknown_provider'
|
|
139
|
+
/** A discovered client could not be found or registered (ADR 0124). */
|
|
140
|
+
| 'registration_failed';
|
|
126
141
|
/** A consent step that cannot proceed. The message names the reason, never a value. */
|
|
127
142
|
export declare class ConsentError extends Error {
|
|
128
143
|
readonly code: ConsentErrorCode;
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
import { createHash, randomBytes } from 'node:crypto';
|
|
39
39
|
import { scopeHashOf } from '../observability/audit.js';
|
|
40
40
|
import { PROVIDER_NAME } from './auth.js';
|
|
41
|
+
import { clientSecretEnvOf, credentialCallProblem } from './credentialHosts.js';
|
|
41
42
|
/**
|
|
42
43
|
* Flows in this process's memory, at most `max` of them: the oldest is
|
|
43
44
|
* dropped first, and an expired one whenever another is put.
|
|
@@ -84,7 +85,9 @@ export class ConsentError extends Error {
|
|
|
84
85
|
? `The authorization at ${p} could not be completed.`
|
|
85
86
|
: code === 'store_failed'
|
|
86
87
|
? `The authorization at ${p} could not be saved.`
|
|
87
|
-
:
|
|
88
|
+
: code === 'registration_failed'
|
|
89
|
+
? `The authorization at ${p} could not be set up.`
|
|
90
|
+
: `No OAuth client is configured for ${p}.`);
|
|
88
91
|
this.name = 'ConsentError';
|
|
89
92
|
this.code = code;
|
|
90
93
|
if (provider !== undefined)
|
|
@@ -119,7 +122,21 @@ function checkEndpoint(value, what) {
|
|
|
119
122
|
throw new Error(`${what} must not carry credentials.`);
|
|
120
123
|
return url;
|
|
121
124
|
}
|
|
122
|
-
const RESERVED_PARAMS = new Set(['state', 'redirect_uri', 'code_challenge', 'code_challenge_method', 'client_id', 'response_type', 'scope']);
|
|
125
|
+
const RESERVED_PARAMS = new Set(['state', 'redirect_uri', 'code_challenge', 'code_challenge_method', 'client_id', 'response_type', 'scope', 'resource']);
|
|
126
|
+
/** A client's configuration, checked: the endpoints, the client id, the parameters. Throws, naming the provider. */
|
|
127
|
+
function checkClient(name, client) {
|
|
128
|
+
checkEndpoint(client.authorizationUrl, `${name}: authorizationUrl`);
|
|
129
|
+
checkEndpoint(client.tokenUrl, `${name}: tokenUrl`);
|
|
130
|
+
if (typeof client.clientId !== 'string' || !client.clientId)
|
|
131
|
+
throw new Error(`${name}: clientId is required.`);
|
|
132
|
+
for (const key of Object.keys(client.authorizationParams ?? {})) {
|
|
133
|
+
if (RESERVED_PARAMS.has(key))
|
|
134
|
+
throw new Error(`${name}: authorizationParams may not set "${key}".`);
|
|
135
|
+
}
|
|
136
|
+
if (client.resource !== undefined)
|
|
137
|
+
checkEndpoint(client.resource, `${name}: resource`);
|
|
138
|
+
return { ...client, scopes: [...(client.scopes ?? [])] };
|
|
139
|
+
}
|
|
123
140
|
/** The consent manager over a credential store. Validates the configuration at once: a bad URL throws here, at boot. */
|
|
124
141
|
export function oauthConsent(options) {
|
|
125
142
|
const redirect = checkEndpoint(options.redirectUri, 'The OAuth redirect URI');
|
|
@@ -129,16 +146,23 @@ export function oauthConsent(options) {
|
|
|
129
146
|
for (const [name, client] of Object.entries(options.providers)) {
|
|
130
147
|
if (!PROVIDER_NAME.test(name))
|
|
131
148
|
throw new Error(`OAuth provider "${name.slice(0, 64)}" must match ${PROVIDER_NAME}.`);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (typeof client.clientId !== 'string' || !client.clientId)
|
|
135
|
-
throw new Error(`${name}: clientId is required.`);
|
|
136
|
-
for (const key of Object.keys(client.authorizationParams ?? {})) {
|
|
137
|
-
if (RESERVED_PARAMS.has(key))
|
|
138
|
-
throw new Error(`${name}: authorizationParams may not set "${key}".`);
|
|
139
|
-
}
|
|
140
|
-
providers.set(name, { ...client, scopes: [...(client.scopes ?? [])] });
|
|
149
|
+
// A source is checked each time it answers; a configured client here, at boot.
|
|
150
|
+
providers.set(name, typeof client === 'function' ? client : checkClient(name, client));
|
|
141
151
|
}
|
|
152
|
+
/** The provider's client now, or undefined; a source that fails is a ConsentError('registration_failed'). */
|
|
153
|
+
const clientOf = async (name) => {
|
|
154
|
+
const entry = providers.get(name);
|
|
155
|
+
if (typeof entry !== 'function')
|
|
156
|
+
return entry;
|
|
157
|
+
try {
|
|
158
|
+
return checkClient(name, await entry());
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
// A discovery refusal names roles and hosts, never a value (lib/tools/oauthDiscovery.ts); anything else is named by kind only.
|
|
162
|
+
console.warn(`[OAuth] ${name}: the client could not be set up: ${err instanceof Error && err.name === 'OAuthDiscoveryError' ? err.message : err instanceof Error ? err.name : 'unknown error'}`);
|
|
163
|
+
throw new ConsentError('registration_failed', name);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
142
166
|
const states = options.states ?? memoryConsentStates({ ...(options.now ? { now: options.now } : {}) });
|
|
143
167
|
const ttlMs = options.ttlMs ?? 10 * 60_000;
|
|
144
168
|
const now = options.now ?? Date.now;
|
|
@@ -192,8 +216,16 @@ export function oauthConsent(options) {
|
|
|
192
216
|
client_id: client.clientId,
|
|
193
217
|
code_verifier: flow.codeVerifier,
|
|
194
218
|
});
|
|
195
|
-
if (client.
|
|
219
|
+
if (client.resource)
|
|
220
|
+
body.set('resource', client.resource);
|
|
221
|
+
if (client.clientSecret) {
|
|
222
|
+
// The secret goes only to a host the operator binds its variable to (ADR 0122), checked now:
|
|
223
|
+
// the allowlist can change after boot. Refused, nothing is sent.
|
|
224
|
+
const secretEnv = clientSecretEnvOf(client);
|
|
225
|
+
if (secretEnv && credentialCallProblem(secretEnv, client.tokenUrl))
|
|
226
|
+
throw new ConsentError('exchange_failed', flow.provider);
|
|
196
227
|
body.set('client_secret', client.clientSecret);
|
|
228
|
+
}
|
|
197
229
|
let json;
|
|
198
230
|
try {
|
|
199
231
|
const res = await doFetch(client.tokenUrl, {
|
|
@@ -231,9 +263,9 @@ export function oauthConsent(options) {
|
|
|
231
263
|
redirectUri: options.redirectUri,
|
|
232
264
|
has: (provider) => typeof provider === 'string' && providers.has(provider),
|
|
233
265
|
async begin(binding) {
|
|
234
|
-
|
|
235
|
-
if (!client)
|
|
266
|
+
if (!providers.has(binding.provider))
|
|
236
267
|
throw new ConsentError('unknown_provider', PROVIDER_NAME.test(binding.provider) ? binding.provider : undefined);
|
|
268
|
+
const client = (await clientOf(binding.provider));
|
|
237
269
|
for (const field of ['appName', 'userId', 'sessionId', 'functionCallId']) {
|
|
238
270
|
if (typeof binding[field] !== 'string' || !binding[field])
|
|
239
271
|
throw new Error(`A consent flow needs the paused call's ${field}.`);
|
|
@@ -262,6 +294,8 @@ export function oauthConsent(options) {
|
|
|
262
294
|
url.searchParams.set('state', state);
|
|
263
295
|
url.searchParams.set('code_challenge', s256Challenge(codeVerifier));
|
|
264
296
|
url.searchParams.set('code_challenge_method', 'S256');
|
|
297
|
+
if (client.resource)
|
|
298
|
+
url.searchParams.set('resource', client.resource);
|
|
265
299
|
const authUri = url.toString();
|
|
266
300
|
const authConfig = {
|
|
267
301
|
credentialKey: binding.provider,
|
|
@@ -307,7 +341,15 @@ export function oauthConsent(options) {
|
|
|
307
341
|
const code = input.code;
|
|
308
342
|
if (typeof code !== 'string' || !CODE_FORMAT.test(code))
|
|
309
343
|
fail('invalid_request');
|
|
310
|
-
|
|
344
|
+
if (!providers.has(flow.provider))
|
|
345
|
+
fail('unknown_provider');
|
|
346
|
+
let client;
|
|
347
|
+
try {
|
|
348
|
+
client = await clientOf(flow.provider);
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return fail('registration_failed');
|
|
352
|
+
}
|
|
311
353
|
if (!client)
|
|
312
354
|
fail('unknown_provider');
|
|
313
355
|
let tokens;
|