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.
- package/.env.example +272 -0
- package/AGENT_SETUP.md +19 -14
- package/CHANGELOG.md +623 -0
- package/DOCUMENTATION.md +375 -109
- package/QUICKSTART.md +55 -10
- package/README.md +23 -3
- package/config/agents/README.md +34 -0
- package/config/agents/examples/ares.yaml +116 -40
- package/config/agents/examples/assistant.yaml +74 -24
- package/config/agents/examples/augustin.yaml +105 -44
- package/config/agents/examples/cartographers.yaml +90 -23
- package/config/agents/examples/claude.yaml +44 -6
- package/config/agents/examples/council.yaml +22 -8
- package/config/agents/examples/critic.yaml +56 -21
- package/config/agents/examples/delegation.yaml +75 -9
- package/config/agents/examples/hierarchical.yaml +54 -8
- package/config/agents/examples/image_production.yaml +53 -16
- package/config/agents/examples/librarian.yaml +69 -14
- package/config/agents/examples/model_zoo.yaml +102 -27
- package/config/agents/examples/patient_advocate.yaml +101 -34
- package/config/agents/examples/research.yaml +49 -8
- package/config/agents/examples/scribe.yaml +111 -79
- package/config/agents/examples/scriptorium.yaml +86 -24
- package/config/agents/examples/style_council.yaml +30 -9
- package/config/agents/examples/syndicate.yaml +28 -10
- package/config/agents/examples/tutor.yaml +9 -3
- package/config/agents/syndicateSchema.yaml +39 -9
- package/config/agents/templates/README.md +7 -2
- package/config/agents/templates/account_memory.yaml +76 -18
- package/config/agents/templates/case_desk.yaml +84 -13
- package/config/agents/templates/conversational.yaml +38 -9
- package/config/agents/templates/draft_review.yaml +40 -13
- package/config/agents/templates/intake_extractor.yaml +33 -16
- package/config/agents/templates/research_brief.yaml +72 -26
- package/config/agents/templates/research_desk.yaml +74 -30
- package/config/agents/templates/review_panel.yaml +65 -30
- package/config/agents/templates/support_triage.yaml +107 -40
- package/config/agents/templates/systems_operator.yaml +76 -25
- package/db/hardening.sql +202 -0
- package/db/memory_v2.sql +77 -0
- package/db/migrations/0001_base.sql +167 -0
- package/db/migrations/0002_erase_scope.sql +172 -0
- package/db/migrations/0003_postgres_storage.sql +58 -0
- package/db/migrations/0004_usage.sql +94 -0
- package/db/migrations/0005_agent_registry.sql +200 -0
- package/db/migrations/0006_task_leases.sql +25 -0
- package/db/migrations/0007_memory_commit.sql +134 -0
- package/db/migrations/0008_memory_retention.sql +37 -0
- package/db/migrations/0009_task_queue.sql +51 -0
- package/db/migrations/0010_erase_complete.sql +180 -0
- package/db/telemetry.sql +401 -0
- package/demo/README.md +77 -0
- package/demo/a2a_demo.mjs +95 -0
- package/dist/lib/a2a/app.d.ts +253 -0
- package/dist/lib/a2a/app.js +751 -0
- package/dist/lib/a2a/executor.d.ts +167 -0
- package/dist/lib/a2a/executor.js +548 -0
- package/dist/lib/a2a/identity.d.ts +122 -0
- package/dist/lib/a2a/identity.js +254 -0
- package/dist/lib/a2a/policy.d.ts +110 -0
- package/dist/lib/a2a/policy.js +178 -0
- package/dist/lib/a2a/redisLimits.d.ts +26 -0
- package/dist/lib/a2a/redisLimits.js +31 -0
- package/dist/lib/a2a/remoteAgent.d.ts +91 -0
- package/dist/lib/a2a/remoteAgent.js +268 -0
- package/dist/lib/a2a/turnLock.d.ts +28 -0
- package/dist/lib/a2a/turnLock.js +76 -0
- package/dist/lib/compile.d.ts +11 -1
- package/dist/lib/compile.js +92 -9
- package/dist/lib/config.d.ts +2 -2
- package/dist/lib/config.js +2 -2
- package/dist/lib/doctor.d.ts +49 -2
- package/dist/lib/doctor.js +159 -12
- package/dist/lib/grounding.d.ts +2 -2
- package/dist/lib/grounding.js +2 -2
- package/dist/lib/guards/index.d.ts +12 -3
- package/dist/lib/guards/index.js +19 -4
- package/dist/lib/guards/science.js +1 -1
- package/dist/lib/index.d.ts +37 -2
- package/dist/lib/index.js +35 -4
- package/dist/lib/loadEnv.d.ts +23 -11
- package/dist/lib/loadEnv.js +77 -29
- package/dist/lib/loadSyndicate.d.ts +69 -5
- package/dist/lib/loadSyndicate.js +127 -71
- package/dist/lib/memory/erase.d.ts +24 -0
- package/dist/lib/memory/erase.js +47 -0
- package/dist/lib/memory/namespace.d.ts +37 -0
- package/dist/lib/memory/namespace.js +74 -0
- package/dist/lib/memory/providers.d.ts +99 -0
- package/dist/lib/memory/providers.js +240 -0
- package/dist/lib/memory/store.d.ts +74 -0
- package/dist/lib/memory/store.js +108 -0
- package/dist/lib/memory/supabaseMemoryService.d.ts +69 -9
- package/dist/lib/memory/supabaseMemoryService.js +239 -125
- package/dist/lib/models/capabilities.d.ts +50 -0
- package/dist/lib/models/capabilities.js +214 -4
- package/dist/lib/models/claudeLlm.d.ts +10 -1
- package/dist/lib/models/claudeLlm.js +57 -27
- package/dist/lib/models/endpoints.d.ts +100 -0
- package/dist/lib/models/endpoints.js +282 -0
- package/dist/lib/models/gateway.d.ts +1 -1
- package/dist/lib/models/gateway.js +4 -3
- package/dist/lib/models/gatewayLlm.d.ts +2 -2
- package/dist/lib/models/gatewayLlm.js +5 -5
- package/dist/lib/models/gptLlm.d.ts +22 -1
- package/dist/lib/models/gptLlm.js +77 -23
- package/dist/lib/models/grokLlm.d.ts +13 -4
- package/dist/lib/models/grokLlm.js +22 -7
- package/dist/lib/models/ollamaLlm.d.ts +26 -0
- package/dist/lib/models/ollamaLlm.js +43 -1
- package/dist/lib/models/openAiCompatibleLlm.d.ts +60 -2
- package/dist/lib/models/openAiCompatibleLlm.js +185 -36
- package/dist/lib/models/providerMap.js +9 -2
- package/dist/lib/models/registry.d.ts +24 -3
- package/dist/lib/models/registry.js +83 -23
- package/dist/lib/models/retry.d.ts +146 -0
- package/dist/lib/models/retry.js +305 -0
- package/dist/lib/models/schemaNormalize.d.ts +20 -0
- package/dist/lib/models/schemaNormalize.js +43 -0
- package/dist/lib/net/addressGuard.d.ts +60 -0
- package/dist/lib/net/addressGuard.js +216 -0
- package/dist/lib/observability/embeddings.d.ts +11 -2
- package/dist/lib/observability/embeddings.js +18 -14
- package/dist/lib/observability/metrics.d.ts +36 -0
- package/dist/lib/observability/metrics.js +86 -0
- package/dist/lib/observability/redact.d.ts +29 -0
- package/dist/lib/observability/redact.js +102 -0
- package/dist/lib/observability/supabaseSpanExporter.d.ts +2 -0
- package/dist/lib/observability/supabaseSpanExporter.js +22 -6
- package/dist/lib/observability/tracer.js +33 -3
- package/dist/lib/persistence/supabaseProvider.d.ts +11 -0
- package/dist/lib/persistence/supabaseProvider.js +32 -6
- package/dist/lib/registry.d.ts +87 -0
- package/dist/lib/registry.js +146 -0
- package/dist/lib/runtime/approvals.d.ts +44 -0
- package/dist/lib/runtime/approvals.js +67 -0
- package/dist/lib/runtime/syndicateTurn.d.ts +233 -0
- package/dist/lib/runtime/syndicateTurn.js +597 -0
- package/dist/lib/runtime/turnControl.d.ts +90 -0
- package/dist/lib/runtime/turnControl.js +131 -0
- package/dist/lib/session/supabaseSessionService.js +1 -1
- package/dist/lib/session/transcript.d.ts +9 -1
- package/dist/lib/session/transcript.js +15 -7
- package/dist/lib/storage/postgres/index.d.ts +98 -0
- package/dist/lib/storage/postgres/index.js +146 -0
- package/dist/lib/storage/postgres/memoryStore.d.ts +11 -0
- package/dist/lib/storage/postgres/memoryStore.js +82 -0
- package/dist/lib/storage/postgres/sessionService.d.ts +51 -0
- package/dist/lib/storage/postgres/sessionService.js +225 -0
- package/dist/lib/storage/postgres/taskQueue.d.ts +22 -0
- package/dist/lib/storage/postgres/taskQueue.js +101 -0
- package/dist/lib/storage/postgres/taskStore.d.ts +55 -0
- package/dist/lib/storage/postgres/taskStore.js +168 -0
- package/dist/lib/storage/schema.d.ts +27 -0
- package/dist/lib/storage/schema.js +50 -0
- package/dist/lib/storage/schemaVersion.d.ts +25 -0
- package/dist/lib/storage/schemaVersion.js +46 -0
- package/dist/lib/syndicateSchema.d.ts +275 -0
- package/dist/lib/syndicateSchema.js +534 -0
- package/dist/lib/toolRegistry.d.ts +15 -0
- package/dist/lib/toolRegistry.js +48 -15
- package/dist/lib/tools/collectionsSearchTool.js +1 -1
- package/dist/lib/tools/mcpServe.d.ts +2 -3
- package/dist/lib/tools/mcpServe.js +3 -4
- package/dist/lib/tools/mcpToolFactory.d.ts +1 -0
- package/dist/lib/tools/mcpToolFactory.js +13 -21
- package/dist/lib/tools/science/corrections.js +2 -2
- package/dist/lib/tools/science/europepmc.js +2 -2
- package/dist/lib/tools/science/openalex.js +2 -2
- package/dist/lib/tools/science/trials.d.ts +1 -1
- package/dist/lib/tools/science/trials.js +2 -2
- package/dist/lib/tools/scienceTools.d.ts +5 -5
- package/dist/lib/tools/scienceTools.js +5 -5
- package/dist/lib/tools/taskTools.d.ts +75 -26
- package/dist/lib/tools/taskTools.js +83 -51
- package/dist/lib/tools/toolContract.d.ts +17 -4
- package/dist/lib/tools/toolContract.js +13 -5
- package/dist/lib/tools/webExtractTool.d.ts +7 -10
- package/dist/lib/tools/webExtractTool.js +15 -51
- package/dist/lib/tools/webSearchTool.d.ts +2 -1
- package/dist/lib/tools/webSearchTool.js +4 -3
- package/dist/lib/tools/wikiTools.d.ts +2 -2
- package/dist/lib/tools/wikiTools.js +12 -12
- package/dist/lib/tools/xApiSearchTool.d.ts +10 -10
- package/dist/lib/tools/xApiSearchTool.js +6 -6
- package/dist/lib/tools/xSearchTool.js +1 -1
- package/dist/lib/wiki/agentRun.js +1 -1
- package/dist/lib/wiki/builder.js +21 -6
- package/dist/lib/wiki/dive.js +2 -2
- package/dist/lib/wiki/entities.d.ts +2 -2
- package/dist/lib/wiki/entities.js +7 -7
- package/dist/lib/wiki/extract.js +5 -1
- package/dist/lib/wiki/fill.js +5 -5
- package/dist/lib/wiki/format.d.ts +7 -5
- package/dist/lib/wiki/format.js +2 -2
- package/dist/lib/wiki/graph.js +2 -2
- package/dist/lib/wiki/lint.js +2 -2
- package/dist/lib/wiki/markdown.d.ts +1 -1
- package/dist/lib/wiki/markdown.js +6 -1
- package/dist/lib/wiki/pipeline.js +2 -2
- package/dist/lib/wiki/vault.js +2 -2
- package/dist/scripts/a2a_server.d.ts +17 -11
- package/dist/scripts/a2a_server.js +405 -1094
- package/dist/scripts/assistant_worker.d.ts +5 -3
- package/dist/scripts/assistant_worker.js +88 -58
- package/dist/scripts/db.d.ts +26 -0
- package/dist/scripts/db.js +179 -0
- package/dist/scripts/doctor.d.ts +2 -0
- package/dist/scripts/doctor.js +45 -2
- package/dist/scripts/init.d.ts +43 -0
- package/dist/scripts/init.js +152 -0
- package/dist/scripts/registry.d.ts +25 -0
- package/dist/scripts/registry.js +182 -0
- package/dist/scripts/skills_install.js +1 -1
- package/dist/scripts/syndicate_chat.js +159 -276
- package/package.json +75 -13
- package/skills/README.md +1 -1
- package/skills/briefs/melchizedek-author.md +3 -3
- package/skills/briefs/melchizedek-memory.md +3 -1
- package/skills/briefs/melchizedek-models.md +2 -2
- package/skills/briefs/melchizedek-serve.md +16 -12
- package/skills/briefs/skills-readme.md +2 -2
- package/skills/melchizedek-author/SKILL.md +2 -2
- package/skills/melchizedek-author/templates/minimal.yaml +1 -1
- package/skills/melchizedek-memory/SKILL.md +2 -0
- package/skills/melchizedek-models/SKILL.md +1 -1
- package/skills/melchizedek-serve/SKILL.md +18 -9
- package/dist/lib/tools/xPacket.d.ts +0 -102
- 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
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
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
|
-
| `
|
|
95
|
-
| `
|
|
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
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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 **
|
|
269
|
-
turns added since the last ingestion. It is in
|
|
270
|
-
|
|
271
|
-
|
|
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
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
|
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`
|
|
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
|
|
599
|
-
`A2A_SERVER_SECRET`
|
|
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
|