melchizedek-agents 0.9.6 → 0.16.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 (246) hide show
  1. package/.env.example +272 -0
  2. package/AGENT_SETUP.md +39 -23
  3. package/CHANGELOG.md +623 -0
  4. package/DOCUMENTATION.md +429 -109
  5. package/QUICKSTART.md +92 -18
  6. package/README.md +45 -5
  7. package/config/agents/README.md +46 -1
  8. package/config/agents/examples/ares.yaml +117 -40
  9. package/config/agents/examples/assistant.yaml +159 -0
  10. package/config/agents/examples/augustin.yaml +154 -51
  11. package/config/agents/examples/cartographers.yaml +91 -23
  12. package/config/agents/examples/claude.yaml +45 -6
  13. package/config/agents/examples/council.yaml +23 -8
  14. package/config/agents/examples/critic.yaml +57 -21
  15. package/config/agents/examples/delegation.yaml +76 -9
  16. package/config/agents/examples/hierarchical.yaml +55 -8
  17. package/config/agents/examples/image_production.yaml +54 -16
  18. package/config/agents/examples/librarian.yaml +70 -14
  19. package/config/agents/examples/model_zoo.yaml +105 -29
  20. package/config/agents/examples/patient_advocate.yaml +102 -34
  21. package/config/agents/examples/research.yaml +250 -0
  22. package/config/agents/examples/scribe.yaml +242 -0
  23. package/config/agents/examples/scriptorium.yaml +87 -24
  24. package/config/agents/examples/style_council.yaml +31 -9
  25. package/config/agents/examples/syndicate.yaml +29 -10
  26. package/config/agents/examples/tutor.yaml +10 -3
  27. package/config/agents/syndicateSchema.yaml +44 -9
  28. package/config/agents/templates/README.md +58 -0
  29. package/config/agents/templates/account_memory.yaml +153 -0
  30. package/config/agents/templates/case_desk.yaml +188 -0
  31. package/config/agents/templates/conversational.yaml +110 -0
  32. package/config/agents/templates/draft_review.yaml +130 -0
  33. package/config/agents/templates/intake_extractor.yaml +152 -0
  34. package/config/agents/templates/research_brief.yaml +169 -0
  35. package/config/agents/templates/research_desk.yaml +152 -0
  36. package/config/agents/templates/review_panel.yaml +188 -0
  37. package/config/agents/templates/support_triage.yaml +273 -0
  38. package/config/agents/templates/systems_operator.yaml +157 -0
  39. package/db/hardening.sql +202 -0
  40. package/db/memory_v2.sql +77 -0
  41. package/db/migrations/0001_base.sql +167 -0
  42. package/db/migrations/0002_erase_scope.sql +172 -0
  43. package/db/migrations/0003_postgres_storage.sql +58 -0
  44. package/db/migrations/0004_usage.sql +94 -0
  45. package/db/migrations/0005_agent_registry.sql +200 -0
  46. package/db/migrations/0006_task_leases.sql +25 -0
  47. package/db/migrations/0007_memory_commit.sql +134 -0
  48. package/db/migrations/0008_memory_retention.sql +37 -0
  49. package/db/migrations/0009_task_queue.sql +51 -0
  50. package/db/migrations/0010_erase_complete.sql +180 -0
  51. package/db/telemetry.sql +401 -0
  52. package/demo/README.md +77 -0
  53. package/demo/a2a_demo.mjs +95 -0
  54. package/dist/lib/a2a/app.d.ts +253 -0
  55. package/dist/lib/a2a/app.js +751 -0
  56. package/dist/lib/a2a/executor.d.ts +167 -0
  57. package/dist/lib/a2a/executor.js +548 -0
  58. package/dist/lib/a2a/identity.d.ts +122 -0
  59. package/dist/lib/a2a/identity.js +254 -0
  60. package/dist/lib/a2a/policy.d.ts +110 -0
  61. package/dist/lib/a2a/policy.js +178 -0
  62. package/dist/lib/a2a/redisLimits.d.ts +26 -0
  63. package/dist/lib/a2a/redisLimits.js +31 -0
  64. package/dist/lib/a2a/remoteAgent.d.ts +91 -0
  65. package/dist/lib/a2a/remoteAgent.js +268 -0
  66. package/dist/lib/a2a/turnLock.d.ts +28 -0
  67. package/dist/lib/a2a/turnLock.js +76 -0
  68. package/dist/lib/compile.d.ts +11 -1
  69. package/dist/lib/compile.js +109 -8
  70. package/dist/lib/config.d.ts +7 -7
  71. package/dist/lib/config.js +7 -7
  72. package/dist/lib/doctor.d.ts +145 -0
  73. package/dist/lib/doctor.js +517 -0
  74. package/dist/lib/grounding.d.ts +2 -2
  75. package/dist/lib/grounding.js +2 -2
  76. package/dist/lib/guards/index.d.ts +37 -0
  77. package/dist/lib/guards/index.js +66 -0
  78. package/dist/lib/guards/science.d.ts +117 -0
  79. package/dist/lib/guards/science.js +444 -0
  80. package/dist/lib/index.d.ts +37 -2
  81. package/dist/lib/index.js +35 -4
  82. package/dist/lib/loadEnv.d.ts +23 -11
  83. package/dist/lib/loadEnv.js +77 -29
  84. package/dist/lib/loadSyndicate.d.ts +97 -5
  85. package/dist/lib/loadSyndicate.js +176 -76
  86. package/dist/lib/memory/erase.d.ts +24 -0
  87. package/dist/lib/memory/erase.js +47 -0
  88. package/dist/lib/memory/namespace.d.ts +37 -0
  89. package/dist/lib/memory/namespace.js +74 -0
  90. package/dist/lib/memory/providers.d.ts +99 -0
  91. package/dist/lib/memory/providers.js +240 -0
  92. package/dist/lib/memory/store.d.ts +74 -0
  93. package/dist/lib/memory/store.js +108 -0
  94. package/dist/lib/memory/supabaseMemoryService.d.ts +69 -9
  95. package/dist/lib/memory/supabaseMemoryService.js +239 -125
  96. package/dist/lib/models/capabilities.d.ts +103 -0
  97. package/dist/lib/models/capabilities.js +291 -0
  98. package/dist/lib/models/claudeLlm.d.ts +10 -1
  99. package/dist/lib/models/claudeLlm.js +57 -27
  100. package/dist/lib/models/endpoints.d.ts +100 -0
  101. package/dist/lib/models/endpoints.js +282 -0
  102. package/dist/lib/models/gateway.d.ts +107 -0
  103. package/dist/lib/models/gateway.js +157 -0
  104. package/dist/lib/models/gatewayLlm.d.ts +57 -0
  105. package/dist/lib/models/gatewayLlm.js +105 -0
  106. package/dist/lib/models/gptLlm.d.ts +51 -2
  107. package/dist/lib/models/gptLlm.js +169 -26
  108. package/dist/lib/models/grokLlm.d.ts +19 -10
  109. package/dist/lib/models/grokLlm.js +29 -14
  110. package/dist/lib/models/ollamaLlm.d.ts +26 -0
  111. package/dist/lib/models/ollamaLlm.js +43 -1
  112. package/dist/lib/models/openAiCompatibleLlm.d.ts +67 -2
  113. package/dist/lib/models/openAiCompatibleLlm.js +200 -37
  114. package/dist/lib/models/providerMap.js +9 -2
  115. package/dist/lib/models/registry.d.ts +42 -5
  116. package/dist/lib/models/registry.js +140 -29
  117. package/dist/lib/models/retry.d.ts +146 -0
  118. package/dist/lib/models/retry.js +305 -0
  119. package/dist/lib/models/schemaNormalize.d.ts +20 -0
  120. package/dist/lib/models/schemaNormalize.js +43 -0
  121. package/dist/lib/net/addressGuard.d.ts +60 -0
  122. package/dist/lib/net/addressGuard.js +216 -0
  123. package/dist/lib/observability/embeddings.d.ts +11 -2
  124. package/dist/lib/observability/embeddings.js +18 -14
  125. package/dist/lib/observability/metrics.d.ts +36 -0
  126. package/dist/lib/observability/metrics.js +86 -0
  127. package/dist/lib/observability/redact.d.ts +29 -0
  128. package/dist/lib/observability/redact.js +102 -0
  129. package/dist/lib/observability/supabaseSpanExporter.d.ts +2 -0
  130. package/dist/lib/observability/supabaseSpanExporter.js +22 -6
  131. package/dist/lib/observability/tracer.d.ts +10 -0
  132. package/dist/lib/observability/tracer.js +70 -3
  133. package/dist/lib/persistence/supabaseProvider.d.ts +11 -0
  134. package/dist/lib/persistence/supabaseProvider.js +32 -6
  135. package/dist/lib/registry.d.ts +87 -0
  136. package/dist/lib/registry.js +146 -0
  137. package/dist/lib/runtime/approvals.d.ts +44 -0
  138. package/dist/lib/runtime/approvals.js +67 -0
  139. package/dist/lib/runtime/syndicateTurn.d.ts +233 -0
  140. package/dist/lib/runtime/syndicateTurn.js +597 -0
  141. package/dist/lib/runtime/turnControl.d.ts +90 -0
  142. package/dist/lib/runtime/turnControl.js +131 -0
  143. package/dist/lib/session/supabaseSessionService.js +1 -1
  144. package/dist/lib/session/transcript.d.ts +9 -1
  145. package/dist/lib/session/transcript.js +15 -7
  146. package/dist/lib/skills.d.ts +50 -0
  147. package/dist/lib/skills.js +138 -0
  148. package/dist/lib/storage/postgres/index.d.ts +98 -0
  149. package/dist/lib/storage/postgres/index.js +146 -0
  150. package/dist/lib/storage/postgres/memoryStore.d.ts +11 -0
  151. package/dist/lib/storage/postgres/memoryStore.js +82 -0
  152. package/dist/lib/storage/postgres/sessionService.d.ts +51 -0
  153. package/dist/lib/storage/postgres/sessionService.js +225 -0
  154. package/dist/lib/storage/postgres/taskQueue.d.ts +22 -0
  155. package/dist/lib/storage/postgres/taskQueue.js +101 -0
  156. package/dist/lib/storage/postgres/taskStore.d.ts +55 -0
  157. package/dist/lib/storage/postgres/taskStore.js +168 -0
  158. package/dist/lib/storage/schema.d.ts +27 -0
  159. package/dist/lib/storage/schema.js +50 -0
  160. package/dist/lib/storage/schemaVersion.d.ts +25 -0
  161. package/dist/lib/storage/schemaVersion.js +46 -0
  162. package/dist/lib/syndicateSchema.d.ts +275 -0
  163. package/dist/lib/syndicateSchema.js +534 -0
  164. package/dist/lib/toolRegistry.d.ts +15 -0
  165. package/dist/lib/toolRegistry.js +59 -10
  166. package/dist/lib/tools/collectionsSearchTool.js +1 -1
  167. package/dist/lib/tools/mcpServe.d.ts +45 -0
  168. package/dist/lib/tools/mcpServe.js +88 -0
  169. package/dist/lib/tools/mcpToolFactory.d.ts +2 -0
  170. package/dist/lib/tools/mcpToolFactory.js +41 -21
  171. package/dist/lib/tools/science/corrections.d.ts +42 -0
  172. package/dist/lib/tools/science/corrections.js +115 -0
  173. package/dist/lib/tools/science/europepmc.d.ts +51 -0
  174. package/dist/lib/tools/science/europepmc.js +127 -0
  175. package/dist/lib/tools/science/http.d.ts +31 -0
  176. package/dist/lib/tools/science/http.js +104 -0
  177. package/dist/lib/tools/science/openalex.d.ts +30 -0
  178. package/dist/lib/tools/science/openalex.js +81 -0
  179. package/dist/lib/tools/science/trials.d.ts +58 -0
  180. package/dist/lib/tools/science/trials.js +203 -0
  181. package/dist/lib/tools/science/types.d.ts +69 -0
  182. package/dist/lib/tools/science/types.js +48 -0
  183. package/dist/lib/tools/scienceTools.d.ts +100 -0
  184. package/dist/lib/tools/scienceTools.js +277 -0
  185. package/dist/lib/tools/taskTools.d.ts +204 -0
  186. package/dist/lib/tools/taskTools.js +374 -0
  187. package/dist/lib/tools/toolContract.d.ts +17 -4
  188. package/dist/lib/tools/toolContract.js +13 -5
  189. package/dist/lib/tools/webExtractTool.d.ts +13 -10
  190. package/dist/lib/tools/webExtractTool.js +23 -55
  191. package/dist/lib/tools/webSearchTool.d.ts +2 -1
  192. package/dist/lib/tools/webSearchTool.js +4 -3
  193. package/dist/lib/tools/wikiTools.d.ts +2 -2
  194. package/dist/lib/tools/wikiTools.js +12 -12
  195. package/dist/lib/tools/xApiSearchTool.d.ts +194 -0
  196. package/dist/lib/tools/xApiSearchTool.js +583 -0
  197. package/dist/lib/tools/xSearchTool.js +1 -1
  198. package/dist/lib/wiki/agentRun.js +1 -1
  199. package/dist/lib/wiki/builder.js +21 -6
  200. package/dist/lib/wiki/dive.js +2 -2
  201. package/dist/lib/wiki/entities.d.ts +2 -2
  202. package/dist/lib/wiki/entities.js +7 -7
  203. package/dist/lib/wiki/extract.js +5 -1
  204. package/dist/lib/wiki/fill.js +5 -5
  205. package/dist/lib/wiki/format.d.ts +7 -5
  206. package/dist/lib/wiki/format.js +2 -2
  207. package/dist/lib/wiki/graph.js +2 -2
  208. package/dist/lib/wiki/lint.js +2 -2
  209. package/dist/lib/wiki/markdown.d.ts +1 -1
  210. package/dist/lib/wiki/markdown.js +6 -1
  211. package/dist/lib/wiki/pipeline.js +2 -2
  212. package/dist/lib/wiki/vault.js +2 -2
  213. package/dist/scripts/a2a_server.d.ts +17 -11
  214. package/dist/scripts/a2a_server.js +405 -1028
  215. package/dist/scripts/assistant_worker.d.ts +33 -0
  216. package/dist/scripts/assistant_worker.js +193 -0
  217. package/dist/scripts/db.d.ts +26 -0
  218. package/dist/scripts/db.js +179 -0
  219. package/dist/scripts/doctor.d.ts +23 -0
  220. package/dist/scripts/doctor.js +90 -0
  221. package/dist/scripts/init.d.ts +43 -0
  222. package/dist/scripts/init.js +152 -0
  223. package/dist/scripts/registry.d.ts +25 -0
  224. package/dist/scripts/registry.js +182 -0
  225. package/dist/scripts/skills_install.d.ts +7 -0
  226. package/dist/scripts/skills_install.js +137 -0
  227. package/dist/scripts/syndicate_chat.js +159 -276
  228. package/package.json +90 -16
  229. package/skills/README.md +74 -0
  230. package/skills/briefs/README.md +16 -0
  231. package/skills/briefs/_shared.md +16 -0
  232. package/skills/briefs/melchizedek-author.md +60 -0
  233. package/skills/briefs/melchizedek-memory.md +47 -0
  234. package/skills/briefs/melchizedek-models.md +47 -0
  235. package/skills/briefs/melchizedek-scribe.md +37 -0
  236. package/skills/briefs/melchizedek-serve.md +56 -0
  237. package/skills/briefs/melchizedek.md +69 -0
  238. package/skills/briefs/skills-readme.md +33 -0
  239. package/skills/melchizedek/SKILL.md +159 -0
  240. package/skills/melchizedek-author/SKILL.md +99 -0
  241. package/skills/melchizedek-author/templates/minimal.yaml +60 -0
  242. package/skills/melchizedek-memory/SKILL.md +138 -0
  243. package/skills/melchizedek-models/SKILL.md +137 -0
  244. package/skills/melchizedek-scribe/SKILL.md +72 -0
  245. package/skills/melchizedek-scribe/templates/brief.md +27 -0
  246. package/skills/melchizedek-serve/SKILL.md +119 -0
