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.
Files changed (146) hide show
  1. package/.env.example +19 -0
  2. package/AGENT_SETUP.md +3 -1
  3. package/CHANGELOG.md +373 -0
  4. package/DOCUMENTATION.md +193 -21
  5. package/ONBOARDING.md +378 -0
  6. package/QUICKSTART.md +15 -2
  7. package/README.md +26 -3
  8. package/config/agents/examples/connectors.yaml +82 -0
  9. package/config/agents/examples/council.yaml +1 -1
  10. package/config/agents/examples/critic.yaml +8 -8
  11. package/config/agents/examples/image_production.yaml +4 -3
  12. package/config/agents/syndicate.schema.json +389 -33
  13. package/config/agents/syndicateSchema.yaml +18 -6
  14. package/config/agents/templates/intake_extractor.yaml +3 -2
  15. package/config/agents/templates/systems_operator.yaml +5 -2
  16. package/dist/lib/a2a/app.d.ts +12 -0
  17. package/dist/lib/a2a/app.js +25 -11
  18. package/dist/lib/a2a/executor.js +14 -6
  19. package/dist/lib/a2a/oauthSetup.d.ts +16 -0
  20. package/dist/lib/a2a/oauthSetup.js +77 -8
  21. package/dist/lib/agentDialect.d.ts +18 -8
  22. package/dist/lib/agentDialect.js +36 -19
  23. package/dist/lib/chatgpt/adapter.d.ts +50 -0
  24. package/dist/lib/chatgpt/adapter.js +107 -0
  25. package/dist/lib/chatgpt/cli.d.ts +23 -0
  26. package/dist/lib/chatgpt/cli.js +77 -0
  27. package/dist/lib/chatgpt/oauth.d.ts +103 -0
  28. package/dist/lib/chatgpt/oauth.js +448 -0
  29. package/dist/lib/chatgpt/state.d.ts +98 -0
  30. package/dist/lib/chatgpt/state.js +145 -0
  31. package/dist/lib/chatgpt/store.d.ts +24 -0
  32. package/dist/lib/chatgpt/store.js +94 -0
  33. package/dist/lib/compile.d.ts +38 -7
  34. package/dist/lib/compile.js +97 -62
  35. package/dist/lib/compileNative.d.ts +11 -3
  36. package/dist/lib/compileNative.js +18 -1
  37. package/dist/lib/dispatch.js +6 -2
  38. package/dist/lib/doctor.d.ts +107 -0
  39. package/dist/lib/doctor.js +217 -7
  40. package/dist/lib/loadSyndicate.d.ts +15 -0
  41. package/dist/lib/mcp/server.d.ts +195 -0
  42. package/dist/lib/mcp/server.js +564 -0
  43. package/dist/lib/memory/supabaseMemoryService.js +2 -2
  44. package/dist/lib/models/adapterResolver.js +6 -1
  45. package/dist/lib/models/capabilities.d.ts +9 -0
  46. package/dist/lib/models/capabilities.js +24 -0
  47. package/dist/lib/models/chatCompletionsAdapter.d.ts +11 -3
  48. package/dist/lib/models/chatCompletionsAdapter.js +19 -5
  49. package/dist/lib/models/claudeAdapter.js +4 -0
  50. package/dist/lib/models/contract.d.ts +5 -2
  51. package/dist/lib/models/gateway.d.ts +3 -2
  52. package/dist/lib/models/gateway.js +8 -4
  53. package/dist/lib/models/gatewayAdapter.d.ts +7 -1
  54. package/dist/lib/models/gatewayAdapter.js +8 -0
  55. package/dist/lib/models/geminiAdapter.js +8 -2
  56. package/dist/lib/models/genaiMapping.d.ts +8 -4
  57. package/dist/lib/models/genaiMapping.js +23 -9
  58. package/dist/lib/models/gptAdapter.d.ts +4 -2
  59. package/dist/lib/models/gptAdapter.js +11 -4
  60. package/dist/lib/models/grokAdapter.d.ts +3 -1
  61. package/dist/lib/models/grokAdapter.js +11 -3
  62. package/dist/lib/models/kimiAdapter.d.ts +2 -1
  63. package/dist/lib/models/kimiAdapter.js +3 -3
  64. package/dist/lib/models/reasoning.d.ts +30 -2
  65. package/dist/lib/models/reasoning.js +56 -5
  66. package/dist/lib/onboarding.d.ts +135 -0
  67. package/dist/lib/onboarding.js +578 -0
  68. package/dist/lib/runtime/approvals.d.ts +2 -0
  69. package/dist/lib/runtime/approvals.js +4 -3
  70. package/dist/lib/runtime/credentials.d.ts +6 -0
  71. package/dist/lib/runtime/native/agentLoop.d.ts +5 -2
  72. package/dist/lib/runtime/native/agentLoop.js +2 -0
  73. package/dist/lib/runtime/native/checkpoint.d.ts +12 -5
  74. package/dist/lib/runtime/native/checkpoint.js +13 -19
  75. package/dist/lib/runtime/native/delegate.d.ts +82 -1
  76. package/dist/lib/runtime/native/delegate.js +157 -2
  77. package/dist/lib/runtime/native/interrupts.d.ts +88 -5
  78. package/dist/lib/runtime/native/interrupts.js +220 -30
  79. package/dist/lib/runtime/syndicateTurn.d.ts +4 -2
  80. package/dist/lib/runtime/syndicateTurn.js +324 -77
  81. package/dist/lib/runtime/turnControl.d.ts +41 -0
  82. package/dist/lib/session/transcript.d.ts +9 -0
  83. package/dist/lib/session/transcript.js +20 -1
  84. package/dist/lib/skills.d.ts +18 -0
  85. package/dist/lib/skills.js +73 -1
  86. package/dist/lib/storage/postgres/index.d.ts +6 -1
  87. package/dist/lib/storage/postgres/index.js +1 -1
  88. package/dist/lib/storage/postgres/taskQueue.d.ts +7 -3
  89. package/dist/lib/storage/postgres/taskQueue.js +10 -3
  90. package/dist/lib/syndicateSchema.d.ts +184 -9
  91. package/dist/lib/syndicateSchema.js +143 -29
  92. package/dist/lib/tools/auth.d.ts +13 -1
  93. package/dist/lib/tools/auth.js +10 -2
  94. package/dist/lib/tools/credentialHosts.d.ts +88 -0
  95. package/dist/lib/tools/credentialHosts.js +165 -0
  96. package/dist/lib/tools/credentialUses.d.ts +98 -0
  97. package/dist/lib/tools/credentialUses.js +120 -0
  98. package/dist/lib/tools/generateImageTool.js +2 -1
  99. package/dist/lib/tools/inspectImageTool.js +1 -1
  100. package/dist/lib/tools/mcpToolFactory.d.ts +29 -1
  101. package/dist/lib/tools/mcpToolFactory.js +72 -26
  102. package/dist/lib/tools/mcpTransports.d.ts +12 -0
  103. package/dist/lib/tools/mcpTransports.js +11 -0
  104. package/dist/lib/tools/oauthConsent.d.ts +18 -3
  105. package/dist/lib/tools/oauthConsent.js +57 -15
  106. package/dist/lib/tools/oauthDiscovery.d.ts +202 -0
  107. package/dist/lib/tools/oauthDiscovery.js +616 -0
  108. package/dist/lib/tools/oauthHosts.d.ts +2 -0
  109. package/dist/lib/tools/oauthHosts.js +2 -2
  110. package/dist/lib/tools/oauthTools.d.ts +54 -13
  111. package/dist/lib/tools/oauthTools.js +106 -23
  112. package/dist/lib/tools/openapiTools.d.ts +7 -0
  113. package/dist/lib/tools/openapiTools.js +49 -3
  114. package/dist/lib/tools/taskTools.d.ts +32 -2
  115. package/dist/lib/tools/taskTools.js +93 -33
  116. package/dist/lib/tools/xApiSearchTool.js +2 -2
  117. package/dist/lib/workflow/turn.d.ts +22 -3
  118. package/dist/lib/workflow/turn.js +151 -31
  119. package/dist/lib/workflow.d.ts +10 -7
  120. package/dist/lib/workflow.js +10 -7
  121. package/dist/scripts/a2a_server.d.ts +2 -2
  122. package/dist/scripts/a2a_server.js +19 -2
  123. package/dist/scripts/assistant_worker.d.ts +2 -0
  124. package/dist/scripts/assistant_worker.js +24 -5
  125. package/dist/scripts/doctor.d.ts +4 -2
  126. package/dist/scripts/doctor.js +6 -4
  127. package/dist/scripts/mcp_server.d.ts +12 -0
  128. package/dist/scripts/mcp_server.js +345 -0
  129. package/dist/scripts/setup.d.ts +25 -0
  130. package/dist/scripts/setup.js +181 -0
  131. package/dist/scripts/skills_install.d.ts +3 -1
  132. package/dist/scripts/skills_install.js +17 -2
  133. package/dist/scripts/syndicate_chat.js +12 -3
  134. package/dist/scripts/yaml_codemod.d.ts +12 -2
  135. package/dist/scripts/yaml_codemod.js +115 -33
  136. package/package.json +8 -2
  137. package/skills/README.md +11 -2
  138. package/skills/briefs/_shared.md +1 -1
  139. package/skills/briefs/melchizedek-models.md +4 -2
  140. package/skills/briefs/skills-readme.md +3 -1
  141. package/skills/melchizedek-models/SKILL.md +6 -2
  142. package/skills/melchizedek-onboard/SKILL.md +71 -0
  143. package/skills/melchizedek-onboard-cloud/SKILL.md +38 -0
  144. package/skills/melchizedek-onboard-keys/SKILL.md +47 -0
  145. package/skills/melchizedek-onboard-local/SKILL.md +41 -0
  146. 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: string;
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
- env?: NodeJS.ProcessEnv;
198
- load?: (ref: string) => SyndicateLike;
199
- allowlist?: OAuthHostAllowlist | null;
200
- }): Record<string, OAuthClientConfig>;
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; `allowPrivate` lets a private or loopback
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
- urls.push(['token_url', oauth2.token_url]);
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 transport = tokenTransportProblem(oauth2.token_url);
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(oauth2.token_url).hostname);
301
+ const blocked = blockedHostReason(new URL(tokenUrl).hostname);
285
302
  if (blocked)
286
- throw new Error(`${where}: refusing token endpoint ${new URL(oauth2.token_url).hostname}: ${blocked}`);
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: oauth2.token_url,
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
- refuse(oauth2.token_url);
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
- * The OAuth clients the consent step needs for these syndicates' declared
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; `allowPrivate` lets a private or loopback
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, client] of Object.entries(clients)) {
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, credential, oauth) {
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;