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
@@ -35,7 +35,7 @@
35
35
  import { remoteAgentOwnTool } from './a2a/remoteAgent.js';
36
36
  import { compileSpec, compileSubagentSpec, workflowAgentSpecs } from './compile.js';
37
37
  import { resolveAdapter } from './models/registry.js';
38
- import { subagentTool, workflowSubagentTool } from './runtime/native/delegate.js';
38
+ import { nestedDispatchWalk, subagentTool, workflowSubagentTool } from './runtime/native/delegate.js';
39
39
  import { unsupportedOnNative } from './runtime/runtimeFlag.js';
40
40
  export { UnsupportedOnRuntimeError, unsupportedOnNative } from './runtime/runtimeFlag.js';
41
41
  import { instructionToolOf, toolOf, toolsetOf } from './tools/tool.js';
@@ -108,6 +108,8 @@ export function compileNative(spec) {
108
108
  tools.push(subagentTool(compileNative(entry.agent)));
109
109
  else if (entry.kind === 'workflow')
110
110
  tools.push(workflowSubagentTool(workflowSubagentOf(compileNativeWorkflow(entry.workflow))));
111
+ else if (entry.kind === 'dispatch')
112
+ tools.push(workflowSubagentTool(dispatchSubagentOf(entry.dispatch)));
111
113
  else if (entry.kind === 'remote')
112
114
  tools.push(remoteAgentOwnTool({ name: entry.name, description: entry.description, url: entry.url }));
113
115
  else
@@ -182,6 +184,8 @@ export function compileNativeWorkflow(spec) {
182
184
  const workflows = new Map();
183
185
  for (const { yaml, workflow } of spec.workflows)
184
186
  workflows.set(yaml.name, compileNativeWorkflow(workflow));
187
+ for (const { yaml, dispatch } of spec.dispatches ?? [])
188
+ workflows.set(yaml.name, dispatchSubagentOf(dispatch));
185
189
  refuseUnrunnableNodes(graph, spec.resolveTool);
186
190
  return { name: spec.name, description: spec.description, graph, agents, resolveTool: spec.resolveTool, workflows };
187
191
  }
@@ -214,6 +218,19 @@ export function workflowSubagentOf(workflow) {
214
218
  }),
215
219
  };
216
220
  }
221
+ /**
222
+ * A nested dispatch syndicate as the delegated subagent the native loop
223
+ * calls, or the node a walk runs (ADR 0120): one call runs it as its own
224
+ * turn (nestedDispatchWalk) on the child session lib/runtime/native/delegate.ts
225
+ * (or lib/workflow/turn.ts) opened.
226
+ */
227
+ export function dispatchSubagentOf(dispatch) {
228
+ return {
229
+ name: dispatch.name,
230
+ ...(dispatch.description ? { description: dispatch.description } : {}),
231
+ walk: (run) => nestedDispatchWalk({ name: dispatch.name, config: dispatch.config, compile: dispatch.compile }, run),
232
+ };
233
+ }
217
234
  /** One subagent entry (a dispatch route) for the native loop: compileSubagentSpec, then compileNative. */
