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
|
@@ -51,10 +51,18 @@
|
|
|
51
51
|
* (`oauthCallProblem`). The client-credentials token endpoint passes the
|
|
52
52
|
* same SSRF guard as the server it serves (ALLOW_PRIVATE_OPENAPI or
|
|
53
53
|
* ALLOW_PRIVATE_MCP), and no redirect from it is followed.
|
|
54
|
+
*
|
|
55
|
+
* WHERE A CLIENT SECRET MAY GO: to the token endpoint, and, when the
|
|
56
|
+
* operator configures a credential host allowlist
|
|
57
|
+
* (lib/tools/credentialHosts.ts, ADR 0122), only to a host it binds the
|
|
58
|
+
* `client_secret_env` variable to: checked at compile, when the consent
|
|
59
|
+
* clients are built, and before each client-credentials token request,
|
|
60
|
+
* code exchange (lib/tools/oauthConsent.ts) and refresh.
|
|
54
61
|
*/
|
|
55
62
|
import type { OAuthProvider } from './auth.ts';
|
|
56
63
|
import type { OAuthHostAllowlist } from './oauthHosts.ts';
|
|
57
|
-
import type { OAuthClientConfig } from './oauthConsent.ts';
|
|
64
|
+
import type { OAuthClientConfig, OAuthClientSource } from './oauthConsent.ts';
|
|
65
|
+
import type { DynamicOAuthGrant } from './oauthDiscovery.ts';
|
|
58
66
|
import type { ToolContext } from './tool.ts';
|
|
59
67
|
/** The `oauth2` block of an `auth` (OpenAPI) or an `mcp_auth` (MCP). */
|
|
60
68
|
export interface OAuth2AuthConfig {
|
|
@@ -63,8 +71,14 @@ export interface OAuth2AuthConfig {
|
|
|
63
71
|
grant: 'authorization_code' | 'client_credentials';
|
|
64
72
|
/** The provider's authorization endpoint (authorization_code only). */
|
|
65
73
|
authorization_url?: string;
|
|
66
|
-
/** The provider's token endpoint. */
|
|
67
|
-
token_url
|
|
74
|
+
/** The provider's token endpoint. Absent only with `client_registration: 'dynamic'`, which discovers it. */
|
|
75
|
+
token_url?: string;
|
|
76
|
+
/**
|
|
77
|
+
* `'dynamic'` (MCP servers, authorization_code only, ADR 0124): no
|
|
78
|
+
* pre-issued client. The authorization server is discovered from the MCP
|
|
79
|
+
* server and the deployment registers itself (lib/tools/oauthDiscovery.ts).
|
|
80
|
+
*/
|
|
81
|
+
client_registration?: 'dynamic';
|
|
68
82
|
/** The client id, written out (it is not a secret) … */
|
|
69
83
|
client_id?: string;
|
|
70
84
|
/** … or the environment variable holding it. */
|
|
@@ -87,7 +101,7 @@ export interface OAuthGrantUse {
|
|
|
87
101
|
tools: string;
|
|
88
102
|
oauth2: OAuth2AuthConfig;
|
|
89
103
|
}
|
|
90
|
-
/** An agent's declared OAuth grants, OpenAPI entries first, then its MCP server. */
|
|
104
|
+
/** An agent's declared OAuth grants, OpenAPI entries first, then its MCP server, then each of its `mcp_servers` (ADR 0124). */
|
|
91
105
|
export declare function agentOAuthGrants(agent: {
|
|
92
106
|
name?: string;
|
|
93
107
|
openapi?: Array<{
|
|
@@ -100,6 +114,12 @@ export declare function agentOAuthGrants(agent: {
|
|
|
100
114
|
mcp_auth?: {
|
|
101
115
|
oauth2?: OAuth2AuthConfig;
|
|
102
116
|
};
|
|
117
|
+
mcp_servers?: Array<{
|
|
118
|
+
url?: string;
|
|
119
|
+
auth?: {
|
|
120
|
+
oauth2?: OAuth2AuthConfig;
|
|
121
|
+
};
|
|
122
|
+
}>;
|
|
103
123
|
}, prefix?: string): OAuthGrantUse[];
|
|
104
124
|
/** The environment variables a grant reads, by role. */
|
|
105
125
|
export declare function oauthEnvNames(oauth2: OAuth2AuthConfig): string[];
|
|
@@ -149,7 +169,9 @@ export declare function clientCredentialsGrant(options: ClientCredentialsOptions
|
|
|
149
169
|
/**
|
|
150
170
|
* What a tool calls for the token of one call, naming where that call goes.
|
|
151
171
|
* Throws ToolCredentialError, whose message names no value: `host_refused`
|
|
152
|
-
* when the destination is not one of the provider's hosts
|
|
172
|
+
* when the destination is not one of the provider's hosts (`allowlist`
|
|
173
|
+
* `'oauth'`), or when a client secret's token endpoint is not bound to its
|
|
174
|
+
* variable (`allowlist` `'credential'`).
|
|
153
175
|
*/
|
|
154
176
|
export type OAuthTokenSource = (ctx: ToolContext | undefined, destination: string) => Promise<string>;
|
|
155
177
|
/**
|
|
@@ -178,12 +200,24 @@ interface AgentLike {
|
|
|
178
200
|
mcp_auth?: {
|
|
179
201
|
oauth2?: OAuth2AuthConfig;
|
|
180
202
|
};
|
|
203
|
+
mcp_servers?: Array<{
|
|
204
|
+
url?: string;
|
|
205
|
+
auth?: {
|
|
206
|
+
oauth2?: OAuth2AuthConfig;
|
|
207
|
+
};
|
|
208
|
+
}>;
|
|
181
209
|
yaml_reference?: string;
|
|
182
210
|
}
|
|
183
211
|
interface SyndicateLike {
|
|
184
212
|
orchestrator?: AgentLike;
|
|
185
213
|
subagents?: AgentLike[];
|
|
186
214
|
}
|
|
215
|
+
/** The options oauthClientsFor and dynamicOAuthGrantsFor share. */
|
|
216
|
+
interface ClientsForOptions {
|
|
217
|
+
env?: NodeJS.ProcessEnv;
|
|
218
|
+
load?: (ref: string) => SyndicateLike;
|
|
219
|
+
allowlist?: OAuthHostAllowlist | null;
|
|
220
|
+
}
|
|
187
221
|
/**
|
|
188
222
|
* The OAuth clients the consent step needs for these syndicates' declared
|
|
189
223
|
* authorization-code grants, by provider: pass them as
|
|
@@ -191,13 +225,19 @@ interface SyndicateLike {
|
|
|
191
225
|
* secrets are read from the variables the YAML names. Two declarations of
|
|
192
226
|
* one provider must agree on its endpoints, client and scopes, or this
|
|
193
227
|
* throws: a provider's grant is one grant, whichever tool asks for it.
|
|
194
|
-
* `load` reads a nested `yaml_reference:` file, when given.
|
|
228
|
+
* `load` reads a nested `yaml_reference:` file, when given. A grant with
|
|
229
|
+
* `client_registration: dynamic` has no client until it is registered: it
|
|
230
|
+
* is in `dynamicOAuthGrantsFor`, not here.
|
|
195
231
|
*/
|
|
196
|
-
export declare function oauthClientsFor(configs: readonly SyndicateLike[], options?:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
232
|
+
export declare function oauthClientsFor(configs: readonly SyndicateLike[], options?: ClientsForOptions): Record<string, OAuthClientConfig>;
|
|
233
|
+
/**
|
|
234
|
+
* The dynamic grants these syndicates declare (`client_registration:
|
|
235
|
+
* dynamic`, ADR 0124), by provider, checked as oauthClientsFor checks the
|
|
236
|
+
* others (one grant per provider, the MCP server on the allowlist): each
|
|
237
|
+
* becomes a consent client through `dynamicOAuthClient`
|
|
238
|
+
* (lib/tools/oauthDiscovery.ts).
|
|
239
|
+
*/
|
|
240
|
+
export declare function dynamicOAuthGrantsFor(configs: readonly SyndicateLike[], options?: ClientsForOptions): Record<string, DynamicOAuthGrant>;
|
|
201
241
|
/**
|
|
202
242
|
* Every OAuth grant these syndicates declare, orchestrators and subagents,
|
|
203
243
|
* following a nested `yaml_reference:` through `load` when given (once per
|
|
@@ -221,10 +261,11 @@ export declare function syndicateOAuthHostProblems(configs: readonly SyndicateLi
|
|
|
221
261
|
* (`credentialStore({ providers })`, lib/tools/credentialStore.ts): an
|
|
222
262
|
* expired user token is renewed at the provider's token endpoint with its
|
|
223
263
|
* refresh token (RFC 6749 6) instead of asking the person again. Built from
|
|
224
|
-
* `oauthClientsFor`'s clients
|
|
264
|
+
* `oauthClientsFor`'s clients, and from `dynamicOAuthClient`'s sources (a
|
|
265
|
+
* discovered client, ADR 0124); `allowPrivate` lets a private or loopback
|
|
225
266
|
* token endpoint through the guard (development).
|
|
226
267
|
*/
|
|
227
|
-
export declare function oauthRefreshProviders(clients: Record<string, OAuthClientConfig>, options?: {
|
|
268
|
+
export declare function oauthRefreshProviders(clients: Record<string, OAuthClientConfig | OAuthClientSource>, options?: {
|
|
228
269
|
allowPrivate?: boolean;
|
|
229
270
|
fetch?: typeof fetch;
|
|
230
271
|
timeoutMs?: number;
|
|
@@ -51,12 +51,20 @@
|
|
|
51
51
|
* (`oauthCallProblem`). The client-credentials token endpoint passes the
|
|
52
52
|
* same SSRF guard as the server it serves (ALLOW_PRIVATE_OPENAPI or
|
|
53
53
|
* ALLOW_PRIVATE_MCP), and no redirect from it is followed.
|
|
54
|
+
*
|
|
55
|
+
* WHERE A CLIENT SECRET MAY GO: to the token endpoint, and, when the
|
|
56
|
+
* operator configures a credential host allowlist
|
|
57
|
+
* (lib/tools/credentialHosts.ts, ADR 0122), only to a host it binds the
|
|
58
|
+
* `client_secret_env` variable to: checked at compile, when the consent
|
|
59
|
+
* clients are built, and before each client-credentials token request,
|
|
60
|
+
* code exchange (lib/tools/oauthConsent.ts) and refresh.
|
|
54
61
|
*/
|
|
55
62
|
import { blockedHostReason, checkHost } from '../net/addressGuard.js';
|
|
56
63
|
import { PROVIDER_NAME, ToolCredentialError } from './auth.js';
|
|
57
64
|
import { readCredentialEnv } from './credentialEnv.js';
|
|
65
|
+
import { CLIENT_SECRET_ENV, clientSecretEnvOf, credentialCallProblem, credentialHostProblem } from './credentialHosts.js';
|
|
58
66
|
import { oauthHostProblem, oauthHosts } from './oauthHosts.js';
|
|
59
|
-
/** An agent's declared OAuth grants, OpenAPI entries first, then its MCP server. */
|
|
67
|
+
/** An agent's declared OAuth grants, OpenAPI entries first, then its MCP server, then each of its `mcp_servers` (ADR 0124). */
|
|
60
68
|
export function agentOAuthGrants(agent, prefix = '') {
|
|
61
69
|
const name = `${prefix}${agent.name ?? ''}`;
|
|
62
70
|
const out = [];
|
|
@@ -66,6 +74,10 @@ export function agentOAuthGrants(agent, prefix = '') {
|
|
|
66
74
|
}
|
|
67
75
|
if (agent.mcp_auth?.oauth2)
|
|
68
76
|
out.push({ agent: name, tools: `mcp ${agent.mcp_server_url ?? ''}`, oauth2: agent.mcp_auth.oauth2 });
|
|
77
|
+
for (const server of agent.mcp_servers ?? []) {
|
|
78
|
+
if (server?.auth?.oauth2)
|
|
79
|
+
out.push({ agent: name, tools: `mcp ${server.url ?? ''}`, oauth2: server.auth.oauth2 });
|
|
80
|
+
}
|
|
69
81
|
return out;
|
|
70
82
|
}
|
|
71
83
|
/** The environment variables a grant reads, by role. */
|
|
@@ -102,7 +114,9 @@ export function tokenTransportProblem(raw) {
|
|
|
102
114
|
export function oauthGrantHostProblems(oauth2, servers, allowlist) {
|
|
103
115
|
const list = allowlist === undefined ? oauthHosts() ?? null : allowlist;
|
|
104
116
|
const urls = servers.map((s) => ['server', s]);
|
|
105
|
-
|
|
117
|
+
// A dynamic grant names no endpoint: discovery checks each one it finds (lib/tools/oauthDiscovery.ts).
|
|
118
|
+
if (oauth2.token_url)
|
|
119
|
+
urls.push(['token_url', oauth2.token_url]);
|
|
106
120
|
if (oauth2.authorization_url)
|
|
107
121
|
urls.push(['authorization_url', oauth2.authorization_url]);
|
|
108
122
|
const out = new Set();
|
|
@@ -264,7 +278,7 @@ export function oauthTokenSource(oauth2, where, server, options = {}) {
|
|
|
264
278
|
/** The call-time check (ADR 0114): before any token is read or fetched. */
|
|
265
279
|
const refuse = (destination) => {
|
|
266
280
|
if (oauthCallProblem(oauth2, destination))
|
|
267
|
-
throw new ToolCredentialError('host_refused', provider);
|
|
281
|
+
throw new ToolCredentialError('host_refused', provider, 'oauth');
|
|
268
282
|
};
|
|
269
283
|
if (oauth2.grant === 'authorization_code') {
|
|
270
284
|
return async (ctx, destination) => {
|
|
@@ -277,17 +291,25 @@ export function oauthTokenSource(oauth2, where, server, options = {}) {
|
|
|
277
291
|
const env = options.env ?? process.env;
|
|
278
292
|
if (!oauth2.client_secret_env)
|
|
279
293
|
throw new Error(`${where}: a client_credentials grant needs client_secret_env`);
|
|
280
|
-
const
|
|
294
|
+
const tokenUrl = oauth2.token_url;
|
|
295
|
+
if (!tokenUrl)
|
|
296
|
+
throw new Error(`${where}: a client_credentials grant needs token_url`);
|
|
297
|
+
const transport = tokenTransportProblem(tokenUrl);
|
|
281
298
|
if (transport)
|
|
282
299
|
throw new Error(`${where}: token_url: ${transport}`);
|
|
283
300
|
if (options.allowPrivate !== true) {
|
|
284
|
-
const blocked = blockedHostReason(new URL(
|
|
301
|
+
const blocked = blockedHostReason(new URL(tokenUrl).hostname);
|
|
285
302
|
if (blocked)
|
|
286
|
-
throw new Error(`${where}: refusing token endpoint ${new URL(
|
|
303
|
+
throw new Error(`${where}: refusing token endpoint ${new URL(tokenUrl).hostname}: ${blocked}`);
|
|
287
304
|
}
|
|
305
|
+
// The client secret goes only to a host the operator binds its variable to (ADR 0122).
|
|
306
|
+
const secretEnv = oauth2.client_secret_env;
|
|
307
|
+
const unbound = credentialHostProblem(secretEnv, tokenUrl, 'token_url');
|
|
308
|
+
if (unbound)
|
|
309
|
+
throw new Error(`${where}: ${unbound}`);
|
|
288
310
|
const grant = clientCredentialsGrant({
|
|
289
311
|
provider,
|
|
290
|
-
tokenUrl
|
|
312
|
+
tokenUrl,
|
|
291
313
|
clientId: oauth2.client_id ?? readCredentialEnv(oauth2.client_id_env ?? '', where, env),
|
|
292
314
|
clientSecret: readCredentialEnv(oauth2.client_secret_env, where, env),
|
|
293
315
|
...(oauth2.scopes?.length ? { scopes: [...oauth2.scopes] } : {}),
|
|
@@ -296,36 +318,57 @@ export function oauthTokenSource(oauth2, where, server, options = {}) {
|
|
|
296
318
|
});
|
|
297
319
|
return async (ctx, destination) => {
|
|
298
320
|
refuse(destination);
|
|
299
|
-
// The client secret goes to the token endpoint: held to the same rule
|
|
300
|
-
|
|
321
|
+
// The client secret goes to the token endpoint: held to the same rule,
|
|
322
|
+
// and to the credential host allowlist (ADR 0122).
|
|
323
|
+
refuse(tokenUrl);
|
|
324
|
+
if (credentialCallProblem(secretEnv, tokenUrl))
|
|
325
|
+
throw new ToolCredentialError('host_refused', provider, 'credential');
|
|
301
326
|
return grant.token(ctx?.signal);
|
|
302
327
|
};
|
|
303
328
|
}
|
|
304
|
-
/**
|
|
305
|
-
|
|
306
|
-
* authorization-code grants, by provider: pass them as
|
|
307
|
-
* `oauthConsent({ providers })` (lib/tools/oauthConsent.ts). Client ids and
|
|
308
|
-
* secrets are read from the variables the YAML names. Two declarations of
|
|
309
|
-
* one provider must agree on its endpoints, client and scopes, or this
|
|
310
|
-
* throws: a provider's grant is one grant, whichever tool asks for it.
|
|
311
|
-
* `load` reads a nested `yaml_reference:` file, when given.
|
|
312
|
-
*/
|
|
313
|
-
export function oauthClientsFor(configs, options = {}) {
|
|
329
|
+
/** Every declared authorization-code grant, by provider: configured clients and dynamic grants, one grant per provider. */
|
|
330
|
+
function collectOAuthClients(configs, options) {
|
|
314
331
|
const env = options.env ?? process.env;
|
|
315
332
|
const allowlist = options.allowlist === undefined ? oauthHosts(env) ?? null : options.allowlist;
|
|
316
333
|
const clients = {};
|
|
334
|
+
const dynamic = {};
|
|
317
335
|
const signature = new Map();
|
|
318
336
|
for (const use of syndicateOAuthGrants(configs, options.load)) {
|
|
319
337
|
const o = use.oauth2;
|
|
320
338
|
if (o.grant !== 'authorization_code')
|
|
321
339
|
continue;
|
|
322
340
|
const where = `${use.agent} · ${use.tools} · oauth2`;
|
|
341
|
+
if (o.client_registration === 'dynamic') {
|
|
342
|
+
// Discovered and registered when first needed (ADR 0124): the MCP server is all the YAML names.
|
|
343
|
+
const server = use.tools.startsWith('mcp ') && use.tools.length > 4 ? use.tools.slice(4) : '';
|
|
344
|
+
if (!server)
|
|
345
|
+
throw new Error(`${where}: client_registration: dynamic is for an MCP server's grant`);
|
|
346
|
+
const hostProblems = oauthGrantHostProblems(o, [server], allowlist);
|
|
347
|
+
if (hostProblems.length)
|
|
348
|
+
throw new Error(`${where}: ${hostProblems.join('; ')}`);
|
|
349
|
+
const sig = JSON.stringify(['dynamic', server, [...(o.scopes ?? [])].sort(), o.authorization_params ?? {}]);
|
|
350
|
+
const before = signature.get(o.provider);
|
|
351
|
+
if (before !== undefined) {
|
|
352
|
+
if (before !== sig)
|
|
353
|
+
throw new Error(`${where}: provider "${o.provider}" is declared twice with different endpoints, client or scopes`);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
signature.set(o.provider, sig);
|
|
357
|
+
dynamic[o.provider] = { provider: o.provider, server, scopes: [...(o.scopes ?? [])], ...(o.authorization_params ? { authorizationParams: { ...o.authorization_params } } : {}) };
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
323
360
|
if (!o.authorization_url)
|
|
324
361
|
throw new Error(`${where}: an authorization_code grant needs authorization_url`);
|
|
362
|
+
if (!o.token_url)
|
|
363
|
+
throw new Error(`${where}: an authorization_code grant needs token_url`);
|
|
325
364
|
// The consent step sends the client secret, the code and later the refresh token to these endpoints (ADR 0114).
|
|
326
365
|
const hostProblems = oauthGrantHostProblems(o, [], allowlist);
|
|
327
366
|
if (hostProblems.length)
|
|
328
367
|
throw new Error(`${where}: ${hostProblems.join('; ')}`);
|
|
368
|
+
// The client secret goes to the token endpoint: a host the operator binds its variable to (ADR 0122).
|
|
369
|
+
const secretProblem = o.client_secret_env ? credentialHostProblem(o.client_secret_env, o.token_url, 'token_url') : null;
|
|
370
|
+
if (secretProblem)
|
|
371
|
+
throw new Error(`${where}: ${secretProblem}`);
|
|
329
372
|
const sig = JSON.stringify([o.authorization_url, o.token_url, o.client_id ?? null, o.client_id_env ?? null, o.client_secret_env ?? null, [...(o.scopes ?? [])].sort(), o.authorization_params ?? {}]);
|
|
330
373
|
const before = signature.get(o.provider);
|
|
331
374
|
if (before !== undefined) {
|
|
@@ -341,9 +384,35 @@ export function oauthClientsFor(configs, options = {}) {
|
|
|
341
384
|
...(o.client_secret_env ? { clientSecret: readCredentialEnv(o.client_secret_env, where, env) } : {}),
|
|
342
385
|
scopes: [...(o.scopes ?? [])],
|
|
343
386
|
...(o.authorization_params ? { authorizationParams: { ...o.authorization_params } } : {}),
|
|
387
|
+
// The variable the secret came from, so the exchange and the refresh check its host at call time.
|
|
388
|
+
...(o.client_secret_env ? { [CLIENT_SECRET_ENV]: o.client_secret_env } : {}),
|
|
344
389
|
};
|
|
345
390
|
}
|
|
346
|
-
return clients;
|
|
391
|
+
return { clients, dynamic };
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* The OAuth clients the consent step needs for these syndicates' declared
|
|
395
|
+
* authorization-code grants, by provider: pass them as
|
|
396
|
+
* `oauthConsent({ providers })` (lib/tools/oauthConsent.ts). Client ids and
|
|
397
|
+
* secrets are read from the variables the YAML names. Two declarations of
|
|
398
|
+
* one provider must agree on its endpoints, client and scopes, or this
|
|
399
|
+
* throws: a provider's grant is one grant, whichever tool asks for it.
|
|
400
|
+
* `load` reads a nested `yaml_reference:` file, when given. A grant with
|
|
401
|
+
* `client_registration: dynamic` has no client until it is registered: it
|
|
402
|
+
* is in `dynamicOAuthGrantsFor`, not here.
|
|
403
|
+
*/
|
|
404
|
+
export function oauthClientsFor(configs, options = {}) {
|
|
405
|
+
return collectOAuthClients(configs, options).clients;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* The dynamic grants these syndicates declare (`client_registration:
|
|
409
|
+
* dynamic`, ADR 0124), by provider, checked as oauthClientsFor checks the
|
|
410
|
+
* others (one grant per provider, the MCP server on the allowlist): each
|
|
411
|
+
* becomes a consent client through `dynamicOAuthClient`
|
|
412
|
+
* (lib/tools/oauthDiscovery.ts).
|
|
413
|
+
*/
|
|
414
|
+
export function dynamicOAuthGrantsFor(configs, options = {}) {
|
|
415
|
+
return collectOAuthClients(configs, options).dynamic;
|
|
347
416
|
}
|
|
348
417
|
/**
|
|
349
418
|
* Every OAuth grant these syndicates declare, orchestrators and subagents,
|
|
@@ -393,20 +462,34 @@ export function syndicateOAuthHostProblems(configs, options = {}) {
|
|
|
393
462
|
* (`credentialStore({ providers })`, lib/tools/credentialStore.ts): an
|
|
394
463
|
* expired user token is renewed at the provider's token endpoint with its
|
|
395
464
|
* refresh token (RFC 6749 6) instead of asking the person again. Built from
|
|
396
|
-
* `oauthClientsFor`'s clients
|
|
465
|
+
* `oauthClientsFor`'s clients, and from `dynamicOAuthClient`'s sources (a
|
|
466
|
+
* discovered client, ADR 0124); `allowPrivate` lets a private or loopback
|
|
397
467
|
* token endpoint through the guard (development).
|
|
398
468
|
*/
|
|
399
469
|
export function oauthRefreshProviders(clients, options = {}) {
|
|
400
470
|
const providers = {};
|
|
401
|
-
for (const [name,
|
|
471
|
+
for (const [name, entry] of Object.entries(clients)) {
|
|
402
472
|
providers[name] = {
|
|
403
473
|
refresh: async (refreshToken, context) => {
|
|
474
|
+
let client;
|
|
475
|
+
try {
|
|
476
|
+
client = typeof entry === 'function' ? await entry() : entry;
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
throw new ToolCredentialError('refresh_failed', name);
|
|
480
|
+
}
|
|
404
481
|
// A refresh token goes only to a token endpoint the allowlist binds to its provider (ADR 0114).
|
|
405
482
|
if (oauthCallProblem({ provider: name, grant: 'authorization_code' }, client.tokenUrl))
|
|
406
|
-
throw new ToolCredentialError('host_refused', name);
|
|
483
|
+
throw new ToolCredentialError('host_refused', name, 'oauth');
|
|
484
|
+
// The client secret too: only to a host the operator binds its variable to (ADR 0122).
|
|
485
|
+
const secretEnv = clientSecretEnvOf(client);
|
|
486
|
+
if (client.clientSecret && secretEnv && credentialCallProblem(secretEnv, client.tokenUrl))
|
|
487
|
+
throw new ToolCredentialError('host_refused', name, 'credential');
|
|
407
488
|
const body = new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: client.clientId });
|
|
408
489
|
if (client.clientSecret)
|
|
409
490
|
body.set('client_secret', client.clientSecret);
|
|
491
|
+
if (client.resource)
|
|
492
|
+
body.set('resource', client.resource);
|
|
410
493
|
return tokenRequest(client.tokenUrl, body, name, {
|
|
411
494
|
allowPrivate: options.allowPrivate === true,
|
|
412
495
|
doFetch: options.fetch ?? fetch,
|
|
@@ -45,6 +45,13 @@ export declare function boundResult(result: unknown): unknown;
|
|
|
45
45
|
* an operation the spec does not have, or a server the guard refuses.
|
|
46
46
|
*/
|
|
47
47
|
export declare function buildOpenApiOwnTools(entry: OpenApiConfig, baseDir?: string): Promise<Tool[]>;
|
|
48
|
+
/**
|
|
49
|
+
* The server URLs one `openapi:` entry calls: its `base_url`, else its
|
|
50
|
+
* spec's server. Reads and parses the spec (bounded); throws as the compile
|
|
51
|
+
* would on a spec that cannot be read. For the load-time credential host
|
|
52
|
+
* check (lib/tools/credentialUses.ts, ADR 0122).
|
|
53
|
+
*/
|
|
54
|
+
export declare function openApiServers(entry: OpenApiConfig, baseDir?: string): string[];
|
|
48
55
|
/**
|
|
49
56
|
* The tools one `openapi:` entry gives an agent: buildOpenApiOwnTools' own
|
|
50
57
|
* Tools, each marked so require_approval can name it by its operationId.
|
|
@@ -36,6 +36,12 @@
|
|
|
36
36
|
* instead, fetched per call: the run's user's own (authorization_code,
|
|
37
37
|
* through `ctx.accessToken` and the consent pause) or the server's own
|
|
38
38
|
* (client_credentials), lib/tools/oauthTools.ts, ADR 0112.
|
|
39
|
+
* - STATIC CREDENTIALS ONLY TO THEIR HOSTS. When the operator configures a
|
|
40
|
+
* credential host allowlist (MELCHIZEDEK_CREDENTIAL_HOSTS, or
|
|
41
|
+
* createA2AApp's credentialHosts; lib/tools/credentialHosts.ts,
|
|
42
|
+
* ADR 0122), a `bearer_env` or `api_key` variable is sent only to a host
|
|
43
|
+
* it binds that variable to: checked at compile (every server), and
|
|
44
|
+
* again before each call, which is refused with nothing sent.
|
|
39
45
|
* - THE SSRF GUARD. Every server URL the tools will call must be http(s)
|
|
40
46
|
* and pass lib/net/addressGuard.ts: its literal rules at compile time
|
|
41
47
|
* (offline), and the full check with DNS before each call (a name can
|
|
@@ -67,6 +73,7 @@ import { callOperation, hostProblem } from './openapi/call.js';
|
|
|
67
73
|
import { MAX_SPEC_BYTES, operationNamed, parseOpenApiSpec } from './openapi/parse.js';
|
|
68
74
|
import { ToolCredentialError } from './auth.js';
|
|
69
75
|
import { readCredentialEnv } from './credentialEnv.js';
|
|
76
|
+
import { credentialCallProblem, credentialHostProblem } from './credentialHosts.js';
|
|
70
77
|
import { oauthTokenSource, tokenTransportProblem } from './oauthTools.js';
|
|
71
78
|
export { credentialEnvProblem } from './credentialEnv.js';
|
|
72
79
|
export { namesTool, toSnake } from './openapi/parse.js';
|
|
@@ -89,11 +96,15 @@ function credentialFor(auth, spec) {
|
|
|
89
96
|
return undefined;
|
|
90
97
|
const read = (env) => readCredentialEnv(env, `openapi ${spec}`);
|
|
91
98
|
if (auth.bearer_env)
|
|
92
|
-
return { kind: 'bearer', token: read(auth.bearer_env) };
|
|
99
|
+
return { credential: { kind: 'bearer', token: read(auth.bearer_env) }, env: auth.bearer_env };
|
|
93
100
|
if (auth.api_key)
|
|
94
|
-
return { kind: 'api_key', in: auth.api_key.in, name: auth.api_key.name, value: read(auth.api_key.env) };
|
|
101
|
+
return { credential: { kind: 'api_key', in: auth.api_key.in, name: auth.api_key.name, value: read(auth.api_key.env) }, env: auth.api_key.env };
|
|
95
102
|
return undefined;
|
|
96
103
|
}
|
|
104
|
+
/** The variable a static `auth` sends (bearer_env or api_key.env), or undefined. */
|
|
105
|
+
function staticCredentialEnv(auth) {
|
|
106
|
+
return auth?.bearer_env ?? auth?.api_key?.env;
|
|
107
|
+
}
|
|
97
108
|
/**
|
|
98
109
|
* Cut an API result to MAX_RESULT_CHARS, saying so. Kept for consumers of
|
|
99
110
|
* this module; the tools cut with capResult (lib/tools/tool.ts).
|
|
@@ -138,11 +149,25 @@ function readSpec(specPath, source) {
|
|
|
138
149
|
* provider and what to do, never a value (and, for a user who has not
|
|
139
150
|
* granted it, the run's consent step has already asked).
|
|
140
151
|
*/
|
|
141
|
-
function operationTool(op,
|
|
152
|
+
function operationTool(op, held, oauth) {
|
|
153
|
+
const credential = held?.credential;
|
|
142
154
|
const tool = {
|
|
143
155
|
name: op.name,
|
|
144
156
|
declaration: () => op.declaration,
|
|
145
157
|
async execute(args, ctx) {
|
|
158
|
+
// The operator's credential host allowlist, again at each call (ADR 0122): the
|
|
159
|
+
// compile checked it, and it can change. Refused, nothing is sent; the
|
|
160
|
+
// message names the host, never the value.
|
|
161
|
+
if (held && credentialCallProblem(held.env, op.baseUrl)) {
|
|
162
|
+
let host = op.baseUrl;
|
|
163
|
+
try {
|
|
164
|
+
host = new URL(op.baseUrl).hostname;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// A server that is not a URL is named as written.
|
|
168
|
+
}
|
|
169
|
+
return { error: `${op.name} was not called: the operator's credential host allowlist does not let this API's credential be sent to ${host}` };
|
|
170
|
+
}
|
|
146
171
|
let sent = credential;
|
|
147
172
|
if (oauth) {
|
|
148
173
|
try {
|
|
@@ -186,12 +211,17 @@ export async function buildOpenApiOwnTools(entry, baseDir = process.cwd()) {
|
|
|
186
211
|
picked = all.filter((o) => o.method === 'get');
|
|
187
212
|
}
|
|
188
213
|
const chosen = entry.base_url ? picked.map((o) => ({ ...o, baseUrl: entry.base_url })) : picked;
|
|
214
|
+
const credentialEnv = staticCredentialEnv(entry.auth);
|
|
189
215
|
for (const baseUrl of new Set(chosen.map((o) => o.baseUrl))) {
|
|
190
216
|
if (!baseUrl)
|
|
191
217
|
throw new Error(`openapi ${entry.spec}: the spec names no server; set base_url`);
|
|
192
218
|
const problem = await hostProblem(baseUrl, false);
|
|
193
219
|
if (problem)
|
|
194
220
|
throw new Error(`openapi ${entry.spec}: ${problem}`);
|
|
221
|
+
// A static credential goes only to a host the operator binds its variable to (ADR 0122).
|
|
222
|
+
const bound = credentialEnv ? credentialHostProblem(credentialEnv, baseUrl) : null;
|
|
223
|
+
if (bound)
|
|
224
|
+
throw new Error(`openapi ${entry.spec}: ${bound}`);
|
|
195
225
|
}
|
|
196
226
|
let oauth;
|
|
197
227
|
if (entry.auth?.oauth2 && chosen.length) {
|
|
@@ -205,6 +235,22 @@ export async function buildOpenApiOwnTools(entry, baseDir = process.cwd()) {
|
|
|
205
235
|
}
|
|
206
236
|
return chosen.map((o) => operationTool(o, credential, oauth));
|
|
207
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* The server URLs one `openapi:` entry calls: its `base_url`, else its
|
|
240
|
+
* spec's server. Reads and parses the spec (bounded); throws as the compile
|
|
241
|
+
* would on a spec that cannot be read. For the load-time credential host
|
|
242
|
+
* check (lib/tools/credentialUses.ts, ADR 0122).
|
|
243
|
+
*/
|
|
244
|
+
export function openApiServers(entry, baseDir = process.cwd()) {
|
|
245
|
+
if (entry.base_url)
|
|
246
|
+
return [entry.base_url];
|
|
247
|
+
const specPath = resolve(baseDir, entry.spec);
|
|
248
|
+
const ext = extname(specPath).toLowerCase();
|
|
249
|
+
if (!['.yaml', '.yml', '.json'].includes(ext))
|
|
250
|
+
throw new Error(`openapi ${entry.spec}: a spec is .yaml, .yml or .json`);
|
|
251
|
+
const all = parseOpenApiSpec(readSpec(specPath, entry.spec), ext === '.json' ? 'json' : 'yaml', { source: entry.spec });
|
|
252
|
+
return [...new Set(all.map((o) => o.baseUrl).filter((u) => u !== ''))];
|
|
253
|
+
}
|
|
208
254
|
/**
|
|
209
255
|
* The tools one `openapi:` entry gives an agent: buildOpenApiOwnTools' own
|
|
210
256
|
* Tools, each marked so require_approval can name it by its operationId.
|
|
@@ -33,7 +33,10 @@
|
|
|
33
33
|
* read-modify-write, not a lock. A running job's step checkpoints
|
|
34
34
|
* (ADR 0113) sit in a sidecar, <store>.checkpoints.json, keyed by id;
|
|
35
35
|
* every write of the store drops those of jobs no longer running or
|
|
36
|
-
* queued.
|
|
36
|
+
* queued. A checkpoint above the cap (TaskBackendOptions.
|
|
37
|
+
* checkpointMaxBytes, default 5 MiB of JSON) is not saved, on either
|
|
38
|
+
* store: the job keeps its previous one and one line names the job id
|
|
39
|
+
* and the sizes.
|
|
37
40
|
* - With Postgres (postgresStorage().taskQueue, migration 0009) each
|
|
38
41
|
* caller has its own list, scoped by the caller the tool call carries
|
|
39
42
|
* (the A2A server's scope key), and any number of workers claim jobs
|
|
@@ -129,6 +132,31 @@ export interface TaskBackend {
|
|
|
129
132
|
/** The checkpoint a re-queued job left, or null. */
|
|
130
133
|
loadCheckpoint?(job: OwnedTask): Promise<object | null>;
|
|
131
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* The largest checkpoint a backend stores by default: 5 MiB of serialized
|
|
137
|
+
* JSON (ADR 0113). A save above the cap is skipped and the job's previous
|
|
138
|
+
* checkpoint kept, so a huge run resumes from an older step boundary instead
|
|
139
|
+
* of failing or bloating the table.
|
|
140
|
+
*/
|
|
141
|
+
export declare const DEFAULT_CHECKPOINT_MAX_BYTES: number;
|
|
142
|
+
/** The worker's variable that moves the cap, in bytes (scripts/assistant_worker.ts). */
|
|
143
|
+
export declare const CHECKPOINT_MAX_BYTES_ENV = "MELCHIZEDEK_CHECKPOINT_MAX_BYTES";
|
|
144
|
+
/** How a backend stores checkpoints (fileTaskBackendWith, postgresTaskBackend). */
|
|
145
|
+
export interface TaskBackendOptions {
|
|
146
|
+
/** The largest checkpoint stored, in bytes of serialized JSON. Default DEFAULT_CHECKPOINT_MAX_BYTES; a value that is not a positive integer is the default. */
|
|
147
|
+
checkpointMaxBytes?: number;
|
|
148
|
+
/** Where a skipped save is reported: one line naming the job id and the sizes, never the checkpoint. Default console.warn. */
|
|
149
|
+
log?: (line: string) => void;
|
|
150
|
+
}
|
|
151
|
+
/** The cap a variable's value sets: a positive whole number of bytes, else the default. */
|
|
152
|
+
export declare function checkpointMaxBytesSetting(raw: string | undefined): number;
|
|
153
|
+
/**
|
|
154
|
+
* The checkpoint serialized once, as the backend stores it, or null when it
|
|
155
|
+
* is above the cap: then one line is logged with the job id and the sizes
|
|
156
|
+
* only (the checkpoint carries the user's text and tool results), and the
|
|
157
|
+
* caller keeps the previous checkpoint.
|
|
158
|
+
*/
|
|
159
|
+
export declare function checkpointJson(job: Pick<OwnedTask, 'id'>, checkpoint: object, options?: TaskBackendOptions): string | null;
|
|
132
160
|
/** Records kept at most; finished ones are pruned oldest-first to make room. */
|
|
133
161
|
export declare const MAX_TASKS = 500;
|
|
134
162
|
/** A worker result is stored up to this length, then cut with a marker. */
|
|
@@ -154,7 +182,9 @@ export declare function finishJob(id: string, outcome: JobOutcome): void;
|
|
|
154
182
|
export declare function applyFinish(store: TaskStore, id: string, outcome: JobOutcome): void;
|
|
155
183
|
/** The interrupted-job rule for one record: back to the queue, or failed. */
|
|
156
184
|
export declare function applyInterrupted(t: TaskRecord): 'requeued' | 'failed';
|
|
157
|
-
/** The single-user JSON file (the default backend). */
|
|
185
|
+
/** The single-user JSON file, its checkpoints capped by `options` (the default backend is fileTaskBackend). */
|
|
186
|
+
export declare function fileTaskBackendWith(options?: TaskBackendOptions): TaskBackend;
|
|
187
|
+
/** The single-user JSON file (the default backend), checkpoints capped at DEFAULT_CHECKPOINT_MAX_BYTES. */
|
|
158
188
|
export declare const fileTaskBackend: TaskBackend;
|
|
159
189
|
/** Plugs in where tasks live (postgresStorage().taskQueue); default the JSON file. */
|
|
160
190
|
export declare function setTaskBackend(backend: TaskBackend): void;
|