melchizedek-agents 1.0.3 → 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 (172) hide show
  1. package/.env.example +29 -2
  2. package/AGENT_SETUP.md +3 -1
  3. package/CHANGELOG.md +524 -0
  4. package/DOCUMENTATION.md +178 -43
  5. package/ONBOARDING.md +339 -0
  6. package/QUICKSTART.md +15 -2
  7. package/README.md +6 -2
  8. package/config/agents/README.md +2 -2
  9. package/config/agents/examples/ares.yaml +6 -10
  10. package/config/agents/examples/assistant.yaml +6 -6
  11. package/config/agents/examples/augustin.yaml +9 -15
  12. package/config/agents/examples/cartographers.yaml +6 -6
  13. package/config/agents/examples/council.yaml +7 -7
  14. package/config/agents/examples/critic.yaml +24 -24
  15. package/config/agents/examples/harness.yaml +4 -4
  16. package/config/agents/examples/image_production.yaml +37 -35
  17. package/config/agents/examples/librarian.yaml +4 -4
  18. package/config/agents/examples/patient_advocate.yaml +6 -10
  19. package/config/agents/examples/pipeline.yaml +20 -19
  20. package/config/agents/examples/research.yaml +36 -43
  21. package/config/agents/examples/scribe.yaml +31 -32
  22. package/config/agents/examples/scriptorium.yaml +6 -6
  23. package/config/agents/examples/structured_critic.yaml +82 -0
  24. package/config/agents/examples/style_council.yaml +9 -11
  25. package/config/agents/examples/syndicate.yaml +3 -5
  26. package/config/agents/examples/tutor.yaml +2 -2
  27. package/config/agents/examples/weather.yaml +2 -2
  28. package/config/agents/syndicate.schema.json +859 -13
  29. package/config/agents/syndicateSchema.yaml +122 -73
  30. package/config/agents/templates/README.md +1 -1
  31. package/config/agents/templates/account_memory.yaml +4 -4
  32. package/config/agents/templates/case_desk.yaml +6 -6
  33. package/config/agents/templates/conversational.yaml +2 -2
  34. package/config/agents/templates/draft_review.yaml +6 -6
  35. package/config/agents/templates/intake_extractor.yaml +64 -62
  36. package/config/agents/templates/research_brief.yaml +6 -6
  37. package/config/agents/templates/research_desk.yaml +6 -6
  38. package/config/agents/templates/review_panel.yaml +8 -8
  39. package/config/agents/templates/support_triage.yaml +30 -29
  40. package/config/agents/templates/systems_operator.yaml +24 -5
  41. package/db/migrations/0014_durable_runs.sql +31 -0
  42. package/dist/lib/a2a/app.d.ts +27 -0
  43. package/dist/lib/a2a/app.js +64 -5
  44. package/dist/lib/a2a/executor.d.ts +7 -1
  45. package/dist/lib/a2a/executor.js +43 -5
  46. package/dist/lib/a2a/oauthSetup.d.ts +106 -0
  47. package/dist/lib/a2a/oauthSetup.js +259 -0
  48. package/dist/lib/agentDialect.d.ts +76 -0
  49. package/dist/lib/agentDialect.js +151 -0
  50. package/dist/lib/compile.d.ts +42 -6
  51. package/dist/lib/compile.js +97 -45
  52. package/dist/lib/compileNative.d.ts +11 -3
  53. package/dist/lib/compileNative.js +20 -1
  54. package/dist/lib/dispatch.d.ts +10 -5
  55. package/dist/lib/dispatch.js +10 -5
  56. package/dist/lib/doctor.d.ts +110 -1
  57. package/dist/lib/doctor.js +205 -9
  58. package/dist/lib/loadSyndicate.d.ts +54 -2
  59. package/dist/lib/loadSyndicate.js +22 -2
  60. package/dist/lib/memory/namespace.d.ts +6 -5
  61. package/dist/lib/memory/namespace.js +6 -5
  62. package/dist/lib/memory/providers.js +3 -4
  63. package/dist/lib/models/capabilities.d.ts +25 -1
  64. package/dist/lib/models/capabilities.js +79 -1
  65. package/dist/lib/models/chatCompletionsAdapter.d.ts +11 -3
  66. package/dist/lib/models/chatCompletionsAdapter.js +19 -5
  67. package/dist/lib/models/claudeAdapter.js +4 -0
  68. package/dist/lib/models/contract.d.ts +5 -2
  69. package/dist/lib/models/gateway.d.ts +2 -1
  70. package/dist/lib/models/gateway.js +4 -2
  71. package/dist/lib/models/gatewayAdapter.d.ts +7 -1
  72. package/dist/lib/models/gatewayAdapter.js +8 -0
  73. package/dist/lib/models/geminiAdapter.js +8 -2
  74. package/dist/lib/models/genaiMapping.d.ts +8 -4
  75. package/dist/lib/models/genaiMapping.js +23 -9
  76. package/dist/lib/models/gptAdapter.d.ts +4 -2
  77. package/dist/lib/models/gptAdapter.js +11 -4
  78. package/dist/lib/models/grokAdapter.d.ts +3 -1
  79. package/dist/lib/models/grokAdapter.js +11 -3
  80. package/dist/lib/models/kimiAdapter.d.ts +2 -1
  81. package/dist/lib/models/kimiAdapter.js +3 -3
  82. package/dist/lib/models/reasoning.d.ts +30 -2
  83. package/dist/lib/models/reasoning.js +56 -5
  84. package/dist/lib/observability/tracer.d.ts +8 -1
  85. package/dist/lib/observability/tracer.js +33 -5
  86. package/dist/lib/onboarding.d.ts +134 -0
  87. package/dist/lib/onboarding.js +539 -0
  88. package/dist/lib/runtime/approvals.d.ts +8 -0
  89. package/dist/lib/runtime/approvals.js +4 -3
  90. package/dist/lib/runtime/credentials.d.ts +6 -0
  91. package/dist/lib/runtime/native/agentLoop.d.ts +23 -3
  92. package/dist/lib/runtime/native/agentLoop.js +107 -11
  93. package/dist/lib/runtime/native/checkpoint.d.ts +171 -0
  94. package/dist/lib/runtime/native/checkpoint.js +359 -0
  95. package/dist/lib/runtime/native/delegate.d.ts +222 -18
  96. package/dist/lib/runtime/native/delegate.js +390 -39
  97. package/dist/lib/runtime/native/interrupts.d.ts +184 -2
  98. package/dist/lib/runtime/native/interrupts.js +391 -1
  99. package/dist/lib/runtime/native/request.d.ts +13 -3
  100. package/dist/lib/runtime/native/request.js +28 -4
  101. package/dist/lib/runtime/native/telemetry.js +10 -15
  102. package/dist/lib/runtime/questions.d.ts +7 -4
  103. package/dist/lib/runtime/sessions.d.ts +1 -1
  104. package/dist/lib/runtime/syndicateTurn.d.ts +11 -3
  105. package/dist/lib/runtime/syndicateTurn.js +353 -68
  106. package/dist/lib/runtime/turnControl.d.ts +56 -0
  107. package/dist/lib/runtime/turnControl.js +10 -0
  108. package/dist/lib/session/transcript.d.ts +9 -0
  109. package/dist/lib/session/transcript.js +20 -1
  110. package/dist/lib/skills.d.ts +18 -0
  111. package/dist/lib/skills.js +73 -1
  112. package/dist/lib/storage/postgres/index.d.ts +12 -2
  113. package/dist/lib/storage/postgres/index.js +8 -3
  114. package/dist/lib/storage/postgres/taskQueue.d.ts +20 -4
  115. package/dist/lib/storage/postgres/taskQueue.js +29 -5
  116. package/dist/lib/storage/postgres/taskStore.d.ts +26 -2
  117. package/dist/lib/storage/postgres/taskStore.js +74 -8
  118. package/dist/lib/syndicateSchema.d.ts +316 -1
  119. package/dist/lib/syndicateSchema.js +203 -29
  120. package/dist/lib/tools/auth.d.ts +20 -6
  121. package/dist/lib/tools/auth.js +21 -5
  122. package/dist/lib/tools/credentialEnv.d.ts +28 -0
  123. package/dist/lib/tools/credentialEnv.js +52 -0
  124. package/dist/lib/tools/credentialHosts.d.ts +88 -0
  125. package/dist/lib/tools/credentialHosts.js +165 -0
  126. package/dist/lib/tools/credentialStore.js +9 -2
  127. package/dist/lib/tools/credentialUses.d.ts +92 -0
  128. package/dist/lib/tools/credentialUses.js +112 -0
  129. package/dist/lib/tools/mcpToolFactory.d.ts +25 -2
  130. package/dist/lib/tools/mcpToolFactory.js +179 -21
  131. package/dist/lib/tools/oauthConsent.js +8 -1
  132. package/dist/lib/tools/oauthHosts.d.ts +73 -0
  133. package/dist/lib/tools/oauthHosts.js +154 -0
  134. package/dist/lib/tools/oauthTools.d.ts +242 -0
  135. package/dist/lib/tools/oauthTools.js +447 -0
  136. package/dist/lib/tools/openapiTools.d.ts +11 -6
  137. package/dist/lib/tools/openapiTools.js +88 -38
  138. package/dist/lib/tools/taskTools.d.ts +61 -6
  139. package/dist/lib/tools/taskTools.js +145 -21
  140. package/dist/lib/workflow/turn.d.ts +22 -3
  141. package/dist/lib/workflow/turn.js +151 -31
  142. package/dist/lib/workflow.d.ts +14 -7
  143. package/dist/lib/workflow.js +14 -7
  144. package/dist/lib/workflowConfig.d.ts +6 -0
  145. package/dist/scripts/a2a_server.d.ts +36 -0
  146. package/dist/scripts/a2a_server.js +131 -1
  147. package/dist/scripts/assistant_worker.d.ts +12 -0
  148. package/dist/scripts/assistant_worker.js +81 -22
  149. package/dist/scripts/doctor.d.ts +7 -2
  150. package/dist/scripts/doctor.js +9 -4
  151. package/dist/scripts/setup.d.ts +23 -0
  152. package/dist/scripts/setup.js +154 -0
  153. package/dist/scripts/skills_install.d.ts +3 -1
  154. package/dist/scripts/skills_install.js +17 -2
  155. package/dist/scripts/yaml_codemod.d.ts +62 -0
  156. package/dist/scripts/yaml_codemod.js +652 -0
  157. package/package.json +8 -2
  158. package/skills/README.md +11 -2
  159. package/skills/briefs/_shared.md +1 -1
  160. package/skills/briefs/melchizedek-author.md +7 -5
  161. package/skills/briefs/melchizedek-models.md +7 -4
  162. package/skills/briefs/melchizedek.md +1 -0
  163. package/skills/briefs/skills-readme.md +3 -1
  164. package/skills/melchizedek/SKILL.md +1 -0
  165. package/skills/melchizedek-author/SKILL.md +7 -4
  166. package/skills/melchizedek-author/assets/minimal.yaml +13 -13
  167. package/skills/melchizedek-models/SKILL.md +11 -4
  168. package/skills/melchizedek-onboard/SKILL.md +49 -0
  169. package/skills/melchizedek-onboard-cloud/SKILL.md +38 -0
  170. package/skills/melchizedek-onboard-keys/SKILL.md +35 -0
  171. package/skills/melchizedek-onboard-local/SKILL.md +41 -0
  172. package/skills/melchizedek-onboard-serve/SKILL.md +43 -0
