melchizedek-agents 1.1.0 → 1.2.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 (118) hide show
  1. package/.env.example +10 -0
  2. package/AGENT_SETUP.md +3 -1
  3. package/CHANGELOG.md +232 -0
  4. package/DOCUMENTATION.md +91 -16
  5. package/ONBOARDING.md +339 -0
  6. package/QUICKSTART.md +15 -2
  7. package/README.md +5 -2
  8. package/config/agents/examples/council.yaml +1 -1
  9. package/config/agents/examples/critic.yaml +8 -8
  10. package/config/agents/examples/image_production.yaml +4 -3
  11. package/config/agents/syndicate.schema.json +65 -7
  12. package/config/agents/syndicateSchema.yaml +18 -6
  13. package/config/agents/templates/intake_extractor.yaml +3 -2
  14. package/config/agents/templates/systems_operator.yaml +5 -2
  15. package/dist/lib/a2a/app.d.ts +12 -0
  16. package/dist/lib/a2a/app.js +22 -11
  17. package/dist/lib/a2a/executor.js +14 -6
  18. package/dist/lib/a2a/oauthSetup.d.ts +16 -0
  19. package/dist/lib/a2a/oauthSetup.js +57 -3
  20. package/dist/lib/agentDialect.d.ts +18 -8
  21. package/dist/lib/agentDialect.js +36 -19
  22. package/dist/lib/compile.d.ts +38 -7
  23. package/dist/lib/compile.js +57 -37
  24. package/dist/lib/compileNative.d.ts +11 -3
  25. package/dist/lib/compileNative.js +18 -1
  26. package/dist/lib/doctor.d.ts +76 -0
  27. package/dist/lib/doctor.js +159 -7
  28. package/dist/lib/loadSyndicate.d.ts +9 -0
  29. package/dist/lib/models/capabilities.d.ts +9 -0
  30. package/dist/lib/models/capabilities.js +24 -0
  31. package/dist/lib/models/chatCompletionsAdapter.d.ts +11 -3
  32. package/dist/lib/models/chatCompletionsAdapter.js +19 -5
  33. package/dist/lib/models/claudeAdapter.js +4 -0
  34. package/dist/lib/models/contract.d.ts +5 -2
  35. package/dist/lib/models/gateway.d.ts +2 -1
  36. package/dist/lib/models/gateway.js +4 -2
  37. package/dist/lib/models/gatewayAdapter.d.ts +7 -1
  38. package/dist/lib/models/gatewayAdapter.js +8 -0
  39. package/dist/lib/models/geminiAdapter.js +8 -2
  40. package/dist/lib/models/genaiMapping.d.ts +8 -4
  41. package/dist/lib/models/genaiMapping.js +23 -9
  42. package/dist/lib/models/gptAdapter.d.ts +4 -2
  43. package/dist/lib/models/gptAdapter.js +11 -4
  44. package/dist/lib/models/grokAdapter.d.ts +3 -1
  45. package/dist/lib/models/grokAdapter.js +11 -3
  46. package/dist/lib/models/kimiAdapter.d.ts +2 -1
  47. package/dist/lib/models/kimiAdapter.js +3 -3
  48. package/dist/lib/models/reasoning.d.ts +30 -2
  49. package/dist/lib/models/reasoning.js +56 -5
  50. package/dist/lib/onboarding.d.ts +134 -0
  51. package/dist/lib/onboarding.js +539 -0
  52. package/dist/lib/runtime/approvals.d.ts +2 -0
  53. package/dist/lib/runtime/approvals.js +4 -3
  54. package/dist/lib/runtime/credentials.d.ts +6 -0
  55. package/dist/lib/runtime/native/agentLoop.d.ts +5 -2
  56. package/dist/lib/runtime/native/agentLoop.js +2 -0
  57. package/dist/lib/runtime/native/checkpoint.d.ts +12 -5
  58. package/dist/lib/runtime/native/checkpoint.js +13 -19
  59. package/dist/lib/runtime/native/delegate.d.ts +82 -1
  60. package/dist/lib/runtime/native/delegate.js +157 -2
  61. package/dist/lib/runtime/native/interrupts.d.ts +88 -5
  62. package/dist/lib/runtime/native/interrupts.js +220 -30
  63. package/dist/lib/runtime/syndicateTurn.d.ts +4 -2
  64. package/dist/lib/runtime/syndicateTurn.js +295 -62
  65. package/dist/lib/runtime/turnControl.d.ts +41 -0
  66. package/dist/lib/session/transcript.d.ts +9 -0
  67. package/dist/lib/session/transcript.js +20 -1
  68. package/dist/lib/skills.d.ts +18 -0
  69. package/dist/lib/skills.js +73 -1
  70. package/dist/lib/storage/postgres/index.d.ts +6 -1
  71. package/dist/lib/storage/postgres/index.js +1 -1
  72. package/dist/lib/storage/postgres/taskQueue.d.ts +7 -3
  73. package/dist/lib/storage/postgres/taskQueue.js +10 -3
  74. package/dist/lib/syndicateSchema.d.ts +38 -1
  75. package/dist/lib/syndicateSchema.js +25 -18
  76. package/dist/lib/tools/auth.d.ts +13 -1
  77. package/dist/lib/tools/auth.js +10 -2
  78. package/dist/lib/tools/credentialHosts.d.ts +88 -0
  79. package/dist/lib/tools/credentialHosts.js +165 -0
  80. package/dist/lib/tools/credentialUses.d.ts +92 -0
  81. package/dist/lib/tools/credentialUses.js +112 -0
  82. package/dist/lib/tools/mcpToolFactory.js +1 -1
  83. package/dist/lib/tools/oauthConsent.js +8 -1
  84. package/dist/lib/tools/oauthHosts.d.ts +2 -0
  85. package/dist/lib/tools/oauthHosts.js +2 -2
  86. package/dist/lib/tools/oauthTools.d.ts +10 -1
  87. package/dist/lib/tools/oauthTools.js +29 -3
  88. package/dist/lib/tools/openapiTools.d.ts +7 -0
  89. package/dist/lib/tools/openapiTools.js +49 -3
  90. package/dist/lib/tools/taskTools.d.ts +32 -2
  91. package/dist/lib/tools/taskTools.js +93 -33
  92. package/dist/lib/workflow/turn.d.ts +22 -3
  93. package/dist/lib/workflow/turn.js +151 -31
  94. package/dist/lib/workflow.d.ts +10 -7
  95. package/dist/lib/workflow.js +10 -7
  96. package/dist/scripts/a2a_server.d.ts +2 -2
  97. package/dist/scripts/a2a_server.js +11 -2
  98. package/dist/scripts/assistant_worker.d.ts +2 -0
  99. package/dist/scripts/assistant_worker.js +13 -4
  100. package/dist/scripts/doctor.d.ts +4 -2
  101. package/dist/scripts/doctor.js +6 -4
  102. package/dist/scripts/setup.d.ts +23 -0
  103. package/dist/scripts/setup.js +154 -0
  104. package/dist/scripts/skills_install.d.ts +3 -1
  105. package/dist/scripts/skills_install.js +17 -2
  106. package/dist/scripts/yaml_codemod.d.ts +12 -2
  107. package/dist/scripts/yaml_codemod.js +115 -33
  108. package/package.json +4 -1
  109. package/skills/README.md +11 -2
  110. package/skills/briefs/_shared.md +1 -1
  111. package/skills/briefs/melchizedek-models.md +4 -2
  112. package/skills/briefs/skills-readme.md +3 -1
  113. package/skills/melchizedek-models/SKILL.md +6 -2
  114. package/skills/melchizedek-onboard/SKILL.md +49 -0
  115. package/skills/melchizedek-onboard-cloud/SKILL.md +38 -0
  116. package/skills/melchizedek-onboard-keys/SKILL.md +35 -0
  117. package/skills/melchizedek-onboard-local/SKILL.md +41 -0
  118. package/skills/melchizedek-onboard-serve/SKILL.md +43 -0
