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
@@ -41,16 +41,23 @@
41
41
  * A delegated `yaml_reference` to a workflow syndicate is the whole graph,
42
42
  * walked under the subagent entry's name and description on a child
43
43
  * session (lib/compileNative.ts workflowSubagentOf); its answer is the
44
- * graph's last event's text. A nested workflow with an
45
- * `ask_user` node is refused by name (lib/compile.ts compileWorkflowSpec): a
46
- * pause inside a tool call cannot reach the caller (ADR 0028).
44
+ * graph's last event's text. A walk that pauses (an `ask_user` node, a
45
+ * gated agent node) leaves the call open, and the turn ends input-required
46
+ * with the agent path down to the node; the answer walks the graph again
47
+ * (lib/runtime/native/delegate.ts resumeWorkflowSubagent, ADR 0111).
47
48
  *
48
49
  * ── As a dispatch route or a workflow node (ADR 0106) ────────────────────
49
50
  * A `yaml_reference` to a workflow syndicate runs its whole graph there too,
50
- * on the child session filed under the entry's name, as a subagent does. A
51
- * route's answer is what the graph would answer as its own syndicate
52
- * (lib/runtime/syndicateTurn.ts); a node's is the nested walk's
53
- * (lib/workflow/turn.ts). A map over one is refused (lib/compile.ts).
51
+ * on the child session filed under the agent path and its kind
52
+ * (`<app>/route:<entry>`, `<walk's app>/node:<entry>`, ADR 0119, ADR 0120),
53
+ * as a subagent does. A route's answer is what the graph would
54
+ * answer as its own syndicate (lib/runtime/syndicateTurn.ts); a node's is
55
+ * the nested walk's (lib/workflow/turn.ts). A map over one is refused
56
+ * (lib/compile.ts). Its pauses (an `ask_user` node, a gate) reach the turn
57
+ * with the path from the entry down to the node that asked: a route's
58
+ * through the route's pause record in the conversation, a node's through
59
+ * its own pause on the caller's walk; the answer walks the nested graph
60
+ * again (ADR 0119).
54
61
  *
55
62
  * ── Approval gates (ADR 0098, ADR 0106) ──────────────────────────────────
56
63
  * A tool in a node agent's `require_approval`, and a skill script
@@ -80,6 +80,12 @@ export interface PendingInput {
80
80
  payload?: unknown;
81
81
  /** JSON Schema a structured answer must satisfy, when the node declared one. */
82
82
  schema?: unknown;
83
+ /**
84
+ * For a question asked inside a delegated subagent (ADR 0110): the agents
85
+ * from the turn's own agent down to the one that asked, `node` last.
86
+ * Absent when the turn's own agent (or a workflow node) asked.
87
+ */
88
+ path?: string[];
83
89
  }
84
90
  /** ADK's function-call name for a workflow input request. */
85
91
  export declare const INPUT_REQUEST = "adk_request_input";
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import type { Server } from 'node:http';
3
3
  import { createA2AApp } from '../lib/a2a/app.ts';
4
+ import type { SyndicateYamlConfig } from '../lib/loadSyndicate.ts';
5
+ import type { OAuthServerSetup } from '../lib/a2a/oauthSetup.ts';
6
+ import type { AuditSink } from '../lib/observability/audit.ts';
7
+ import type { CredentialRows } from '../lib/tools/credentialStore.ts';
4
8
  export { describeTurnError } from '../lib/a2a/executor.ts';
5
9
  /**
6
10
  * What a deployment adds on top of the environment-driven server: its own
@@ -15,4 +19,36 @@ export interface ServerExtensions {
15
19
  /** Any other createA2AApp option; wins over what the environment sets. */
16
20
  options?: Partial<Parameters<typeof createA2AApp>[0]>;
17
21
  }
