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
@@ -340,6 +340,15 @@ export class ProjectedSessionService {
340
340
  * The event lands in the projected session the runtime holds and, through
341
341
  * the store, in the real one `get` read. A session `get` did not hand out
342
342
  * is the real one, and the store alone applies the event to it.
343
+ *
344
+ * A store may answer an append with events of its own: a durable run's
345
+ * store restoring a checkpoint (lib/runtime/native/checkpoint.ts, ADR 0113)
346
+ * answers the opening message with the run's stored events, the stored
347
+ * opening returned in place of the one given. The projected session then
348
+ * takes, in place of the event given, every event the real session gained,
349
+ * unprojected: they are the current turn, which is never projected. The
350
+ * agent loop thereby reads the restored steps, as it would on the store's
351
+ * own session.
343
352
  */
344
353
  async append(session, event) {
345
354
  if (event.partial)
@@ -347,7 +356,17 @@ export class ProjectedSessionService {
347
356
  const real = this.stored.get(this.key(session.appName, session.userId, session.id));
348
357
  if (!real || real === session)
349
358
  return this.own.append(session, event);
359
+ const before = real.events.length;
360
+ const at = session.events.length;
350
361
  applyEvent(session, event);
351
- return this.own.append(real, event);
362
+ const stored = await this.own.append(real, event);
363
+ if (stored.id !== event.id) {
364
+ // The store answered with its own events: they replace the one applied above.
365
+ if (session.events[at]?.id === event.id)
366
+ session.events.splice(at, 1);
367
+ for (const gained of real.events.slice(before))
368
+ applyEvent(session, gained);
369
+ }
370
+ return stored;
352
371
  }
353
372
  }
@@ -48,3 +48,21 @@ export interface InstallResult {
48
48
  export declare function destinationsFor(opts: InstallOptions): string[];
49
49
  /** Copy the suite into every destination. Returns one result per destination. */
50
50
  export declare function installSkills(opts?: InstallOptions): InstallResult[];
51
+ /** The markers that delimit the block this installer owns inside AGENTS.md. */
52
+ export declare const AGENTS_MD_BEGIN = "<!-- melchizedek-skills:begin -->";
53
+ export declare const AGENTS_MD_END = "<!-- melchizedek-skills:end -->";
54
+ /** The pointer: where the skills are, and the onboarding entry point. */
55
+ export declare function agentsMdBlock(destinations: string[], projectRoot: string): string;
56
+ export interface AgentsMdResult {
57
+ path: string;
58
+ status: 'created' | 'appended' | 'updated' | 'unchanged';
59
+ }
60
+ /**
61
+ * Write the pointer into `<projectRoot>/AGENTS.md`, the instructions file
62
+ * Codex and other agents read: create the file when absent, append the block
63
+ * when the file has none, or replace only the block between the markers.
64
+ * Nothing outside the markers is ever changed.
65
+ */
66
+ export declare function writeAgentsMdPointer(projectRoot: string, destinations: string[], opts?: {
67
+ dryRun?: boolean;
68
+ }): AgentsMdResult;
@@ -15,7 +15,7 @@
15
15
  * A SKILL.md's frontmatter is read by the skills harness's own parser
16
16
  * (lib/tools/skills/frontmatter.ts), the one an agent's `skills:` uses.
17
17
  */
18
- import { closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
18
+ import { closeSync, constants as fsConstants, existsSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, readSync, readdirSync, readFileSync, statSync, writeFileSync, writeSync } from 'node:fs';
19
19
  import { homedir } from 'node:os';
20
20
  import { dirname, join, relative, resolve } from 'node:path';
21
21
  import { fileURLToPath } from 'node:url';
@@ -162,3 +162,75 @@ export function installSkills(opts = {}) {
162
162
  }
163
163
  return results;
164
164
  }
165
+ // ── The AGENTS.md pointer ────────────────────────────────────────────────────
166
+ /** The markers that delimit the block this installer owns inside AGENTS.md. */
167
+ export const AGENTS_MD_BEGIN = '<!-- melchizedek-skills:begin -->';
168
+ export const AGENTS_MD_END = '<!-- melchizedek-skills:end -->';
169
+ /** The pointer: where the skills are, and the onboarding entry point. */
170
+ export function agentsMdBlock(destinations, projectRoot) {
171
+ const where = destinations.map((d) => `\`${relative(projectRoot, d) || '.'}/\``).join(' and ');
172
+ return [
173
+ AGENTS_MD_BEGIN,
174
+ '## Melchizedek agent skills',
175
+ '',
176
+ `The melchizedek-agents skills are installed in ${where}, one directory per skill with a SKILL.md.`,
177
+ 'To onboard someone, start with `melchizedek-onboard/SKILL.md`, or run `npx melchizedek-setup --auto`.',
178
+ 'Never ask for an API key in chat and never print a value from `.env`: the person types keys into `.env` themselves.',
179
+ AGENTS_MD_END,
180
+ ].join('\n');
181
+ }
182
+ /**
183
+ * Write the pointer into `<projectRoot>/AGENTS.md`, the instructions file
184
+ * Codex and other agents read: create the file when absent, append the block
185
+ * when the file has none, or replace only the block between the markers.
186
+ * Nothing outside the markers is ever changed.
187
+ */
188
+ export function writeAgentsMdPointer(projectRoot, destinations, opts = {}) {
189
+ const path = join(resolve(projectRoot), 'AGENTS.md');
190
+ const block = agentsMdBlock(destinations, resolve(projectRoot));
191
+ const plan = (text) => {
192
+ if (text === undefined)
193
+ return { status: 'created', next: `# AGENTS.md\n\n${block}\n` };
194
+ const start = text.indexOf(AGENTS_MD_BEGIN);
195
+ const end = text.indexOf(AGENTS_MD_END);
196
+ if (start !== -1 && end > start) {
197
+ const next = text.slice(0, start) + block + text.slice(end + AGENTS_MD_END.length);
198
+ return { status: next === text ? 'unchanged' : 'updated', next };
199
+ }
200
+ return { status: 'appended', next: `${text}${text.endsWith('\n') ? '' : '\n'}\n${block}\n` };
201
+ };
202
+ if (opts.dryRun) {
203
+ // Nothing is written, so a path check is enough to report what would happen.
204
+ if (existsSync(path) && lstatSync(path).isSymbolicLink())
205
+ throw new Error(`refusing to write through a symlink: ${path}`);
206
+ return { path, status: plan(existsSync(path) ? readFileSync(path, 'utf-8') : undefined).status };
207
+ }
208
+ // One handle for the read and the write, so the file read is the file written:
209
+ // created if absent, never followed through a symlink (O_NOFOLLOW).
210
+ let fd;
211
+ try {
212
+ fd = openSync(path, fsConstants.O_RDWR | fsConstants.O_CREAT | (fsConstants.O_NOFOLLOW ?? 0), 0o644);
213
+ }
214
+ catch (err) {
215
+ if (err.code === 'ELOOP')
216
+ throw new Error(`refusing to write through a symlink: ${path}`);
217
+ throw err;
218
+ }
219
+ let status;
220
+ try {
221
+ const stat = fstatSync(fd);
222
+ if (!stat.isFile())
223
+ throw new Error(`not a regular file: ${path}`);
224
+ const existing = stat.size > 0 ? readFileSync(fd, 'utf-8') : undefined;
225
+ const result = plan(existing);
226
+ status = result.status;
227
+ if (status !== 'unchanged') {
228
+ ftruncateSync(fd, 0);
229
+ writeSync(fd, result.next, 0);
230
+ }
231
+ }
232
+ finally {
233
+ closeSync(fd);
234
+ }
235
+ return { path, status };
236
+ }
@@ -21,7 +21,7 @@ import type { CredentialStoreOptions } from '../../tools/credentialStore.ts';
21
21
  import type { CredentialStore } from '../../tools/auth.ts';
22
22
  import type { AuditSink } from '../../observability/audit.ts';
23
23
  import type { RlsHardeningStatus } from '../rlsStatus.ts';
24
- import type { TaskBackend } from '../../tools/taskTools.ts';
24
+ import type { TaskBackend, TaskBackendOptions } from '../../tools/taskTools.ts';
25
25
  import type { TurnLock } from '../../a2a/turnLock.ts';
26
26
  import type { PostgresTaskStoreOptions } from './taskStore.ts';
27
27
  export { PostgresSessionService } from './sessionService.ts';
@@ -73,6 +73,11 @@ export interface PostgresStorageOptions {
73
73
  * go to this storage's audit trail.
74
74
  */
75
75
  credentials?: Omit<CredentialStoreOptions, 'rows' | 'audit'>;
76
+ /**
77
+ * The job queue's checkpoints (ADR 0113): the largest one stored, in bytes
78
+ * of JSON (default 5 MiB), and where a skipped save is logged.
79
+ */
80
+ taskQueue?: TaskBackendOptions;
76
81
  }
77
82
  export interface PostgresStorage {
78
83
  pool: Pool;
@@ -120,7 +120,7 @@ export function postgresStorage(options) {
120
120
  reap: () => reapExpiredTasks(pool),
121
121
  cancelRequested: () => cancelRequestedTasks(pool, lease),
122
122
  },
123
- taskQueue: postgresTaskBackend(pool),
123
+ taskQueue: postgresTaskBackend(pool, options.taskQueue),
124
124
  async schemaVersion() {
125
125
  try {
126
126
  const r = await pool.query('SELECT max(version) AS v FROM melchizedek_schema_version');
@@ -27,8 +27,12 @@
27
27
  * survives while the job is running or queued: recover's requeue keeps it,
28
28
  * so the next claim resumes from it, and claimNext leaves it; a mutate that
29
29
  * moves the job to any other status, or recover failing it, clears it; a
30
- * deleted row takes it along.
30
+ * deleted row takes it along. A checkpoint whose JSON is above the cap
31
+ * (TaskBackendOptions.checkpointMaxBytes, default 5 MiB) is not written:
32
+ * the column keeps the previous one, the save still reports the claim, and
33
+ * one line names the job id and the sizes.
31
34
  */
32
35
  import type { Pool } from 'pg';
33
- import type { TaskBackend } from '../../tools/taskTools.ts';
34
- export declare function postgresTaskBackend(pool: Pool): TaskBackend;
36
+ import type { TaskBackend, TaskBackendOptions } from '../../tools/taskTools.ts';
37
+ /** The task backend on `pool`; `options` caps the checkpoint a save stores (TaskBackendOptions). */
38
+ export declare function postgresTaskBackend(pool: Pool, options?: TaskBackendOptions): TaskBackend;
@@ -1,4 +1,4 @@
1
- import { applyFinish, applyInterrupted } from '../../tools/taskTools.js';
1
+ import { applyFinish, applyInterrupted, checkpointJson } from '../../tools/taskTools.js';
2
2
  const seqOf = (id) => Number(id.slice(1)) || 0;
3
3
  async function loadOwner(db, owner, lock) {
4
4
  const head = await db.query(`SELECT next_id FROM melchizedek_task_owners WHERE owner = $1${lock ? ' FOR UPDATE' : ''}`, [owner]);
@@ -21,7 +21,8 @@ async function inTransaction(pool, run) {
21
21
  client.release();
22
22
  }
23
23
  }
24
- export function postgresTaskBackend(pool) {
24
+ /** The task backend on `pool`; `options` caps the checkpoint a save stores (TaskBackendOptions). */
25
+ export function postgresTaskBackend(pool, options = {}) {
25
26
  return {
26
27
  read: (owner) => loadOwner(pool, owner, false),
27
28
  async mutate(owner, change) {
@@ -82,8 +83,14 @@ export function postgresTaskBackend(pool) {
82
83
  return (r.rowCount ?? 0) > 0;
83
84
  },
84
85
  async saveCheckpoint(worker, job, checkpoint) {
86
+ const json = checkpointJson(job, checkpoint, options);
87
+ if (json === null) {
88
+ // Over the cap: the previous checkpoint is kept; the answer is still whether the claim holds.
89
+ const held = await pool.query(`SELECT 1 FROM melchizedek_tasks WHERE owner = $1 AND id = $2 AND lease_owner = $3 AND status = 'running'`, [job.owner, job.id, worker.workerId]);
90
+ return (held.rowCount ?? 0) > 0;
91
+ }
85
92
  const r = await pool.query(`UPDATE melchizedek_tasks SET checkpoint = $4::jsonb, checkpoint_at = NOW()
86
- WHERE owner = $1 AND id = $2 AND lease_owner = $3 AND status = 'running'`, [job.owner, job.id, worker.workerId, JSON.stringify(checkpoint)]);
93
+ WHERE owner = $1 AND id = $2 AND lease_owner = $3 AND status = 'running'`, [job.owner, job.id, worker.workerId, json]);
87
94
  return (r.rowCount ?? 0) > 0;
88
95
  },
89
96
  async loadCheckpoint(job) {
@@ -24,8 +24,9 @@
24
24
  import { z } from 'zod';
25
25
  import type { SyndicateYamlConfig } from './loadSyndicate.ts';
26
26
  export declare const MEMORY_SYSTEMS: readonly ['internal-only', 'session-only', 'long-term'];
27
- export declare const REASONING_LEVELS: readonly ['none', 'low', 'medium', 'high'];
27
+ export declare const REASONING_LEVELS: readonly ['none', 'low', 'medium', 'high', 'xhigh', 'max'];
28
28
  export declare const OUTPUT_MIMES: readonly ['application/json', 'text/plain'];
29
+ export declare const TOOL_CHOICE_MODES: readonly ['auto', 'none', 'required'];
29
30
  export declare const SKILL_SCRIPT_MODES: readonly ['none', 'local'];
30
31
  export declare const agentSchema: z.ZodObject<{
31
32
  name: z.ZodString;
@@ -67,8 +68,10 @@ export declare const agentSchema: z.ZodObject<{
67
68
  reasoning: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
68
69
  high: "high";
69
70
  low: "low";
71
+ max: "max";
70
72
  medium: "medium";
71
73
  none: "none";
74
+ xhigh: "xhigh";
72
75
  }>, z.ZodObject<{
73
76
  budget_tokens: z.ZodNumber;
74
77
  }, z.core.$strict>]>>;
@@ -85,6 +88,13 @@ export declare const agentSchema: z.ZodObject<{
85
88
  "text/plain": "text/plain";
86
89
  }>>;
87
90
  }, z.core.$strict>>;
91
+ tool_choice: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
92
+ auto: "auto";
93
+ none: "none";
94
+ required: "required";
95
+ }>, z.ZodObject<{
96
+ name: z.ZodString;
97
+ }, z.core.$strict>]>>;
88
98
  model_overrides: z.ZodOptional<z.ZodObject<{
89
99
  gemini: z.ZodOptional<z.ZodObject<{
90
100
  instruction: z.ZodOptional<z.ZodString>;
@@ -233,8 +243,10 @@ export declare const subagentSchema: z.ZodObject<{
233
243
  reasoning: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
234
244
  high: "high";
235
245
  low: "low";
246
+ max: "max";
236
247
  medium: "medium";
237
248
  none: "none";
249
+ xhigh: "xhigh";
238
250
  }>, z.ZodObject<{
239
251
  budget_tokens: z.ZodNumber;
240
252
  }, z.core.$strict>]>>;
@@ -251,6 +263,13 @@ export declare const subagentSchema: z.ZodObject<{
251
263
  "text/plain": "text/plain";
252
264
  }>>;
253
265
  }, z.core.$strict>>;
266
+ tool_choice: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
267
+ auto: "auto";
268
+ none: "none";
269
+ required: "required";
270
+ }>, z.ZodObject<{
271
+ name: z.ZodString;
272
+ }, z.core.$strict>]>>;
254
273
  model_overrides: z.ZodOptional<z.ZodObject<{
255
274
  gemini: z.ZodOptional<z.ZodObject<{
256
275
  instruction: z.ZodOptional<z.ZodString>;
@@ -403,8 +422,10 @@ export declare const syndicateSchema: z.ZodObject<{
403
422
  reasoning: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
404
423
  high: "high";
405
424
  low: "low";
425
+ max: "max";
406
426
  medium: "medium";
407
427
  none: "none";
428
+ xhigh: "xhigh";
408
429
  }>, z.ZodObject<{
409
430
  budget_tokens: z.ZodNumber;
410
431
  }, z.core.$strict>]>>;
@@ -421,6 +442,13 @@ export declare const syndicateSchema: z.ZodObject<{
421
442
  "text/plain": "text/plain";
422
443
  }>>;
423
444
  }, z.core.$strict>>;
445
+ tool_choice: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
446
+ auto: "auto";
447
+ none: "none";
448
+ required: "required";
449
+ }>, z.ZodObject<{
450
+ name: z.ZodString;
451
+ }, z.core.$strict>]>>;
424
452
  model_overrides: z.ZodOptional<z.ZodObject<{
425
453
  gemini: z.ZodOptional<z.ZodObject<{
426
454
  instruction: z.ZodOptional<z.ZodString>;
@@ -563,8 +591,10 @@ export declare const syndicateSchema: z.ZodObject<{
563
591
  reasoning: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
564
592
  high: "high";
565
593
  low: "low";
594
+ max: "max";
566
595
  medium: "medium";
567
596
  none: "none";
597
+ xhigh: "xhigh";
568
598
  }>, z.ZodObject<{
569
599
  budget_tokens: z.ZodNumber;
570
600
  }, z.core.$strict>]>>;
@@ -581,6 +611,13 @@ export declare const syndicateSchema: z.ZodObject<{
581
611
  "text/plain": "text/plain";
582
612
  }>>;
583
613
  }, z.core.$strict>>;
614
+ tool_choice: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
615
+ auto: "auto";
616
+ none: "none";
617
+ required: "required";
618
+ }>, z.ZodObject<{
619
+ name: z.ZodString;
620
+ }, z.core.$strict>]>>;
584
621
  model_overrides: z.ZodOptional<z.ZodObject<{
585
622
  gemini: z.ZodOptional<z.ZodObject<{
586
623
  instruction: z.ZodOptional<z.ZodString>;
@@ -45,7 +45,7 @@ const thinkingConfig = z
45
45
  includeThoughts: z.boolean().optional().describe('Stream the thinking trace in the response.'),
46
46
  })
47
47
  .describe('Older spelling of `reasoning` for Gemini and Claude (thinkingLevel or thinkingBudget). Cannot be combined with `reasoning`.');
48
- export const REASONING_LEVELS = ['none', 'low', 'medium', 'high'];
48
+ export const REASONING_LEVELS = ['none', 'low', 'medium', 'high', 'xhigh', 'max'];
49
49
  /**
50
50
  * The provider-neutral reasoning key (ADR 0047): a level, or a token budget.
51
51
  * The compiler maps it per provider (lib/compile.ts reasoningConfig).
@@ -62,7 +62,7 @@ const reasoningSchema = z
62
62
  return `must be one of ${REASONING_LEVELS.join(' | ')}, or { budget_tokens: <integer ≥ 0> } (got ${describeValue(iss.input)}${hint ? ` — did you mean "${hint}"?` : ''})`;
63
63
  },
64
64
  })
65
- .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).');
66
66
  /**
67
67
  * Loose on purpose: provider-specific fields (e.g. `toolConfig`) pass through
68
68
  * to the SDK. Only the documented fields are typed, so `temperature: hot`
@@ -89,7 +89,7 @@ const generateContentConfig = z
89
89
  .optional()
90
90
  .describe('Older spelling of `reasoning` for the chat-completions and Responses providers. Cannot be combined with `reasoning`.'),
91
91
  })
92
- .describe('Deprecated v1 spelling (ADR 0115): write `sampling` (temperature, top_p, max_output_tokens, stop), `output.mime` (responseMimeType) and `reasoning` (thinkingConfig, reasoningEffort) instead; npx melchizedek-codemod rewrites a file. Still read: the Gemini GenerateContentConfig shape the engine takes. 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
93
  .meta({ deprecated: true });
94
94
  // ── YAML v2 agent keys (ADR 0115) ────────────────────────────────────────────
95
95
  /** `sampling` and `output` must say something: an empty block is a leftover. */
@@ -116,6 +116,20 @@ const outputSchemaV2 = z
116
116
  })
117
117
  .refine(nonEmpty, { message: 'must set schema or mime', when: quiet })
118
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.');
119
133
  const modelOverrideEntry = z
120
134
  .strictObject({
121
135
  instruction: z.string().min(1).optional().describe('Replaces the agent\'s instruction on this provider.'),
@@ -302,6 +316,7 @@ const agentFields = {
302
316
  reasoning: reasoningSchema.optional(),
303
317
  sampling: samplingSchema.optional(),
304
318
  output: outputSchemaV2.optional(),
319
+ tool_choice: toolChoiceSchema.optional(),
305
320
  model_overrides: modelOverridesSchema.optional(),
306
321
  outputSchema: z
307
322
  .record(z.string(), z.unknown())
@@ -374,7 +389,7 @@ export const subagentSchema = z
374
389
  .string()
375
390
  .min(1)
376
391
  .optional()
377
- .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 classifier (its routes never run nested), and a workflow\'s ask_user nodes and gates when it is delegated to as a 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).'),
378
393
  a2a_agent_url: z
379
394
  .string()
380
395
  .min(1)
@@ -706,17 +721,11 @@ function crossFieldProblems(raw) {
706
721
  });
707
722
  };
708
723
  // A skill script run pauses for approval the same way, so it is allowed in
709
- // the same places; `skills.tools` may only unlock tools the agent does not
710
- // already carry outright.
711
- 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) => {
712
727
  if (!isObj(agent.skills))
713
728
  return;
714
- if (agent.skills.scripts === 'local' && !allowed) {
715
- out.push({
716
- path: [...path, 'skills', 'scripts'],
717
- message: 'skill scripts on a delegated subagent are not supported yet; run them on the orchestrator or a plan-dispatch route (ADR 0110)',
718
- });
719
- }
720
729
  const tools = Array.isArray(agent.tools) ? agent.tools : [];
721
730
  (Array.isArray(agent.skills.tools) ? agent.skills.tools : []).forEach((name, j) => {
722
731
  if (typeof name === 'string' && tools.includes(name)) {
@@ -823,19 +832,17 @@ function crossFieldProblems(raw) {
823
832
  v2Problems(sub, ['subagents', i]);
824
833
  if (isObj(raw.orchestrator)) {
825
834
  gateProblems(raw.orchestrator, ['orchestrator'], true);
826
- skillProblems(raw.orchestrator, ['orchestrator'], true);
835
+ skillProblems(raw.orchestrator, ['orchestrator']);
827
836
  questionProblems(raw.orchestrator, ['orchestrator'], true);
828
837
  }
829
- const dispatching = isObj(raw.dispatch);
830
838
  // A workflow's subagents are its nodes, not delegated tools: a gated call pauses its node, and
831
839
  // the walk resumes it (ADR 0098); a skill script run pauses on the same approval (ADR 0106). A map item cannot (workflowProblems).
832
- const pausing = dispatching || isObj(raw.workflow);
833
840
  subs.forEach((sub, i) => {
834
841
  if (!isObj(sub))
835
842
  return;
836
- // A delegated subagent's gate and question pause the turn through the open call (ADR 0110); its skill scripts stay refused.
843
+ // A delegated subagent's gate, question and skill script run pause the turn through the open call (ADR 0110, ADR 0118).
837
844
  gateProblems(sub, ['subagents', i], true);
838
- skillProblems(sub, ['subagents', i], pausing);
845
+ skillProblems(sub, ['subagents', i]);
839
846
  questionProblems(sub, ['subagents', i], true);
840
847
  const hasRef = typeof sub.yaml_reference === 'string';
841
848
  const hasRemote = typeof sub.a2a_agent_url === 'string';
@@ -93,14 +93,26 @@ export interface CredentialStore {
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
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;
@@ -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.`
@@ -53,11 +57,15 @@ export class ToolCredentialError extends Error {
53
57
  : code === 'grant_failed'
54
58
  ? `The server's own ${p} authorization could not be obtained from the provider's token endpoint.`
55
59
  : code === 'host_refused'
56
- ? `The ${p} authorization may not be sent to this host: the operator's OAuth host allowlist does not include it.`
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.`
57
63
  : `Invalid credential request for ${p}.`);
58
64
  this.name = 'ToolCredentialError';
59
65
  this.code = code;
60
66
  this.provider = provider;
67
+ if (code === 'host_refused')
68
+ this.allowlist = allowlist ?? 'oauth';
61
69
  }
62
70
  }
63
71
  /** A provider name: lowercase letters, digits, and . _ - inside; at most 64 characters. */
@@ -0,0 +1,88 @@
1
+ /**
2
+ * lib/tools/credentialHosts.ts — the operator's credential host allowlist:
3
+ * which hosts the value of each static credential variable may be sent to
4
+ * (ADR 0122).
5
+ *
6
+ * WHY this file exists:
7
+ * A syndicate YAML (possibly registry-stored) names a credential variable
8
+ * and, beside it, the host its value goes to: an OpenAPI `auth` sends
9
+ * `bearer_env` or `api_key.env` to the spec's server (or `base_url`), and
10
+ * an `oauth2` block sends `client_secret_env` to its `token_url`.
11
+ * credentialEnvProblem (lib/tools/credentialEnv.ts) keeps the framework's
12
+ * own secrets out of reach, and the OAuth host allowlist (ADR 0114) binds
13
+ * a provider's tokens, but nothing bound a static secret to its API: a
14
+ * YAML that names TRACKER_TOKEN could aim it at a host of its own. The
15
+ * binding is the operator's, never the YAML's: an environment variable or
16
+ * `createA2AApp({ credentialHosts })`.
17
+ *
18
+ * THE RULE (ADR 0122):
19
+ * - With no allowlist configured, nothing changes: a credential goes to
20
+ * the host the YAML names, as before. The doctor and the server binary
21
+ * name every credential variable a served YAML sends, as unbound.
22
+ * - With an allowlist configured, it is the whole list: every credential
23
+ * variable a YAML sends must be on it, and every host its value goes to
24
+ * must be one of that variable's hosts.
25
+ * - The check runs when a syndicate is loaded to be served, when its tools
26
+ * are compiled, and again before each call sends the value.
27
+ *
28
+ * WHAT IT COVERS: `bearer_env`, `api_key.env` and `client_secret_env` (both
29
+ * grants: the client-credentials token request, the consent step's code
30
+ * exchange and its refresh). Not `client_id_env`: a client id is not a
31
+ * secret. Not MCP_BEARER_TOKENS: it maps each token to its host already,
32
+ * and the operator writes both.
33
+ *
34
+ * THE FORMAT: `MELCHIZEDEK_CREDENTIAL_HOSTS="TRACKER_TOKEN=api.tracker.example.com;WEATHER_KEY=api.weather.example.com,*.weather.example.com"`.
35
+ * Entries are separated by `;` (or a newline), a variable from its hosts by
36
+ * `=`, hosts by `,`. A host is what the OAuth host allowlist takes
37
+ * (lib/tools/oauthHosts.ts): a hostname, `*.example.com`, IPv4 or
38
+ * `[IPv6]`, with no scheme, port or path.
39
+ *
40
+ * Nothing here reads or prints a credential's value: variable names and
41
+ * hosts only. No regular expression here can backtrack: the separators are
42
+ * single characters, and a variable name is one bounded character class.
43
+ */
44
+ /** The environment variable the allowlist is read from when no option sets it. */
45
+ export declare const CREDENTIAL_HOSTS_ENV = "MELCHIZEDEK_CREDENTIAL_HOSTS";
46
+ /** Credential variable name → the hosts its value may be sent to. */
47
+ export type CredentialHostAllowlist = Readonly<Record<string, readonly string[]>>;
48
+ /**
49
+ * The allowlist, checked and normalized (hosts lowercased, duplicates
50
+ * dropped). Throws, naming the variable and the host, on anything malformed.
51
+ */
52
+ export declare function checkCredentialHosts(allowlist: Record<string, readonly string[]>, source?: string): Record<string, string[]>;
53
+ /** The allowlist from its text form (MELCHIZEDEK_CREDENTIAL_HOSTS). Throws on a malformed entry. */
54
+ export declare function parseCredentialHosts(text: string): Record<string, string[]>;
55
+ /**
56
+ * Sets this process's allowlist (createA2AApp's `credentialHosts` does),
57
+ * which wins over the environment variable. `undefined` returns to the
58
+ * variable.
59
+ */
60
+ export declare function setCredentialHosts(allowlist: Record<string, readonly string[]> | undefined): void;
61
+ /**
62
+ * The allowlist in force: the one set by `setCredentialHosts`, else the
63
+ * environment variable's, else undefined (not configured). A malformed
64
+ * variable throws, so the first check refuses rather than allowing.
65
+ */
66
+ export declare function credentialHosts(env?: NodeJS.ProcessEnv): CredentialHostAllowlist | undefined;
67
+ /**
68
+ * Why the value of credential variable `name` may not be sent to `url`, or
69
+ * null. `role` names the URL in the message (`server`, `token_url`).
70
+ * `allowlist` defaults to the one in force; `null` means none is
71
+ * configured, which allows (the behaviour before ADR 0122).
72
+ */
73
+ export declare function credentialHostProblem(name: string, url: string, role?: string, allowlist?: CredentialHostAllowlist | null | undefined): string | null;
74
+ /**
75
+ * The call-time check: why the value of `name` may not be sent to
76
+ * `destination` now, or null. A malformed allowlist refuses.
77
+ */
78
+ export declare function credentialCallProblem(name: string, destination: string): string | null;
79
+ /**
80
+ * Marks an OAuth client config (lib/tools/oauthConsent.ts) with the
81
+ * variable its client secret came from, so the consent step's code exchange
82
+ * and the refresh hook can check the token endpoint at call time. Set by
83
+ * oauthClientsFor (lib/tools/oauthTools.ts); a client built by hand carries
84
+ * none and is not checked.
85
+ */
86
+ export declare const CLIENT_SECRET_ENV: unique symbol;
87
+ /** The variable a client config's secret came from, when oauthClientsFor marked it. */
88
+ export declare function clientSecretEnvOf(client: object): string | undefined;