package/DOCUMENTATION.md CHANGED
@@ -26,7 +26,7 @@ see [`QUICKSTART.md`](./QUICKSTART.md).
26
26
  config/agents/ YOUR syndicate definitions (the engine's input)
27
27
  config/agents/examples/ the starter pack — shipped example syndicates
28
28
  lib/loadSyndicate.ts YAML → validated config (+ variable binding)
29
- lib/dispatch.ts plan-dispatch route resolution (§6, A2A-only)
29
+ lib/dispatch.ts plan-dispatch route resolution (§6)
30
30
  lib/toolRegistry.ts tool name → live ADK tool instance
31
31
  lib/models/claudeLlm.ts Claude adapter registered into the ADK registry
32
32
  lib/models/ollamaLlm.ts open-weight local adapter (Ollama, keyless)
@@ -35,8 +35,12 @@ scripts/demo_mcp_server.ts demo MCP server (library catalog, SSE)
35
35
  lib/session/… Supabase-backed session service
36
36
  lib/memory/… pgvector long-term memory service
37
37
  lib/observability/… OpenTelemetry run tracing
38
- scripts/syndicate_chat.ts CLI REPL / one-shot runner (compiles the graph)
39
- scripts/a2a_server.ts HTTP JSON-RPC service mode (same compiler)
38
+ lib/runtime/syndicateTurn.ts THE turn runner: every surface below calls it
39
+ lib/a2a/app.ts the A2A server as a library (createA2AApp)
40
+ lib/a2a/remoteAgent.ts A2A client: remote agents as subagents
41
+ scripts/syndicate_chat.ts CLI REPL / one-shot runner
42
+ scripts/a2a_server.ts the A2A server bin (melchizedek-serve)
43
+ db/schema.sql the base schema (sessions, memory, expiry)
40
44
  db/hardening.sql deny-by-default RLS for the Supabase tables
41
45
  tests/agents.test.ts compiles every shipped syndicate; opt-in live check
42
46
  ```
@@ -56,6 +60,8 @@ Minimal complete example:
56
60
  syndicate_name: "My Council"
57
61
  memory_system: "session-only" # internal-only | session-only | long-term
58
62
 
63
+ # guards: [my_guard] # [optional] post-answer guards, by name
64
+
59
65
  variables: # bound into {{placeholders}} at load
60
66
  headline_count: 5 # current_date is injected automatically
61
67
 
@@ -85,17 +91,23 @@ Field reference:
85
91
 
86
92
  | Field | Where | Meaning |
87
93
  |---|---|---|
88
- | `syndicate_name` | root | Display name; also namespaces memory user keys. |
89
- | `memory_system` | root | `internal-only` (nothing persists), `session-only` (transcript persists in Supabase), `long-term` (adds fact distillation + vector recall). |
94
+ | `syndicate_name` | root | Display name. On the CLI it also namespaces memory user keys; on the A2A server memory is keyed by the caller's silo across every long-term syndicate that server serves. |
95
+ | `memory_system` | root | `internal-only` (nothing persists — on the server too: its transcripts stay in process memory), `session-only` (transcript persists in Supabase), `long-term` (adds fact distillation + vector recall). |
90
96
  | `variables` | root | Key/values bound into `{{placeholders}}` anywhere in instructions. `current_date` is always injected; CLI `--bind key=value` overrides. |
91
97
  | `memory_extraction_rules` | root | Domain rules appended to the shared fact-extraction prompt for THIS syndicate only (requires `memory_system: "long-term"`). The extraction prompt is global — anything domain-specific belongs here, never edited into it. Unset, the prompt renders byte-identical to before the slot existed (§4). |
92
- | `dispatch` | root | Switches the syndicate from DELEGATE to PLAN-DISPATCH routing (§6). `default_route` (required) names the fail-static subagent; `route_key` / `reason_key` name the router's JSON properties (defaults `route` / `reason`). A2A-only. |
98
+ | `memory_extraction_model` | root | The model that distils this syndicate's turns into memory records (long-term only); any model id. Default: the deployment's `MEMORY_EXTRACTION_MODEL`. |
99
+ | `memory_retention_days` | root | Days a fact in this syndicate's namespace is kept; the server deletes older facts when it loads the syndicate and daily after. Requires `memory_namespace` (never applied to the shared default namespace). |
100
+ | `dispatch` | root | Switches the syndicate from DELEGATE to PLAN-DISPATCH routing (§6). `default_route` (required) names the fail-static subagent; `route_key` / `reason_key` name the router's JSON properties (defaults `route` / `reason`). Honoured by every surface (the CLI, the server, the worker, evals). |
101
+ | `guards` | root | Optional list of post-answer guard NAMES (`lib/guards/index.ts`). Each runs after the answering turn and before the reply publishes, receiving the final text plus every tool-result text of that turn, and rewrites in place rather than re-asking the model; its notes land in the `[STATUS]` stream. Guards declared by a syndicate reached through `yaml_reference:` count too — the server resolves the union via `collectGuards()`. Resolved by name, never by module path, so adding one is a deliberate act in code; **the published registry ships one, `science`** (citation checks for `research.yaml`); `registerGuard()` adds your own from code, and an unregistered name is warned about and skipped. |
93
102
  | `name` / `model` / `instruction` | agent | The agent triple. Any Gemini id, `claude-*`, or `ollama/*` for open-weight local models (see §5). |
94
103
  | `description` | subagent | **The delegation API.** The orchestrator reads this when deciding to hand off — write it like a function signature ("Use this subagent to…, pass it…"). |
95
104
  | `tools` | agent | Names resolved by the tool registry (§3). Long-term memory agents add `preload_memory` / `load_memory`. |
96
105
  | `generateContentConfig` | agent | Temperature, output caps, thinking budget/level. |
97
106
  | `outputSchema` | agent | Structured-JSON contract. **Constraint:** an agent holding `outputSchema` cannot also hold transfer powers — the ADK deadlocks it. Keep schema-holders as leaf agents (see `critic.yaml`'s header comment for the war story). |
98
107
  | `yaml_reference` | subagent | Mount another syndicate file as a nested subagent. |
108
+ | `a2a_agent_url` | subagent | A REMOTE agent over A2A (§6): the orchestrator delegates to it with one `request` argument; in plan-dispatch it can be a route. No `model`/`instruction` — the remote agent has its own. Credentials come from `A2A_AGENT_TOKENS`, never YAML. |
109
+ | `max_steps` | root | Cap on model calls per turn, counted across every agent the turn reaches (orchestrator, subagents, nested syndicates). Exceeding it fails the turn with `STEP_LIMIT`. |
110
+ | `includeContents` / `outputKey` / `globalInstruction` / `disallowTransferToParent` / `disallowTransferToPeers` | agent | Passed through to ADK's LlmAgent. `includeContents: none` makes an agent see only the current message. |
99
111
  | `mcp_server_url` | subagent | Discover this subagent's tools from a remote MCP server at load time (§3). SSRF-guarded; `ALLOW_PRIVATE_MCP=true` permits localhost for development. |
100
112
 
101
113
  Validation happens at load: missing names, legacy option blocks, and
@@ -119,6 +131,8 @@ instance:
119
131
  | `load_memory` | ADK built-in | Explicit tool call to search the fact store (deliberate recall). |
120
132
  | `generate_image` | FunctionTool | Calls the Gemini image model directly, saves the result under `outputs/`, returns the path. A FunctionTool because binary `inlineData` cannot survive the AgentTool text boundary. |
121
133
  | `inspect_image` | FunctionTool | **Blind visual inventory** of a file under `outputs/`: subjects with exact counts, composition, light, palette, medium cues, artifacts — zero quality judgments. Its signature accepts *only* a file path, so an orchestrator cannot leak expectations into the observation (see `image_production.yaml`). |
134
+ | `task_add` / `task_list` / `task_get` / `task_update` | FunctionTool | A to-do list and job queue. Default: a single-user JSON file (`MELCHIZEDEK_TASKS_FILE`, default `outputs/tasks.json`), so every caller of a shared endpoint shares one list. With `DATABASE_URL` (migration 0009) each caller has its own list, scoped by the caller's scope key, and any number of workers take jobs safely. |
135
+ | `task_queue` | FunctionTool | Queues a background job (a self-contained instruction). The tool only writes the queue; `npm run assistant:worker` (`melchizedek-worker`) claims each job, runs it through one agent compiled from YAML (default: the Assistant's Worker), and writes the result back for `task_get`. `--once` drains and exits, for cron. |
122
136
 
123
137
  **MCP tools** are the exception to the registry: a subagent with
124
138
  `mcp_server_url:` in its YAML gets its tools from a remote MCP server at
@@ -154,93 +168,33 @@ Two Supabase tables carry the two kinds of remembering:
154
168
  history); recall is cosine similarity re-ranked by keys and dates.
155
169
  Full pipeline: [`lib/memory/README.md`](./lib/memory/README.md).
156
170
 
157
- Provision both in the Supabase SQL Editor:
158
-
159
- ```sql
160
- -- 1. Enable pgvector
161
- CREATE EXTENSION IF NOT EXISTS vector;
162
-
163
- -- 2. Sessions
164
- CREATE TABLE adk_sessions (
165
- id TEXT PRIMARY KEY,
166
- app_name TEXT NOT NULL,
167
- user_id TEXT NOT NULL,
168
- state JSONB DEFAULT '{}'::jsonb,
169
- events JSONB DEFAULT '[]'::jsonb,
170
- last_update_time BIGINT,
171
- expire_at TIMESTAMPTZ,
172
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
173
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
174
- );
175
-
176
- -- 3. Memory facts (structured records: date, source, status, index keys
177
- -- live beside the embedding — see lib/memory/README.md)
178
- CREATE TABLE adk_memory_facts (
179
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
180
- user_key TEXT NOT NULL,
181
- fact TEXT NOT NULL,
182
- embedding vector(768),
183
- tag TEXT,
184
- fact_date DATE,
185
- source TEXT,
186
- status TEXT NOT NULL DEFAULT 'active',
187
- keys TEXT[] NOT NULL DEFAULT '{}',
188
- superseded_by UUID,
189
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
190
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
191
- );
192
-
193
- -- 4. Indexes: similarity, entity keys, dates
194
- CREATE INDEX ON adk_memory_facts USING ivfflat (embedding vector_cosine_ops)
195
- WITH (lists = 100);
196
- CREATE INDEX adk_memory_facts_keys_idx ON adk_memory_facts USING gin (keys);
197
- CREATE INDEX adk_memory_facts_date_idx ON adk_memory_facts (user_key, fact_date);
198
-
199
- -- 5. Cosine-similarity RPC (returns the structured columns so the
200
- -- service can re-rank by keys/dates and relabel superseded records).
201
- -- filter_user_key is REQUIRED: a NULL key would return every user's facts.
202
- CREATE OR REPLACE FUNCTION match_memory_facts (
203
- query_embedding vector(768),
204
- filter_user_key text,
205
- match_count int DEFAULT 10
206
- ) RETURNS TABLE (
207
- id UUID,
208
- user_key TEXT,
209
- fact TEXT,
210
- tag TEXT,
211
- fact_date DATE,
212
- source TEXT,
213
- status TEXT,
214
- keys TEXT[],
215
- created_at TIMESTAMPTZ,
216
- similarity float
217
- )
218
- LANGUAGE plpgsql
219
- AS $$
220
- BEGIN
221
- IF filter_user_key IS NULL THEN
222
- RAISE EXCEPTION 'filter_user_key is required';
223
- END IF;
224
- RETURN QUERY
225
- SELECT
226
- adk_memory_facts.id,
227
- adk_memory_facts.user_key,
228
- adk_memory_facts.fact,
229
- adk_memory_facts.tag,
230
- adk_memory_facts.fact_date,
231
- adk_memory_facts.source,
232
- adk_memory_facts.status,
233
- adk_memory_facts.keys,
234
- adk_memory_facts.created_at,
235
- 1 - (adk_memory_facts.embedding <=> query_embedding) AS similarity
236
- FROM adk_memory_facts
237
- WHERE adk_memory_facts.user_key = filter_user_key
238
- ORDER BY adk_memory_facts.embedding <=> query_embedding
239
- LIMIT match_count;
240
- END;
241
- $$;
171
+ Install both, with their indexes, the recall function and the nightly
172
+ session expiry, from the one canonical file:
173
+
174
+ ```bash
175
+ npx melchizedek-db print # paste into the Supabase SQL Editor (clone: npm run db -- print)
176
+ npx melchizedek-db apply # or apply with psql, DATABASE_URL set
177
+ npx melchizedek-db status # schema version, hardening, session counts
178
+ ```
179
+
180
+ That runs the migrations in [`db/migrations/`](./db/migrations/) and then
181
+ [`db/hardening.sql`](./db/hardening.sql). Both are idempotent, so re-running
182
+ them is also the upgrade path from any earlier layout.
183
+
184
+ The tables go in the `public` schema by default. To keep them out of reach of
185
+ a REST layer (Supabase exposes `public`), install into a private schema and
186
+ run the server with the same setting:
187
+
188
+ ```bash
189
+ MELCHIZEDEK_DB_SCHEMA=melchizedek npx melchizedek-db apply # or: apply --schema melchizedek
190
+ MELCHIZEDEK_DB_SCHEMA=melchizedek DATABASE_URL=... npx melchizedek-a2a
242
191
  ```
243
192
 
193
+ A private schema needs `DATABASE_URL` (the pg driver); supabase-js storage
194
+ reaches only exposed schemas, so the server refuses that combination. Moving an
195
+ existing `public` install is a data migration, not a setting: keep `public`
196
+ there.
197
+
244
198
  Then run [`db/hardening.sql`](./db/hardening.sql) (RLS deny-by-default;
245
199
  see §8). Upgrading an existing project to the structured columns:
246
200
  [`db/memory_v2.sql`](./db/memory_v2.sql). `npm run db:purge` clears both
@@ -260,10 +214,17 @@ extraction model rephrases on each pass. Two mechanisms in
260
214
  `lib/memory/supabaseMemoryService.ts` prevent the store from filling
261
215
  with restatements:
262
216
 
263
- - A per-session **high-water mark** (`eventsToIngest`) distils only the
264
- turns added since the last ingestion. It is in-process on purpose — a
265
- restart re-reads one session once, absorbed by the check below, which
266
- is why it needs no table of its own.
217
+ - A per-session **processed marker** (`eventsToIngest`) distils only the
218
+ turns added since the last ingestion. It is stored in
219
+ `melchizedek_memory_ingest` (migration 0007) and advances in the same
220
+ transaction as the facts (`melchizedek_memory_commit`, which also retires
221
+ superseded rows), so a restart re-extracts nothing and a failure leaves
222
+ facts, supersessions and marker as they were. A custom store without the
223
+ commit keeps the marker in process memory.
224
+ - At boot the server compares the embedding column's size
225
+ (`melchizedek_memory_dimensions()`) with the configured embedder and
226
+ refuses a mismatch: changing the embedding model is a re-embed, never a
227
+ dropped table.
267
228
  - A **similarity probe** before each insert (`isSemanticDuplicate`,
268
229
  `MEMORY_DEDUP_SIMILARITY = 0.93`) drops restatements, reusing the same
269
230
  `match_memory_facts` RPC the supersession path calls. It requires the
@@ -299,6 +260,8 @@ accordingly — model optionality is a single YAML line per agent:
299
260
  | `claude-*` | Anthropic | `lib/models/claudeLlm.ts` | `ANTHROPIC_API_KEY` | ✅ server tool |
300
261
  | `gpt-*`, o-series | OpenAI | `lib/models/gptLlm.ts` (Responses API) | `OPENAI_API_KEY` | ✅ web_search tool |
301
262
  | `grok-*` | xAI | `lib/models/grokLlm.ts` (Responses API) | `XAI_API_KEY` | ✅ Agent Tools search |
263
+ | `ollama/*` | Local Ollama | `lib/models/ollamaLlm.ts` | none | ⚠ omitted + warning |
264
+ | *any cloud id whose direct key is absent* | the id's own provider, via a gateway | `lib/models/gatewayLlm.ts` (chat completions) | `MODEL_GATEWAY` + `MODEL_GATEWAY_API_KEY` | ⚠ omitted + reported |
302
265
 
303
266
  The xAI adapter carries the deepest capability surface: `grok-4.5`
304
267
  requests pin `reasoning.effort: "medium"` (`lib/config.ts`), SSE
@@ -307,7 +270,64 @@ partial delta events stream, one aggregated event persists with usage),
307
270
  structured outputs ride `outputSchema` → `text.format`, and two
308
271
  xAI-only tools — `x_search` and `collections_search` (§3) — turn on
309
272
  live X search and hosted-document RAG. All verified live on grok-4.5.
310
- | `ollama/*` | Local Ollama | `lib/models/ollamaLlm.ts` | none | ⚠ omitted + warning |
273
+
274
+ **Which keys do I need?** `npm run doctor` (the `melchizedek-doctor` bin)
275
+ reads every syndicate YAML, resolves each agent's model under your `.env`,
276
+ and prints one table — agent, model, provider, which declared server-side
277
+ tools that path runs natively (✓) or drops (✗), and whether the path is
278
+ funded — with one verdict per syndicate and the variables that would
279
+ unlock the most. Read-only; no key value is ever printed. Every
280
+ starter-pack file opens with a `# tier:` header (`keyless`, one provider
281
+ such as `gemini`, or `multi-provider`) the doctor checks against the
282
+ models. `GOOGLE_GENAI_API_KEY` alone runs fourteen of the nineteen examples.
283
+
284
+ **One key instead of several — the gateway fallback.** Direct adapters
285
+ are canonical: the native features above exist only on a provider's own
286
+ endpoint. But with `MODEL_GATEWAY=vercel` (or `openrouter`) and
287
+ `MODEL_GATEWAY_API_KEY` set, any cloud model id whose direct key is
288
+ *absent* is served through that gateway's OpenAI-compatible endpoint
289
+ (`lib/models/gateway.ts` owns the rule). A present provider key always
290
+ wins for its own ids, Ollama never routes through a gateway, and adding a
291
+ direct key later restores that provider's native search with no YAML
292
+ change. Through the gateway every server-side sentinel (`web_search`,
293
+ `google_search`, `x_search`, `collections_search`) is dropped — the doctor
294
+ and the startup log say so per agent, and the span records
295
+ `llm.transport = gateway:<id>` and `llm.capability.dropped` while
296
+ `llm.provider` keeps the upstream attribution. Tool calling, structured
297
+ output, `reasoning_effort` and streaming work unchanged. Optional dials:
298
+ `MODEL_GATEWAY_BASE_URL` (a self-hosted proxy speaking the same dialect),
299
+ `MODEL_GATEWAY_MODEL_MAP` (`from=to,…` wire-name overrides; the mapper
300
+ already turns `claude-sonnet-4-6` into `anthropic/claude-sonnet-4.6`). The
301
+ A2A `X-API-Key` never selects the gateway — the gateway key is server
302
+ environment only.
303
+
304
+ **Cloud platforms and proxies (ADR 0023).** The model id picks the provider;
305
+ `<PROVIDER>_PLATFORM` picks how it is reached. Keys in the environment against
306
+ the vendor's public API stay the default.
307
+
308
+ | Path | Set | Credential |
309
+ |---|---|---|
310
+ | Gemini on Vertex AI | `GEMINI_PLATFORM=vertex` (or genai's `GOOGLE_GENAI_USE_VERTEXAI=true`), `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_LOCATION` | Google Application Default Credentials |
311
+ | Claude on Bedrock | `ANTHROPIC_PLATFORM=bedrock`, `AWS_REGION` | the AWS credential chain; needs `@anthropic-ai/bedrock-sdk` |
312
+ | Claude on Vertex AI | `ANTHROPIC_PLATFORM=vertex`, `ANTHROPIC_VERTEX_PROJECT_ID` (or `GOOGLE_CLOUD_PROJECT`), `CLOUD_ML_REGION` | Google ADC; needs `@anthropic-ai/vertex-sdk` |
313
+ | GPT on Azure OpenAI | `OPENAI_PLATFORM=azure`, `AZURE_OPENAI_ENDPOINT` | `AZURE_OPENAI_API_KEY`, else Entra ID through `@azure/identity` |
314
+ | A proxy in front of Anthropic or OpenAI | `ANTHROPIC_BASE_URL` / `OPENAI_BASE_URL` | the provider's key |
315
+
316
+ `ANTHROPIC_MODEL_MAP`, `OPENAI_MODEL_MAP` and `GEMINI_MODEL_MAP` (JSON, YAML id
317
+ → platform id) translate ids a platform names differently: a Bedrock inference
318
+ profile, a Vertex AI version suffix, an Azure deployment name. Unlisted ids pass
319
+ through. The A2A `credentials` plug point may return, per request, an API key
320
+ or a partial endpoint (`baseURL`, `apiKey`, a `token` source, `project`,
321
+ `location`, `region`, `models`) merged over the environment's. Memory
322
+ extraction and embeddings follow the Gemini platform, so a Vertex AI
323
+ deployment needs no AI Studio key.
324
+
325
+ Native web search is not sent on Bedrock, Claude-on-Vertex or Azure (Gemini
326
+ grounding is kept on Vertex AI); the capability report, the startup log and
327
+ the doctor say so per agent. `npm run doctor` prints one `endpoint` line per
328
+ configured platform with its credential source and anything missing. These
329
+ paths are tested against mocked SDK clients and have **not** been run against
330
+ the live clouds from this repository.
311
331
 
312
332
  `lib/models/registry.ts` is the single routing seam:
313
333
  `registerAvailableProviders()` registers every adapter whose key is
@@ -357,7 +377,8 @@ route through `lib/models/ollamaLlm.ts` to a local Ollama daemon over
357
377
  its OpenAI-compatible API (`OLLAMA_BASE_URL`, default
358
378
  `http://localhost:11434/v1`). No key is required, and a syndicate whose
359
379
  *every* agent is `ollama/*` runs with no `.env` at all —
360
- `config/agents/examples/tutor.yaml` (single agent) and `council.yaml` (council)
380
+ `config/agents/examples/tutor.yaml` (single agent), `council.yaml` (council)
381
+ and `assistant.yaml` (conversation, summaries, a task list, background jobs)
361
382
  are the worked examples. The adapter translates ADK content to
362
383
  OpenAI-style messages, including tool calls (so delegation works),
363
384
  image parts as data URIs (so `ollama/qwen3-vl:8b` can see), and JSON
@@ -374,13 +395,288 @@ itself; delegation is exercised through AgentTool function calls.
374
395
 
375
396
  ## 6. A2A service mode
376
397
 
377
- `npm run start:a2a` serves a syndicate as a stateless JSON-RPC
378
- agent-to-agent endpoint (Express): an agent card describing the service,
379
- `message/send` for turns, bearer-token auth via `A2A_SERVER_SECRET`, and
380
- rate limiting. With `PUBLIC_URL` set (a real deployment), the server
381
- refuses to start without the secret and refuses to run against an
382
- unhardened database unless explicitly overridden. `demo/a2a_demo.mjs` is
383
- a complete client.
398
+ `npm run start:a2a -- <file>.yaml` (package: `npx melchizedek-serve
399
+ <file>.yaml`) serves a syndicate as an A2A 1.0 agent that also accepts A2A
400
+ 0.3 clients (most platforms still speak 0.3; a request without an
401
+ `A2A-Version` header is treated as 0.3, per the spec): an agent card listing
402
+ both versions' endpoints, JSON-RPC and REST transports, and the task
403
+ lifecycle. In your own Express
404
+ app, mount `(await createA2AApp(options)).app` instead — same server, same
405
+ options. `demo/a2a_demo.mjs` is a complete client.
406
+
407
+ #### Routes
408
+
409
+ | Route | Auth | Purpose |
410
+ |---|---|---|
411
+ | `GET /healthz` | none | liveness — always 200 while the process runs |
412
+ | `GET /readyz` | none | readiness — 503 while draining for shutdown |
413
+ | `GET /metrics` | `A2A_METRICS_TOKEN` | Prometheus text: tasks by agent, outcome and caller; model calls; tokens by kind; task duration; tasks in flight. Absent unless the token is set |
414
+ | `GET /.well-known/agent-card.json` | bearer | the default syndicate's card |
415
+ | `POST /a2a/jsonrpc`, `/a2a/rest` | bearer | the default syndicate |
416
+ | `GET /<agentId>/.well-known/agent-card.json` | bearer | another syndicate's card; its URLs point at `/<agentId>/a2a/...` |
417
+ | `POST /<agentId>/a2a/jsonrpc`, `/<agentId>/a2a/rest` | bearer | another syndicate (`A2A_SERVED_AGENTS` restricts which) |
418
+ | `DELETE /memory` | bearer | erase everything stored for the calling scope: facts, sessions (with subagent rows) and ledger rows, with per-store counts; `?all=1` covers every memory namespace |
419
+
420
+ "bearer" means the credential `A2A_AUTH` asks for (below); with no
421
+ `A2A_SERVER_SECRET` and no authenticator the server binds `127.0.0.1` only.
422
+ In BYOK mode every task route also needs `X-API-Key`. The card declares
423
+ what is required in `securitySchemes`.
424
+
425
+ #### Who the caller is, and whose data it is
426
+
427
+ `A2A_AUTH` picks a built-in authenticator (`lib/a2a/identity.ts`, export
428
+ `melchizedek-agents/a2a/identity`), and the authenticator decides the
429
+ scope key every session and memory is stored under:
430
+
431
+ - **`secret`** (default): every caller presents `A2A_SERVER_SECRET`, and
432
+ the calling backend names its end user in `X-User-Id`.
433
+ - **`callers`**: one bearer token per calling backend. `A2A_CALLERS` lists
434
+ `name:sha256[:scope]` entries separated by `;`, so the configuration holds
435
+ only token hashes. A caller owns its scope (its name, unless one is given),
436
+ and an `X-User-Id` it sends nests beneath it. The scope does not depend on
437
+ any model key. Mint a caller with
438
+ `npx melchizedek-serve --new-caller <name> [--scope <scope>] [--token-file <path>]`.
439
+ While `A2A_SERVER_SECRET` is still set it keeps working beside the tokens,
440
+ with its old scoping, so callers move over one at a time.
441
+ - **`jwt`**: a JWT from your identity provider: `A2A_JWT_JWKS_URL` (or an
442
+ HS256 `A2A_JWT_SECRET`), with `A2A_JWT_ISSUER` and `A2A_JWT_AUDIENCE`
443
+ required and `exp` checked. The scope is the `sub` claim
444
+ (`A2A_JWT_SCOPE_CLAIM`), under `A2A_JWT_TENANT_CLAIM` when set.
445
+ `X-User-Id` is ignored: the token is the user.
446
+ - **`header`**: a gateway in front authenticates users and names them in
447
+ `A2A_TRUSTED_USER_HEADER`. Requires `A2A_SERVER_SECRET`, which only the
448
+ gateway may hold, so no other client can set the header.
449
+
450
+ In code, spread an authenticator into the factory:
451
+ `createA2AApp({ ...callerTokens(parseCallers(spec)), keyMode: 'byok', … })`,
452
+ or pass your own `resolveRequest`.
453
+
454
+ #### Budgets, the task record, and the ledger
455
+
456
+ - **Budgets** (`A2A_BUDGETS`, option `policy: budgets(config)`): daily limits
457
+ per UTC day on tasks, model calls and tokens, per caller (`perCaller`, with
458
+ overrides by name in `callers`) and per scope (`perScope`, one end user).
459
+ Example: `{"perCaller":{"tokens":5000000},"callers":{"reports":{"tasks":100}},"perScope":{"tasks":50}}`.
460
+ A task over budget ends `rejected` with the reason, before it takes a slot.
461
+ The counts live in the `melchizedek_usage` table (`db/migrations/0004_usage.sql`)
462
+ when Postgres or Supabase is configured, else in process memory. A store
463
+ that cannot be read refuses the task.
464
+ - **One record per task** (option `onTaskEnd`): agent, caller, a hash of the
465
+ scope, status, reason, duration, model calls and tokens.
466
+ `A2A_LOG_FORMAT=json` prints every server line as JSON, this record included.
467
+ - **Rate limit**: with an authenticator it counts per caller (an operator's
468
+ backend) or per scope (an end user); under the shared secret, per IP.
469
+ - **The telemetry ledger is redacted before it is written**:
470
+ `TELEMETRY_REDACT=secret` (default) removes key-shaped credentials; add
471
+ `email`, `phone`, `card` (Luhn-checked) or `ssn`, or plug in your own with
472
+ `setTelemetryRedactor(fn)`. Sessions and memory are not redacted; use
473
+ `DELETE /memory` there.
474
+
475
+ #### Who pays
476
+
477
+ `A2A_KEY_MODE` (option `keyMode`), whatever the authenticator:
478
+
479
+ - **`server`** (default): the server's own provider keys pay for every
480
+ model call (or your `credentials` plug point supplies a key per request).
481
+ - **`byok`**: the caller's `X-API-Key` funds agents on the provider named
482
+ by `X-Provider`; agents on other providers, tools and memory extraction
483
+ still run on the server's keys. Under `A2A_AUTH=secret` only, the key's
484
+ hash also scopes the data (`a2a-<hash>`, with `X-User-Id` beneath it).
485
+ That was the only behaviour before 0.16. A deployment holding data from
486
+ then keeps reaching it with `byok`, or by moving to `A2A_AUTH=callers`
487
+ with each caller's scope set to its existing `a2a-<hash>` silo.
488
+
489
+ #### Headers
490
+
491
+ | Header | Meaning |
492
+ |---|---|
493
+ | `Authorization: Bearer <token>` | the server secret, this caller's token, or a JWT, per `A2A_AUTH` |
494
+ | `X-User-Id` | the end user this request is for (`[A-Za-z0-9._-]{1,64}`); sessions and memory are stored under it (under a caller token, beneath the caller's scope; ignored with a JWT). Authenticate your users before sending it. |
495
+ | `X-API-Key` | BYOK mode only: the caller's model key (see above) |
496
+ | `X-Provider` | BYOK mode only: which provider `X-API-Key` belongs to (default `google`) |
497
+ | `X-Surface`, `X-Surface-Guild`, `-Channel`, `-User` | optional, telemetry only |
498
+
499
+ #### Sessions
500
+
501
+ `message.contextId` names the conversation — it goes **inside** `message`.
502
+ Calls with the same value continue one session; a `contextId` beside
503
+ `message` is ignored and every call starts fresh. Sessions are durable with
504
+ Supabase and expire seven days after the last turn (a nightly prune from
505
+ `db/migrations/0001_base.sql` deletes them). A syndicate declaring `memory_system: internal-only` keeps its
506
+ transcripts in process memory even when Supabase is configured.
507
+
508
+ #### Tasks, streaming, cancel
509
+
510
+ (Method names below are 0.3's; 1.0 clients use `SendMessage`,
511
+ `GetTask`, `SendStreamingMessage`, `CancelTask` and the 1.0 enum spellings.)
512
+ `message/send` blocks until the turn finishes unless the request sets
513
+ `configuration.blocking: false`; then poll `tasks/get`. `message/stream`
514
+ emits `[STATUS]` progress updates (tool calls, the chosen route, guard
515
+ notes) and the answer as the final status message. With
516
+ `A2A_STREAM_TEXT=true` (`streamText` in code) it also streams the answer as
517
+ the model writes it, as chunks of an artifact named `answer`: the first
518
+ chunk opens it, later chunks append, text the agent wrote before calling a
519
+ tool is withdrawn (an empty replacement), and the artifact is closed
520
+ (`lastChunk`) with the text the user actually receives, which the final
521
+ status message repeats. A syndicate with `guards:` never streams, because a
522
+ guard reads the whole answer before any of it leaves; the dispatch
523
+ classifier never streams either. One conversation runs one turn at a time:
524
+ a second message on a busy `contextId` waits for the first turn (up to
525
+ `A2A_TURN_LOCK_WAIT_MS`, default 30 s) and is then `rejected` with "still
526
+ running". The lock is in-process by default and a Postgres advisory lock
527
+ with `DATABASE_URL`, so it holds across instances. `tasks/cancel` stops a running task, including the model call
528
+ in flight, and the task ends `canceled`. Final states: `completed`,
529
+ `failed` (the message names the stage and the provider's reason),
530
+ `canceled`, `rejected` (a file part, an empty message, or the server at
531
+ capacity). Message parts may be `text` or `data` (sent to the model as
532
+ JSON); `file` parts are refused.
533
+
534
+ #### Approval gates (`require_approval`)
535
+
536
+ A tool can run only after a person approves the exact call
537
+ ([ADR 0028](./wiki/decisions/0028-approval-gates.md)). The agent lists it:
538
+
539
+ ```yaml
540
+ orchestrator:
541
+ name: Desk
542
+ tools: [send_email]
543
+ require_approval: [send_email] # names from this agent's own tools
544
+ ```
545
+
546
+ When the model calls it, nothing runs: the task ends `input-required`, final.
547
+ Its status message says what is waiting ("Desk wants to run
548
+ send_email({...})") and carries a data part
549
+ `{ type: 'approval_request', approval_id, agent, tool, args }`. Answer on the
550
+ same conversation (same `contextId`; the same `taskId` works too) with the text
551
+ `approve` or `reject`, or a data part
552
+ `{ "approval": { "id": "<approval_id>", "approved": true } }`. Approved, the
553
+ call runs with the arguments shown; rejected, the model is told and answers
554
+ without it. A message that is not an answer gets the same request back,
555
+ without a model call. ADK pins the call and its arguments, so an approval
556
+ cannot run a different call.
557
+
558
+ Gates are allowed on the orchestrator and on the subagents of a
559
+ plan-dispatch syndicate, which run as the turn's own agent. A delegated
560
+ subagent runs inside a tool call, where a pause cannot reach the caller, so a
561
+ gate there is a load error, as is any gate inside a nested `yaml_reference`
562
+ syndicate. Only function tools from the registry can be gated, not MCP tools
563
+ or native-search sentinels. In code, `runSyndicateTurn` returns
564
+ `status: 'input-required'` with `approval`, and the next turn's part
565
+ `approvalResponsePart(approval.id, approved)` answers it.
566
+
567
+ #### Limits
568
+
569
+ | Setting | Default |
570
+ |---|---|
571
+ | `A2A_RATE_LIMIT_MAX` per `A2A_RATE_LIMIT_WINDOW_MS`, per client IP (POSTs) | 60 per 15 min |
572
+ | `A2A_AUTH_FAILURE_MAX` failed logins per IP per 15 min, then blocked | 30 |
573
+ | `A2A_TASK_TIMEOUT_MS` per task | 15 min |
574
+ | `A2A_MAX_CONCURRENT_TASKS` | unlimited |
575
+ | `max_steps` (YAML): model calls per turn, subagents included | none |
576
+ | `A2A_BODY_LIMIT` | 1 MB |
577
+ | `A2A_SHUTDOWN_GRACE_MS`: SIGTERM waits for running tasks | 25 s |
578
+
579
+ The rate limit and the failed-login limit count in the process by default,
580
+ so each replica keeps its own window. With `A2A_REDIS_URL` set (and the
581
+ optional `redis` package installed) both count in Redis, one window across
582
+ every replica; in code, pass `limitStore` with
583
+ `redisRateLimitStore({ command })` from `melchizedek-agents/a2a/limits`, which
584
+ takes any client's raw-command function (node-redis `sendCommand`, ioredis
585
+ `call`). Budgets need no Redis: they are Postgres counters.
586
+
587
+ #### What is per-process
588
+
589
+ The per-agent config cache (a config change needs a restart), the
590
+ rate-limit counters and the concurrency count always live in the process. Tasks do too unless `DATABASE_URL` is set: then
591
+ Postgres holds sessions, memory, A2A tasks and budget counters, and several
592
+ replicas are safe behind one load balancer (rate limits and the concurrency
593
+ cap then apply per replica, unless `A2A_REDIS_URL` shares the rate limits).
594
+ With Postgres, a running task is leased to the
595
+ instance running it (renewed every 20 s; `taskLeaseMs`, default 60 s): if
596
+ that instance dies, another marks the task `failed` ("the server running
597
+ this task stopped") instead of a client polling it forever, and one turn
598
+ runs at a time per conversation across replicas. With Supabase only, or no
599
+ store, run one replica.
600
+
601
+ #### Posture at boot
602
+
603
+ Without `A2A_SERVER_SECRET` the server binds `127.0.0.1` only; binding
604
+ another `HOST` requires the secret or `ALLOW_UNAUTHENTICATED=true`. With
605
+ `PUBLIC_URL` set it refuses to start without the secret, with the
606
+ `.env.example` placeholder as the secret, or against an unhardened
607
+ database (unless `ALLOW_UNHARDENED_DB=true`). With durable storage it reads
608
+ `melchizedek_schema_version` and refuses to start on a database behind the
609
+ migrations it ships, naming `melchizedek-db apply` (or
610
+ `ALLOW_SCHEMA_MISMATCH=true` to start anyway); a database ahead of it only
611
+ warns, so a rolling deploy can migrate first. Storage through supabase-js
612
+ (Supabase credentials without `DATABASE_URL`) logs a deprecation notice:
613
+ set `DATABASE_URL` to the database's Postgres connection string.
614
+ Conversation content is not printed to stdout unless
615
+ `OTEL_CONSOLE_SPANS=true`.
616
+
617
+ #### Calling remote agents
618
+
619
+ A subagent with `a2a_agent_url: https://other-team.example/billing` is a
620
+ REMOTE agent, speaking A2A 1.0 or 0.3 (the card decides): the orchestrator
621
+ delegates to it as to a local subagent (one `request` argument), and in
622
+ plan-dispatch it can be a route. Its card and
623
+ endpoint pass the SSRF guard (`ALLOW_PRIVATE_A2A=true` for local hosts);
624
+ credentials come from `A2A_AGENT_TOKENS`, a JSON map of host → bearer
625
+ token or host → headers, sent only over https (or to loopback). The same
626
+ local conversation keeps talking to the same remote conversation.
627
+
628
+ #### Deploying
629
+
630
+ `Dockerfile` builds the compiled server and runs it as a non-root user with
631
+ a health check; `compose.yaml` adds optional Ollama and Phoenix (traces).
632
+ `npx melchizedek-db print|apply|status` installs and checks the database.
633
+ Set `PUBLIC_URL`, `A2A_SERVER_SECRET` and the provider keys from your
634
+ secret manager; give the orchestrator's stop timeout at least
635
+ `A2A_SHUTDOWN_GRACE_MS`.
636
+
637
+ #### The agent registry
638
+
639
+ A file is the default home of a syndicate. The registry is for a definition
640
+ an operator changes without a redeploy: `registry:<id>` loads it, and so does
641
+ a bare id listed in `A2A_REGISTRY_AGENTS`. It lives in two tables (migration
642
+ `0005_agent_registry.sql`):
643
+
644
+ - `adk_agent_registry` — one row per id, the active definition. The server
645
+ reads only this table.
646
+ - `adk_agent_registry_versions` — every definition an id has held, numbered
647
+ per id, with its config hash, author, note and publish time. It is
648
+ append-only: updates and deletes are refused.
649
+
650
+ The database records a version on every write to the active table, whoever
651
+ makes it, so history does not depend on the tool. Writing a definition an id
652
+ already held re-activates that version rather than duplicating it, which is
653
+ how a rollback works.
654
+
655
+ ```bash
656
+ npx melchizedek-registry publish config/agents/desk.yaml desk --note "tighter triage"
657
+ npx melchizedek-registry versions desk # * marks the active version
658
+ npx melchizedek-registry diff desk 3 # v3 against the active version
659
+ npx melchizedek-registry rollback desk 3 --note "v4 misroutes refunds"
660
+ npx melchizedek-registry retire desk --yes # stop serving; history stays
661
+ ```
662
+
663
+ `publish` and `rollback` validate against the same schema the loader uses
664
+ before anything is written. The author recorded is
665
+ `MELCHIZEDEK_REGISTRY_AUTHOR`, else the local user name; a direct write
666
+ records the database role. `registry:<id>@<version>` loads one stored
667
+ version (as the boot syndicate or from code), so a version can be tried
668
+ before it is activated; the HTTP routes accept only active ids. A running
669
+ server caches each agent for its lifetime: **restart it after a publish or
670
+ a rollback.** The library is `melchizedek-agents/registry`.
671
+
672
+ A syndicate is versioned as a unit. `publish` reads every nested
673
+ `yaml_reference` the definition reaches (nested ones of nested ones too) from
674
+ the agents root, validates each, and stores them with it under
675
+ `bundled_references`; the server loads nested syndicates of that version from
676
+ the bundle, never from files that may have changed since. Change a nested file
677
+ and the parent changes only when it is published again. `--no-bundle` stores
678
+ the references bare, to resolve from files at run time; a version published
679
+ before bundling existed behaves that way too.
384
680
 
385
681
  ### Plan-dispatch routing (`dispatch:`) — the second orchestration method
386
682
 
@@ -527,22 +823,46 @@ interface, register it behind a model-id prefix.
527
823
  subagent. `scripts/demo_mcp_server.ts` is a complete server to copy —
528
824
  tool definitions, SSE wiring, and persistent state in ~250 lines.
529
825
 
826
+ **Teach your coding agent the framework**: `skills/` holds six Agent
827
+ Skills (the open SKILL.md standard — a directory per skill, `name` and
828
+ `description` frontmatter) that give Claude Code, Codex, Cursor,
829
+ OpenCode or Gemini CLI the catalog of syndicates and the procedures in
830
+ this document: `melchizedek` (find and run, delegate a task),
831
+ `melchizedek-author`, `melchizedek-serve`, `melchizedek-memory`,
832
+ `melchizedek-models`, `melchizedek-scribe`. `npx melchizedek-skills
833
+ install` copies them into `.claude/skills/` and `.agents/skills/` of the
834
+ current project (`--for claude,codex,cursor,opencode,gemini,agents,all`,
835
+ `--global`, `--dir <path>`, `--only <names>`, `--force`, `--dry-run`;
836
+ `npm run skills:install` in a clone). The installer (`lib/skills.ts`)
837
+ copies only from the package's own `skills/` directory, follows no
838
+ symlinks, and never overwrites a differing file without `--force`. Each
839
+ SKILL.md body was written by the Scribe syndicate
840
+ (`config/agents/examples/scribe.yaml`) from a brief of facts and reviewed
841
+ by a person; `skills/README.md` records the procedure.
842
+
530
843
  ## 8. Security notes
531
844
 
532
845
  - **Secrets** live in `.env` only; `.env.example` documents every
533
- variable. Nothing in the repo ships a key.
846
+ variable and ships no values (placeholders are ignored if copied in).
847
+ Nothing in the repo ships a key. Report vulnerabilities per SECURITY.md.
534
848
  - **Database**: default Supabase leaves `public`-schema tables readable
535
849
  by the anon key over REST. `db/hardening.sql` enables deny-by-default
536
850
  RLS and revokes anon/authenticated privileges on every table it finds:
537
851
  `adk_sessions`, `adk_memory_facts`, and — where they exist — the
538
- optional `adk_telemetry` sink and `adk_agent_registry` (an unprotected
539
- registry is worst of all: agent definitions writable with the anon key
852
+ optional `adk_telemetry` sink and `adk_agent_registry` with its version
853
+ history (an unprotected registry is worst of all: agent definitions writable with the anon key
540
854
  means anyone can rewrite the instructions your server boots). The A2A
541
855
  server verifies hardening at boot and is fatal on public deployments
542
856
  without it. Note `service_role` bypasses RLS by design — the hardening
543
857
  constrains the API surface, not the trusted server.
544
- - **A2A**: bearer auth + rate limiting are built in; set
545
- `A2A_SERVER_SECRET` before exposing anything.
858
+ - **A2A**: bearer auth, a failed-login limiter and a request rate limit
859
+ are built in; without `A2A_SERVER_SECRET` the server binds loopback only.
860
+ See §6 for the posture checks at boot.
861
+ - **Outbound fetches** (`web_extract`, MCP servers, remote A2A agents) pass
862
+ one SSRF guard (`lib/net/addressGuard.ts`): local names and non-public
863
+ addresses in every encoding are refused, and names are resolved and
864
+ refused when any address is non-public. DNS rebinding between the check
865
+ and the connection is the remaining, stated limit.
546
866
  - **Image tools** write only under `outputs/`, and `inspect_image` reads
547
867
  only from there.
548
868
  - **MCP** is an outbound trust decision: `mcpToolFactory` blocks