22
+ /**
23
+ * The syndicates this server serves from FILES: the default (unless it is a
24
+ * registry row), each served id that is not a registry id, and, when every
25
+ * file is served (A2A_SERVED_AGENTS unset or *), every syndicate at the
26
+ * agents directory's root. The consent step's OAuth clients come from these
27
+ * only (ADR 0114): a registry row may name a provider they declare, never
28
+ * define one. A file that does not load is skipped with a warning (its route
29
+ * fails on its own).
30
+ */
31
+ export declare function servedFileSyndicates(syndicateName: string, servedAgents: string[] | undefined, registryAgents: string[] | undefined, warn?: (m: string) => void): SyndicateYamlConfig[];
32
+ /** The server's tool credentials, from the environment: what createA2AApp takes, and what to print. */
33
+ export interface ServerOAuth extends OAuthServerSetup {
34
+ toolCredentials?: OAuthServerSetup['toolCredentials'] & {
35
+ requireCallerIdentity?: boolean;
36
+ };
37
+ }
38
+ /**
39
+ * Tool credentials and the consent step for the served syndicates, from
40
+ * MELCHIZEDEK_CREDENTIAL_KEY, OAUTH_REDIRECT_URI, OAUTH_CALLBACK_IDENTITY,
41
+ * MELCHIZEDEK_OAUTH_HOSTS and MELCHIZEDEK_CREDENTIAL_HOSTS (lib/a2a/oauthSetup.ts). Throws, naming
42
+ * variables only, on a configuration that is unsafe or cannot work.
43
+ */
44
+ export declare function serverOAuth(options: {
45
+ syndicateName: string;
46
+ servedAgents?: string[];
47
+ registryAgents?: string[];
48
+ rows?: CredentialRows;
49
+ audit?: AuditSink;
50
+ env?: NodeJS.ProcessEnv;
51
+ fetch?: typeof fetch;
52
+ warn?: (m: string) => void;
53
+ }): ServerOAuth;
18
54
  export declare function startServer(syndicateName?: string, extensions?: ServerExtensions): Promise<Server>;
@@ -75,9 +75,29 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
75
75
  * running turn (default 20). Behind a session-mode
76
76
  * pooler (Supabase :5432) keep the two sums under its
77
77
  * client limit, per instance.
