melchizedek-agents 0.15.0 → 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 (229) hide show
  1. package/.env.example +272 -0
  2. package/AGENT_SETUP.md +19 -14
  3. package/CHANGELOG.md +623 -0
  4. package/DOCUMENTATION.md +375 -109
  5. package/QUICKSTART.md +55 -10
  6. package/README.md +23 -3
  7. package/config/agents/README.md +34 -0
  8. package/config/agents/examples/ares.yaml +116 -40
  9. package/config/agents/examples/assistant.yaml +74 -24
  10. package/config/agents/examples/augustin.yaml +105 -44
  11. package/config/agents/examples/cartographers.yaml +90 -23
  12. package/config/agents/examples/claude.yaml +44 -6
  13. package/config/agents/examples/council.yaml +22 -8
  14. package/config/agents/examples/critic.yaml +56 -21
  15. package/config/agents/examples/delegation.yaml +75 -9
  16. package/config/agents/examples/hierarchical.yaml +54 -8
  17. package/config/agents/examples/image_production.yaml +53 -16
  18. package/config/agents/examples/librarian.yaml +69 -14
  19. package/config/agents/examples/model_zoo.yaml +102 -27
  20. package/config/agents/examples/patient_advocate.yaml +101 -34
  21. package/config/agents/examples/research.yaml +49 -8
  22. package/config/agents/examples/scribe.yaml +111 -79
  23. package/config/agents/examples/scriptorium.yaml +86 -24
  24. package/config/agents/examples/style_council.yaml +30 -9
  25. package/config/agents/examples/syndicate.yaml +28 -10
  26. package/config/agents/examples/tutor.yaml +9 -3
  27. package/config/agents/syndicateSchema.yaml +39 -9
  28. package/config/agents/templates/README.md +7 -2
  29. package/config/agents/templates/account_memory.yaml +76 -18
  30. package/config/agents/templates/case_desk.yaml +84 -13
  31. package/config/agents/templates/conversational.yaml +38 -9
  32. package/config/agents/templates/draft_review.yaml +40 -13
  33. package/config/agents/templates/intake_extractor.yaml +33 -16
  34. package/config/agents/templates/research_brief.yaml +72 -26
  35. package/config/agents/templates/research_desk.yaml +74 -30
  36. package/config/agents/templates/review_panel.yaml +65 -30
  37. package/config/agents/templates/support_triage.yaml +107 -40
  38. package/config/agents/templates/systems_operator.yaml +76 -25
  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 +92 -9
  70. package/dist/lib/config.d.ts +2 -2
  71. package/dist/lib/config.js +2 -2
  72. package/dist/lib/doctor.d.ts +49 -2
  73. package/dist/lib/doctor.js +159 -12
  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 +12 -3
  77. package/dist/lib/guards/index.js +19 -4
  78. package/dist/lib/guards/science.js +1 -1
  79. package/dist/lib/index.d.ts +37 -2
  80. package/dist/lib/index.js +35 -4
  81. package/dist/lib/loadEnv.d.ts +23 -11
  82. package/dist/lib/loadEnv.js +77 -29
  83. package/dist/lib/loadSyndicate.d.ts +69 -5
  84. package/dist/lib/loadSyndicate.js +127 -71
  85. package/dist/lib/memory/erase.d.ts +24 -0
  86. package/dist/lib/memory/erase.js +47 -0
  87. package/dist/lib/memory/namespace.d.ts +37 -0
  88. package/dist/lib/memory/namespace.js +74 -0
  89. package/dist/lib/memory/providers.d.ts +99 -0
  90. package/dist/lib/memory/providers.js +240 -0
  91. package/dist/lib/memory/store.d.ts +74 -0
  92. package/dist/lib/memory/store.js +108 -0
  93. package/dist/lib/memory/supabaseMemoryService.d.ts +69 -9
  94. package/dist/lib/memory/supabaseMemoryService.js +239 -125
  95. package/dist/lib/models/capabilities.d.ts +50 -0
  96. package/dist/lib/models/capabilities.js +214 -4
  97. package/dist/lib/models/claudeLlm.d.ts +10 -1
  98. package/dist/lib/models/claudeLlm.js +57 -27
  99. package/dist/lib/models/endpoints.d.ts +100 -0
  100. package/dist/lib/models/endpoints.js +282 -0
  101. package/dist/lib/models/gateway.d.ts +1 -1
  102. package/dist/lib/models/gateway.js +4 -3
  103. package/dist/lib/models/gatewayLlm.d.ts +2 -2
  104. package/dist/lib/models/gatewayLlm.js +5 -5
  105. package/dist/lib/models/gptLlm.d.ts +22 -1
  106. package/dist/lib/models/gptLlm.js +77 -23
  107. package/dist/lib/models/grokLlm.d.ts +13 -4
  108. package/dist/lib/models/grokLlm.js +22 -7
  109. package/dist/lib/models/ollamaLlm.d.ts +26 -0
  110. package/dist/lib/models/ollamaLlm.js +43 -1
  111. package/dist/lib/models/openAiCompatibleLlm.d.ts +60 -2
  112. package/dist/lib/models/openAiCompatibleLlm.js +185 -36
  113. package/dist/lib/models/providerMap.js +9 -2
  114. package/dist/lib/models/registry.d.ts +24 -3
  115. package/dist/lib/models/registry.js +83 -23
  116. package/dist/lib/models/retry.d.ts +146 -0
  117. package/dist/lib/models/retry.js +305 -0
  118. package/dist/lib/models/schemaNormalize.d.ts +20 -0
  119. package/dist/lib/models/schemaNormalize.js +43 -0
  120. package/dist/lib/net/addressGuard.d.ts +60 -0
  121. package/dist/lib/net/addressGuard.js +216 -0
  122. package/dist/lib/observability/embeddings.d.ts +11 -2
  123. package/dist/lib/observability/embeddings.js +18 -14
  124. package/dist/lib/observability/metrics.d.ts +36 -0
  125. package/dist/lib/observability/metrics.js +86 -0
  126. package/dist/lib/observability/redact.d.ts +29 -0
  127. package/dist/lib/observability/redact.js +102 -0
  128. package/dist/lib/observability/supabaseSpanExporter.d.ts +2 -0
  129. package/dist/lib/observability/supabaseSpanExporter.js +22 -6
  130. package/dist/lib/observability/tracer.js +33 -3
  131. package/dist/lib/persistence/supabaseProvider.d.ts +11 -0
  132. package/dist/lib/persistence/supabaseProvider.js +32 -6
  133. package/dist/lib/registry.d.ts +87 -0
  134. package/dist/lib/registry.js +146 -0
  135. package/dist/lib/runtime/approvals.d.ts +44 -0
  136. package/dist/lib/runtime/approvals.js +67 -0
  137. package/dist/lib/runtime/syndicateTurn.d.ts +233 -0
  138. package/dist/lib/runtime/syndicateTurn.js +597 -0
  139. package/dist/lib/runtime/turnControl.d.ts +90 -0
  140. package/dist/lib/runtime/turnControl.js +131 -0
  141. package/dist/lib/session/supabaseSessionService.js +1 -1
  142. package/dist/lib/session/transcript.d.ts +9 -1
  143. package/dist/lib/session/transcript.js +15 -7
  144. package/dist/lib/storage/postgres/index.d.ts +98 -0
  145. package/dist/lib/storage/postgres/index.js +146 -0
  146. package/dist/lib/storage/postgres/memoryStore.d.ts +11 -0
  147. package/dist/lib/storage/postgres/memoryStore.js +82 -0
  148. package/dist/lib/storage/postgres/sessionService.d.ts +51 -0
  149. package/dist/lib/storage/postgres/sessionService.js +225 -0
  150. package/dist/lib/storage/postgres/taskQueue.d.ts +22 -0
  151. package/dist/lib/storage/postgres/taskQueue.js +101 -0
  152. package/dist/lib/storage/postgres/taskStore.d.ts +55 -0
  153. package/dist/lib/storage/postgres/taskStore.js +168 -0
  154. package/dist/lib/storage/schema.d.ts +27 -0
  155. package/dist/lib/storage/schema.js +50 -0
  156. package/dist/lib/storage/schemaVersion.d.ts +25 -0
  157. package/dist/lib/storage/schemaVersion.js +46 -0
  158. package/dist/lib/syndicateSchema.d.ts +275 -0
  159. package/dist/lib/syndicateSchema.js +534 -0
  160. package/dist/lib/toolRegistry.d.ts +15 -0
  161. package/dist/lib/toolRegistry.js +48 -15
  162. package/dist/lib/tools/collectionsSearchTool.js +1 -1
  163. package/dist/lib/tools/mcpServe.d.ts +2 -3
  164. package/dist/lib/tools/mcpServe.js +3 -4
  165. package/dist/lib/tools/mcpToolFactory.d.ts +1 -0
  166. package/dist/lib/tools/mcpToolFactory.js +13 -21
  167. package/dist/lib/tools/science/corrections.js +2 -2
  168. package/dist/lib/tools/science/europepmc.js +2 -2
  169. package/dist/lib/tools/science/openalex.js +2 -2
  170. package/dist/lib/tools/science/trials.d.ts +1 -1
  171. package/dist/lib/tools/science/trials.js +2 -2
  172. package/dist/lib/tools/scienceTools.d.ts +5 -5
  173. package/dist/lib/tools/scienceTools.js +5 -5
  174. package/dist/lib/tools/taskTools.d.ts +75 -26
  175. package/dist/lib/tools/taskTools.js +83 -51
  176. package/dist/lib/tools/toolContract.d.ts +17 -4
  177. package/dist/lib/tools/toolContract.js +13 -5
  178. package/dist/lib/tools/webExtractTool.d.ts +7 -10
  179. package/dist/lib/tools/webExtractTool.js +15 -51
  180. package/dist/lib/tools/webSearchTool.d.ts +2 -1
  181. package/dist/lib/tools/webSearchTool.js +4 -3
  182. package/dist/lib/tools/wikiTools.d.ts +2 -2
  183. package/dist/lib/tools/wikiTools.js +12 -12
  184. package/dist/lib/tools/xApiSearchTool.d.ts +10 -10
  185. package/dist/lib/tools/xApiSearchTool.js +6 -6
  186. package/dist/lib/tools/xSearchTool.js +1 -1
  187. package/dist/lib/wiki/agentRun.js +1 -1
  188. package/dist/lib/wiki/builder.js +21 -6
  189. package/dist/lib/wiki/dive.js +2 -2
  190. package/dist/lib/wiki/entities.d.ts +2 -2
  191. package/dist/lib/wiki/entities.js +7 -7
  192. package/dist/lib/wiki/extract.js +5 -1
  193. package/dist/lib/wiki/fill.js +5 -5
  194. package/dist/lib/wiki/format.d.ts +7 -5
  195. package/dist/lib/wiki/format.js +2 -2
  196. package/dist/lib/wiki/graph.js +2 -2
  197. package/dist/lib/wiki/lint.js +2 -2
  198. package/dist/lib/wiki/markdown.d.ts +1 -1
  199. package/dist/lib/wiki/markdown.js +6 -1
  200. package/dist/lib/wiki/pipeline.js +2 -2
  201. package/dist/lib/wiki/vault.js +2 -2
  202. package/dist/scripts/a2a_server.d.ts +17 -11
  203. package/dist/scripts/a2a_server.js +405 -1094
  204. package/dist/scripts/assistant_worker.d.ts +5 -3
  205. package/dist/scripts/assistant_worker.js +88 -58
  206. package/dist/scripts/db.d.ts +26 -0
  207. package/dist/scripts/db.js +179 -0
  208. package/dist/scripts/doctor.d.ts +2 -0
  209. package/dist/scripts/doctor.js +45 -2
  210. package/dist/scripts/init.d.ts +43 -0
  211. package/dist/scripts/init.js +152 -0
  212. package/dist/scripts/registry.d.ts +25 -0
  213. package/dist/scripts/registry.js +182 -0
  214. package/dist/scripts/skills_install.js +1 -1
  215. package/dist/scripts/syndicate_chat.js +159 -276
  216. package/package.json +75 -13
  217. package/skills/README.md +1 -1
  218. package/skills/briefs/melchizedek-author.md +3 -3
  219. package/skills/briefs/melchizedek-memory.md +3 -1
  220. package/skills/briefs/melchizedek-models.md +2 -2
  221. package/skills/briefs/melchizedek-serve.md +16 -12
  222. package/skills/briefs/skills-readme.md +2 -2
  223. package/skills/melchizedek-author/SKILL.md +2 -2
  224. package/skills/melchizedek-author/templates/minimal.yaml +1 -1
  225. package/skills/melchizedek-memory/SKILL.md +2 -0
  226. package/skills/melchizedek-models/SKILL.md +1 -1
  227. package/skills/melchizedek-serve/SKILL.md +18 -9
  228. package/dist/lib/tools/xPacket.d.ts +0 -102
  229. package/dist/lib/tools/xPacket.js +0 -226
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
  ```
@@ -87,18 +91,23 @@ Field reference:
87
91
 
88
92
  | Field | Where | Meaning |
89
93
  |---|---|---|
90
- | `syndicate_name` | root | Display name; also namespaces memory user keys. |
91
- | `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). |
92
96
  | `variables` | root | Key/values bound into `{{placeholders}}` anywhere in instructions. `current_date` is always injected; CLI `--bind key=value` overrides. |