218
235
  export async function compileNativeSubagent(subCfg, opts = {}) {
219
236
  return compileNative(await compileSubagentSpec(subCfg, opts));
@@ -73,6 +73,8 @@ function matchSubagent(candidate, subagents) {
73
73
  return undefined;
74
74
  return subagents.find(sub => norm(sub.name) === target);
75
75
  }
76
+ /** What a subagent name can look like: one token, linear to test. */
77
+ const ROUTE_SHAPED = /^[A-Za-z0-9_.-]{1,64}$/;
76
78
  /**
77
79
  * Turn the router's raw output into a route that definitely exists.
78
80
  *
@@ -108,7 +110,8 @@ export function resolveRoute(rawOutput, config) {
108
110
  parsed = JSON.parse(unfence(rawOutput));
109
111
  }
110
112
  catch {
111
- return defaultResolution(`router output was not JSON: ${rawOutput.trim().slice(0, 80)}`);
113
+ // The reason reaches the server log: the size, never the text (the router can echo the person).
114
+ return defaultResolution(`router output was not JSON (${rawOutput.trim().length} chars)`);
112
115
  }
113
116
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
114
117
  return defaultResolution('router output was not a JSON object');
@@ -120,7 +123,8 @@ export function resolveRoute(rawOutput, config) {
120
123
  }
121
124
  const matched = matchSubagent(rawRoute, subagents);
122
125
  if (!matched) {
123
- return defaultResolution(`router chose unknown route '${rawRoute}'`);
126
+ // A route-shaped value is named; anything else is model text and is only measured.
127
+ return defaultResolution(ROUTE_SHAPED.test(rawRoute) ? `router chose unknown route '${rawRoute}'` : `router chose an unknown route (${rawRoute.length} chars)`);
124
128
  }
125
129
  const rawReason = record[reasonKey];
126
130
  return {
@@ -75,6 +75,21 @@ export interface DoctorOAuth {
75
75
  set: Record<string, boolean>;
76
76
  problems: string[];
77
77
  }
78
+ /**
79
+ * The static credential variables the syndicates send, under the operator's
80
+ * credential host allowlist (ADR 0122). Present when any syndicate sends one
81
+ * or MELCHIZEDEK_CREDENTIAL_HOSTS is set. Names and hosts only.
82
+ */
83
+ export interface DoctorCredentials {
84
+ /** Whether MELCHIZEDEK_CREDENTIAL_HOSTS is set. */
85
+ set: boolean;
86
+ /** Every variable the syndicates send (bearer_env, api_key.env, client_secret_env), sorted. */
87
+ envs: string[];
88
+ /** With no allowlist: the variables sent wherever the YAML says, named. Not a --check failure. */
89
+ warning?: string;
90
+ /** A malformed allowlist, and each use it refuses (`file: agent · tools · role: why`). A --check failure. */
91
+ problems: string[];
92
+ }
78
93
  export interface DoctorSyndicate {
79
94
  /** Path relative to the agents dir, e.g. "examples/council.yaml". */
80
95
  file: string;
@@ -106,6 +121,11 @@ export interface DoctorSyndicate {
106
121
  };
107
122
  /** The tools that need an OAuth grant (ADR 0112), when any do. */
108
123
  grants?: DoctorGrant[];
124
+ /** The static credential variables its tools send (ADR 0122), and what the allowlist refuses; when it sends any. */
125
+ credentials?: {
126
+ envs: string[];
127
+ problems: string[];
128
+ };
109
129
  }
110
130
  export interface Unlock {
111
131
  env: string;
@@ -141,6 +161,66 @@ export interface DoctorResult {
141
161
  counts: Record<VerdictState, number>;
142
162
  /** The OAuth setup, when any syndicate declares a grant or its variables are set. */
143
163
  oauth?: DoctorOAuth;
164
+ /** The credential host allowlist (ADR 0122), when any syndicate sends a static credential or it is set. */
165
+ credentials?: DoctorCredentials;
166
+ /** How each provider would be reached here: the providers line, as data. */
167
+ providers: ProviderPath[];
168
+ /** How the A2A server would authenticate and bill, when any of its variables is set. */
169
+ serving?: DoctorServing;
170
+ /** A stored Sign in with ChatGPT credential (ADR 0126), when one is present or its variables are set. */
171
+ chatgpt?: DoctorChatGpt;
172
+ }
173
+ /**
174
+ * A Sign in with ChatGPT credential (ADR 0126): where it is, whether it is
175
+ * signed in and carries OpenAI ids here, and what stops it. Never a token.
176
+ */
177
+ export interface DoctorChatGpt {
178
+ file: string;
179
+ /** False when MELCHIZEDEK_CHATGPT_SIGNIN=off. */
180
+ enabled: boolean;
181
+ present: boolean;
182
+ signedIn: boolean;
183
+ /** OpenAI ids run on it here (no OPENAI_API_KEY, OpenAI on its own API). */
184
+ routesOpenAi: boolean;
185
+ /** Epoch milliseconds the stored access token expires (it refreshes on use). */
186
+ expiresAt?: number;
187
+ /** A served configuration on it, or a file that cannot be used. `--check` fails on any. */
188
+ problems: string[];
189
+ }
190
+ /**
191
+ * How one provider would be reached under this environment (the doctor's
192
+ * providers line): funded or not, directly or through the gateway, and on
193
+ * which platform. `melchizedek-setup --auto` reads its levels from this.
194
+ */
195
+ export interface ProviderPath {
196
+ provider: ProviderId;
197
+ label: string;
198
+ funded: boolean;
199
+ transport: 'direct' | 'gateway';
200
+ /** The gateway id when transport is 'gateway'. */
201
+ gateway?: string;
202
+ /** `local` for Ollama; a cloud platform (vertex, bedrock, azure) or `direct` otherwise. */
203
+ platform: Platform | 'local';
204
+ keyEnv: string | null;
205
+ /** `chatgpt-signin` when a stored Sign in with ChatGPT funds the provider instead of its key (ADR 0126). */
206
+ credential?: 'chatgpt-signin';
207
+ }
208
+ /**
209
+ * The A2A server's identity and billing settings (scripts/a2a_server.ts),
210
+ * names and presence only: which `A2A_AUTH` mode, whether the secret and the
211
+ * mode's own variables are set, and who pays (`A2A_KEY_MODE`).
212
+ */
213
+ export interface DoctorServing {
214
+ /** A2A_AUTH as set, or `secret` (the server's default). */
215
+ auth: string;
216
+ /** True when A2A_AUTH is set explicitly. */
217
+ authDeclared: boolean;
218
+ /** A2A_KEY_MODE as set, or `server`. */
219
+ keyMode: string;
220
+ /** Which of the serving variables are set (never their values). */
221
+ set: Record<string, boolean>;
222
+ /** What the server would refuse to start on, by variable name. */
223
+ problems: string[];
144
224
  }
145
225
  export interface DoctorEndpoint {
146
226
  provider: ProviderId;
@@ -166,6 +246,16 @@ export interface DoctorEndpoint {
166
246
  export declare function runtimeReport(env?: NodeJS.ProcessEnv): DoctorRuntime;
167
247
  /** One row per provider that is not on its vendor's public API with an env key. */
168
248
  export declare function endpointRows(): DoctorEndpoint[];
249
+ /** One status per provider, probed with an id of its own prefix (the providers line). */
250
+ export declare function providerPaths(): ProviderPath[];
251
+ /** The serving variables the doctor reports by presence. */
252
+ export declare const SERVING_ENV: readonly ['A2A_AUTH', 'A2A_SERVER_SECRET', 'A2A_CALLERS', 'A2A_JWT_ISSUER', 'A2A_JWT_AUDIENCE', 'A2A_JWT_JWKS_URL', 'A2A_JWT_SECRET', 'A2A_TRUSTED_USER_HEADER', 'A2A_KEY_MODE', 'PUBLIC_URL'];
253
+ /**
254
+ * How the A2A server would authenticate callers and who would pay, from the
255
+ * environment; undefined when none of its variables is set. The checks are
256
+ * the ones scripts/a2a_server.ts stops on, reported by name.
257
+ */
258
+ export declare function servingReport(env?: NodeJS.ProcessEnv): DoctorServing | undefined;
169
259
  export declare const CONSOLE_URLS: Record<ProviderId, string>;
170
260
  /**
171
261
  * Every syndicate file the loader can see: the root (this deployment's
@@ -194,6 +284,23 @@ export declare function runDoctor(options?: {
194
284
  agentsDir?: string;
195
285
  scripts?: Record<string, string>;
196
286
  }): DoctorResult;
287
+ /**
288
+ * The Sign in with ChatGPT line (ADR 0126), when a credential file is present
289
+ * or one of its variables is set. `served`: a serving variable is set, so
290
+ * this configuration is meant for melchizedek-serve, which refuses to start
291
+ * while the sign-in is the OpenAI path.
292
+ */
293
+ export declare function chatGptReport(served: boolean, env?: NodeJS.ProcessEnv): DoctorChatGpt | undefined;
294
+ /**
295
+ * Why `melchizedek-doctor --check` exits non-zero, one line each; empty when
296
+ * it passes. A blocked syndicate, a runtime problem, a `MODEL_GATEWAY` that
297
+ * names no gateway (named with the accepted values only: the value may be a
298
+ * key pasted into the wrong variable), the OAuth setup's and the credential
299
+ * allowlist's problems, and each grant whose hosts the OAuth host allowlist
300
+ * refuses (ADR 0114), and the Sign in with ChatGPT line's problems (ADR 0126).
301
+ * Names and hosts only, never a value.
302
+ */
303
+ export declare function checkProblems(result: DoctorResult): string[];
197
304
  export declare function renderDoctor(result: DoctorResult, opts?: {
198
305
  color?: boolean;
199
306
  }): string;
@@ -34,10 +34,16 @@ import { credentialSource, endpointFromEnv, endpointLabel, endpointProblems, PLA
34
34
  import { GATEWAY_ENV, GATEWAY_KEY_ENV, GATEWAYS, gatewayConfig, gatewayProblem, gatewayUsable, } from './models/gateway.js';
35
35
  import { PROVIDERS } from './models/providerMap.js';
36
36
  import { describeRuntime } from './runtime/runtimeFlag.js';
37
+ import { CHATGPT_SIGNIN_ENV, CHATGPT_SIGNIN_FILE_ENV, chatGptSignInRoutesOpenAi, chatGptSignInStatus } from './chatgpt/state.js';
37
38
  import { agentOAuthGrants, oauthEnvNames } from './tools/oauthTools.js';
38
39
  import { grantHostProblems, OAUTH_REDIRECT_URI_ENV, oauthEnvProblems } from './a2a/oauthSetup.js';
39
40
  import { CREDENTIAL_KEY_ENV } from './tools/credentialCipher.js';
40
41
  import { OAUTH_HOSTS_ENV } from './tools/oauthHosts.js';
42
+ import { CREDENTIAL_HOSTS_ENV, credentialHosts } from './tools/credentialHosts.js';
43
+ import { syndicateCredentialUses } from './tools/credentialUses.js';
44
+ import { credentialHostEnvProblems, unboundCredentialWarning } from './a2a/oauthSetup.js';
45
+ /** The `gateway.id` the doctor reports when `MODEL_GATEWAY` names no known gateway; the value itself is never kept. */
46
+ const UNRECOGNISED_GATEWAY = 'unrecognised';
41
47
  function resolvable(module) {
42
48
  try {
43
49
  createRequire(import.meta.url).resolve(module);
@@ -100,6 +106,70 @@ export function endpointRows() {
100
106
  }
101
107
  return rows;
102
108
  }
109
+ /** One status per provider, probed with an id of its own prefix (the providers line). */
110
+ export function providerPaths() {
111
+ return Object.keys(PROVIDERS).map((p) => {
112
+ const r = describeCapabilities(PROBE_ID[p]);
113
+ return {
114
+ provider: p,
115
+ label: PROVIDERS[p].label,
116
+ funded: r.funded,
117
+ transport: r.transport,
118
+ ...(r.gateway ? { gateway: r.gateway } : {}),
119
+ platform: p === 'ollama' ? 'local' : (r.platform ?? 'direct'),
120
+ keyEnv: r.keyEnv,
121
+ ...(p === 'openai' && r.funded && r.transport === 'direct' && chatGptSignInRoutesOpenAi() ? { credential: 'chatgpt-signin' } : {}),
122
+ };
123
+ });
124
+ }
125
+ /** The serving variables the doctor reports by presence. */
126
+ export const SERVING_ENV = [
127
+ 'A2A_AUTH',
128
+ 'A2A_SERVER_SECRET',
129
+ 'A2A_CALLERS',
130
+ 'A2A_JWT_ISSUER',
131
+ 'A2A_JWT_AUDIENCE',
132
+ 'A2A_JWT_JWKS_URL',
133
+ 'A2A_JWT_SECRET',
134
+ 'A2A_TRUSTED_USER_HEADER',
135
+ 'A2A_KEY_MODE',
136
+ 'PUBLIC_URL',
137
+ ];
138
+ /**
139
+ * How the A2A server would authenticate callers and who would pay, from the
140
+ * environment; undefined when none of its variables is set. The checks are
141
+ * the ones scripts/a2a_server.ts stops on, reported by name.
142
+ */
143
+ export function servingReport(env = process.env) {
144
+ const has = (name) => !!env[name]?.trim();
145
+ const set = Object.fromEntries(SERVING_ENV.map((name) => [name, has(name)]));
146
+ if (!Object.values(set).some(Boolean))
147
+ return undefined;
148
+ // A value outside the known modes is reported as `invalid`, never echoed:
149
+ // a secret pasted into the wrong variable must not reach the terminal.
150
+ const rawAuth = (env.A2A_AUTH ?? '').trim().toLowerCase() || 'secret';
151
+ const rawKeyMode = (env.A2A_KEY_MODE ?? '').trim().toLowerCase() || 'server';
152
+ const auth = ['secret', 'callers', 'jwt', 'header'].includes(rawAuth) ? rawAuth : 'invalid';
153
+ const keyMode = rawKeyMode === 'server' || rawKeyMode === 'byok' ? rawKeyMode : 'invalid';
154
+ const problems = [];
155
+ if (auth === 'invalid')
156
+ problems.push('A2A_AUTH must be secret, callers, jwt or header');
157
+ if (auth === 'callers' && !has('A2A_CALLERS'))
158
+ problems.push('A2A_AUTH=callers needs A2A_CALLERS');
159
+ if (auth === 'jwt' && (!has('A2A_JWT_ISSUER') || !has('A2A_JWT_AUDIENCE')))
160
+ problems.push('A2A_AUTH=jwt needs A2A_JWT_ISSUER and A2A_JWT_AUDIENCE');
161
+ if (auth === 'jwt' && !has('A2A_JWT_JWKS_URL') && !has('A2A_JWT_SECRET'))
162
+ problems.push('A2A_AUTH=jwt needs A2A_JWT_JWKS_URL or A2A_JWT_SECRET');
163
+ if (auth === 'header' && (!has('A2A_TRUSTED_USER_HEADER') || !has('A2A_SERVER_SECRET')))
164
+ problems.push('A2A_AUTH=header needs A2A_TRUSTED_USER_HEADER and A2A_SERVER_SECRET');
165
+ if (keyMode !== 'server' && keyMode !== 'byok')
166
+ problems.push("A2A_KEY_MODE must be 'server' or 'byok'");
167
+ if (has('PUBLIC_URL') && !has('A2A_AUTH'))
168
+ problems.push('PUBLIC_URL is set, so A2A_AUTH must be set explicitly (with A2A_SERVED_AGENTS and A2A_TRUST_PROXY)');
169
+ if (auth === 'secret' && !has('A2A_SERVER_SECRET') && keyMode === 'byok')
170
+ problems.push('A2A_KEY_MODE=byok without A2A_SERVER_SECRET (or caller tokens) serves unauthenticated, loopback only');
171
+ return { auth, authDeclared: has('A2A_AUTH'), keyMode, set, problems };
172
+ }
103
173
  // ── Where to get a key ───────────────────────────────────────────────────────
104
174
  export const CONSOLE_URLS = {
105
175
  gemini: 'https://aistudio.google.com',
@@ -305,6 +375,17 @@ export function diagnoseSyndicate(file, agentsDir, scripts) {
305
375
  const nestedErrors = [];
306
376
  const grants = [];
307
377
  walk(config, '', { load, seen: new Set([file, path.basename(file)]), nestedErrors, grants }, rows);
378
+ // Static credentials and the credential host allowlist (ADR 0122); a nested file that cannot load is reported above.
379
+ const loadQuiet = (ref) => {
380
+ try {
381
+ return load(ref);
382
+ }
383
+ catch {
384
+ return {};
385
+ }
386
+ };
387
+ const credentialEnvs = [...new Set(syndicateCredentialUses([config], loadQuiet).map((u) => u.env))].sort();
388
+ const credentials = credentialEnvs.length ? { envs: credentialEnvs, problems: credentialHostEnvProblems([config], process.env, loadQuiet) } : undefined;
308
389
  const memory = config.memory_system === 'long-term'
309
390
  ? {
310
391
  namespace: config.memory_namespace ?? LEGACY_MEMORY_APP_NAME,
@@ -324,6 +405,7 @@ export function diagnoseSyndicate(file, agentsDir, scripts) {
324
405
  ...(nestedErrors.length ? { error: nestedErrors.join('\n') } : {}),
325
406
  ...(memory ? { memory } : {}),
326
407
  ...(grants.length ? { grants } : {}),
408
+ ...(credentials ? { credentials } : {}),
327
409
  };
328
410
  }
329
411
  catch (err) {
@@ -405,8 +487,9 @@ export function runDoctor(options = {}) {
405
487
  const rawGateway = (process.env[GATEWAY_ENV] ?? '').trim();
406
488
  const gateway = rawGateway
407
489
  ? {
408
- id: cfg?.gateway.id ?? rawGateway,
409
- label: cfg?.gateway.label ?? rawGateway,
490
+ // An unrecognised value is never echoed: it may be a key pasted into the wrong variable.
491
+ id: cfg?.gateway.id ?? UNRECOGNISED_GATEWAY,
492
+ label: cfg?.gateway.label ?? 'an unrecognised gateway',
410
493
  usable: gatewayUsable(),
411
494
  ...(gatewayProblem() ? { problem: gatewayProblem() } : {}),
412
495
  }
@@ -424,7 +507,68 @@ export function runDoctor(options = {}) {
424
507
  const grants = syndicates.flatMap((s) => s.grants ?? []).map((g) => ({ agent: g.agent, tools: g.tools, oauth2: { provider: g.provider, grant: g.grant, token_url: '' } }));
425
508
  const set = Object.fromEntries([CREDENTIAL_KEY_ENV, OAUTH_REDIRECT_URI_ENV, OAUTH_HOSTS_ENV].map((name) => [name, !!process.env[name]?.trim()]));
426
509
  const oauth = grants.length || Object.values(set).some(Boolean) ? { set, problems: oauthEnvProblems(grants) } : undefined;
427
- return { agentsDir, runtime: runtimeReport(), syndicates, unlocks, gateway, endpoints: endpointRows(), counts, ...(oauth ? { oauth } : {}) };
510
+ // The credential host allowlist (ADR 0122): unbound variables named, refusals listed, names and hosts only.
511
+ const credentialSet = !!process.env[CREDENTIAL_HOSTS_ENV]?.trim();
512
+ const envs = [...new Set(syndicates.flatMap((s) => s.credentials?.envs ?? []))].sort();
513
+ let credentials;
514
+ if (envs.length || credentialSet) {
515
+ const problems = [];
516
+ try {
517
+ credentialHosts();
518
+ }
519
+ catch (err) {
520
+ problems.push(err instanceof Error ? err.message : `${CREDENTIAL_HOSTS_ENV} is malformed`);
521
+ }
522
+ if (!problems.length)
523
+ for (const s of syndicates)
524
+ for (const p of s.credentials?.problems ?? [])
525
+ problems.push(`${s.file}: ${p}`);
526
+ const warning = unboundCredentialWarning(envs);
527
+ credentials = { set: credentialSet, envs, ...(warning ? { warning } : {}), problems };
528
+ }
529
+ const serving = servingReport();
530
+ const chatgpt = chatGptReport(!!serving);
531
+ return {
532
+ agentsDir,
533
+ runtime: runtimeReport(),
534
+ syndicates,
535
+ unlocks,
536
+ gateway,
537
+ endpoints: endpointRows(),
538
+ counts,
539
+ ...(oauth ? { oauth } : {}),
540
+ ...(credentials ? { credentials } : {}),
541
+ providers: providerPaths(),
542
+ ...(serving ? { serving } : {}),
543
+ ...(chatgpt ? { chatgpt } : {}),
544
+ };
545
+ }
546
+ /**
547
+ * The Sign in with ChatGPT line (ADR 0126), when a credential file is present
548
+ * or one of its variables is set. `served`: a serving variable is set, so
549
+ * this configuration is meant for melchizedek-serve, which refuses to start
550
+ * while the sign-in is the OpenAI path.
551
+ */
552
+ export function chatGptReport(served, env = process.env) {
553
+ const status = chatGptSignInStatus(env);
554
+ if (!status.present && !env[CHATGPT_SIGNIN_FILE_ENV]?.trim() && !env[CHATGPT_SIGNIN_ENV]?.trim())
555
+ return undefined;
556
+ const routesOpenAi = chatGptSignInRoutesOpenAi(env);
557
+ const problems = [];
558
+ if (status.problem)
559
+ problems.push(status.problem);
560
+ if (served && routesOpenAi) {
561
+ problems.push(`Sign in with ChatGPT is local only, and it is the OpenAI path in this served configuration: melchizedek-serve and melchizedek-worker refuse to start. Set OPENAI_API_KEY (or Azure OpenAI), or ${CHATGPT_SIGNIN_ENV}=off for the served process.`);
562
+ }
563
+ return {
564
+ file: status.file,
565
+ enabled: status.enabled,
566
+ present: status.present,
567
+ signedIn: status.signedIn,
568
+ routesOpenAi,
569
+ ...(status.expiresAt ? { expiresAt: status.expiresAt } : {}),
570
+ problems,
571
+ };
428
572
  }
429
573
  // ── Rendering ────────────────────────────────────────────────────────────────
430
574
  const ANSI = {
@@ -445,6 +589,37 @@ const PROBE_ID = {
445
589
  moonshot: 'kimi-x',
446
590
  ollama: 'ollama/x',
447
591
  };
592
+ /**
593
+ * Why `melchizedek-doctor --check` exits non-zero, one line each; empty when
594
+ * it passes. A blocked syndicate, a runtime problem, a `MODEL_GATEWAY` that
595
+ * names no gateway (named with the accepted values only: the value may be a
596
+ * key pasted into the wrong variable), the OAuth setup's and the credential
597
+ * allowlist's problems, and each grant whose hosts the OAuth host allowlist
598
+ * refuses (ADR 0114), and the Sign in with ChatGPT line's problems (ADR 0126).
599
+ * Names and hosts only, never a value.
600
+ */
601
+ export function checkProblems(result) {
602
+ const problems = [];
603
+ if (result.counts.blocked > 0)
604
+ problems.push(`${result.counts.blocked} syndicate(s) blocked`);
605
+ if (result.runtime.problem)
606
+ problems.push(result.runtime.problem);
607
+ if (result.gateway?.id === UNRECOGNISED_GATEWAY) {
608
+ problems.push(`gateway: ${GATEWAY_ENV} is set to an unrecognised value (not shown); it must be one of: ${Object.keys(GATEWAYS).join(', ')}`);
609
+ }
610
+ for (const p of result.oauth?.problems ?? [])
611
+ problems.push(`oauth: ${p}`);
612
+ for (const p of result.credentials?.problems ?? [])
613
+ problems.push(`credentials: ${p}`);
614
+ for (const p of result.chatgpt?.problems ?? [])
615
+ problems.push(`chatgpt: ${p}`);
616
+ for (const s of result.syndicates) {
617
+ for (const g of s.grants ?? [])
618
+ for (const p of g.hostProblems ?? [])
619
+ problems.push(`${s.file}: ${g.agent} · ${g.provider}: ${p}`);
620
+ }
621
+ return problems;
622
+ }
448
623
  function pad(s, n) {
449
624
  return s.length >= n ? s.slice(0, n - 1) + '…' : s.padEnd(n);
450
625
  }
@@ -471,11 +646,10 @@ export function renderDoctor(result, opts = {}) {
471
646
  // Provider line: what is funded, and how. Each provider is probed with an
472
647
  // id of its own prefix, so the line names ITS key (a fallthrough once put
473
648
  // xAI's key beside Moonshot).
474
- const providerBits = Object.keys(PROVIDERS).map((p) => {
475
- const r = describeCapabilities(PROBE_ID[p]);
649
+ const providerBits = (result.providers ?? providerPaths()).map((r) => {
476
650
  const mark = !r.funded ? `${c.red}✗${c.reset}` : r.transport === 'gateway' ? `${c.yellow}◇${c.reset}` : `${c.green}✓${c.reset}`;
477
- const how = !r.funded ? `${c.dim}${r.keyEnv} not set${c.reset}` : r.transport === 'gateway' ? `${c.dim}via gateway:${r.gateway}${c.reset}` : p === 'ollama' ? `${c.dim}local${c.reset}` : `${c.dim}${r.platform ?? 'direct'}${c.reset}`;
478
- return `${mark} ${PROVIDERS[p].label} ${how}`;
651
+ const how = !r.funded ? `${c.dim}${r.keyEnv} not set${c.reset}` : r.transport === 'gateway' ? `${c.dim}via gateway:${r.gateway}${c.reset}` : r.platform === 'local' ? `${c.dim}local${c.reset}` : r.credential === 'chatgpt-signin' ? `${c.dim}ChatGPT sign-in, local only${c.reset}` : `${c.dim}${r.platform}${c.reset}`;
652
+ return `${mark} ${r.label} ${how}`;
479
653
  });
480
654
  lines.push('providers ' + providerBits.join(' '));
481
655
  for (const e of result.endpoints ?? []) {
@@ -490,6 +664,32 @@ export function renderDoctor(result, opts = {}) {
490
664
  ? `gateway ${c.red}✗${c.reset} ${result.gateway.problem}`
491
665
  : `gateway ${c.yellow}◇${c.reset} ${result.gateway.label} fills in for any provider whose direct key is absent ${c.dim}(native search is lost on that path)${c.reset}`);
492
666
  }
667
+ if (result.serving) {
668
+ // Names only: the A2A identity mode, who pays, and what the server would refuse.
669
+ const sv = result.serving;
670
+ const mark = sv.problems.length ? `${c.red}✗${c.reset}` : `${c.green}✓${c.reset}`;
671
+ const set = Object.entries(sv.set).filter(([, on]) => on).map(([name]) => name).join(', ');
672
+ lines.push(`serving ${mark} A2A_AUTH=${sv.auth}${sv.authDeclared ? '' : ' (default)'} · A2A_KEY_MODE=${sv.keyMode} ${c.dim}· set: ${set || 'none'}${c.reset}`);
673
+ for (const p of sv.problems)
674
+ lines.push(` ${c.red}✗ ${p}${c.reset}`);
675
+ }
676
+ if (result.chatgpt) {
677
+ // Presence and the file's path, never a token (ADR 0126).
678
+ const g = result.chatgpt;
679
+ const mark = g.problems.length ? `${c.red}✗${c.reset}` : g.routesOpenAi ? `${c.green}✓${c.reset}` : `${c.dim}·${c.reset}`;
680
+ const state = !g.enabled
681
+ ? `${CHATGPT_SIGNIN_ENV}=off, ignored`
682
+ : !g.present
683
+ ? 'not signed in'
684
+ : !g.signedIn
685
+ ? 'signed out (run melchizedek-setup --chatgpt-signin)'
686
+ : g.routesOpenAi
687
+ ? 'signed in · carries OpenAI ids · local only'
688
+ : 'signed in · not used (OPENAI_API_KEY or another OpenAI endpoint wins)';
689
+ lines.push(`chatgpt ${mark} ${state} ${c.dim}· ${g.file}${c.reset}`);
690
+ for (const p of g.problems)
691
+ lines.push(` ${c.red}✗ ${p}${c.reset}`);
692
+ }
493
693
  if (result.oauth) {
494
694
  // Names only: which variables are set, never their values (ADR 0114).
495
695
  const set = Object.entries(result.oauth.set).map(([name, on]) => `${name} ${on ? 'set' : 'unset'}`).join(' · ');
@@ -498,6 +698,16 @@ export function renderDoctor(result, opts = {}) {
498
698
  for (const p of result.oauth.problems)
499
699
  lines.push(` ${c.red}✗ ${p}${c.reset}`);
500
700
  }
701
+ if (result.credentials) {
702
+ // Names only: which variables are sent, never their values (ADR 0122).
703
+ const cr = result.credentials;
704
+ const mark = cr.problems.length ? `${c.red}✗${c.reset}` : cr.warning ? `${c.yellow}⚠${c.reset}` : `${c.green}✓${c.reset}`;
705
+ lines.push(`credentials ${mark} ${c.dim}${CREDENTIAL_HOSTS_ENV} ${cr.set ? 'set' : 'unset'}${cr.envs.length ? ` · sent: ${cr.envs.join(', ')}` : ''}${c.reset}`);
706
+ if (cr.warning)
707
+ lines.push(` ${c.yellow}⚠ ${cr.warning}${c.reset}`);
708
+ for (const p of cr.problems)
709
+ lines.push(` ${c.red}✗ ${p}${c.reset}`);
710
+ }
501
711
  lines.push('');
502
712
  // The table: one line per syndicate (name, verdict, run command), then
503
713
  // one indented line per agent.
@@ -3,6 +3,8 @@ export type { DispatchConfig } from './dispatch.ts';
3
3
  import type { SkillsConfig } from './tools/skillToolset.ts';
4
4
  import type { OpenApiConfig } from './tools/openapiTools.ts';
5
5
  import type { McpAuthConfig } from './tools/oauthTools.ts';
6
+ import type { McpTransportKind } from './tools/mcpTransports.ts';
7
+ import type { McpServerConfig } from './tools/mcpToolFactory.ts';
6
8
  import type { ContextConfig, ExampleConfig } from './compile.ts';
7
9
  export type { OpenApiConfig } from './tools/openapiTools.ts';
8
10
  export type { SkillsConfig } from './tools/skillToolset.ts';
@@ -129,6 +131,15 @@ export interface AgentYamlConfig {
129
131
  schema?: Record<string, unknown>;
130
132
  mime?: 'application/json' | 'text/plain';
131
133
  };
134
+ /**
135
+ * Which tools the model may call (v2, ADR 0117): `auto`, `none`,
136
+ * `required`, or `{ name }` for one tool. Replaces
137
+ * generateContentConfig.toolConfig.functionCallingConfig; folded into it
138
+ * by the loader (lib/agentDialect.ts toEngineAgent).
139
+ */
140
+ tool_choice?: 'auto' | 'none' | 'required' | {
141
+ name: string;
142
+ };
132
143
  /**
133
144
  * Per-provider instruction (v2), keyed by provider id (lib/models/providerMap.ts):
134
145
  * `instruction` replaces the agent's instruction, `instruction_append` is
@@ -144,6 +155,10 @@ export interface AgentYamlConfig {
144
155
  mcp_tools?: string[];
145
156
  /** The OAuth grant the MCP server takes (ADR 0112): each user's, or the server's own. */
146
157
  mcp_auth?: McpAuthConfig;
158
+ /** How mcp_server_url is reached: auto (the default), streamable_http or sse (ADR 0124). */
159
+ mcp_transport?: McpTransportKind;
160
+ /** Several MCP servers, each with its tools, grant and transport (ADR 0124). Not with mcp_server_url. */
161
+ mcp_servers?: McpServerConfig[];
147
162
  /** Answers when `model` fails provider-side, or its provider's circuit is open (ADR 0044). */
148
163
  fallback_model?: string;
149
164
  /**