78
+ * MELCHIZEDEK_CREDENTIAL_KEY 32 random bytes (base64 or hex) that seal each user's
79
+ * third-party OAuth tokens (ADR 0072). Unset = no tool
80
+ * credentials. Rows live in Postgres with DATABASE_URL,
81
+ * else in process memory
82
+ * OAUTH_REDIRECT_URI the consent callback, exactly as registered at every
83
+ * provider (https://agents.example.com/oauth/callback);
84
+ * mounts the callback and lets a run ask a user to
85
+ * connect (ADR 0085). Needs MELCHIZEDEK_CREDENTIAL_KEY
86
+ * OAUTH_CALLBACK_IDENTITY required (default): the callback's browser must carry the
87
+ * flow's user's identity (A2A_AUTH=header behind a gateway);
88
+ * state: the single-use state nonce alone binds it (ADR 0114)
89
+ * MELCHIZEDEK_OAUTH_HOSTS provider=host,host;…: the hosts each provider's tokens may
90
+ * be sent to. Unset: authorization_code grants are refused,
91
+ * client_credentials allowed (ADR 0114)
92
+ * MELCHIZEDEK_CREDENTIAL_HOSTS VARIABLE=host,host;…: the hosts each static credential
93
+ * variable a YAML sends (bearer_env, api_key.env,
94
+ * client_secret_env) may be sent to. Set, it is the whole
95
+ * list. Unset: sent where the YAML says, and the boot names
96
+ * each unbound variable (ADR 0122)
78
97
  */
79
98
  import { randomBytes } from 'node:crypto';
80
- import { realpathSync, writeFileSync } from 'node:fs';
99
+ import { readdirSync, realpathSync, writeFileSync } from 'node:fs';
100
+ import path from 'node:path';
81
101
  import { pathToFileURL } from 'node:url';
82
102
  import { createA2AApp } from '../lib/a2a/app.js';
83
103
  import { callerTokens, firstOf, hashCallerToken, jwtIdentity, parseCallers, sharedSecret, trustedHeader, } from '../lib/a2a/identity.js';
@@ -90,6 +110,11 @@ import { postgresStorage } from '../lib/storage/postgres/index.js';
90
110
  import { isPlaceholderValue, loadEnv } from '../lib/loadEnv.js';
91
111
  import { flushTracing } from '../lib/observability/tracer.js';
92
112
  import { setLogLevel } from '../lib/runtime/logging.js';
113
+ import { loadSyndicate } from '../lib/loadSyndicate.js';
114
+ import { callbackIdentity, callbackIdentityProblem, oauthServerSetup } from '../lib/a2a/oauthSetup.js';
115
+ import { postgresCredentialRows } from '../lib/storage/postgres/credentialStore.js';
116
+ import { setOAuthHosts } from '../lib/tools/oauthHosts.js';
117
+ import { setCredentialHosts } from '../lib/tools/credentialHosts.js';
93
118
  // Re-exported for existing importers (tests/failNarration.test.ts).
94
119
  export { describeTurnError } from '../lib/a2a/executor.js';
95
120
  /** Minimum bearer-secret length below which the server warns. */
@@ -132,6 +157,87 @@ function fatal(message) {
132
157
  emit('error', `[A2A] ✗ FATAL: ${message}`);
133
158
  process.exit(1);
134
159
  }
160
+ /**
161
+ * The syndicates this server serves from FILES: the default (unless it is a
162
+ * registry row), each served id that is not a registry id, and, when every
163
+ * file is served (A2A_SERVED_AGENTS unset or *), every syndicate at the
164
+ * agents directory's root. The consent step's OAuth clients come from these
165
+ * only (ADR 0114): a registry row may name a provider they declare, never
166
+ * define one. A file that does not load is skipped with a warning (its route
167
+ * fails on its own).
168
+ */
169
+ export function servedFileSyndicates(syndicateName, servedAgents, registryAgents, warn = () => { }) {
170
+ const registry = new Set(registryAgents ?? []);
171
+ const files = new Set();
172
+ const add = (id) => {
173
+ if (id.startsWith('registry:') || registry.has(id))
174
+ return;
175
+ files.add(id.endsWith('.yaml') || id.endsWith('.yml') ? id : `${id}.yaml`);
176
+ };
177
+ add(syndicateName);
178
+ if (servedAgents) {
179
+ for (const id of servedAgents)
180
+ add(id);
181
+ }
182
+ else {
183
+ const agentsDir = path.resolve(process.env.MELCHIZEDEK_AGENTS_DIR ?? path.join(process.cwd(), 'config', 'agents'));
184
+ let names = [];
185
+ try {
186
+ names = readdirSync(agentsDir);
187
+ }
188
+ catch {
189
+ names = [];
190
+ }
191
+ for (const f of names.sort())
192
+ if ((f.endsWith('.yaml') || f.endsWith('.yml')) && f !== 'syndicateSchema.yaml')
193
+ files.add(f);
194
+ }
195
+ const out = [];
196
+ for (const file of files) {
197
+ try {
198
+ out.push(loadSyndicate(file, { shippedFallback: servedAgents !== undefined || file === syndicateName }));
199
+ }
200
+ catch (err) {
201
+ if (err?.code !== 'ENOENT')
202
+ warn(`OAuth setup skipped ${file}: ${err instanceof Error ? err.message.split('\n')[0] : String(err)}`);
203
+ }
204
+ }
205
+ return out;
206
+ }
207
+ /**
208
+ * Tool credentials and the consent step for the served syndicates, from
209
+ * MELCHIZEDEK_CREDENTIAL_KEY, OAUTH_REDIRECT_URI, OAUTH_CALLBACK_IDENTITY,
210
+ * MELCHIZEDEK_OAUTH_HOSTS and MELCHIZEDEK_CREDENTIAL_HOSTS (lib/a2a/oauthSetup.ts). Throws, naming
211
+ * variables only, on a configuration that is unsafe or cannot work.
212
+ */
213
+ export function serverOAuth(options) {
214
+ const env = options.env ?? process.env;
215
+ const identity = callbackIdentity(env);
216
+ const setup = oauthServerSetup({
217
+ configs: servedFileSyndicates(options.syndicateName, options.servedAgents, options.registryAgents, options.warn),
218
+ load: (ref) => loadSyndicate(ref),
219
+ env,
220
+ ...(options.rows ? { rows: options.rows } : {}),
221
+ ...(options.audit ? { audit: options.audit } : {}),
222
+ allowPrivate: env.ALLOW_PRIVATE_OPENAPI === 'true' || env.ALLOW_PRIVATE_MCP === 'true',
223
+ ...(options.fetch ? { fetch: options.fetch } : {}),
224
+ });
225
+ if (!setup.toolCredentials?.consent)
226
+ return setup;
227
+ const warnings = [...setup.warnings];
228
+ const refused = callbackIdentityProblem(env);
229
+ if (identity === 'state') {
230
+ warnings.push('OAUTH_CALLBACK_IDENTITY=state: the single-use state alone binds a consent callback to its user, so a forwarded authorization link can connect the forwarder\'s account to the user who was asked.');
231
+ }
232
+ else if (refused) {
233
+ warnings.push(`${refused.charAt(0).toUpperCase()}${refused.slice(1)}.`);
234
+ }
235
+ return {
236
+ ...setup,
237
+ warnings,
238
+ toolCredentials: { ...setup.toolCredentials, requireCallerIdentity: identity === 'required' },
239
+ };
240
+ }
135
241
  export async function startServer(syndicateName = 'syndicate.yaml', extensions = {}) {
136
242
  loadEnv(import.meta.url);
137
243
  // Conversation content stays out of stdout unless the operator opts in:
@@ -319,6 +425,27 @@ export async function startServer(syndicateName = 'syndicate.yaml', extensions =
319
425
  if (metricsToken && (isPlaceholderValue(metricsToken) || metricsToken.length < 24)) {
320
426
  fatal('A2A_METRICS_TOKEN must be a long random value (openssl rand -hex 32).');
321
427
  }
428
+ // ── Tool credentials and the OAuth consent step (ADR 0072, 0085, 0114) ────
429
+ let oauth;
430
+ try {
431
+ // An extension's allowlist wins over the environment's here too, as createA2AApp applies it.
432
+ if (extensions.options?.oauthHosts)
433
+ setOAuthHosts(extensions.options.oauthHosts);
434
+ if (extensions.options?.credentialHosts)
435
+ setCredentialHosts(extensions.options.credentialHosts);
436
+ oauth = serverOAuth({
437
+ syndicateName,
438
+ servedAgents,
439
+ registryAgents,
440
+ ...(pgStorage ? { rows: postgresCredentialRows(pgStorage.pool), audit: pgStorage.audit } : {}),
441
+ warn: (m) => emit('warn', `[A2A] ⚠ ${m}`),
442
+ });
443
+ }
444
+ catch (err) {
445
+ fatal(err instanceof Error ? err.message : String(err));
446
+ }
447
+ for (const w of oauth.warnings)
448
+ emit('warn', `[A2A] ⚠ ${w}`);
322
449
  let built;
323
450
  try {
324
451
  built = await createA2AApp({
@@ -345,6 +472,7 @@ export async function startServer(syndicateName = 'syndicate.yaml', extensions =
345
472
  keyMode,
346
473
  ...(pgStorage ? { storage: pgStorage } : {}),
347
474
  ...(policy ? { policy } : {}),
475
+ ...(oauth.toolCredentials ? { toolCredentials: oauth.toolCredentials } : {}),
348
476
  metricsToken,
349
477
  log: (m) => emit('info', `[A2A] ${m}`),
350
478
  warn: (m) => emit('warn', `[A2A] ⚠ ${m}`),
@@ -388,6 +516,8 @@ export async function startServer(syndicateName = 'syndicate.yaml', extensions =
388
516
  }
389
517
  if (budgetLabel)
390
518
  emit('info', `[A2A] budgets ${budgetLabel}`);
519
+ emit('info', `[A2A] oauth ${oauth.summary}`);
520
+ emit('info', `[A2A] creds ${oauth.credentialSummary}`);
391
521
  if (metricsToken)
392
522
  emit('info', `[A2A] metrics ${base}/metrics (bearer A2A_METRICS_TOKEN)`);
393
523
  emit('info', `[A2A] health ${base}/healthz ${base}/readyz`);
@@ -21,6 +21,16 @@
21
21
  * instead of abandoning it while it keeps spending. Any syndicate and any
22
22
  * subagent in it can serve as the worker.
23
23
  *
24
+ * A run is durable (ADR 0113): runDurableTurn (lib/runtime/native/
25
+ * checkpoint.ts) checkpoints the run's sessions at every step boundary
26
+ * beside the job (TaskBackend.saveCheckpoint), and a job claimed again
27
+ * after its worker stopped resumes from its last checkpoint instead of
28
+ * starting over. SIGTERM aborts the step in flight and puts the job back
29
+ * in the queue with its checkpoint; the next worker finishes it. A job
30
+ * cancelled while it runs (task_update) loses its claim: the next lease
31
+ * renewal or checkpoint save sees it, and the run is aborted without
32
+ * writing a result over the cancellation.
33
+ *
24
34
  * Usage:
25
35
  * npm run assistant:worker poll every 30 s until Ctrl-C
26
36
  * npm run assistant:worker -- --once drain the queue, then exit (cron)
@@ -30,5 +40,7 @@
30
40
  * --agent names a subagent of the syndicate, or its orchestrator.
31
41
  * MELCHIZEDEK_TASKS_FILE moves the store (default: outputs/tasks.json),
32
42
  * and must match the chat process's value. Run ONE worker per store.
43
+ * MELCHIZEDEK_CHECKPOINT_MAX_BYTES caps a stored checkpoint (bytes of
44
+ * JSON, default 5 MiB); a larger one is skipped and the previous kept.
33
45
  */
34
46
  export {};
@@ -21,6 +21,16 @@
21
21
  * instead of abandoning it while it keeps spending. Any syndicate and any
22
22
  * subagent in it can serve as the worker.
23
23
  *
24
+ * A run is durable (ADR 0113): runDurableTurn (lib/runtime/native/
25
+ * checkpoint.ts) checkpoints the run's sessions at every step boundary
26
+ * beside the job (TaskBackend.saveCheckpoint), and a job claimed again
27
+ * after its worker stopped resumes from its last checkpoint instead of
28
+ * starting over. SIGTERM aborts the step in flight and puts the job back
29
+ * in the queue with its checkpoint; the next worker finishes it. A job
30
+ * cancelled while it runs (task_update) loses its claim: the next lease
31
+ * renewal or checkpoint save sees it, and the run is aborted without
32
+ * writing a result over the cancellation.
33
+ *
24
34
  * Usage:
25
35
  * npm run assistant:worker poll every 30 s until Ctrl-C
26
36
  * npm run assistant:worker -- --once drain the queue, then exit (cron)
@@ -30,16 +40,17 @@
30
40
  * --agent names a subagent of the syndicate, or its orchestrator.
31
41
  * MELCHIZEDEK_TASKS_FILE moves the store (default: outputs/tasks.json),
32
42
  * and must match the chat process's value. Run ONE worker per store.
43
+ * MELCHIZEDEK_CHECKPOINT_MAX_BYTES caps a stored checkpoint (bytes of
44
+ * JSON, default 5 MiB); a larger one is skipped and the previous kept.
33
45
  */
34
46
  import { randomUUID } from 'node:crypto';
35
47
  import { loadEnv } from '../lib/loadEnv.js';
36
48
  import { loadSyndicate } from '../lib/loadSyndicate.js';
37
- import { runSyndicateTurn } from '../lib/runtime/syndicateTurn.js';
49
+ import { runDurableTurn } from '../lib/runtime/native/checkpoint.js';
38
50
  import { runtimeSetting } from '../lib/runtime/runtimeFlag.js';
39
51
  import { setLogLevel } from '../lib/runtime/logging.js';
40
- import { InProcessSessionService } from '../lib/runtime/sessions.js';
41
52
  import { PROVIDERS, providerForModel, providerKeyPresent, } from '../lib/models/registry.js';
42
- import { getTaskBackend, setTaskBackend, taskStorePath } from '../lib/tools/taskTools.js';
53
+ import { CHECKPOINT_MAX_BYTES_ENV, applyInterrupted, checkpointMaxBytesSetting, fileTaskBackendWith, getTaskBackend, setTaskBackend, taskStorePath, } from '../lib/tools/taskTools.js';
43
54
  import { postgresStorage } from '../lib/storage/postgres/index.js';
44
55
  import { dbSchema } from '../lib/storage/schema.js';
45
56
  import { hostname } from 'node:os';
@@ -81,10 +92,17 @@ if (!providerKeyPresent(provider)) {
81
92
  const log = (message) => console.log(`[worker] ${message}`);
82
93
  // The queue: the JSON file, or Postgres when DATABASE_URL is set, where any
83
94
  // number of workers may run (each claim is FOR UPDATE SKIP LOCKED).
95
+ // A checkpoint above MELCHIZEDEK_CHECKPOINT_MAX_BYTES (bytes of JSON, default
96
+ // 5 MiB) is not saved: the job keeps its previous one (ADR 0113).
97
+ const rawCap = process.env[CHECKPOINT_MAX_BYTES_ENV];
98
+ const checkpointMaxBytes = checkpointMaxBytesSetting(rawCap);
99
+ if (rawCap?.trim() && Number(rawCap.trim()) !== checkpointMaxBytes) {
100
+ log(`${CHECKPOINT_MAX_BYTES_ENV} is not a positive whole number of bytes; using ${checkpointMaxBytes}`);
101
+ }
102
+ const taskQueue = { checkpointMaxBytes, log };
84
103
  const databaseUrl = process.env.DATABASE_URL?.trim();
85
- const pg = databaseUrl ? postgresStorage({ connectionString: databaseUrl, schema: dbSchema() }) : undefined;
86
- if (pg)
87
- setTaskBackend(pg.taskQueue);
104
+ const pg = databaseUrl ? postgresStorage({ connectionString: databaseUrl, schema: dbSchema(), taskQueue }) : undefined;
105
+ setTaskBackend(pg ? pg.taskQueue : fileTaskBackendWith(taskQueue));
88
106
  const backend = getTaskBackend();
89
107
  /** A claimed job stays this worker's while it renews; the job timeout bounds a turn. */
90
108
  const lease = { workerId: `${hostname()}:${process.pid}:${randomUUID().slice(0, 8)}`, leaseMs: 60_000 };
@@ -105,24 +123,43 @@ const workerConfig = !sub
105
123
  };
106
124
  /** The job in hand, so a signal can cancel it. */
107
125
  let current;
108
- // ── One job = one fresh single-turn session ─────────────────────────────────
126
+ /** Set by SIGTERM: the job in hand goes back to the queue with its checkpoint. */
127
+ let interrupted = false;
128
+ /** The run lost its claim on the job (cancelled while it ran, or taken by another worker). */
129
+ class ClaimLost extends Error {
130
+ }
131
+ /** SIGTERM stopped the run; the job is re-queued, not failed. */
132
+ class Interrupted extends Error {
133
+ }
134
+ // ── One job = one durable single-turn run (ADR 0113) ────────────────────────
109
135
  async function runJob(job) {
110
- current = new AbortController();
136
+ const controller = new AbortController();
137
+ current = controller;
111
138
  try {
112
- const result = await runSyndicateTurn({
139
+ // A fresh in-process store per attempt, restored from the job's last
140
+ // checkpoint when an earlier attempt left one (ADR 0080, ADR 0113).
141
+ const result = await runDurableTurn({
113
142
  config: workerConfig,
114
143
  parts: [{ text: job.instruction ?? job.title }],
115
144
  appName: 'assistant-worker',
116
145
  userId: 'local-user',
117
- sessionId: randomUUID(),
118
- // A fresh in-process store per job, as the turn runner's signature
119
- // names it (ADR 0080). The runtime follows MELCHIZEDEK_RUNTIME.
120
- sessionService: new InProcessSessionService(),
146
+ runId: `${job.owner}:${job.id}`,
147
+ checkpoints: {
148
+ load: async () => ((await backend.loadCheckpoint?.(job)) ?? null),
149
+ save: async (checkpoint) => (backend.saveCheckpoint ? backend.saveCheckpoint(lease, job, checkpoint) : true),
150
+ },
151
+ onSaveError: (e) => log(`checkpoint not saved: ${e instanceof Error ? e.message : String(e)}`),
121
152
  compile: { log, onUnknownTool: (n) => log(`unknown tool '${n}' skipped`) },
122
- signal: current.signal,
153
+ signal: controller.signal,
123
154
  deadlineMs: JOB_TIMEOUT_MS,
124
155
  events: { warn: (m) => log(`⚠ ${m}`) },
125
156
  });
157
+ if (result.resumedFromStep > 0)
158
+ log(`${job.id} resumed from step ${result.resumedFromStep}`);
159
+ if (result.lostClaim || controller.signal.reason instanceof ClaimLost)
160
+ throw new ClaimLost('the job was cancelled or claimed elsewhere');
161
+ if (interrupted)
162
+ throw new Interrupted('stopped by SIGTERM');
126
163
  if (result.status !== 'completed') {
127
164
  throw new Error(`${result.error?.code ?? 'ERROR'}: ${result.error?.message ?? ''}`.trim());
128
165
  }
@@ -142,10 +179,17 @@ async function drain() {
142
179
  const started = Date.now();
143
180
  const label = job.owner ? `${job.id} (${job.owner})` : job.id;
144
181
  log(`${label} started: ${job.title}`);
145
- // Renew the lease while the job runs, so no other worker takes it back.
182
+ // Renew the lease while the job runs, so no other worker takes it back;
183
+ // a renewal that finds the claim gone (the job was cancelled) stops the run.
146
184
  const held = job;
147
185
  const heartbeat = setInterval(() => {
148
- backend.renew(lease, held).catch((e) => log(`lease renewal failed: ${e instanceof Error ? e.message : e}`));
186
+ backend
187
+ .renew(lease, held)
188
+ .then((kept) => {
189
+ if (kept === false)
190
+ current?.abort(new ClaimLost('claim lost'));
191
+ })
192
+ .catch((e) => log(`lease renewal failed: ${e instanceof Error ? e.message : e}`));
149
193
  }, Math.floor(lease.leaseMs / 3));
150
194
  try {
151
195
  const result = await runJob(job);
@@ -153,8 +197,22 @@ async function drain() {
153
197
  log(`${label} done in ${Math.round((Date.now() - started) / 1000)} s`);
154
198
  }
155
199
  catch (error) {
156
- await backend.finish(job, { error: String(error?.message ?? error) });
157
- log(`${label} failed: ${error?.message ?? error}`);
200
+ if (error instanceof ClaimLost) {
201
+ // Cancelled while it ran: the record already says so; nothing is written over it.
202
+ log(`${label} stopped: ${error.message}`);
203
+ }
204
+ else if (error instanceof Interrupted) {
205
+ // Back to the queue with its checkpoint, or failed after MAX_ATTEMPTS interruptions.
206
+ const outcome = await backend.mutate(job.owner, (store) => {
207
+ const t = store.tasks.find((r) => r.id === held.id);
208
+ return t && t.status === 'running' ? applyInterrupted(t) : 'left as is';
209
+ });
210
+ log(`${label} interrupted: ${outcome}`);
211
+ }
212
+ else {
213
+ await backend.finish(job, { error: String(error?.message ?? error) });
214
+ log(`${label} failed: ${error?.message ?? error}`);
215
+ }
158
216
  }
159
217
  finally {
160
218
  clearInterval(heartbeat);
@@ -183,12 +241,13 @@ process.on('SIGINT', () => {
183
241
  stopping = true;
184
242
  log('stopping after the current job (Ctrl-C again to quit now)');
185
243
  });
186
- // A container stop: cancel the job in hand (it is recorded as failed and the
187
- // store stays consistent), then exit.
244
+ // A container stop: abort the step in flight and put the job back in the
245
+ // queue with its checkpoint, so the next worker resumes it (ADR 0113).
188
246
  process.on('SIGTERM', () => {
189
247
  stopping = true;
190
- log('SIGTERM — canceling the current job');
191
- current?.abort();
248
+ interrupted = true;
249
+ log('SIGTERM — stopping the current job; it resumes from its last checkpoint');
250
+ current?.abort(new Interrupted('SIGTERM'));
192
251
  });
193
252
  while (!stopping) {
194
253
  await drain();
@@ -8,7 +8,12 @@
8
8
  * gateway stand-in, or local), which declared server-side tools it keeps or
9
9
  * drops, and one verdict per syndicate. Says first which runtime a turn runs
10
10
  * on (native; a leftover MELCHIZEDEK_RUNTIME=adk is a problem, since 1.0.0
11
- * removed that runtime). Closes with the env vars that would
11
+ * removed that runtime). Lists the tools that need an OAuth grant (an
12
+ * `auth: { oauth2 }` or `mcp_auth`, ADR 0112): each user's consent, or the
13
+ * server's own client credentials and whether their variables are set
14
+ * (names only). When any A2A identity or billing variable is set, a
15
+ * `serving` line names the A2A_AUTH mode and A2A_KEY_MODE and what the
16
+ * server would refuse to start on. Closes with the env vars that would
12
17
  * unlock the most, where to get each, and the first command to try.
13
18
  *
14
19
  * Read-only: never edits .env, never sends a request, never prints a key
@@ -17,7 +22,7 @@
17
22
  * Usage:
18
23
  * npm run doctor
19
24
  * npm run doctor -- --json # machine-readable
20
- * npm run doctor -- --check # exit 1 when any syndicate is blocked
25
+ * npm run doctor -- --check # exit 1 on a blocked syndicate, a runtime problem, or an OAuth or credential host problem
21
26
  * npm run doctor -- --matrix # the provider × capability matrix
22
27
  * npm run doctor -- --fix-namespaces [file…] # give long-term syndicates a memory_namespace
23
28
  * MELCHIZEDEK_AGENTS_DIR=/path npm run doctor
@@ -8,7 +8,12 @@
8
8
  * gateway stand-in, or local), which declared server-side tools it keeps or
9
9
  * drops, and one verdict per syndicate. Says first which runtime a turn runs
10
10
  * on (native; a leftover MELCHIZEDEK_RUNTIME=adk is a problem, since 1.0.0
11
- * removed that runtime). Closes with the env vars that would
11
+ * removed that runtime). Lists the tools that need an OAuth grant (an
12
+ * `auth: { oauth2 }` or `mcp_auth`, ADR 0112): each user's consent, or the
13
+ * server's own client credentials and whether their variables are set
14
+ * (names only). When any A2A identity or billing variable is set, a
15
+ * `serving` line names the A2A_AUTH mode and A2A_KEY_MODE and what the
16
+ * server would refuse to start on. Closes with the env vars that would
12
17
  * unlock the most, where to get each, and the first command to try.
13
18
  *
14
19
  * Read-only: never edits .env, never sends a request, never prints a key
@@ -17,7 +22,7 @@
17
22
  * Usage:
18
23
  * npm run doctor
19
24
  * npm run doctor -- --json # machine-readable
20
- * npm run doctor -- --check # exit 1 when any syndicate is blocked
25
+ * npm run doctor -- --check # exit 1 on a blocked syndicate, a runtime problem, or an OAuth or credential host problem
21
26
  * npm run doctor -- --matrix # the provider × capability matrix
22
27
  * npm run doctor -- --fix-namespaces [file…] # give long-term syndicates a memory_namespace
23
28
  * MELCHIZEDEK_AGENTS_DIR=/path npm run doctor
@@ -25,7 +30,7 @@
25
30
  import fs from 'node:fs';
26
31
  import path from 'node:path';
27
32
  import { loadEnv } from '../lib/loadEnv.js';
28
- import { renderDoctor, runDoctor } from '../lib/doctor.js';
33
+ import { checkProblems, renderDoctor, runDoctor } from '../lib/doctor.js';
29
34
  import { renderCapabilityMatrix } from '../lib/models/capabilities.js';
30
35
  import { assignMemoryNamespace, LEGACY_MEMORY_APP_NAME } from '../lib/memory/namespace.js';
31
36
  loadEnv();
@@ -88,5 +93,5 @@ if (json) {
88
93
  else {
89
94
  console.log(renderDoctor(result, { color: !noColor }));
90
95
  }
91
- if (check && (result.counts.blocked > 0 || result.runtime.problem))
96
+ if (check && checkProblems(result).length)
92
97
  process.exit(1);
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/setup.ts — the `melchizedek-setup` bin and `npm run setup`.
4
+ *
5
+ * The onboarding command: a menu of authentication levels (no key, one
6
+ * provider, several, a gateway, a cloud platform, per-caller keys, caller
7
+ * tokens, OAuth tool grants, and an honest entry for subscription sign-ins),
8
+ * each with a startup guide: the variables to set (names and shapes only),
9
+ * the doctor command that confirms it, the shipped files that run there and
10
+ * the first commands to try. The levels and guides live in lib/onboarding.ts;
11
+ * ONBOARDING.md is generated from the same function.
12
+ *
13
+ * npx melchizedek-setup # the menu (interactive terminal)
14
+ * npx melchizedek-setup --auto # detect from the environment, names only
15
+ * npx melchizedek-setup --level gateway # one guide, by id or number
16
+ * npx melchizedek-setup --level 2 --write-env # also create .env from .env.example
17
+ * npx melchizedek-setup --list
18
+ * npx melchizedek-setup --markdown # ONBOARDING.md, regenerated
19
+ *
20
+ * Never prints a value. `--write-env` never overwrites `.env`, leaves every
21
+ * name blank, and refuses unless `git check-ignore` confirms `.env` is ignored.
22
+ */
23
+ export declare function main(argv?: string[]): Promise<number>;