93
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). |
94
- | `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. |
95
- | `guards` | root | Optional list of post-answer guard NAMES (`lib/guards/index.ts`). Each runs in the A2A server 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 registering one is a deliberate edit to `lib/guards/index.ts`; **the published registry ships empty**, and an unregistered name is warned about and skipped. 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. |
96
102
  | `name` / `model` / `instruction` | agent | The agent triple. Any Gemini id, `claude-*`, or `ollama/*` for open-weight local models (see §5). |
97
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…"). |
98
104
  | `tools` | agent | Names resolved by the tool registry (§3). Long-term memory agents add `preload_memory` / `load_memory`. |
99
105
  | `generateContentConfig` | agent | Temperature, output caps, thinking budget/level. |
100
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). |
101
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. |
102
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. |
103
112
 
104
113
  Validation happens at load: missing names, legacy option blocks, and
@@ -122,7 +131,7 @@ instance:
122
131
  | `load_memory` | ADK built-in | Explicit tool call to search the fact store (deliberate recall). |
123
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. |
124
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`). |
125
- | `task_add` / `task_list` / `task_get` / `task_update` | FunctionTool | A to-do list in a local JSON store (`MELCHIZEDEK_TASKS_FILE`, default `outputs/tasks.json`). Single-user: the store has no caller identity, so never serve these tools on a shared A2A endpoint. |
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. |
126
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. |
127
136
 
128
137
  **MCP tools** are the exception to the registry: a subagent with
@@ -159,93 +168,33 @@ Two Supabase tables carry the two kinds of remembering:
159
168
  history); recall is cosine similarity re-ranked by keys and dates.
160
169
  Full pipeline: [`lib/memory/README.md`](./lib/memory/README.md).
161
170
 
162
- Provision both in the Supabase SQL Editor:
163
-
164
- ```sql
165
- -- 1. Enable pgvector
166
- CREATE EXTENSION IF NOT EXISTS vector;
167
-
168
- -- 2. Sessions
169
- CREATE TABLE adk_sessions (
170
- id TEXT PRIMARY KEY,
171
- app_name TEXT NOT NULL,
172
- user_id TEXT NOT NULL,
173
- state JSONB DEFAULT '{}'::jsonb,
174
- events JSONB DEFAULT '[]'::jsonb,
175
- last_update_time BIGINT,
176
- expire_at TIMESTAMPTZ,
177
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
178
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
179
- );
180
-
181
- -- 3. Memory facts (structured records: date, source, status, index keys
182
- -- live beside the embedding — see lib/memory/README.md)
183
- CREATE TABLE adk_memory_facts (
184
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
185
- user_key TEXT NOT NULL,
186
- fact TEXT NOT NULL,
187
- embedding vector(768),
188
- tag TEXT,
189
- fact_date DATE,
190
- source TEXT,
191
- status TEXT NOT NULL DEFAULT 'active',
192
- keys TEXT[] NOT NULL DEFAULT '{}',
193
- superseded_by UUID,
194
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
195
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
196
- );
197
-
198
- -- 4. Indexes: similarity, entity keys, dates
199
- CREATE INDEX ON adk_memory_facts USING ivfflat (embedding vector_cosine_ops)
200
- WITH (lists = 100);
201
- CREATE INDEX adk_memory_facts_keys_idx ON adk_memory_facts USING gin (keys);
202
- CREATE INDEX adk_memory_facts_date_idx ON adk_memory_facts (user_key, fact_date);
203
-
204
- -- 5. Cosine-similarity RPC (returns the structured columns so the
205
- -- service can re-rank by keys/dates and relabel superseded records).
206
- -- filter_user_key is REQUIRED: a NULL key would return every user's facts.
207
- CREATE OR REPLACE FUNCTION match_memory_facts (
208
- query_embedding vector(768),
209
- filter_user_key text,
210
- match_count int DEFAULT 10
211
- ) RETURNS TABLE (
212
- id UUID,
213
- user_key TEXT,
214
- fact TEXT,
215
- tag TEXT,
216
- fact_date DATE,
217
- source TEXT,
218
- status TEXT,
219
- keys TEXT[],
220
- created_at TIMESTAMPTZ,
221
- similarity float
222
- )
223
- LANGUAGE plpgsql
224
- AS $$
225
- BEGIN
226
- IF filter_user_key IS NULL THEN
227
- RAISE EXCEPTION 'filter_user_key is required';
228
- END IF;
229
- RETURN QUERY
230
- SELECT
231
- adk_memory_facts.id,
232
- adk_memory_facts.user_key,
233
- adk_memory_facts.fact,
234
- adk_memory_facts.tag,
235
- adk_memory_facts.fact_date,
236
- adk_memory_facts.source,
237
- adk_memory_facts.status,
238
- adk_memory_facts.keys,
239
- adk_memory_facts.created_at,
240
- 1 - (adk_memory_facts.embedding <=> query_embedding) AS similarity
241
- FROM adk_memory_facts
242
- WHERE adk_memory_facts.user_key = filter_user_key
243
- ORDER BY adk_memory_facts.embedding <=> query_embedding
244
- LIMIT match_count;
245
- END;
246
- $$;
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
247
191
  ```
248
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
+
249
198
  Then run [`db/hardening.sql`](./db/hardening.sql) (RLS deny-by-default;
250
199
  see §8). Upgrading an existing project to the structured columns:
251
200
  [`db/memory_v2.sql`](./db/memory_v2.sql). `npm run db:purge` clears both
@@ -265,10 +214,17 @@ extraction model rephrases on each pass. Two mechanisms in
265
214
  `lib/memory/supabaseMemoryService.ts` prevent the store from filling
266
215
  with restatements:
267
216
 
268
- - A per-session **high-water mark** (`eventsToIngest`) distils only the
269
- turns added since the last ingestion. It is in-process on purpose — a
270
- restart re-reads one session once, absorbed by the check below, which
271
- 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.
272
228
  - A **similarity probe** before each insert (`isSemanticDuplicate`,
273
229
  `MEMORY_DEDUP_SIMILARITY = 0.93`) drops restatements, reusing the same
274
230
  `match_memory_facts` RPC the supersession path calls. It requires the
@@ -345,6 +301,34 @@ already turns `claude-sonnet-4-6` into `anthropic/claude-sonnet-4.6`). The
345
301
  A2A `X-API-Key` never selects the gateway — the gateway key is server
346
302
  environment only.
347
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.
331
+
348
332
  `lib/models/registry.ts` is the single routing seam:
349
333
  `registerAvailableProviders()` registers every adapter whose key is
350
334
  present (Ollama needs none) into the ADK's LLM registry, so the YAML
@@ -411,13 +395,288 @@ itself; delegation is exercised through AgentTool function calls.
411
395
 
412
396
  ## 6. A2A service mode
413
397
 
414
- `npm run start:a2a` serves a syndicate as a stateless JSON-RPC
415
- agent-to-agent endpoint (Express): an agent card describing the service,
416
- `message/send` for turns, bearer-token auth via `A2A_SERVER_SECRET`, and
417
- rate limiting. With `PUBLIC_URL` set (a real deployment), the server
418
- refuses to start without the secret and refuses to run against an
419
- unhardened database unless explicitly overridden. `demo/a2a_demo.mjs` is
420
- 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.
421
680
 
422
681
  ### Plan-dispatch routing (`dispatch:`) — the second orchestration method
423
682
 
@@ -584,19 +843,26 @@ by a person; `skills/README.md` records the procedure.
584
843
  ## 8. Security notes
585
844
 
586
845
  - **Secrets** live in `.env` only; `.env.example` documents every
587
- 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.
588
848
  - **Database**: default Supabase leaves `public`-schema tables readable
589
849
  by the anon key over REST. `db/hardening.sql` enables deny-by-default
590
850
  RLS and revokes anon/authenticated privileges on every table it finds:
591
851
  `adk_sessions`, `adk_memory_facts`, and — where they exist — the
592
- optional `adk_telemetry` sink and `adk_agent_registry` (an unprotected
593
- 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
594
854
  means anyone can rewrite the instructions your server boots). The A2A
595
855
  server verifies hardening at boot and is fatal on public deployments
596
856
  without it. Note `service_role` bypasses RLS by design — the hardening
597
857
  constrains the API surface, not the trusted server.
598
- - **A2A**: bearer auth + rate limiting are built in; set
599
- `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.
600
866
  - **Image tools** write only under `outputs/`, and `inspect_image` reads
601
867
  only from there.
602
868
  - **MCP** is an outbound trust decision: `mcpToolFactory` blocks