@@ -365,7 +365,7 @@ orchestrator:
365
365
 
366
366
  # ── reasoning ─────────────────────────────────────────────
367
367
  # [optional] How hard the agent reasons, on any provider (ADR 0047):
368
- # none | low | medium | high, or { budget_tokens: <int> }.
368
+ # none | low | medium | high | xhigh | max, or { budget_tokens: <int> }.
369
369
  # The compiler sends each provider the field it reads:
370
370
  # gemini-3 and later → thinkingConfig.thinkingLevel
371
371
  # (none = MINIMAL, then LOW, MEDIUM, HIGH)
@@ -382,6 +382,10 @@ orchestrator:
382
382
  # A budget is sent as-is to Gemini and to Claude 4.6 and earlier, and as
383
383
  # the smallest level that covers it everywhere else. Unset, each provider
384
384
  # keeps its default.
385
+ # xhigh and max (ADR 0117) reach the models that take them: both on later
386
+ # claude-*, max on kimi-k3 (xhigh rounds up to it), xhigh on gpt-5.2 and
387
+ # later and grok-4.7. Elsewhere the model's highest setting is sent and
388
+ # the span carries llm.reasoning.weakened; a gateway gets high.
385
389
  # Cannot be combined with generateContentConfig.thinkingConfig or
386
390
  # generateContentConfig.reasoningEffort, its deprecated v1 spelling.
387
391
  reasoning: medium
@@ -422,6 +426,14 @@ orchestrator:
422
426
  # answer: { type: string }
423
427
  # required: [answer]
424
428
 
429
+ # ── tool_choice ───────────────────────────────────────────
430
+ # [optional] Which tools the model may call, on any provider (ADR 0117):
431
+ # auto (the default) | none | required (some tool)
432
+ # | { name: <tool> } (that one tool).
433
+ # A provider that rejects forcing a tool gets a weaker choice, and the
434
+ # span carries llm.tool_choice.weakened.
435
+ # tool_choice: auto
436
+
425
437
  # ── model_overrides ───────────────────────────────────────
426
438
  # [optional] A prompt nuance for one provider (ADR 0115). Keys are the
427
439
  # providers a model id routes to: gemini, anthropic, openai, xai,
@@ -438,11 +450,11 @@ orchestrator:
438
450
  # The v1 spelling: @google/genai's GenerateContentConfig, with
439
451
  # outputSchema beside it. Still loads and behaves as before, with one
440
452
  # deprecation line per file; `npx melchizedek-codemod <file>` rewrites
441
- # it into sampling, output and reasoning. A v2 key beside its v1
442
- # spelling is a load error. Only a few fields have no v2 form yet:
443
- # toolConfig (tool choice) and the effort words xhigh and max
444
- # (reasoningEffort). topK, seed, the penalties, candidateCount,
445
- # safetySettings and includeThoughts reach no provider.
453
+ # it into sampling, output, reasoning and tool_choice. A v2 key beside
454
+ # its v1 spelling is a load error. Only toolConfig's VALIDATED mode and
455
+ # Gemini's dynamic budget (thinkingBudget: -1) have no v2 form. topK,
456
+ # seed, the penalties, candidateCount, safetySettings and
457
+ # includeThoughts reach no provider.
446
458
 
447
459
  # ── Subagents ───────────────────────────────────────────────
448
460
  # Each entry is an agent the orchestrator calls as a tool (DELEGATE),
@@ -23,8 +23,9 @@
23
23
  #
24
24
  # SHAPE: a single agent with an `outputSchema`, so the reply is JSON the
25
25
  # schema describes, parseable without a second model call. No subagents and
26
- # no tools: the document is the only input, and the extractor may not add
27
- # anything the document does not say.
26
+ # no tools, by choice rather than by rule (an agent that delegates or calls
27
+ # tools may hold a schema too, ADR 0109): the document is the only input,
28
+ # and the extractor may not add anything the document does not say.
28
29
  #