@@ -24,6 +24,8 @@
24
24
  import { z } from 'zod';
25
25
  import { DEFAULT_ROUTE_KEY, NODE_KINDS, ROUTE_STEP_SUFFIX, START_NAME, elementNames, nodeKind } from './workflowConfig.js';
26
26
  import { isSecretShapedEnvName } from './tools/skills/env.js';
27
+ import { OAUTH_SCOPE, PROVIDER_NAME } from './tools/auth.js';
28
+ import { V2_AGENT_KEYS, conflictMessage, conflictsOf, toEngineAgent } from './agentDialect.js';
27
29
  // ── Leaf rules ───────────────────────────────────────────────────────────────
28
30
  /**
29
31
  * ADK's agent-name rule (validateAgentName in ADK's base_agent), kept: checked here
@@ -43,7 +45,7 @@ const thinkingConfig = z
43
45
  includeThoughts: z.boolean().optional().describe('Stream the thinking trace in the response.'),
44
46
  })
45
47
  .describe('Older spelling of `reasoning` for Gemini and Claude (thinkingLevel or thinkingBudget). Cannot be combined with `reasoning`.');
46
- export const REASONING_LEVELS = ['none', 'low', 'medium', 'high'];
48
+ export const REASONING_LEVELS = ['none', 'low', 'medium', 'high', 'xhigh', 'max'];
47
49
  /**
48
50
  * The provider-neutral reasoning key (ADR 0047): a level, or a token budget.
49
51
  * The compiler maps it per provider (lib/compile.ts reasoningConfig).
@@ -60,7 +62,7 @@ const reasoningSchema = z
60
62
  return `must be one of ${REASONING_LEVELS.join(' | ')}, or { budget_tokens: <integer ≥ 0> } (got ${describeValue(iss.input)}${hint ? ` — did you mean "${hint}"?` : ''})`;
61
63
  },
62
64
  })
63
- .describe('How hard this agent reasons, on any provider: none | low | medium | high, or { budget_tokens: <int> }. Replaces generateContentConfig.thinkingConfig and reasoningEffort (ADR 0047).');
65
+ .describe('How hard this agent reasons, on any provider: none | low | medium | high | xhigh | max, or { budget_tokens: <int> }. xhigh and max reach the models that take them; elsewhere they go as the model\'s highest setting (ADR 0117). Replaces generateContentConfig.thinkingConfig and reasoningEffort (ADR 0047).');
64
66
  /**
65
67
  * Loose on purpose: provider-specific fields (e.g. `toolConfig`) pass through
66
68
  * to the SDK. Only the documented fields are typed, so `temperature: hot`
@@ -87,8 +89,82 @@ const generateContentConfig = z
87
89
  .optional()
88
90
  .describe('Older spelling of `reasoning` for the chat-completions and Responses providers. Cannot be combined with `reasoning`.'),
89
91
  })
90
- .describe('Model generation config (@google/genai GenerateContentConfig). Do not set tools here.');
92
+ .describe('Deprecated v1 spelling (ADR 0115): write `sampling` (temperature, top_p, max_output_tokens, stop), `output.mime` (responseMimeType), `reasoning` (thinkingConfig, reasoningEffort) and `tool_choice` (toolConfig.functionCallingConfig) instead; npx melchizedek-codemod rewrites a file. Still read: the Gemini GenerateContentConfig shape the engine takes. Do not set tools here.')
93
+ .meta({ deprecated: true });
94
+ // ── YAML v2 agent keys (ADR 0115) ────────────────────────────────────────────
95
+ /** `sampling` and `output` must say something: an empty block is a leftover. */
96
+ const nonEmpty = (o) => Object.keys(o).length > 0;
97
+ /** Only when the block has no other problem: `sampling: { topP: 1 }` is a typo, not an empty block. */
98
+ const quiet = (payload) => payload.issues.length === 0;
99
+ const samplingSchema = z
100
+ .strictObject({
101
+ temperature: z.number().optional().describe('Sampling temperature. Replaces generateContentConfig.temperature.'),
102
+ top_p: z.number().optional().describe('Nucleus sampling. Replaces generateContentConfig.topP.'),
103
+ max_output_tokens: z.number().int().positive().optional().describe('Cap on the reply\'s tokens. Replaces generateContentConfig.maxOutputTokens.'),
104
+ stop: z.array(z.string()).min(1).optional().describe('Stop sequences. Replaces generateContentConfig.stopSequences.'),
105
+ })
106
+ .refine(nonEmpty, { message: 'must set at least one of temperature, top_p, max_output_tokens, stop', when: quiet })
107
+ .describe('How this agent samples, on any provider (ADR 0115). Replaces the same keys under generateContentConfig; cannot be combined with them.');
108
+ export const OUTPUT_MIMES = ['application/json', 'text/plain'];
109
+ const outputSchemaV2 = z
110
+ .strictObject({
111
+ schema: z
112
+ .record(z.string(), z.unknown())
113
+ .optional()
114
+ .describe('JSON Schema the final reply is one object of. Replaces outputSchema; same behaviour beside tools and subagents (ADR 0109).'),
115
+ mime: z.enum(OUTPUT_MIMES).optional().describe('application/json: JSON mode without a schema. Replaces generateContentConfig.responseMimeType.'),
116
+ })
117
+ .refine(nonEmpty, { message: 'must set schema or mime', when: quiet })
118
+ .describe('What this agent\'s final reply is (ADR 0115). Replaces outputSchema and generateContentConfig.responseMimeType; cannot be combined with them.');
119
+ export const TOOL_CHOICE_MODES = ['auto', 'none', 'required'];
120
+ const toolChoiceSchema = z
121
+ .union([
122
+ z.enum(TOOL_CHOICE_MODES),
123
+ z.strictObject({
124
+ name: z.string().min(1).describe('The one tool the model must call: a name from this agent\'s tools, subagents or MCP tools.'),
125
+ }),
126
+ ], {
127
+ error: (iss) => {
128
+ const hint = typeof iss.input === 'string' ? suggest(iss.input, TOOL_CHOICE_MODES) : undefined;
129
+ return `must be one of ${TOOL_CHOICE_MODES.join(' | ')}, or { name: <tool> } (got ${describeValue(iss.input)}${hint ? ` — did you mean "${hint}"?` : ''})`;
130
+ },
131
+ })
132
+ .describe('Which tools the model may call, on any provider (ADR 0117): auto (the default), none, required (some tool), or { name: <tool> } (that tool). A provider that rejects forcing sends a weaker choice and marks the span llm.tool_choice.weakened. Replaces generateContentConfig.toolConfig.functionCallingConfig; cannot be combined with it.');
133
+ const modelOverrideEntry = z
134
+ .strictObject({
135
+ instruction: z.string().min(1).optional().describe('Replaces the agent\'s instruction on this provider.'),
136
+ instruction_append: z.string().min(1).optional().describe('Appended to the agent\'s instruction, after a blank line, on this provider.'),
137
+ })
138
+ .refine((o) => (o.instruction === undefined) !== (o.instruction_append === undefined), 'exactly one of instruction or instruction_append')
139
+ .describe('The instruction on one provider: exactly one of instruction or instruction_append.');
140
+ const modelOverridesSchema = z
141
+ .strictObject({
142
+ gemini: modelOverrideEntry.optional(),
143
+ anthropic: modelOverrideEntry.optional(),
144
+ openai: modelOverrideEntry.optional(),
145
+ xai: modelOverrideEntry.optional(),
146
+ moonshot: modelOverrideEntry.optional(),
147
+ ollama: modelOverrideEntry.optional(),
148
+ })
149
+ .describe('Per-provider instruction, keyed by the provider of the agent\'s model id (lib/models/providerMap.ts). Applied at compile time before the skills index; not to fallback_model, and never to globalInstruction (ADR 0115).');
150
+ /** Keys valid inside the v2 blocks, for did-you-mean (knownKeysAt). */
151
+ function v2KnownKeysAt(key) {
152
+ const agentKey = key.replace(/^(orchestrator|subagents\/#)\//, '');
153
+ if (agentKey === key)
154
+ return [];
155
+ if (agentKey === 'sampling')
156
+ return Object.keys(samplingSchema.shape);
157
+ if (agentKey === 'output')
158
+ return Object.keys(outputSchemaV2.shape);
159
+ if (agentKey === 'model_overrides')
160
+ return Object.keys(modelOverridesSchema.shape);
161
+ if (agentKey.startsWith('model_overrides/') && !agentKey.slice('model_overrides/'.length).includes('/'))
162
+ return Object.keys(modelOverrideEntry.shape);
163
+ return [];
164
+ }
91
165
  // ── Skills ───────────────────────────────────────────────────────────────────
166
+ /** The most delegated calls one step may run at once (`max_concurrency`, ADR 0116): each is a model run that costs money. */
167
+ const MAX_DELEGATION_CONCURRENCY = 32;
92
168
  export const SKILL_SCRIPT_MODES = ['none', 'local'];
93
169
  const envName = z.string().regex(/^[A-Z_][A-Z0-9_]*$/, 'an environment variable name (A–Z, 0–9, _)');
94
170
  /**
@@ -131,6 +207,50 @@ const skillsSchema = z
131
207
  }
132
208
  })
133
209
  .describe('Agent Skills (SKILL.md directories) this agent reads the way a coding harness does.');
210
+ // ── OAuth (ADR 0112) ─────────────────────────────────────────────────────────
211
+ const oauth2Auth = z
212
+ .strictObject({
213
+ provider: z
214
+ .string()
215
+ .regex(PROVIDER_NAME, 'a provider name: lowercase letters, digits, and . _ - inside, at most 64 characters')
216
+ .describe('The credential store\'s name for the provider (e.g. github). Every tool naming it shares one grant.'),
217
+ grant: z
218
+ .enum(['authorization_code', 'client_credentials'])
219
+ .describe('authorization_code: each user\'s own token, granted through the consent pause (ADR 0085). client_credentials: the server\'s own token, from the token endpoint.'),
220
+ authorization_url: z.string().url().optional().describe('The provider\'s authorization endpoint (https). authorization_code only.'),
221
+ token_url: z.string().url().describe('The provider\'s token endpoint (https).'),
222
+ client_id: z.string().min(1).max(512).optional().describe('The OAuth client id, written out (it is not a secret). Or client_id_env.'),
223
+ client_id_env: envName.optional().describe('Environment variable holding the client id. Or client_id.'),
224
+ client_secret_env: envName
225
+ .optional()
226
+ .describe('Environment variable NAME holding the client secret, never the value. Required for client_credentials; omit for a public client (PKCE alone).'),
227
+ scopes: z.array(z.string().regex(OAUTH_SCOPE, 'an OAuth scope: visible ASCII, no space, quote or backslash')).max(64).optional().describe('The scopes the grant asks for.'),
228
+ authorization_params: z
229
+ .record(z.string().regex(/^[A-Za-z0-9_.-]{1,64}$/), z.string().max(512))
230
+ .optional()
231
+ .describe('Extra authorization parameters (e.g. access_type: offline). authorization_code only; never state, redirect_uri, scope, client_id or the PKCE pair.'),
232
+ })
233
+ .superRefine((o, ctx) => {
234
+ if (!!o.client_id === !!o.client_id_env)
235
+ ctx.addIssue({ code: 'custom', path: ['client_id'], message: 'exactly one of client_id or client_id_env' });
236
+ if (o.grant === 'authorization_code' && !o.authorization_url) {
237
+ ctx.addIssue({ code: 'custom', path: ['authorization_url'], message: 'an authorization_code grant needs authorization_url' });
238
+ }
239
+ if (o.grant === 'client_credentials') {
240
+ if (!o.client_secret_env)
241
+ ctx.addIssue({ code: 'custom', path: ['client_secret_env'], message: 'a client_credentials grant needs client_secret_env' });
242
+ for (const key of ['authorization_url', 'authorization_params']) {
243
+ if (o[key] !== undefined)
244
+ ctx.addIssue({ code: 'custom', path: [key], message: `${key} is for authorization_code only` });
245
+ }
246
+ }
247
+ const reserved = ['state', 'redirect_uri', 'code_challenge', 'code_challenge_method', 'client_id', 'response_type', 'scope'];
248
+ for (const key of Object.keys(o.authorization_params ?? {})) {
249
+ if (reserved.includes(key))
250
+ ctx.addIssue({ code: 'custom', path: ['authorization_params', key], message: `authorization_params may not set "${key}"` });
251
+ }
252
+ })
253
+ .describe('An OAuth 2 access token sent as a bearer token on every call (lib/tools/oauthTools.ts). Secrets come from environment variable names, never values.');
134
254
  // ── OpenAPI ──────────────────────────────────────────────────────────────────
135
255
  const openapiEntry = z
136
256
  .strictObject({
@@ -149,8 +269,9 @@ const openapiEntry = z
149
269
  .strictObject({ env: envName, in: z.enum(['header', 'query']), name: z.string().min(1) })
150
270
  .optional()
151
271
  .describe('An API key read from `env`, sent in a header or the query under `name`.'),
272
+ oauth2: oauth2Auth.optional(),
152
273
  })
153
- .refine((a) => !!a.bearer_env !== !!a.api_key, 'exactly one of bearer_env or api_key')
274
+ .refine((a) => [a.bearer_env, a.api_key, a.oauth2].filter((x) => x !== undefined).length === 1, 'exactly one of bearer_env, api_key or oauth2')
154
275
  .optional()
155
276
  .describe('Credentials, always from the environment, never written in YAML.'),
156
277
  })
@@ -183,7 +304,7 @@ const agentFields = {
183
304
  require_approval: z
184
305
  .array(z.string().min(1))
185
306
  .optional()
186
- .describe('Tools from this agent\'s `tools` that run only after a person approves the exact call (ADR 0028): the A2A task ends input-required until the caller answers approve or reject. Allowed on the orchestrator and on plan-dispatch routes.'),
307
+ .describe('Tools from this agent\'s `tools` that run only after a person approves the exact call (ADR 0028): the A2A task ends input-required until the caller answers approve or reject. Allowed on any agent but one a workflow map node runs; inside a delegated subagent the pause reaches the caller through its open call (ADR 0110).'),
187
308
  includeContents: z
188
309
  .enum(['default', 'none'])
189
310
  .optional()
@@ -193,10 +314,15 @@ const agentFields = {
193
314
  outputKey: z.string().optional().describe('Session-state key the final reply is saved under.'),
194
315
  generateContentConfig: generateContentConfig.optional(),
195
316
  reasoning: reasoningSchema.optional(),
317
+ sampling: samplingSchema.optional(),
318
+ output: outputSchemaV2.optional(),
319
+ tool_choice: toolChoiceSchema.optional(),
320
+ model_overrides: modelOverridesSchema.optional(),
196
321
  outputSchema: z
197
322
  .record(z.string(), z.unknown())
323
+ .meta({ deprecated: true })
198
324
  .optional()
199
- .describe('JSON Schema for structured output: the agent answers with one JSON object matching it. Hold it on a leaf; an agent that delegates and holds one ends its turn on that JSON.'),
325
+ .describe('Deprecated v1 spelling (ADR 0115): write output.schema. JSON Schema for structured output: the agent answers with one JSON object matching it. An agent that also calls tools or delegates to subagents does that first, then ends its turn on that JSON: the schema travels beside the tools where the model takes both in one request, as a set_model_response tool elsewhere (the capability matrix\'s structured_output_with_tools, ADR 0109).'),
200
326
  mcp_server_url: z
201
327
  .string()
202
328
  .optional()
@@ -206,6 +332,10 @@ const agentFields = {
206
332
  .min(1)
207
333
  .optional()
208
334
  .describe('The MCP server\'s tools this agent may use, by name; any other tool the server lists is not exposed. Without it, every listed tool is. require_approval may name these. Needs mcp_server_url.'),
335
+ mcp_auth: z
336
+ .strictObject({ oauth2: oauth2Auth })
337
+ .optional()
338
+ .describe('The OAuth grant the MCP server takes (ADR 0112). With authorization_code, each user\'s own token on their own connection, and mcp_tools is required. Needs mcp_server_url; replaces MCP_BEARER_TOKENS for this server.'),
209
339
  skills: skillsSchema.optional(),
210
340
  openapi: z.array(openapiEntry).min(1).optional(),
211
341
  code_execution: z
@@ -259,7 +389,7 @@ export const subagentSchema = z
259
389
  .string()
260
390
  .min(1)
261
391
  .optional()
262
- .describe('A whole nested syndicate (filename under the agents dir) used as this subagent.'),
392
+ .describe('A whole nested syndicate (filename under the agents dir) used as this subagent. An approval request or ask_user question raised inside it pauses the turn with the agent path (ADR 0110, ADR 0111): a delegate syndicate\'s gates anywhere, a dispatch syndicate\'s on its routes, which it classifies and runs as at the top, its route\'s answer being its own (ADR 0120), and a workflow\'s ask_user nodes and gates, wherever each runs: delegated to as a subagent, as a dispatch route or as a workflow node (ADR 0119).'),
263
393
  a2a_agent_url: z
264
394
  .string()
265
395
  .min(1)
@@ -391,6 +521,13 @@ export const syndicateSchema = z
391
521
  .positive()
392
522
  .optional()
393
523
  .describe('Hard cap on model calls per turn, subagents included. Default 50 (DEFAULT_MAX_STEPS) when unset.'),
524
+ max_concurrency: z
525
+ .number()
526
+ .int()
527
+ .positive()
528
+ .max(MAX_DELEGATION_CONCURRENCY)
529
+ .optional()
530
+ .describe('Subagent calls from one orchestrator step that run at once; the rest wait their turn, in call order. Default 4; 1 runs them one after another. A delegate syndicate only (ADR 0116).'),
394
531
  bundled_references: z
395
532
  .record(z.string(), z.record(z.string(), z.unknown()))
396
533
  .optional()
@@ -466,7 +603,7 @@ function knownKeysAt(p) {
466
603
  case 'subagents/#/orchestration':
467
604
  return ['role', 'delegates'];
468
605
  default:
469
- return [];
606
+ return v2KnownKeysAt(key);
470
607
  }
471
608
  }
472
609
  function valueAt(root, p) {
@@ -559,16 +696,16 @@ function crossFieldProblems(raw) {
559
696
  });
560
697
  }
561
698
  const subs = Array.isArray(raw.subagents) ? raw.subagents : [];
562
- // Approval gates (ADR 0028): only tools the agent has, and only on agents
563
- // the turn runs directly — a delegated subagent runs inside a tool call,
564
- // where the pause cannot reach the caller and the gated tool silently never runs.
699
+ // Approval gates (ADR 0028): only tools the agent has. A delegated
700
+ // subagent's pause reaches the caller through the open call (ADR 0110),
701
+ // so every agent of a delegate, dispatch or workflow syndicate may gate.
565
702
  const gateProblems = (agent, path, allowed) => {
566
703
  if (agent.require_approval === undefined)
567
704
  return;
568
705
  if (!allowed) {
569
706
  out.push({
570
707
  path: [...path, 'require_approval'],
571
- message: 'approval gates run only on the orchestrator or a plan-dispatch route; a delegated subagent cannot pause the turn (ADR 0028)',
708
+ message: 'approval gates are not supported here (ADR 0028, ADR 0110)',
572
709
  });
573
710
  return;
574
711
  }
@@ -584,17 +721,11 @@ function crossFieldProblems(raw) {
584
721
  });
585
722
  };
586
723
  // A skill script run pauses for approval the same way, so it is allowed in
587
- // the same places; `skills.tools` may only unlock tools the agent does not
588
- // already carry outright.
589
- const skillProblems = (agent, path, allowed) => {
724
+ // the same places, a delegated subagent included (ADR 0118); `skills.tools`
725
+ // may only unlock tools the agent does not already carry outright.
726
+ const skillProblems = (agent, path) => {
590
727
  if (!isObj(agent.skills))
591
728
  return;
592
- if (agent.skills.scripts === 'local' && !allowed) {
593
- out.push({
594
- path: [...path, 'skills', 'scripts'],
595
- message: 'skill scripts pause for approval, which only the orchestrator or a plan-dispatch route can do; a delegated subagent cannot pause the turn (ADR 0028)',
596
- });
597
- }
598
729
  const tools = Array.isArray(agent.tools) ? agent.tools : [];
599
730
  (Array.isArray(agent.skills.tools) ? agent.skills.tools : []).forEach((name, j) => {
600
731
  if (typeof name === 'string' && tools.includes(name)) {
@@ -603,7 +734,8 @@ function crossFieldProblems(raw) {
603
734
  });
604
735
  };
605
736
  // ask_user ends the turn waiting for the person, the same pause as an
606
- // approval, so it is allowed in the same places (lib/runtime/questions.ts).
737
+ // approval, so it is allowed in the same places (lib/runtime/questions.ts),
738
+ // a delegated subagent included (ADR 0110); a workflow node asks through an ask_user node.
607
739
  const questionProblems = (agent, path, allowed) => {
608
740
  const tools = Array.isArray(agent.tools) ? agent.tools : [];
609
741
  const at = tools.indexOf('ask_user');
@@ -613,7 +745,7 @@ function crossFieldProblems(raw) {
613
745
  out.push({ path: [...path, 'tools', at], message: 'ask_user is not supported on a workflow node yet; use an ask_user node (workflow.nodes)' });
614
746
  }
615
747
  else if (!allowed) {
616
- out.push({ path: [...path, 'tools', at], message: 'ask_user pauses the turn, which only the orchestrator or a plan-dispatch route can do; a delegated subagent cannot' });
748
+ out.push({ path: [...path, 'tools', at], message: 'ask_user is not supported here (ADR 0110)' });
617
749
  }
618
750
  };
619
751
  // Execution keys (ADR 0033): where each one means something.
@@ -634,9 +766,24 @@ function crossFieldProblems(raw) {
634
766
  if (agent.mcp_tools !== undefined && typeof agent.mcp_server_url !== 'string') {
635
767
  out.push({ path: [...path, 'mcp_tools'], message: 'mcp_tools chooses among an MCP server\'s tools; it needs mcp_server_url' });
636
768
  }
769
+ if (agent.mcp_auth !== undefined && typeof agent.mcp_server_url !== 'string') {
770
+ out.push({ path: [...path, 'mcp_auth'], message: 'mcp_auth is the grant an MCP server takes; it needs mcp_server_url' });
771
+ }
772
+ if (isObj(agent.mcp_auth) && isObj(agent.mcp_auth.oauth2) && agent.mcp_auth.oauth2.grant === 'authorization_code' && agent.mcp_tools === undefined) {
773
+ out.push({ path: [...path, 'mcp_auth'], message: 'an authorization_code mcp_auth needs mcp_tools: no user\'s grant exists at startup to list the server\'s tools' });
774
+ }
637
775
  };
638
776
  if (isObj(raw.orchestrator))
639
777
  executionProblems(raw.orchestrator, ['orchestrator'], true);
778
+ // max_concurrency bounds an orchestrator's delegated calls (ADR 0116): a workflow bounds its nodes under workflow:, and a dispatch classifier delegates nothing.
779
+ if (raw.max_concurrency !== undefined && (isObj(raw.workflow) || isObj(raw.dispatch))) {
780
+ out.push({
781
+ path: ['max_concurrency'],
782
+ message: isObj(raw.workflow)
783
+ ? 'max_concurrency at the root bounds a delegate orchestrator\'s subagent calls; a workflow bounds its nodes with workflow.max_concurrency'
784
+ : 'max_concurrency bounds a delegate orchestrator\'s subagent calls; a dispatch syndicate\'s classifier delegates nothing (set it in a route\'s own syndicate file)',
785
+ });
786
+ }
640
787
  for (const [i, sub] of (Array.isArray(raw.subagents) ? raw.subagents : []).entries())
641
788
  if (isObj(sub))
642
789
  executionProblems(sub, ['subagents', i], false);
@@ -665,21 +812,38 @@ function crossFieldProblems(raw) {
665
812
  for (const [i, sub] of subs.entries())
666
813
  if (isObj(sub))
667
814
  reasoningProblems(sub, ['subagents', i]);
815
+ // YAML v2 (ADR 0115): one spelling per setting, as for `reasoning` above;
816
+ // and the v2 keys belong to an inline agent, never to a nested or remote one.
817
+ const v2Problems = (agent, path) => {
818
+ if (typeof agent.yaml_reference === 'string' || typeof agent.a2a_agent_url === 'string') {
819
+ for (const key of V2_AGENT_KEYS) {
820
+ if (agent[key] === undefined)
821
+ continue;
822
+ out.push({ path: [...path, key], message: 'applies to an inline agent; a nested syndicate (yaml_reference) or a remote agent (a2a_agent_url) sets its own' });
823
+ }
824
+ }
825
+ for (const clash of conflictsOf(agent))
826
+ out.push({ path: [...path, ...clash.v2], message: conflictMessage(clash) });
827
+ };
828
+ if (isObj(raw.orchestrator))
829
+ v2Problems(raw.orchestrator, ['orchestrator']);
830
+ for (const [i, sub] of subs.entries())
831
+ if (isObj(sub))
832
+ v2Problems(sub, ['subagents', i]);
668
833
  if (isObj(raw.orchestrator)) {
669
834
  gateProblems(raw.orchestrator, ['orchestrator'], true);
670
- skillProblems(raw.orchestrator, ['orchestrator'], true);
835
+ skillProblems(raw.orchestrator, ['orchestrator']);
671
836
  questionProblems(raw.orchestrator, ['orchestrator'], true);
672
837
  }
673
- const dispatching = isObj(raw.dispatch);
674
838
  // A workflow's subagents are its nodes, not delegated tools: a gated call pauses its node, and
675
839
  // the walk resumes it (ADR 0098); a skill script run pauses on the same approval (ADR 0106). A map item cannot (workflowProblems).
676
- const pausing = dispatching || isObj(raw.workflow);
677
840
  subs.forEach((sub, i) => {
678
841
  if (!isObj(sub))
679
842
  return;
680
- gateProblems(sub, ['subagents', i], pausing);
681
- skillProblems(sub, ['subagents', i], pausing);
682
- questionProblems(sub, ['subagents', i], dispatching);
843
+ // A delegated subagent's gate, question and skill script run pause the turn through the open call (ADR 0110, ADR 0118).
844
+ gateProblems(sub, ['subagents', i], true);
845
+ skillProblems(sub, ['subagents', i]);
846
+ questionProblems(sub, ['subagents', i], true);
683
847
  const hasRef = typeof sub.yaml_reference === 'string';
684
848
  const hasRemote = typeof sub.a2a_agent_url === 'string';
685
849
  if (hasRef && hasRemote) {
@@ -933,6 +1097,9 @@ export function validateSyndicateConfig(raw, file) {
933
1097
  // A single-agent syndicate may omit `subagents`; callers always get a list.
934
1098
  if (raw.subagents === undefined)
935
1099
  raw.subagents = [];
1100
+ // YAML v2 (ADR 0115): every loader hands downstream the engine form.
1101
+ raw.orchestrator = toEngineAgent(raw.orchestrator);
1102
+ raw.subagents = raw.subagents.map((sub) => toEngineAgent(sub));
936
1103
  return raw;
937
1104
  }
938
1105
  /**
@@ -959,6 +1126,13 @@ export function syndicateJsonSchema() {
959
1126
  ];
960
1127
  ctx.jsonSchema.not = { required: ['yaml_reference', 'a2a_agent_url'] };
961
1128
  }
1129
+ // YAML v2 (ADR 0115): the refinements, where JSON Schema can say them.
1130
+ if (ctx.zodSchema === samplingSchema || ctx.zodSchema === outputSchemaV2) {
1131
+ ctx.jsonSchema.minProperties = 1;
1132
+ }
1133
+ if (ctx.zodSchema === modelOverrideEntry) {
1134
+ ctx.jsonSchema.oneOf = [{ required: ['instruction'] }, { required: ['instruction_append'] }];
1135
+ }
962
1136
  },
963
1137
  });
964
1138
  return { ...schema, title: 'Melchizedek syndicate' };
@@ -22,9 +22,9 @@
22
22
  * (`pinnedCredentialStore`, like namespacedMemoryService), so a delegated
23
23
  * subagent, which runs under its own app name, reads the root's
24
24
  * credentials. The consent step that puts a token is
25
- * lib/tools/oauthConsent.ts (ADR 0085); the YAML that lets an agent use a
26
- * provider (WS6-3c) comes later; nothing here
27
- * wires a tool to the store.
25
+ * lib/tools/oauthConsent.ts (ADR 0085); the YAML that lets an OpenAPI or
26
+ * MCP tool use a provider (`auth: { oauth2 }`, lib/tools/oauthTools.ts,
27
+ * ADR 0112) is what calls `accessToken` for a declared tool.
28
28
  *
29
29
  * A LEAF: types and plain functions, no runtime imports, so lib/tools/tool.ts
30
30
  * can bind the member without loading crypto, pg or telemetry.
@@ -50,7 +50,7 @@ export interface AccessGrant {
50
50
  scopes: string[];
51
51
  expiresAt?: Date;
52
52
  }
53
- /** How a provider renews and withdraws a token (from the provider's config; WS6-3c). */
53
+ /** How a provider renews and withdraws a token (`oauthRefreshProviders` builds the refresh from a YAML grant, ADR 0112). */
54
54
  export interface OAuthProvider {
55
55
  /**
56
56
  * A new token set for this refresh token. Throw when the provider refuses;
@@ -92,18 +92,32 @@ export interface CredentialStore {
92
92
  }
93
93
  /** What the ToolContext member gives a tool: a valid access token for a provider, for this run's user only. */
94
94
  export type ToolAccessToken = (provider: string) => Promise<string>;
95
- export type ToolCredentialErrorCode = 'not_connected' | 'expired' | 'refresh_failed' | 'unreadable' | 'no_user' | 'invalid';
95
+ export type ToolCredentialErrorCode = 'not_connected' | 'expired' | 'refresh_failed' | 'unreadable' | 'no_user' | 'invalid' | 'unavailable' | 'grant_failed' | 'host_refused';
96
+ /**
97
+ * Which operator allowlist refused a `host_refused` call: the OAuth host
98
+ * allowlist, which binds a provider's tokens to its hosts
99
+ * (`MELCHIZEDEK_OAUTH_HOSTS`, ADR 0114), or the credential host allowlist,
100
+ * which binds a static credential variable such as a client secret to its
101
+ * hosts (`MELCHIZEDEK_CREDENTIAL_HOSTS`, ADR 0122).
102
+ */
103
+ export type HostAllowlist = 'oauth' | 'credential';
96
104
  /**
97
105
  * A credential that cannot be used. The message names the provider and what
98
106
  * to do, never a token, a user or a key, so a tool may return it to the model.
107
+ * A `host_refused` error names the allowlist that refused (`allowlist`,
108
+ * `'oauth'` when not given).
99
109
  */
100
110
  export declare class ToolCredentialError extends Error {
101
111
  readonly code: ToolCredentialErrorCode;
102
112
  readonly provider?: string;
103
- constructor(code: ToolCredentialErrorCode, provider?: string);
113
+ /** For `host_refused`: the allowlist that refused the host. */
114
+ readonly allowlist?: HostAllowlist;
115
+ constructor(code: ToolCredentialErrorCode, provider?: string, allowlist?: HostAllowlist);
104
116
  }
105
117
  /** A provider name: lowercase letters, digits, and . _ - inside; at most 64 characters. */
106
118
  export declare const PROVIDER_NAME: RegExp;
119
+ /** One OAuth scope token (RFC 6749 3.3: visible ASCII but space, `"` and `\`), at most 256 characters. One class, so it cannot backtrack. */
120
+ export declare const OAUTH_SCOPE: RegExp;
107
121
  /**
108
122
  * The ToolContext member: an access token for `provider`, from this app's
109
123
  * and this user's credentials only. A context that does not know its app
@@ -22,9 +22,9 @@
22
22
  * (`pinnedCredentialStore`, like namespacedMemoryService), so a delegated
23
23
  * subagent, which runs under its own app name, reads the root's
24
24
  * credentials. The consent step that puts a token is
25
- * lib/tools/oauthConsent.ts (ADR 0085); the YAML that lets an agent use a
26
- * provider (WS6-3c) comes later; nothing here
27
- * wires a tool to the store.
25
+ * lib/tools/oauthConsent.ts (ADR 0085); the YAML that lets an OpenAPI or
26
+ * MCP tool use a provider (`auth: { oauth2 }`, lib/tools/oauthTools.ts,
27
+ * ADR 0112) is what calls `accessToken` for a declared tool.
28
28
  *
29
29
  * A LEAF: types and plain functions, no runtime imports, so lib/tools/tool.ts
30
30
  * can bind the member without loading crypto, pg or telemetry.
@@ -32,11 +32,15 @@
32
32
  /**
33
33
  * A credential that cannot be used. The message names the provider and what
34
34
  * to do, never a token, a user or a key, so a tool may return it to the model.
35
+ * A `host_refused` error names the allowlist that refused (`allowlist`,
36
+ * `'oauth'` when not given).
35
37
  */
36
38
  export class ToolCredentialError extends Error {
37
39
  code;
38
40
  provider;
39
- constructor(code, provider) {
41
+ /** For `host_refused`: the allowlist that refused the host. */
42
+ allowlist;
43
+ constructor(code, provider, allowlist) {
40
44
  const p = provider ? `"${provider}"` : 'this provider';
41
45
  super(code === 'not_connected'
42
46
  ? `No ${p} account is connected for this user; ask them to connect it.`
@@ -48,14 +52,26 @@ export class ToolCredentialError extends Error {
48
52
  ? `The stored ${p} authorization cannot be read with this server's credential key.`
49
53
  : code === 'no_user'
50
54
  ? 'Third-party access needs the app and user of the run.'
51
- : `Invalid credential request for ${p}.`);
55
+ : code === 'unavailable'
56
+ ? `This server holds no ${p} authorizations: its operator has not configured tool credentials.`
57
+ : code === 'grant_failed'
58
+ ? `The server's own ${p} authorization could not be obtained from the provider's token endpoint.`
59
+ : code === 'host_refused'
60
+ ? allowlist === 'credential'
61
+ ? `The ${p} client secret may not be sent to this host: the operator's credential host allowlist (MELCHIZEDEK_CREDENTIAL_HOSTS) does not bind it there.`
62
+ : `The ${p} authorization may not be sent to this host: the operator's OAuth host allowlist (MELCHIZEDEK_OAUTH_HOSTS) does not include it.`
63
+ : `Invalid credential request for ${p}.`);
52
64
  this.name = 'ToolCredentialError';
53
65
  this.code = code;
54
66
  this.provider = provider;
67
+ if (code === 'host_refused')
68
+ this.allowlist = allowlist ?? 'oauth';
55
69
  }
56
70
  }
57
71
  /** A provider name: lowercase letters, digits, and . _ - inside; at most 64 characters. */
58
72
  export const PROVIDER_NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;
73
+ /** One OAuth scope token (RFC 6749 3.3: visible ASCII but space, `"` and `\`), at most 256 characters. One class, so it cannot backtrack. */
74
+ export const OAUTH_SCOPE = /^[\x21\x23-\x5b\x5d-\x7e]{1,256}$/;
59
75
  /**
60
76
  * The ToolContext member: an access token for `provider`, from this app's
61
77
  * and this user's credentials only. A context that does not know its app
@@ -0,0 +1,28 @@
1
+ /**
2
+ * lib/tools/credentialEnv.ts — which environment variables a YAML may name
3
+ * as a credential, and the one way a credential is read from one.
4
+ *
5
+ * WHY this file exists:
6
+ * A syndicate YAML (possibly registry-stored) names both a variable and
7
+ * the host its value is sent to: an OpenAPI `auth` (bearer_env, api_key,
8
+ * oauth2), an MCP server's `mcp_auth.oauth2`. So a YAML may never name one
9
+ * of the framework's own secrets: the database URL, a provider key or an
10
+ * A2A secret cannot be sent to an API or a token endpoint.
11
+ * OPENAPI_CREDENTIAL_ENVS, when the operator sets it, is the exact list of
12
+ * variables any of these may name.
13
+ *
14
+ * A value is read here and handed to the code that sends it; an error names
15
+ * the variable, never its value.
16
+ */
17
+ /**
18
+ * Why an `auth` may not read this variable, or null. With
19
+ * OPENAPI_CREDENTIAL_ENVS set, only the names it lists are allowed; without
20
+ * it, anything but the framework's own variables is.
21
+ */
22
+ export declare function credentialEnvProblem(name: string, env?: NodeJS.ProcessEnv): string | null;
23
+ /**
24
+ * The credential in `name`, trimmed. Throws when the allowlist refuses the
25
+ * variable or it is not set; `where` prefixes the message, which names the
26
+ * variable and never its value.
27
+ */
28
+ export declare function readCredentialEnv(name: string, where: string, env?: NodeJS.ProcessEnv): string;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * lib/tools/credentialEnv.ts — which environment variables a YAML may name
3
+ * as a credential, and the one way a credential is read from one.
4
+ *
5
+ * WHY this file exists:
6
+ * A syndicate YAML (possibly registry-stored) names both a variable and
7
+ * the host its value is sent to: an OpenAPI `auth` (bearer_env, api_key,
8
+ * oauth2), an MCP server's `mcp_auth.oauth2`. So a YAML may never name one
9
+ * of the framework's own secrets: the database URL, a provider key or an
10
+ * A2A secret cannot be sent to an API or a token endpoint.
11
+ * OPENAPI_CREDENTIAL_ENVS, when the operator sets it, is the exact list of
12
+ * variables any of these may name.
13
+ *
14
+ * A value is read here and handed to the code that sends it; an error names
15
+ * the variable, never its value.
16
+ */
17
+ /** Prefixes and names of variables the framework itself reads: never an API credential. */
18
+ const FRAMEWORK_ENV_PREFIXES = [
19
+ 'A2A_', 'SUPABASE_', 'DATABASE_', 'MCP_', 'MODEL_', 'MEMORY_', 'OTEL_', 'TELEMETRY_', 'MELCHIZEDEK_',
20
+ 'GOOGLE_', 'GEMINI_', 'ANTHROPIC_', 'OPENAI_', 'AZURE_', 'AWS_', 'XAI_', 'MOONSHOT_', 'OLLAMA_', 'WIKI_',
21
+ 'ALLOW_', 'OPENAPI_',
22
+ ];
23
+ const FRAMEWORK_ENV_NAMES = new Set(['PUBLIC_URL', 'HOST', 'PORT', 'PATH', 'HOME', 'NODE_OPTIONS', 'WEB_EXTRACT_CHAR_LIMIT', 'OAUTH_REDIRECT_URI', 'OAUTH_CALLBACK_IDENTITY']);
24
+ /**
25
+ * Why an `auth` may not read this variable, or null. With
26
+ * OPENAPI_CREDENTIAL_ENVS set, only the names it lists are allowed; without
27
+ * it, anything but the framework's own variables is.
28
+ */
29
+ export function credentialEnvProblem(name, env = process.env) {
30
+ const allow = env.OPENAPI_CREDENTIAL_ENVS?.split(',').map((s) => s.trim()).filter(Boolean);
31
+ if (allow?.length) {
32
+ return allow.includes(name) ? null : `${name} is not in OPENAPI_CREDENTIAL_ENVS (${allow.join(', ')})`;
33
+ }
34
+ if (FRAMEWORK_ENV_NAMES.has(name) || FRAMEWORK_ENV_PREFIXES.some((p) => name.startsWith(p))) {
35
+ return `${name} is one of the framework's own settings and may not be sent to an API; give the API its own variable (or list it in OPENAPI_CREDENTIAL_ENVS)`;
36
+ }
37
+ return null;
38
+ }
39
+ /**
40
+ * The credential in `name`, trimmed. Throws when the allowlist refuses the
41
+ * variable or it is not set; `where` prefixes the message, which names the
42
+ * variable and never its value.
43
+ */
44
+ export function readCredentialEnv(name, where, env = process.env) {
45
+ const refused = credentialEnvProblem(name, env);
46
+ if (refused)
47
+ throw new Error(`${where}: ${refused}`);
48
+ const value = env[name]?.trim();
49
+ if (!value)
50
+ throw new Error(`${where}: ${name} is not set (auth reads it from the environment)`);
51
+ return value;
52
+ }