29
30
  # REDACTION: card numbers, bank details, passwords and government IDs are
30
31
  # never copied into the record, not even as a `references` entry; `redacted`
@@ -24,8 +24,11 @@
24
24
  # `mcp_auth: { oauth2 }` (ADR 0112): uncomment the block on Systems
25
25
  # below. client_credentials gives the server's own token on every
26
26
  # request. authorization_code (each person's own token, granted
27
- # through the consent pause) needs the agent to run the turn
28
- # itself: a dispatch route, not a delegated subagent like Systems.
27
+ # through the consent pause) works on Systems too: the first call
28
+ # for a person who has not connected pauses the turn with an
29
+ # authorization link, and their next message after the grant
30
+ # resumes Systems' call (ADR 0118). It also needs mcp_tools, the
31
+ # server's tool names, and an operator allowlist (ADR 0114).
29
32
  # `npm run doctor` lists every tool that needs a grant.
30
33
  # Loopback and private addresses are refused unless
31
34
  # ALLOW_PRIVATE_MCP=true (local development).
@@ -219,6 +219,18 @@ export interface A2AAppOptions {
219
219
  * refused when loaded, when compiled, and at each call.
220
220
  */
221
221
  oauthHosts?: Record<string, string[]>;
222
+ /**
223
+ * The credential host allowlist (ADR 0122): each static credential
224
+ * variable a YAML may send (`bearer_env`, `api_key.env`,
225
+ * `client_secret_env`) and the hosts its value may go to, e.g.
226
+ * `{ TRACKER_TOKEN: ['api.tracker.example.com'] }`. Wins over
227
+ * MELCHIZEDEK_CREDENTIAL_HOSTS; it is set for this process. With neither,
228
+ * a credential goes to the host the YAML names, as before. With one, it is
229
+ * the whole list: a syndicate that sends a variable it does not bind, or
230
+ * to a host outside that variable's, is refused when loaded, when
231
+ * compiled, and at each call.
232
+ */
233
+ credentialHosts?: Record<string, string[]>;
222
234
  toolCredentials?: ToolCredentials & {
223
235
  /** Callback requests per window per client IP. Default 30 per 15 minutes. */
224
236
  callbackLimit?: {
@@ -64,6 +64,8 @@ import { HEADER_VALUE_PATTERN, SCOPE_KEY_PATTERN } from './identity.js';
64
64
  import { createMetrics } from '../observability/metrics.js';
65
65
  import { ConsentError } from '../tools/oauthConsent.js';
66
66
  import { setOAuthHosts } from '../tools/oauthHosts.js';
67
+ import { setCredentialHosts } from '../tools/credentialHosts.js';
68
+ import { syndicateCredentialHostProblems } from '../tools/credentialUses.js';
67
69
  import { syndicateOAuthHostProblems } from '../tools/oauthTools.js';
68
70
  const SURFACE_HEADERS = [
69
71
  ['x-surface', 'name'],
@@ -280,21 +282,30 @@ export async function createA2AApp(options) {
280
282
  // ── The OAuth host allowlist (ADR 0114) ────────────────────────────────────
281
283
  if (options.oauthHosts)
282
284
  setOAuthHosts(options.oauthHosts);
283
- /** A served syndicate whose OAuth grant names a provider or host outside the allowlist is refused at load. */
285
+ // ── The credential host allowlist (ADR 0122) ───────────────────────────────
286
+ if (options.credentialHosts)
287
+ setCredentialHosts(options.credentialHosts);
288
+ /**
289
+ * A served syndicate whose OAuth grant names a provider or host outside the
290
+ * OAuth allowlist, or that sends a static credential the credential
291
+ * allowlist does not bind to its host, is refused at load.
292
+ */
284
293
  const checkOAuthHosts = (cfg, label) => {
285
294
  const nested = nestedLoader(cfg, (ref) => loadSyndicate(ref, { bindings: bindings() }));
286
- const problems = syndicateOAuthHostProblems([cfg], {
287
- load: (ref) => {
288
- try {
289
- return nested(ref);
290
- }
291
- catch {
292
- return {}; // A nested file that cannot load fails its own compile.
293
- }
294
- },
295
- });
295
+ const load = (ref) => {
296
+ try {
297
+ return nested(ref);
298
+ }
299
+ catch {
300
+ return {}; // A nested file that cannot load fails its own compile.
301
+ }
302
+ };
303
+ const problems = syndicateOAuthHostProblems([cfg], { load });
296
304
  if (problems.length)
297
305
  throw new Error(`${label}: refusing an OAuth grant the operator's host allowlist does not permit:\n - ${problems.join('\n - ')}`);
306
+ const credentials = syndicateCredentialHostProblems([cfg], { load });
307
+ if (credentials.length)
308
+ throw new Error(`${label}: refusing a credential the operator's credential host allowlist does not permit:\n - ${credentials.join('\n - ')}`);
298
309
  return cfg;
299
310
  };
300
311
  // ── The default syndicate ──────────────────────────────────────────────────
@@ -22,7 +22,7 @@ import { turnLockKey } from './turnLock.js';
22
22
  import { ingestTurnMemory, runSyndicateTurn } from '../runtime/syndicateTurn.js';
23
23
  import { approvalResponsePart, describeApproval, pendingApproval } from '../runtime/approvals.js';
24
24
  import { declaresApprovals } from '../compile.js';
25
- import { delegatedPauses } from '../runtime/native/interrupts.js';
25
+ import { deepestPause, delegatedPauses, routePause } from '../runtime/native/interrupts.js';
26
26
  import { isWorkflowSyndicate } from '../workflow.js';
27
27
  import { describeInput } from '../runtime/syndicateTurn.js';
28
28
  import { describeConsent } from '../runtime/credentials.js';
@@ -323,7 +323,8 @@ function publishApprovalRequest(eventBus, taskId, contextId, pending) {
323
323
  * naming the provider, with a data part carrying the authorization URL and
324
324
  * its state nonce. The person opens the URL; the server's callback stores
325
325
  * the grant; the next message on the conversation resumes the call. The
326
- * part carries no token, code or verifier.
326
+ * part carries no token, code or verifier. For a call inside a delegated
327
+ * subagent it carries the agent path too (ADR 0118).
327
328
  */
328
329
  function publishConsentRequest(eventBus, taskId, contextId, pending) {
329
330
  const message = statusMessage(taskId, contextId, `Authorization needed: ${describeConsent(pending)}. Open ${pending.authUri} to authorize, then send any message on this conversation to continue.`);
@@ -338,6 +339,8 @@ function publishConsentRequest(eventBus, taskId, contextId, pending) {
338
339
  authorization_url: pending.authUri,
339
340
  state: pending.state,
340
341
  scopes: pending.scopes,
342
+ // A call paused inside a delegated subagent: the agents from the turn's own down to the one that asked (ADR 0118).
343
+ ...(pending.path ? { path: pending.path } : {}),
341
344
  },
342
345
  },
343
346
  metadata: undefined,
@@ -594,13 +597,18 @@ export class SyndicateExecutor {
594
597
  // A call gated inside a delegated subagent waits below an open call
595
598
  // (ADR 0110), wherever the gate is declared, so a syndicate that
596
599
  // delegates is read too.
597
- const delegates = !isWorkflowSyndicate(config) && !!config.subagents?.length;
598
- if (declaresApprovals(config) || delegates) {
600
+ // A workflow's node that is a nested workflow, and a dispatch route that is one, pause on their walk's gates
601
+ // wherever those are declared (ADR 0119), so every workflow, and every syndicate that delegates or routes, is read.
602
+ const workflow = isWorkflowSyndicate(config);
603
+ const delegates = !workflow && !!config.subagents?.length;
604
+ if (declaresApprovals(config) || delegates || workflow) {
599
605
  const session = await this.sessions.get({ appName, userId, sessionId: contextId });
600
606
  // The stored Event JSON (ADR 0052), read as TurnEvents.
601
607
  const events = session?.events ?? [];
602
- const pending = (declaresApprovals(config) ? pendingApproval(events) : undefined) ??
603
- (delegates ? (await delegatedPauses({ sessions: this.sessions, userId, sessionId: contextId, appName }, events))[0]?.approval : undefined);
608
+ const key = { sessions: this.sessions, userId, sessionId: contextId, appName };
609
+ const own = declaresApprovals(config) || workflow ? pendingApproval(events) : undefined;
610
+ const pending = (own && workflow ? await deepestPause(key, own) : own) ??
611
+ (delegates ? ((await routePause(key, events))?.approval ?? (await delegatedPauses(key, events))[0]?.approval) : undefined);
604
612
  if (pending) {
605
613
  const answer = approvalAnswer(rawParts, pending);
606
614
  if (!answer) {
@@ -13,6 +13,7 @@
13
13
  * MELCHIZEDEK_CREDENTIAL_KEY the key that seals each user's tokens (ADR 0072)
14
14
  * OAUTH_REDIRECT_URI the consent callback, as registered at every provider (ADR 0085)
15
15
  * MELCHIZEDEK_OAUTH_HOSTS which hosts each provider's tokens may go to (ADR 0114)
16
+ * MELCHIZEDEK_CREDENTIAL_HOSTS which hosts each static credential variable may go to (ADR 0122)
16
17
  * the client id and secret variables each YAML grant names
17
18
  *
18
19
  * The consent clients are built from the syndicates the server serves from
@@ -54,6 +55,19 @@ export declare function grantHostProblems(use: OAuthGrantUse, env?: NodeJS.Proce
54
55
  * server refuses to start on the ones that would make it unsafe.
55
56
  */
56
57
  export declare function oauthEnvProblems(grants: readonly OAuthGrantUse[], env?: NodeJS.ProcessEnv): string[];
58
+ /**
59
+ * The warning for credential variables no allowlist binds (ADR 0122), or
60
+ * null: with no credential host allowlist, each variable in `envs` (the
61
+ * ones the served YAML sends) still goes to whatever host the YAML names.
62
+ * Names only.
63
+ */
64
+ export declare function unboundCredentialWarning(envs: readonly string[], env?: NodeJS.ProcessEnv): string | null;
65
+ /**
66
+ * What is wrong with the credential host allowlist for these uses, one line
67
+ * each, names and hosts only: a malformed allowlist, or each use it refuses
68
+ * (lib/tools/credentialUses.ts). Empty when none is configured.
69
+ */
70
+ export declare function credentialHostEnvProblems(configs: readonly SyndicateLike[], env?: NodeJS.ProcessEnv, load?: (ref: string) => SyndicateLike): string[];
57
71
  /** The parts of a syndicate config the setup reads. */
58
72
  type SyndicateLike = Parameters<typeof oauthClientsFor>[0][number];
59
73
  export interface OAuthServerSetupOptions {
@@ -76,6 +90,8 @@ export interface OAuthServerSetup {
76
90
  toolCredentials?: ToolCredentials;
77
91
  /** One line for the server's banner: key id, row store, consent path, providers, allowlist. No value. */
78
92
  summary: string;
93
+ /** One line for the server's banner: the credential host allowlist (ADR 0122), variable names only. */
94
+ credentialSummary: string;
79
95
  /** What works less than the YAML asks, names only. */
80
96
  warnings: string[];
81
97
  }
@@ -13,6 +13,7 @@
13
13
  * MELCHIZEDEK_CREDENTIAL_KEY the key that seals each user's tokens (ADR 0072)
14
14
  * OAUTH_REDIRECT_URI the consent callback, as registered at every provider (ADR 0085)
15
15
  * MELCHIZEDEK_OAUTH_HOSTS which hosts each provider's tokens may go to (ADR 0114)
16
+ * MELCHIZEDEK_CREDENTIAL_HOSTS which hosts each static credential variable may go to (ADR 0122)
16
17
  * the client id and secret variables each YAML grant names
17
18
  *
18
19
  * The consent clients are built from the syndicates the server serves from
@@ -27,6 +28,8 @@ import { credentialStore, memoryCredentialRows } from '../tools/credentialStore.
27
28
  import { oauthConsent } from '../tools/oauthConsent.js';
28
29
  import { OAUTH_HOSTS_ENV, oauthHosts } from '../tools/oauthHosts.js';
29
30
  import { oauthClientsFor, oauthGrantHostProblems, oauthRefreshProviders, syndicateOAuthGrants, syndicateOAuthHostProblems } from '../tools/oauthTools.js';
31
+ import { CREDENTIAL_HOSTS_ENV, credentialHosts } from '../tools/credentialHosts.js';
32
+ import { syndicateCredentialHostProblems, syndicateCredentialUses, unboundCredentialEnvs } from '../tools/credentialUses.js';
30
33
  /** The consent callback's URL, exactly as registered at every provider. */
31
34
  export const OAUTH_REDIRECT_URI_ENV = 'OAUTH_REDIRECT_URI';
32
35
  /** Who may complete a consent callback: `required` (the browser carries the user's identity) or `state` (the nonce alone). */
@@ -138,6 +141,42 @@ export function oauthEnvProblems(grants, env = process.env) {
138
141
  }
139
142
  return problems;
140
143
  }
144
+ /**
145
+ * The warning for credential variables no allowlist binds (ADR 0122), or
146
+ * null: with no credential host allowlist, each variable in `envs` (the
147
+ * ones the served YAML sends) still goes to whatever host the YAML names.
148
+ * Names only.
149
+ */
150
+ export function unboundCredentialWarning(envs, env = process.env) {
151
+ let allowlist;
152
+ try {
153
+ allowlist = credentialHosts(env);
154
+ }
155
+ catch {
156
+ return null; // A malformed allowlist is a problem, reported as one.
157
+ }
158
+ if (allowlist)
159
+ return null; // Configured: an unbound variable is refused, not warned about.
160
+ const unbound = [...new Set(envs)].sort();
161
+ if (!unbound.length)
162
+ return null;
163
+ return `${unbound.join(', ')} ${unbound.length === 1 ? 'is' : 'are'} sent to whatever host the YAML names: no ${CREDENTIAL_HOSTS_ENV} binds ${unbound.length === 1 ? 'it' : 'them'} to ${unbound.length === 1 ? 'its' : 'their'} hosts`;
164
+ }
165
+ /**
166
+ * What is wrong with the credential host allowlist for these uses, one line
167
+ * each, names and hosts only: a malformed allowlist, or each use it refuses
168
+ * (lib/tools/credentialUses.ts). Empty when none is configured.
169
+ */
170
+ export function credentialHostEnvProblems(configs, env = process.env, load) {
171
+ let allowlist;
172
+ try {
173
+ allowlist = credentialHosts(env);
174
+ }
175
+ catch (err) {
176
+ return [err instanceof Error ? err.message : `${CREDENTIAL_HOSTS_ENV} is malformed`];
177
+ }
178
+ return syndicateCredentialHostProblems(configs, { allowlist: allowlist ?? null, ...(load ? { load } : {}) });
179
+ }
141
180
  /**
142
181
  * The credential store and consent step from the environment. Throws, naming
143
182
  * variables only, when the configuration would be unsafe or cannot work: a
@@ -163,12 +202,26 @@ export function oauthServerSetup(options) {
163
202
  const refused = syndicateOAuthHostProblems(options.configs, { ...(options.load ? { load: options.load } : {}), allowlist: allowlist ?? null });
164
203
  if (refused.length)
165
204
  throw new Error(`refusing an OAuth grant the operator's host allowlist does not permit:\n - ${refused.join('\n - ')}`);
205
+ // The credential host allowlist (ADR 0122): malformed throws here, at boot; a served file it refuses too.
206
+ const credentialList = credentialHosts(env);
207
+ const credentialRefused = syndicateCredentialHostProblems(options.configs, { ...(options.load ? { load: options.load } : {}), allowlist: credentialList ?? null });
208
+ if (credentialRefused.length)
209
+ throw new Error(`refusing a credential the operator's credential host allowlist does not permit:\n - ${credentialRefused.join('\n - ')}`);
210
+ const uses = syndicateCredentialUses(options.configs, options.load);
211
+ const unboundWarning = unboundCredentialWarning(uses.map((u) => u.env), env);
212
+ if (unboundWarning)
213
+ warnings.push(`${unboundWarning.charAt(0).toUpperCase()}${unboundWarning.slice(1)}.`);
214
+ const credentialSummary = credentialList
215
+ ? `bound: ${Object.keys(credentialList).join(', ')} (${CREDENTIAL_HOSTS_ENV})`
216
+ : uses.length
217
+ ? `no allowlist (${CREDENTIAL_HOSTS_ENV}): ${unboundCredentialEnvs(uses, undefined).join(', ')} unbound`
218
+ : `no allowlist (${CREDENTIAL_HOSTS_ENV}); no served file sends a static credential`;
166
219
  if (!cipher) {
167
220
  if (redirect)
168
221
  throw new Error(`${OAUTH_REDIRECT_URI_ENV} is set but ${CREDENTIAL_KEY_ENV} is not: the consent step has nowhere to keep a grant. Set ${CREDENTIAL_KEY_ENV} (openssl rand -base64 32).`);
169
222
  if (authCode.length)
170
223
  warnings.push(`authorization_code grants (${authCode.join(', ')}) are declared but ${CREDENTIAL_KEY_ENV} is not set: their tools answer unavailable.`);
171
- return { summary: `off (${CREDENTIAL_KEY_ENV} unset) · ${hostsLabel}`, warnings };
224
+ return { summary: `off (${CREDENTIAL_KEY_ENV} unset) · ${hostsLabel}`, credentialSummary, warnings };
172
225
  }
173
226
  const clients = oauthClientsFor(options.configs, { env, ...(options.load ? { load: options.load } : {}), allowlist: allowlist ?? null });
174
227
  const providers = Object.keys(clients);
@@ -184,11 +237,11 @@ export function oauthServerSetup(options) {
184
237
  if (!redirect) {
185
238
  if (providers.length)
186
239
  warnings.push(`${OAUTH_REDIRECT_URI_ENV} is not set: a user who has not connected ${providers.join(', ')} cannot be asked to.`);
187
- return { toolCredentials: { store }, summary: `sealed (key ${cipher.keyId}) in ${where}; no consent step · ${hostsLabel}`, warnings };
240
+ return { toolCredentials: { store }, summary: `sealed (key ${cipher.keyId}) in ${where}; no consent step · ${hostsLabel}`, credentialSummary, warnings };
188
241
  }
189
242
  if (!providers.length) {
190
243
  warnings.push(`${OAUTH_REDIRECT_URI_ENV} is set but no served syndicate declares an authorization_code grant: no consent callback is mounted.`);
191
- return { toolCredentials: { store }, summary: `sealed (key ${cipher.keyId}) in ${where}; no consent step · ${hostsLabel}`, warnings };
244
+ return { toolCredentials: { store }, summary: `sealed (key ${cipher.keyId}) in ${where}; no consent step · ${hostsLabel}`, credentialSummary, warnings };
192
245
  }
193
246
  const consent = oauthConsent({
194
247
  providers: clients,
@@ -200,6 +253,7 @@ export function oauthServerSetup(options) {
200
253
  return {
201
254
  toolCredentials: { store, consent },
202
255
  summary: `sealed (key ${cipher.keyId}) in ${where}; consent at ${new URL(redirect).pathname} for ${providers.join(', ')} · ${hostsLabel}`,
256
+ credentialSummary,
203
257
  warnings,
204
258
  };
205
259
  }
@@ -7,13 +7,15 @@
7
7
  * Gemini's dialect (`generateContentConfig.temperature`, `outputSchema`),
8
8
  * which is what the engine still reads (lib/runtime/native/request.ts
9
9
  * buildModelRequest). YAML v2 spells them provider-neutrally (`sampling`,
10
- * `output`) and adds per-provider instruction overrides (`model_overrides`).
10
+ * `output`, and `tool_choice` from ADR 0117) and adds per-provider
11
+ * instruction overrides (`model_overrides`).
11
12
  * This module is the one place the two spellings meet:
12
13
  *
13
14
  * - `V2_SPELLINGS`: each v2 key path and the v1 path it replaces. The
14
15
  * loader (lib/syndicateSchema.ts crossFieldProblems) refuses a file that
15
16
  * sets both; `toEngineAgent` refuses a config built in code that does.
16
- * - `toEngineAgent`: folds `sampling` and `output` into the engine form.
17
+ * - `toEngineAgent`: folds `sampling`, `output` and `tool_choice` into
18
+ * the engine form.
17
19
  * validateSyndicateConfig applies it to every agent it returns, so every
18
20
  * loader hands downstream the engine form; lib/compile.ts applies it
19
21
  * again (idempotent) for configs built in code.
@@ -27,13 +29,21 @@
27
29
  import type { AgentYamlConfig } from './loadSyndicate.ts';
28
30
  /** One v2 key and the v1 (engine) path it replaces. Both paths are relative to the agent. */
29
31
  export interface V2Spelling {
30
- readonly v2: readonly [string, string];
32
+ readonly v2: readonly string[];
31
33
  readonly v1: readonly string[];
34
+ /** The engine value for the v2 value, when the two are spelled differently. */
35
+ readonly toV1?: (value: unknown) => unknown;
32
36
  }
37
+ /**
38
+ * `tool_choice` as Gemini's function-calling config, which the engine reads
39
+ * (lib/models/genaiMapping.ts toolChoiceOf): `auto` is AUTO, `none` NONE,
40
+ * `required` ANY, and `{ name }` ANY with that one name allowed (ADR 0117).
41
+ */
42
+ export declare function functionCallingConfigOf(choice: unknown): Record<string, unknown>;
33
43
  /** Every v2 key with a v1 equivalent; the mapper writes the v1 path. */
34
44
  export declare const V2_SPELLINGS: readonly V2Spelling[];
35
45
  /** The v2 agent keys, which an inline agent alone may carry (not a yaml_reference or a2a_agent_url subagent). */
36
- export declare const V2_AGENT_KEYS: readonly ['sampling', 'output', 'model_overrides'];
46
+ export declare const V2_AGENT_KEYS: readonly ['sampling', 'output', 'tool_choice', 'model_overrides'];
37
47
  /** The v1 spellings ADR 0115 deprecates, as agent keys. */
38
48
  export declare const V1_AGENT_KEYS: readonly ['generateContentConfig', 'outputSchema'];
39
49
  /** The v2/v1 pairs an agent sets both of: each is a conflict. */
@@ -41,10 +51,10 @@ export declare function conflictsOf(agent: unknown): V2Spelling[];
41
51
  /** The loader's message for one conflict, reported against the v2 key path. */
42
52
  export declare function conflictMessage(s: V2Spelling): string;
43
53
  /**
44
- * An agent in the engine form: `sampling` and `output` folded into
45
- * `generateContentConfig` and `outputSchema` (the table above) and removed;
46
- * `model_overrides` kept (lib/compile.ts applies it). The same object when
47
- * the agent has neither `sampling` nor `output`, so it is idempotent. Throws
54
+ * An agent in the engine form: `sampling`, `output` and `tool_choice` folded
55
+ * into `generateContentConfig` and `outputSchema` (the table above) and
56
+ * removed; `model_overrides` kept (lib/compile.ts applies it). The same
57
+ * object when the agent has none of the three, so it is idempotent. Throws
48
58
  * when a v2 key and the v1 key it replaces are both set.
49
59
  */
50
60
  export declare function toEngineAgent<T extends object>(agent: T): T;
@@ -7,13 +7,15 @@
7
7
  * Gemini's dialect (`generateContentConfig.temperature`, `outputSchema`),
8
8
  * which is what the engine still reads (lib/runtime/native/request.ts
9
9
  * buildModelRequest). YAML v2 spells them provider-neutrally (`sampling`,
10
- * `output`) and adds per-provider instruction overrides (`model_overrides`).
10
+ * `output`, and `tool_choice` from ADR 0117) and adds per-provider
11
+ * instruction overrides (`model_overrides`).
11
12
  * This module is the one place the two spellings meet:
12
13
  *
13
14
  * - `V2_SPELLINGS`: each v2 key path and the v1 path it replaces. The
14
15
  * loader (lib/syndicateSchema.ts crossFieldProblems) refuses a file that
15
16
  * sets both; `toEngineAgent` refuses a config built in code that does.
16
- * - `toEngineAgent`: folds `sampling` and `output` into the engine form.
17
+ * - `toEngineAgent`: folds `sampling`, `output` and `tool_choice` into
18
+ * the engine form.
17
19
  * validateSyndicateConfig applies it to every agent it returns, so every
18
20
  * loader hands downstream the engine form; lib/compile.ts applies it
19
21
  * again (idempotent) for configs built in code.
@@ -26,6 +28,20 @@
26
28
  */
27
29
  import { providerForModel } from './models/providerMap.js';
28
30
  const isObj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
31
+ /**
32
+ * `tool_choice` as Gemini's function-calling config, which the engine reads
33
+ * (lib/models/genaiMapping.ts toolChoiceOf): `auto` is AUTO, `none` NONE,
34
+ * `required` ANY, and `{ name }` ANY with that one name allowed (ADR 0117).
35
+ */
36
+ export function functionCallingConfigOf(choice) {
37
+ if (choice === 'none')
38
+ return { mode: 'NONE' };
39
+ if (choice === 'required')
40
+ return { mode: 'ANY' };
41
+ if (isObj(choice) && typeof choice.name === 'string')
42
+ return { mode: 'ANY', allowedFunctionNames: [choice.name] };
43
+ return { mode: 'AUTO' };
44
+ }
29
45
  /** Every v2 key with a v1 equivalent; the mapper writes the v1 path. */
30
46
  export const V2_SPELLINGS = [
31
47
  { v2: ['sampling', 'temperature'], v1: ['generateContentConfig', 'temperature'] },
@@ -34,9 +50,10 @@ export const V2_SPELLINGS = [
34
50
  { v2: ['sampling', 'stop'], v1: ['generateContentConfig', 'stopSequences'] },
35
51
  { v2: ['output', 'schema'], v1: ['outputSchema'] },
36
52
  { v2: ['output', 'mime'], v1: ['generateContentConfig', 'responseMimeType'] },
53
+ { v2: ['tool_choice'], v1: ['generateContentConfig', 'toolConfig', 'functionCallingConfig'], toV1: functionCallingConfigOf },
37
54
  ];
38
55
  /** The v2 agent keys, which an inline agent alone may carry (not a yaml_reference or a2a_agent_url subagent). */
39
- export const V2_AGENT_KEYS = ['sampling', 'output', 'model_overrides'];
56
+ export const V2_AGENT_KEYS = ['sampling', 'output', 'tool_choice', 'model_overrides'];
40
57
  /** The v1 spellings ADR 0115 deprecates, as agent keys. */
41
58
  export const V1_AGENT_KEYS = ['generateContentConfig', 'outputSchema'];
42
59
  /** The value at `path` under `root`, or undefined. */
@@ -58,37 +75,37 @@ export function conflictMessage(s) {
58
75
  return `cannot be combined with ${s.v1.join('.')}; ${s.v2.join('.')} replaces it, so keep one (ADR 0115)`;
59
76
  }
60
77
  /**
61
- * An agent in the engine form: `sampling` and `output` folded into
62
- * `generateContentConfig` and `outputSchema` (the table above) and removed;
63
- * `model_overrides` kept (lib/compile.ts applies it). The same object when
64
- * the agent has neither `sampling` nor `output`, so it is idempotent. Throws
78
+ * An agent in the engine form: `sampling`, `output` and `tool_choice` folded
79
+ * into `generateContentConfig` and `outputSchema` (the table above) and
80
+ * removed; `model_overrides` kept (lib/compile.ts applies it). The same
81
+ * object when the agent has none of the three, so it is idempotent. Throws
65
82
  * when a v2 key and the v1 key it replaces are both set.
66
83
  */
67
84
  export function toEngineAgent(agent) {
68
85
  const a = agent;
69
- if (a.sampling === undefined && a.output === undefined)
86
+ if (a.sampling === undefined && a.output === undefined && a.tool_choice === undefined)
70
87
  return agent;
71
88
  const clash = conflictsOf(a);
72
89
  if (clash.length) {
73
90
  const name = typeof a.name === 'string' ? a.name : 'agent';
74
91
  throw new Error(`${name}: ${clash.map((s) => `${s.v2.join('.')} cannot be combined with ${s.v1.join('.')}`).join('; ')}; the v2 key replaces it, so keep one (ADR 0115)`);
75
92
  }
76
- const { sampling: _sampling, output: _output, ...rest } = a;
93
+ const { sampling: _sampling, output: _output, tool_choice: _toolChoice, ...rest } = a;
77
94
  const out = { ...rest };
78
- let gcc;
79
95
  for (const s of V2_SPELLINGS) {
80
- const value = at(a, s.v2);
81
- if (value === undefined)
82
- continue;
83
- if (s.v1.length === 1) {
84
- out[s.v1[0]] = value;
96
+ const raw = at(a, s.v2);
97
+ if (raw === undefined)
85
98
  continue;
99
+ const value = s.toV1 ? s.toV1(raw) : raw;
100
+ // Copy each object on the v1 path, so the caller's config is never written to.
101
+ let parent = out;
102
+ for (const seg of s.v1.slice(0, -1)) {
103
+ const next = { ...(isObj(parent[seg]) ? parent[seg] : {}) };
104
+ parent[seg] = next;
105
+ parent = next;
86
106
  }
87
- gcc ??= { ...(isObj(a.generateContentConfig) ? a.generateContentConfig : {}) };
88
- gcc[s.v1[1]] = value;
107
+ parent[s.v1[s.v1.length - 1]] = value;
89
108
  }
90
- if (gcc)
91
- out.generateContentConfig = gcc;
92
109
  return out;
93
110
  }
94
111
  /**
@@ -110,7 +110,30 @@ export type SpecTool = {
110
110
  } | {
111
111
  kind: 'workflow';
112
112
  workflow: WorkflowSpec;
113
+ } | {
114
+ kind: 'dispatch';
115
+ dispatch: DispatchSpec;
113
116
  };
117
+ /**
118
+ * A nested dispatch (plan-dispatch) syndicate (ADR 0120): it runs as it
119
+ * runs at the top, as a turn of its own on its own conversation (its
120
+ * classifier picks a route, the route answers), so what it compiles to is
121
+ * the file and the options its turn loads its routes with. The turn runner
122
+ * runs it (lib/runtime/syndicateTurn.ts, TurnControl.nestedDispatch), as a
123
+ * delegated call, a dispatch route or a workflow node.
124
+ */
125
+ export interface DispatchSpec {
126
+ /** The entry's name, under which its caller lists it. */
127
+ name: string;
128
+ /** The entry's description: the tool's, when delegated to. */
129
+ description: string;
130
+ /** The nested syndicate, as loaded. */
131
+ config: SyndicateYamlConfig;
132
+ /** The file it came from. */
133
+ ref: string;
134
+ /** The options its routes compile with: the caller's, its nesting chain extended by `ref`. */
135
+ compile: CompileOptions;
136
+ }
114
137
  /**
115
138
  * A workflow syndicate, compiled: lib/compileNative.ts builds the walk's
116
139
  * graph and agents from it. Nested as a subagent (a `yaml_reference` in
@@ -138,6 +161,11 @@ export interface WorkflowSpec {
138
161
  yaml: SubagentYamlConfig;
139
162
  workflow: WorkflowSpec;
140
163
  }>;
164
+ /** Every node that is a `yaml_reference` to a dispatch syndicate, with its YAML entry: the walk runs it as the node, as its own turn (ADR 0120). */
165
+ dispatches?: Array<{
166
+ yaml: SubagentYamlConfig;
167
+ dispatch: DispatchSpec;
168
+ }>;
141
169
  /** The registry entry for a tool node's tool name, or undefined (CompileOptions.onUnknownTool applies). */
142
170
  resolveTool: (name: string) => unknown;
143
171
  }
@@ -186,21 +214,23 @@ export interface AgentSpec {
186
214
  * lookup for its tool nodes. `name` and `description` name a nested one
187
215
  * after its subagent entry (ADR 0098); `ref` is the file it came from.
188
216
  *
189
- * A nested workflow delegated to as a subagent (`delegated`) pauses through
190
- * its caller's open call (ADR 0111). Run as a dispatch route or a workflow
191
- * node, its pause cannot reach the turn yet: an `ask_user` node is refused
192
- * by name there.
217
+ * A nested workflow's pauses (an `ask_user` node, a gate) reach the turn
218
+ * wherever it runs (ADR 0111, ADR 0119). `delegated` is accepted for the
219
+ * callers that pass it and no longer changes what compiles.
193
220
  */
194
221
  export declare function compileWorkflowSpec(config: SyndicateYamlConfig, opts?: CompileOptions, name?: string, description?: string, ref?: string, delegated?: boolean): Promise<WorkflowSpec>;
195
222
  /** Every agent spec a workflow runs, its nested workflow nodes' agents included, for one adapter lookup (nativeAdapterFor). */
196
223
  export declare function workflowAgentSpecs(spec: WorkflowSpec): AgentSpec[];
197
- /** A subagent entry, compiled: one agent, or a nested workflow syndicate's whole graph (ADR 0098, ADR 0106). */
224
+ /** A subagent entry, compiled: one agent, a nested workflow syndicate's whole graph (ADR 0098, ADR 0106), or a nested dispatch syndicate's own turn (ADR 0120). */
198
225
  export type EntrySpec = {
199
226
  kind: 'agent';
200
227
  spec: AgentSpec;
201
228
  } | {
202
229
  kind: 'workflow';
203
230
  workflow: WorkflowSpec;
231
+ } | {
232
+ kind: 'dispatch';
233
+ dispatch: DispatchSpec;
204
234
  };
205
235
  /**
206
236
  * A subagent entry as what it runs: an inline agent or a nested syndicate's
@@ -208,8 +238,9 @@ export type EntrySpec = {
208
238
  * as its whole graph under the entry's name and description. A delegated
209
239
  * subagent, a dispatch route and a workflow node all compile through here,
210
240
  * so the graph a file describes is the graph that runs (ADR 0106).
211
- * `delegated` marks a delegated subagent's entry, whose nested workflow may
212
- * pause (ADR 0111).
241
+ * A nested workflow pauses wherever it runs (ADR 0111, ADR 0119);
242
+ * `delegated` is accepted for the callers that pass it and no longer
243
+ * changes what compiles.
213
244
  */
214
245
  export declare function compileEntrySpec(subCfg: SubagentYamlConfig, opts?: CompileOptions, delegated?: boolean): Promise<EntrySpec>;
215
246
  /**