melchizedek-agents 0.17.0 → 0.18.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 +23 -3
- package/AGENT_SETUP.md +2 -1
- package/CHANGELOG.md +104 -0
- package/DOCUMENTATION.md +75 -9
- package/config/agents/syndicateSchema.yaml +13 -0
- package/db/hardening.sql +4 -3
- package/db/migrations/0012_audit_log.sql +98 -0
- package/dist/lib/a2a/app.d.ts +20 -0
- package/dist/lib/a2a/app.js +94 -4
- package/dist/lib/a2a/executor.d.ts +34 -4
- package/dist/lib/a2a/executor.js +68 -9
- package/dist/lib/compile.d.ts +1 -2
- package/dist/lib/compile.js +33 -6
- package/dist/lib/config.d.ts +7 -0
- package/dist/lib/config.js +7 -0
- package/dist/lib/loadEnv.d.ts +5 -0
- package/dist/lib/loadEnv.js +7 -0
- package/dist/lib/loadSyndicate.d.ts +4 -0
- package/dist/lib/memory/providers.d.ts +27 -0
- package/dist/lib/memory/providers.js +28 -0
- package/dist/lib/models/fallback.d.ts +41 -0
- package/dist/lib/models/fallback.js +105 -0
- package/dist/lib/observability/audit.d.ts +21 -0
- package/dist/lib/observability/audit.js +39 -0
- package/dist/lib/observability/metrics.d.ts +6 -0
- package/dist/lib/observability/tracer.d.ts +13 -0
- package/dist/lib/observability/tracer.js +16 -2
- package/dist/lib/runtime/syndicateTurn.d.ts +3 -1
- package/dist/lib/runtime/syndicateTurn.js +3 -1
- package/dist/lib/storage/postgres/index.d.ts +3 -0
- package/dist/lib/storage/postgres/index.js +2 -0
- package/dist/lib/syndicateSchema.d.ts +8 -0
- package/dist/lib/syndicateSchema.js +19 -4
- package/dist/lib/tools/mcpToolFactory.d.ts +26 -0
- package/dist/lib/tools/mcpToolFactory.js +61 -4
- package/dist/lib/tools/openapiTools.d.ts +6 -0
- package/dist/lib/tools/openapiTools.js +31 -1
- package/dist/scripts/a2a_server.js +51 -8
- package/dist/scripts/db.d.ts +9 -0
- package/dist/scripts/db.js +30 -15
- package/package.json +3 -3
package/.env.example
CHANGED
|
@@ -129,6 +129,8 @@ A2A_SERVER_SECRET=
|
|
|
129
129
|
# Used when compiling the AgentCard that advertises this agent to A2A clients.
|
|
130
130
|
# Defaults to http://localhost:4000 if not set.
|
|
131
131
|
# Only needed for cloud deployments where the server has a real public URL.
|
|
132
|
+
# With it set, the server also refuses to start until A2A_AUTH,
|
|
133
|
+
# A2A_SERVED_AGENTS and A2A_TRUST_PROXY are set explicitly (ADR 0039).
|
|
132
134
|
PUBLIC_URL=
|
|
133
135
|
|
|
134
136
|
# ── A2A server: identity and billing (all optional; ADR 0017, 0025) ─────────
|
|
@@ -141,6 +143,7 @@ PUBLIC_URL=
|
|
|
141
143
|
# jwt a JWT from your identity provider; the scope comes from its claims.
|
|
142
144
|
# header an authenticating gateway in front sets the user header; requires
|
|
143
145
|
# A2A_SERVER_SECRET, which only the gateway holds.
|
|
146
|
+
# Required with PUBLIC_URL; prefer callers or jwt there.
|
|
144
147
|
# A2A_AUTH=secret
|
|
145
148
|
# callers: `name:sha256[:scope]` entries separated by ';'. The config holds only
|
|
146
149
|
# each token's SHA-256. Mint one: npx melchizedek-serve --new-caller <name>
|
|
@@ -171,8 +174,8 @@ PUBLIC_URL=
|
|
|
171
174
|
# The server refuses a database behind the migrations it ships (run
|
|
172
175
|
# `npx melchizedek-db apply`); this starts it anyway.
|
|
173
176
|
# ALLOW_SCHEMA_MISMATCH=false
|
|
174
|
-
# Only these agent ids are served at /<agentId>/... (comma list). Unset
|
|
175
|
-
# syndicate the loader can find.
|
|
177
|
+
# Only these agent ids are served at /<agentId>/... (comma list). Unset, or *,
|
|
178
|
+
# = any syndicate the loader can find. Required with PUBLIC_URL (* allowed).
|
|
176
179
|
# A2A_SERVED_AGENTS=support_triage,research_brief
|
|
177
180
|
# Per-task wall-clock budget (ms; 0 = none). Default 15 minutes.
|
|
178
181
|
# A2A_TASK_TIMEOUT_MS=900000
|
|
@@ -191,11 +194,23 @@ PUBLIC_URL=
|
|
|
191
194
|
# A2A_RATE_LIMIT_MAX=60
|
|
192
195
|
# A2A_RATE_LIMIT_WINDOW_MS=900000
|
|
193
196
|
# A2A_AUTH_FAILURE_MAX=30
|
|
194
|
-
# Express trust-proxy: proxy hop count, true/false, or a subnet list. Default
|
|
197
|
+
# Express trust-proxy: proxy hop count, true/false, or a subnet list. Default
|
|
198
|
+
# false (no proxy is trusted). Required with PUBLIC_URL: 1 behind one load
|
|
199
|
+
# balancer, false when clients connect directly.
|
|
195
200
|
# A2A_TRUST_PROXY=1
|
|
201
|
+
# Concurrent tasks for one end user (scope), and for one caller. 0 = unlimited.
|
|
202
|
+
# A2A_MAX_CONCURRENT_PER_SCOPE=4
|
|
203
|
+
# A2A_MAX_CONCURRENT_PER_CALLER=0
|
|
204
|
+
|
|
205
|
+
# OpenAPI tools: the exact environment variables an `auth:` may name. Unset, any
|
|
206
|
+
# variable but the framework's own settings (this file's) is allowed.
|
|
207
|
+
# OPENAPI_CREDENTIAL_ENVS=WEATHER_API_KEY,CRM_TOKEN
|
|
196
208
|
# A2A_BODY_LIMIT=1mb
|
|
197
209
|
# How long SIGTERM waits for running tasks before canceling them.
|
|
198
210
|
# A2A_SHUTDOWN_GRACE_MS=25000
|
|
211
|
+
# Of that, how long /readyz fails while requests are still served, so a load
|
|
212
|
+
# balancer deregisters this instance before the listener closes.
|
|
213
|
+
# A2A_SHUTDOWN_DELAY_MS=5000
|
|
199
214
|
# Daily budgets per UTC day (ADR 0026), per caller and per scope (one end
|
|
200
215
|
# user), on tasks, model calls and tokens. A task over budget ends `rejected`
|
|
201
216
|
# with the reason. Counted in Postgres/Supabase (db/migrations/0004) when
|
|
@@ -279,6 +294,11 @@ WIKI_AGENT_MODEL=
|
|
|
279
294
|
# retried with jittered backoff, honouring Retry-After up to 60 s, before any
|
|
280
295
|
# output is produced. Total attempts per model call; 1 turns retries off.
|
|
281
296
|
# MODEL_RETRY_MAX_ATTEMPTS=3
|
|
297
|
+
# For agents with a fallback_model: after this many consecutive provider-side
|
|
298
|
+
# failures a provider's circuit opens and those agents answer from their
|
|
299
|
+
# fallback for the cooldown. 0 disables the breaker.
|
|
300
|
+
# MODEL_BREAKER_THRESHOLD=5
|
|
301
|
+
# MODEL_BREAKER_COOLDOWN_MS=30000
|
|
282
302
|
# Per-attempt timeout for xAI (Grok) calls, ms; floor 120000.
|
|
283
303
|
# XAI_TIMEOUT_MS=600000
|
|
284
304
|
|
package/AGENT_SETUP.md
CHANGED
|
@@ -193,7 +193,8 @@ user id.
|
|
|
193
193
|
|
|
194
194
|
Deploying the server itself (Heroku, Fly, a VPS) needs `PUBLIC_URL` set
|
|
195
195
|
— at which point the server refuses to start without the bearer secret,
|
|
196
|
-
|
|
196
|
+
and until `A2A_AUTH`, `A2A_SERVED_AGENTS` and `A2A_TRUST_PROXY` are set
|
|
197
|
+
explicitly, by design. Run `db/hardening.sql` before real user data arrives, and
|
|
197
198
|
read the deployment notes in `lib/memory/README.md` end to end. The
|
|
198
199
|
patient in module 2.02's specimen would expect no less of you.
|
|
199
200
|
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,110 @@ Consumers of the package read this file; it records changes to the
|
|
|
4
4
|
**published API surface** (the exports map in `package.json`, the bins,
|
|
5
5
|
the starter pack and the templates), not the repo's full history.
|
|
6
6
|
|
|
7
|
+
## 0.18.0 — 2026-10-06
|
|
8
|
+
|
|
9
|
+
### Breaking — read before upgrading
|
|
10
|
+
|
|
11
|
+
- **A public server states its posture (ADR 0039).** With `PUBLIC_URL` set,
|
|
12
|
+
the server refuses to start until all three are set explicitly; the boot
|
|
13
|
+
message names each one missing:
|
|
14
|
+
- `A2A_AUTH`: `callers` or `jwt` (recommended), `header`, or `secret`
|
|
15
|
+
(one shared secret: any holder can act as any user via `X-User-Id`).
|
|
16
|
+
- `A2A_SERVED_AGENTS`: the agent ids to serve, or `*` for every agent.
|
|
17
|
+
- `A2A_TRUST_PROXY`: proxies in front (`1` behind one load balancer), or
|
|
18
|
+
`false` when clients connect directly.
|
|
19
|
+
- **`A2A_TRUST_PROXY` defaults to `false`** (was 1): a server reached
|
|
20
|
+
directly no longer takes the client's `X-Forwarded-For` as its address.
|
|
21
|
+
- **A turn stops at 50 model calls** when the YAML sets no `max_steps`
|
|
22
|
+
(`DEFAULT_MAX_STEPS`). A syndicate that needs more sets `max_steps`.
|
|
23
|
+
- **One end user runs at most 4 tasks at once** (`A2A_MAX_CONCURRENT_PER_SCOPE`,
|
|
24
|
+
`maxConcurrentPerScope`; 0 = unlimited). `A2A_MAX_CONCURRENT_PER_CALLER`
|
|
25
|
+
adds a per-caller cap, off by default. The refusal names the cap.
|
|
26
|
+
|
|
27
|
+
- **The `@google/adk` peer range is `~2.2.0`** (was `^2.2.0`): the tested
|
|
28
|
+
minor. A new ADK minor can bring a second `@google/genai` copy, whose
|
|
29
|
+
response shapes the framework pins; the range widens when a release is
|
|
30
|
+
tested against it. ADK 2.2.0 is the latest published, so no install
|
|
31
|
+
changes today.
|
|
32
|
+
|
|
33
|
+
- **An OpenAPI `auth` may not name one of the framework's own settings**
|
|
34
|
+
(the database URL, a provider key, an `A2A_` secret: anything under the
|
|
35
|
+
framework's prefixes). The YAML chooses the host too, so this was a way
|
|
36
|
+
to send a secret anywhere. `OPENAPI_CREDENTIAL_ENVS` makes the rule an
|
|
37
|
+
exact allowlist. ADR 0041.
|
|
38
|
+
- **MCP tool descriptions are cut at 1,000 characters and results at
|
|
39
|
+
20,000**, each marked where it was cut.
|
|
40
|
+
|
|
41
|
+
### Changes
|
|
42
|
+
|
|
43
|
+
- **An append-only audit trail** (`melchizedek_audit`, migration
|
|
44
|
+
`0012_audit_log`; ADR 0042). Every failed authentication, task outcome
|
|
45
|
+
and erasure is recorded with the caller, the source address, the agent
|
|
46
|
+
and task ids and a scope hash, and no content. A trigger refuses UPDATE
|
|
47
|
+
and DELETE; `melchizedek_prune_audit(days)` is the retention path.
|
|
48
|
+
`postgresStorage` supplies the sink (`audit`), and `createA2AApp` takes
|
|
49
|
+
an `audit` option for any other. Run `npx melchizedek-db apply`.
|
|
50
|
+
- **The server says where memory sends transcripts.** Long-term memory's
|
|
51
|
+
extraction and embedding providers default to Gemini whatever the agents
|
|
52
|
+
run on; a syndicate whose agents use other providers now gets one boot
|
|
53
|
+
warning naming the providers memory reaches and the variables that move
|
|
54
|
+
it (`memoryDestinations`, `memoryCrossesProviders`). DOCUMENTATION gains
|
|
55
|
+
"Where your data goes": every destination, its retention, and the setting
|
|
56
|
+
that changes it.
|
|
57
|
+
- **A request can be followed across systems.** Every task record
|
|
58
|
+
(`onTaskEnd`, and the JSON log line) carries `taskId` and `traceId`; the
|
|
59
|
+
trace id is the one the spans and the ledger row carry. A request with a
|
|
60
|
+
W3C `traceparent` header is linked from the turn's root span
|
|
61
|
+
(`caller.trace_id`, and `callerTraceId` on the record); the turn keeps
|
|
62
|
+
its own trace id. `validTraceparent` and `callerSpanContext` are exported
|
|
63
|
+
from `melchizedek-agents/observability/tracer`. ADR 0043.
|
|
64
|
+
- **`fallback_model:`, an agent key, and a per-provider circuit breaker.**
|
|
65
|
+
When an agent's model fails provider-side (5xx, 429, a reset, after its
|
|
66
|
+
retries) before producing output, its `fallback_model` answers; after
|
|
67
|
+
`MODEL_BREAKER_THRESHOLD` consecutive failures (default 5) the provider
|
|
68
|
+
is skipped for `MODEL_BREAKER_COOLDOWN_MS` (30 s) by agents that have a
|
|
69
|
+
fallback. A 4xx, a canceled turn and a half-finished stream are never
|
|
70
|
+
redirected. ADR 0044.
|
|
71
|
+
- **`mcp_tools:`, an agent key**: the MCP server's tools the agent may use;
|
|
72
|
+
any other tool the server lists is not exposed. On a dispatch route,
|
|
73
|
+
`require_approval` may name a listed MCP tool, so a write waits for a
|
|
74
|
+
person. `closeMcpConnections()` closes every open MCP connection.
|
|
75
|
+
- **`melchizedek-db apply` is one transaction under an advisory lock.** The
|
|
76
|
+
whole install (migrations, hardening, telemetry) runs as one psql
|
|
77
|
+
`--single-transaction` script that takes `pg_advisory_xact_lock` first: a
|
|
78
|
+
failure anywhere rolls everything back, and two applies started together
|
|
79
|
+
run one after the other. The output still names each file.
|
|
80
|
+
- `TaskLimiter` takes per-scope and per-caller limits and says which one
|
|
81
|
+
refused (`refusal()`); `createA2AApp` takes `maxConcurrentPerScope` and
|
|
82
|
+
`maxConcurrentPerCaller`; `DEFAULT_MAX_CONCURRENT_PER_SCOPE` is exported
|
|
83
|
+
from `melchizedek-agents/a2a` and `DEFAULT_MAX_STEPS` from
|
|
84
|
+
`melchizedek-agents/config`.
|
|
85
|
+
|
|
86
|
+
## 0.17.1 — 2026-10-06
|
|
87
|
+
|
|
88
|
+
- **Fix: a rolling deploy no longer refuses requests.** On SIGTERM the
|
|
89
|
+
server closed its listener at once, while the load balancer was still
|
|
90
|
+
sending it traffic. It now fails `/readyz` first and keeps serving for
|
|
91
|
+
`A2A_SHUTDOWN_DELAY_MS` (default 5 s), then closes and drains. The delay
|
|
92
|
+
counts inside `A2A_SHUTDOWN_GRACE_MS`, so a stop takes no longer than
|
|
93
|
+
before. `A2AApp.markUnready()` does the same for an embedding app.
|
|
94
|
+
ADR 0038.
|
|
95
|
+
- **`/readyz` fails while durable storage does not answer** (503,
|
|
96
|
+
`reason: 'storage'`), so an instance that lost its database leaves the
|
|
97
|
+
rotation instead of failing turns. The cause is logged once per outage;
|
|
98
|
+
the answer is cached for 2 s and concurrent probes share one read, so the
|
|
99
|
+
unauthenticated route cannot be used to flood the database.
|
|
100
|
+
- **Fix: the MCP client no longer follows redirects past the SSRF guard.**
|
|
101
|
+
The SSE transport used the default fetch, which follows redirects; it now
|
|
102
|
+
uses `mcpFetch`, under the rule 0.17.0 gave OpenAPI tools: same-origin hops
|
|
103
|
+
get the server's check, cross-origin hops the full guard and no
|
|
104
|
+
credential header.
|
|
105
|
+
- **Fix: a failed MCP connect closes its transport.** The SSE stream's
|
|
106
|
+
reconnect timer kept running after `createMcpTools` gave up, for the life
|
|
107
|
+
of the process.
|
|
108
|
+
- **`MELCHIZEDEK_DOTENV=off`** makes every bin ignore `.env` files and run
|
|
109
|
+
on its environment alone.
|
|
110
|
+
|
|
7
111
|
## 0.17.0 — 2026-10-06
|
|
8
112
|
|
|
9
113
|
### Read before upgrading
|
package/DOCUMENTATION.md
CHANGED
|
@@ -110,6 +110,8 @@ Field reference:
|
|
|
110
110
|
| `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. |
|
|
111
111
|
| `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`. |
|
|
112
112
|
| `includeContents` / `outputKey` / `globalInstruction` / `disallowTransferToParent` / `disallowTransferToPeers` | agent | Passed through to ADK's LlmAgent. `includeContents: none` makes an agent see only the current message. |
|
|
113
|
+
| `fallback_model` | any agent | A model, ideally on another provider, that answers when this agent's model fails provider-side (5xx, 429, a connection reset, after its own retries) before producing any output, or while that provider's circuit is open: after `MODEL_BREAKER_THRESHOLD` consecutive provider failures (default 5; 0 disables) the provider is skipped for `MODEL_BREAKER_COOLDOWN_MS` (default 30 s). A 4xx and a canceled turn are never redirected, and a stream that already produced text is never replayed elsewhere ([ADR 0044](./wiki/decisions/0044-fallback-model-and-circuit-breaker.md)). |
|
|
114
|
+
| `mcp_tools` | subagent | The MCP server's tools this agent may use; any other tool the server lists is not exposed. On a dispatch route `require_approval` may name them ([ADR 0041](./wiki/decisions/0041-tool-vendors-get-least-privilege.md)). |
|
|
113
115
|
| `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. |
|
|
114
116
|
| `openapi` | any agent | HTTP APIs as tools, each from an OpenAPI 3 spec file (§3, OpenAPI tools): `spec`, and optionally `operations` (default: the GET operations only), `auth` (from environment variables), `base_url`, `prefix`. |
|
|
115
117
|
| `code_execution` | any Gemini agent | `"gemini"`: the model writes Python and Gemini runs it in Google's server-side sandbox, returning the output to the model; nothing runs on this host. For arithmetic, data and checks a model gets wrong in its head. Gemini models only ([ADR 0033](./wiki/decisions/0033-context-task-code.md)). |
|
|
@@ -180,7 +182,11 @@ writes is exposed only by naming it, and a named operation can be listed
|
|
|
180
182
|
under `require_approval` (as written under `operations`) so a person
|
|
181
183
|
approves each call. `auth` names an environment variable, never a value
|
|
182
184
|
(`bearer_env`, or `api_key` with `in: header | query` and `name`); an unset
|
|
183
|
-
variable fails the compile, and
|
|
185
|
+
variable fails the compile, and so does one of the framework's own settings
|
|
186
|
+
(the database URL, a provider key, an `A2A_` secret: anything `.env.example`
|
|
187
|
+
documents), since the YAML chooses the host it goes to.
|
|
188
|
+
`OPENAPI_CREDENTIAL_ENVS`, when set, is the exact list of variables an `auth`
|
|
189
|
+
may name. A refused or unset variable fails the compile, and a static token is applied to the request,
|
|
184
190
|
never stored in session state. Every server must be http(s) and pass the
|
|
185
191
|
SSRF guard: its literal rules when the agent compiles, the full check with
|
|
186
192
|
DNS before each call; `ALLOW_PRIVATE_OPENAPI=true` permits private hosts for
|
|
@@ -223,7 +229,11 @@ npx melchizedek-db status # schema version, hardening, session counts
|
|
|
223
229
|
|
|
224
230
|
That runs the migrations in [`db/migrations/`](./db/migrations/) and then
|
|
225
231
|
[`db/hardening.sql`](./db/hardening.sql). Both are idempotent, so re-running
|
|
226
|
-
them is also the upgrade path from any earlier layout.
|
|
232
|
+
them is also the upgrade path from any earlier layout. `apply` runs the whole
|
|
233
|
+
install as one transaction under an advisory lock: if any statement fails,
|
|
234
|
+
nothing is changed, and two applies started together run one after the other.
|
|
235
|
+
Rolling back a migration that succeeded is a restore from backup (§6), since
|
|
236
|
+
the migrations only move forward.
|
|
227
237
|
|
|
228
238
|
The tables go in the `public` schema by default. To keep them out of reach of
|
|
229
239
|
a REST layer (Supabase exposes `public`), install into a private schema and
|
|
@@ -545,9 +555,28 @@ or pass your own `resolveRequest`.
|
|
|
545
555
|
The counts live in the `melchizedek_usage` table (`db/migrations/0004_usage.sql`)
|
|
546
556
|
when Postgres or Supabase is configured, else in process memory. A store
|
|
547
557
|
that cannot be read refuses the task.
|
|
548
|
-
- **One record per task** (option `onTaskEnd`):
|
|
549
|
-
scope, status, reason, duration, model calls and
|
|
558
|
+
- **One record per task** (option `onTaskEnd`): task id, trace id, agent,
|
|
559
|
+
caller, a hash of the scope, status, reason, duration, model calls and
|
|
560
|
+
tokens. The trace id is the one the turn's spans and its ledger row carry,
|
|
561
|
+
and the audit trail's `task.end` row names both ids, so one id joins the
|
|
562
|
+
log, the ledger, the audit trail and your tracing backend. A request
|
|
563
|
+
carrying a W3C `traceparent` header is linked, not joined: the turn's root
|
|
564
|
+
span links to the caller's span and records `caller.trace_id`, and the
|
|
565
|
+
record carries `callerTraceId`. The turn keeps a trace id of its own, since
|
|
566
|
+
ledger attribution and erasure key on it and a caller must not choose it.
|
|
550
567
|
`A2A_LOG_FORMAT=json` prints every server line as JSON, this record included.
|
|
568
|
+
- **The audit trail** (`melchizedek_audit`, `db/migrations/0012_audit_log.sql`;
|
|
569
|
+
option `audit`, supplied by `postgresStorage`): one row per failed
|
|
570
|
+
authentication (`auth.failure`), task outcome (`task.end`) and erasure
|
|
571
|
+
(`memory.erase`), with the caller's name, the source address, the agent and
|
|
572
|
+
task ids, and a hash of the scope. Never a scope key and never conversation
|
|
573
|
+
content. A trigger refuses UPDATE and DELETE; rows leave only through
|
|
574
|
+
`SELECT melchizedek_prune_audit(<days>)`, which you schedule (pg_cron, or a
|
|
575
|
+
job) for the retention your evidence needs, since the source address is
|
|
576
|
+
personal data. A failed write is logged once and each event is printed to
|
|
577
|
+
stderr as JSON until writes recover. A database owner can still alter the
|
|
578
|
+
table; ship the rows to a store you do not administer when the evidence must
|
|
579
|
+
survive that.
|
|
551
580
|
- **Rate limit**: with an authenticator it counts per caller (an operator's
|
|
552
581
|
backend) or per scope (an end user); under the shared secret, per IP.
|
|
553
582
|
- **The telemetry ledger is redacted before it is written**:
|
|
@@ -556,6 +585,30 @@ or pass your own `resolveRequest`.
|
|
|
556
585
|
`setTelemetryRedactor(fn)`. Sessions and memory are not redacted; use
|
|
557
586
|
`DELETE /memory` there.
|
|
558
587
|
|
|
588
|
+
#### Where your data goes
|
|
589
|
+
|
|
590
|
+
What a deployment sends where, how long it is kept, and the setting that
|
|
591
|
+
changes it. "Your Postgres" is the database `DATABASE_URL` (or Supabase)
|
|
592
|
+
names; nothing is kept by the framework anywhere else.
|
|
593
|
+
|
|
594
|
+
| Data | Goes to | Kept | Change it with |
|
|
595
|
+
|---|---|---|---|
|
|
596
|
+
| The prompt: the user's message, the conversation, tool results | The model provider of each agent that runs (the model-id prefix) | The provider's policy | The `model:` lines in the YAML; `ollama/` ids stay on your machine |
|
|
597
|
+
| Tool calls (`web_search`, `web_extract`, OpenAPI, MCP) | The tool's own host | That host's policy | The agent's `tools:`, `openapi:`, `mcp_server_url` / `mcp_tools` |
|
|
598
|
+
| Sessions (conversation history) | Your Postgres, `adk_sessions` | 7 days after the last message | `ttlDays` (`postgresStorage`) |
|
|
599
|
+
| Long-term memory facts | Your Postgres, `adk_memory_facts`; **the transcript is also sent to the extraction and embedding providers, Gemini by default whatever the agents run on** (the server warns at boot when they differ) | Until erased, or `memory_retention_days` | `MEMORY_EXTRACTION_MODEL`, `MEMORY_EMBEDDING_PROVIDER`, `memory_retention_days` |
|
|
600
|
+
| The ledger: each turn's input, output and tool results (key-shaped secrets redacted) | Your Postgres, `adk_turns`, `adk_telemetry` | **Until you prune it**: schedule `melchizedek_prune_telemetry(<days>)` | `TELEMETRY_REDACT`, the prune's `turn_days` |
|
|
601
|
+
| Sampled full model requests | Your Postgres, `adk_payloads` | 30 days | `TELEMETRY_PAYLOADS`, `TELEMETRY_PAYLOAD_TTL_DAYS` |
|
|
602
|
+
| Traces, when `OTEL_EXPORTER_OTLP_ENDPOINT` is set | Your OTLP collector, conversation content included with key-shaped secrets redacted (`off` drops it) | Your collector's policy | `OTEL_EXPORT_CONTENT` (`redacted`, `off`, `raw`) |
|
|
603
|
+
| The audit trail (no content) | Your Postgres, `melchizedek_audit` | Until `melchizedek_prune_audit(<days>)` | The prune schedule |
|
|
604
|
+
| The task record (no content) | stdout | Your log platform's policy | `A2A_LOG_FORMAT` |
|
|
605
|
+
|
|
606
|
+
`DELETE /memory` erases a user from every Postgres store above except the
|
|
607
|
+
audit trail, which holds no content, only a hash of the scope. It does not
|
|
608
|
+
reach a provider's own retention: read each provider's data terms, and the
|
|
609
|
+
business associate or data processing agreement you need with it, before
|
|
610
|
+
sending it regulated data.
|
|
611
|
+
|
|
559
612
|
#### Who pays
|
|
560
613
|
|
|
561
614
|
`A2A_KEY_MODE` (option `keyMode`), whatever the authenticator:
|
|
@@ -687,9 +740,13 @@ subagent or a workflow node listing it is a load error. In code,
|
|
|
687
740
|
| `A2A_AUTH_FAILURE_MAX` failed logins per IP per 15 min, then blocked | 30 |
|
|
688
741
|
| `A2A_TASK_TIMEOUT_MS` per task | 15 min |
|
|
689
742
|
| `A2A_MAX_CONCURRENT_TASKS` | unlimited |
|
|
690
|
-
| `
|
|
743
|
+
| `A2A_MAX_CONCURRENT_PER_SCOPE`: tasks at once for one end user | 4 |
|
|
744
|
+
| `A2A_MAX_CONCURRENT_PER_CALLER`: tasks at once for one caller | unlimited |
|
|
745
|
+
| `max_steps` (YAML): model calls per turn, subagents included | 50 |
|
|
746
|
+
| `A2A_TRUST_PROXY`: proxies in front (required with `PUBLIC_URL`) | none trusted |
|
|
691
747
|
| `A2A_BODY_LIMIT` | 1 MB |
|
|
692
748
|
| `A2A_SHUTDOWN_GRACE_MS`: SIGTERM waits for running tasks | 25 s |
|
|
749
|
+
| `A2A_SHUTDOWN_DELAY_MS`: of that, `/readyz` fails while requests are still served | 5 s |
|
|
693
750
|
|
|
694
751
|
The rate limit and the failed-login limit count in the process by default,
|
|
695
752
|
so each replica keeps its own window. With `A2A_REDIS_URL` set (and the
|
|
@@ -719,7 +776,11 @@ Without `A2A_SERVER_SECRET` the server binds `127.0.0.1` only; binding
|
|
|
719
776
|
another `HOST` requires the secret or `ALLOW_UNAUTHENTICATED=true`. With
|
|
720
777
|
`PUBLIC_URL` set it refuses to start without the secret, with the
|
|
721
778
|
`.env.example` placeholder as the secret, or against an unhardened
|
|
722
|
-
database (unless `ALLOW_UNHARDENED_DB=true`)
|
|
779
|
+
database (unless `ALLOW_UNHARDENED_DB=true`), and until the deployment
|
|
780
|
+
states its posture: `A2A_AUTH` (prefer `callers` or `jwt`; `secret` lets any
|
|
781
|
+
holder of the secret act as any user), `A2A_SERVED_AGENTS` (a list, or `*`
|
|
782
|
+
for every agent) and `A2A_TRUST_PROXY` (the number of proxies in front, or
|
|
783
|
+
`false`). The refusal names each one missing. With durable storage it reads
|
|
723
784
|
`melchizedek_schema_version` and refuses to start on a database behind the
|
|
724
785
|
migrations it ships, naming `melchizedek-db apply` (or
|
|
725
786
|
`ALLOW_SCHEMA_MISMATCH=true` to start anyway); a database ahead of it only
|
|
@@ -745,9 +806,14 @@ local conversation keeps talking to the same remote conversation.
|
|
|
745
806
|
`Dockerfile` builds the compiled server and runs it as a non-root user with
|
|
746
807
|
a health check; `compose.yaml` adds optional Ollama and Phoenix (traces).
|
|
747
808
|
`npx melchizedek-db print|apply|status` installs and checks the database.
|
|
748
|
-
Set `PUBLIC_URL`, `A2A_SERVER_SECRET`
|
|
749
|
-
secret manager; give the orchestrator's stop timeout at least
|
|
750
|
-
`A2A_SHUTDOWN_GRACE_MS`.
|
|
809
|
+
Set `PUBLIC_URL`, `A2A_SERVER_SECRET`, `A2A_AUTH`, `A2A_SERVED_AGENTS`,
|
|
810
|
+
`A2A_TRUST_PROXY` and the provider keys from your secret manager; give the orchestrator's stop timeout at least
|
|
811
|
+
`A2A_SHUTDOWN_GRACE_MS`. On SIGTERM the server first fails `/readyz` and
|
|
812
|
+
keeps serving for `A2A_SHUTDOWN_DELAY_MS` so the load balancer deregisters
|
|
813
|
+
it, then closes the listener and drains running tasks for the rest of the
|
|
814
|
+
grace period; point the readiness probe at `/readyz`, which also fails while
|
|
815
|
+
durable storage does not answer. `MELCHIZEDEK_DOTENV=off` makes every bin
|
|
816
|
+
ignore `.env` files and run on its environment alone.
|
|
751
817
|
|
|
752
818
|
#### Backups
|
|
753
819
|
|
|
@@ -185,6 +185,12 @@ orchestrator:
|
|
|
185
185
|
# Subagents inherit this if not set on their own config.
|
|
186
186
|
model: "gemini-3.5-flash-lite"
|
|
187
187
|
|
|
188
|
+
# [optional] A model that answers when `model` fails provider-side (5xx,
|
|
189
|
+
# 429, a connection reset) before producing anything, or while that
|
|
190
|
+
# provider's circuit is open (MODEL_BREAKER_THRESHOLD, ADR 0044). Put it
|
|
191
|
+
# on another provider. A 4xx is never redirected.
|
|
192
|
+
# fallback_model: "claude-haiku-4-5"
|
|
193
|
+
|
|
188
194
|
# [required] The agent's system prompt / persona.
|
|
189
195
|
# Maps to: instruction
|
|
190
196
|
instruction: |
|
|
@@ -414,6 +420,13 @@ subagents:
|
|
|
414
420
|
# See config/agents/librarian.yaml for the worked example.
|
|
415
421
|
# mcp_server_url: "http://localhost:8931/sse"
|
|
416
422
|
|
|
423
|
+
# [optional] The MCP server's tools this agent may use, by name. Any
|
|
424
|
+
# other tool the server lists is not exposed (a server's list is its
|
|
425
|
+
# own to change). Without it, every listed tool is. On a dispatch
|
|
426
|
+
# route, require_approval may name these (ADR 0041). Descriptions are
|
|
427
|
+
# cut at 1,000 characters and results at 20,000.
|
|
428
|
+
# mcp_tools: [wiki_search, wiki_read]
|
|
429
|
+
|
|
417
430
|
# [optional] Agent Skills for this subagent; same block as on the
|
|
418
431
|
# orchestrator. `scripts: "local"` is allowed here only under
|
|
419
432
|
# `dispatch:` (a delegated subagent cannot pause the turn, ADR 0028).
|
package/db/hardening.sql
CHANGED
|
@@ -51,7 +51,7 @@ DO $$
|
|
|
51
51
|
DECLARE t text;
|
|
52
52
|
BEGIN
|
|
53
53
|
FOREACH t IN ARRAY ARRAY['adk_telemetry', 'adk_turns', 'adk_payloads', 'adk_verdicts', 'adk_labels',
|
|
54
|
-
'adk_session_events', 'adk_a2a_tasks', 'melchizedek_usage'] LOOP
|
|
54
|
+
'adk_session_events', 'adk_a2a_tasks', 'melchizedek_usage', 'melchizedek_audit'] LOOP
|
|
55
55
|
IF to_regclass('public.' || t) IS NOT NULL THEN
|
|
56
56
|
EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t);
|
|
57
57
|
EXECUTE format('REVOKE ALL ON %I FROM anon, authenticated', t);
|
|
@@ -133,7 +133,7 @@ BEGIN
|
|
|
133
133
|
'melchizedek_registry_versions_immutable', 'melchizedek_registry_publish',
|
|
134
134
|
'melchizedek_registry_activate', 'melchizedek_memory_commit',
|
|
135
135
|
'melchizedek_memory_dimensions', 'melchizedek_prune_memory_ingest',
|
|
136
|
-
'melchizedek_prune_memory')
|
|
136
|
+
'melchizedek_prune_memory', 'melchizedek_prune_audit')
|
|
137
137
|
LOOP
|
|
138
138
|
EXECUTE format('REVOKE ALL ON FUNCTION %s FROM PUBLIC, anon, authenticated', fn);
|
|
139
139
|
IF has_service_role THEN
|
|
@@ -160,7 +160,8 @@ AS $$
|
|
|
160
160
|
'adk_turns', 'adk_payloads', 'adk_verdicts', 'adk_labels',
|
|
161
161
|
'adk_agent_registry', 'adk_agent_registry_versions',
|
|
162
162
|
'adk_session_events', 'adk_a2a_tasks', 'melchizedek_usage',
|
|
163
|
-
'melchizedek_memory_ingest', 'melchizedek_tasks', 'melchizedek_task_owners'
|
|
163
|
+
'melchizedek_memory_ingest', 'melchizedek_tasks', 'melchizedek_task_owners',
|
|
164
|
+
'melchizedek_audit');
|
|
164
165
|
$$;
|
|
165
166
|
|
|
166
167
|
REVOKE ALL ON FUNCTION melchizedek_rls_status() FROM PUBLIC, anon, authenticated;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 0012_audit_log.sql — an append-only record of who did what, when (ADR 0042)
|
|
3
|
+
-- ============================================================================
|
|
4
|
+
-- One row per security-relevant event: a failed authentication, a task's
|
|
5
|
+
-- outcome, an erasure. The ledger (adk_turns) records what a turn said;
|
|
6
|
+
-- this records who asked, how they authenticated, from where, and what
|
|
7
|
+
-- happened, which is what an auditor asks for.
|
|
8
|
+
--
|
|
9
|
+
-- What a row holds: the event, its outcome, the caller's name and auth
|
|
10
|
+
-- method, a SHA-256 prefix of the scope (never the scope key itself), the
|
|
11
|
+
-- source address, the agent and task ids, and a small detail object. No
|
|
12
|
+
-- conversation content, ever.
|
|
13
|
+
--
|
|
14
|
+
-- Append-only: a trigger refuses UPDATE and DELETE. Rows leave only
|
|
15
|
+
-- through melchizedek_prune_audit(days), which removes rows older than the
|
|
16
|
+
-- retention period (the source address is personal data; keep it no longer
|
|
17
|
+
-- than the evidence needs). TRUNCATE is revoked from every API role. A
|
|
18
|
+
-- database superuser can still alter the table; ship the rows to a log
|
|
19
|
+
-- store you do not administer when the evidence must survive that.
|
|
20
|
+
-- Idempotent.
|
|
21
|
+
-- ============================================================================
|
|
22
|
+
|
|
23
|
+
CREATE TABLE IF NOT EXISTS melchizedek_audit (
|
|
24
|
+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
25
|
+
at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
26
|
+
event TEXT NOT NULL, -- auth.failure | task.end | memory.erase
|
|
27
|
+
outcome TEXT, -- denied | completed | failed | rejected | canceled | input-required | ok
|
|
28
|
+
caller TEXT, -- the authenticated caller's name ('shared-secret', a caller token's name, 'jwt')
|
|
29
|
+
scope_hash TEXT, -- SHA-256 prefix of the scope key; joins rows without naming a user
|
|
30
|
+
source_ip TEXT,
|
|
31
|
+
agent_id TEXT,
|
|
32
|
+
task_id TEXT,
|
|
33
|
+
detail JSONB NOT NULL DEFAULT '{}'::jsonb
|
|
34
|
+
);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS melchizedek_audit_at_idx ON melchizedek_audit (at);
|
|
36
|
+
CREATE INDEX IF NOT EXISTS melchizedek_audit_event_idx ON melchizedek_audit (event, at);
|
|
37
|
+
|
|
38
|
+
CREATE OR REPLACE FUNCTION melchizedek_audit_append_only()
|
|
39
|
+
RETURNS trigger
|
|
40
|
+
LANGUAGE plpgsql
|
|
41
|
+
SET search_path = public
|
|
42
|
+
AS $$
|
|
43
|
+
BEGIN
|
|
44
|
+
-- The retention function sets this for its own transaction only.
|
|
45
|
+
IF TG_OP = 'DELETE' AND coalesce(current_setting('melchizedek.audit_prune', true), '') = 'on' THEN
|
|
46
|
+
RETURN OLD;
|
|
47
|
+
END IF;
|
|
48
|
+
RAISE EXCEPTION 'melchizedek_audit is append-only (% refused); rows leave only through melchizedek_prune_audit', TG_OP;
|
|
49
|
+
END;
|
|
50
|
+
$$;
|
|
51
|
+
|
|
52
|
+
DROP TRIGGER IF EXISTS melchizedek_audit_append_only ON melchizedek_audit;
|
|
53
|
+
CREATE TRIGGER melchizedek_audit_append_only
|
|
54
|
+
BEFORE UPDATE OR DELETE ON melchizedek_audit
|
|
55
|
+
FOR EACH ROW EXECUTE FUNCTION melchizedek_audit_append_only();
|
|
56
|
+
|
|
57
|
+
-- Retention: delete rows older than p_days (at least 1). Returns the count.
|
|
58
|
+
-- SECURITY DEFINER: the API role holds SELECT and INSERT only, so pruning
|
|
59
|
+
-- runs with the owner's rights, through this function and nothing else.
|
|
60
|
+
CREATE OR REPLACE FUNCTION melchizedek_prune_audit(p_days integer)
|
|
61
|
+
RETURNS bigint
|
|
62
|
+
LANGUAGE plpgsql
|
|
63
|
+
SECURITY DEFINER
|
|
64
|
+
SET search_path = public
|
|
65
|
+
AS $$
|
|
66
|
+
DECLARE n bigint;
|
|
67
|
+
BEGIN
|
|
68
|
+
IF p_days IS NULL OR p_days < 1 THEN
|
|
69
|
+
RAISE EXCEPTION 'melchizedek_prune_audit: p_days must be at least 1';
|
|
70
|
+
END IF;
|
|
71
|
+
PERFORM set_config('melchizedek.audit_prune', 'on', true);
|
|
72
|
+
DELETE FROM melchizedek_audit WHERE at < now() - make_interval(days => p_days);
|
|
73
|
+
GET DIAGNOSTICS n = ROW_COUNT;
|
|
74
|
+
PERFORM set_config('melchizedek.audit_prune', '', true);
|
|
75
|
+
RETURN n;
|
|
76
|
+
END;
|
|
77
|
+
$$;
|
|
78
|
+
|
|
79
|
+
DO $$
|
|
80
|
+
BEGIN
|
|
81
|
+
REVOKE ALL ON melchizedek_audit FROM PUBLIC;
|
|
82
|
+
REVOKE ALL ON FUNCTION melchizedek_prune_audit(integer) FROM PUBLIC;
|
|
83
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN
|
|
84
|
+
REVOKE ALL ON melchizedek_audit FROM anon;
|
|
85
|
+
END IF;
|
|
86
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN
|
|
87
|
+
REVOKE ALL ON melchizedek_audit FROM authenticated;
|
|
88
|
+
END IF;
|
|
89
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN
|
|
90
|
+
-- The server appends and reads; nothing else, so no UPDATE, DELETE or TRUNCATE.
|
|
91
|
+
GRANT SELECT, INSERT ON melchizedek_audit TO service_role;
|
|
92
|
+
GRANT EXECUTE ON FUNCTION melchizedek_prune_audit(integer) TO service_role;
|
|
93
|
+
END IF;
|
|
94
|
+
END $$;
|
|
95
|
+
|
|
96
|
+
INSERT INTO melchizedek_schema_version (version, name)
|
|
97
|
+
VALUES (12, '0012_audit_log')
|
|
98
|
+
ON CONFLICT (version) DO NOTHING;
|
package/dist/lib/a2a/app.d.ts
CHANGED
|
@@ -32,6 +32,9 @@ import type { SyndicateYamlConfig } from '../loadSyndicate.ts';
|
|
|
32
32
|
import type { ProviderEndpoint, ProviderId } from '../models/registry.ts';
|
|
33
33
|
import type { TurnLock } from './turnLock.ts';
|
|
34
34
|
import type { RlsHardeningStatus } from '../storage/rlsStatus.ts';
|
|
35
|
+
import type { AuditSink } from '../observability/audit.ts';
|
|
36
|
+
/** One end user's concurrent tasks, unless maxConcurrentPerScope says otherwise (ADR 0039). */
|
|
37
|
+
export declare const DEFAULT_MAX_CONCURRENT_PER_SCOPE = 4;
|
|
35
38
|
import type { EraseCounts } from '../memory/erase.ts';
|
|
36
39
|
import type { Embedder, MemoryExtractor } from '../memory/providers.ts';
|
|
37
40
|
import type { A2AContext } from './executor.ts';
|
|
@@ -75,6 +78,8 @@ export interface A2AAppOptions {
|
|
|
75
78
|
* fatal with `requireHardenedDb`, a warning otherwise.
|
|
76
79
|
*/
|
|
77
80
|
rlsHardening?: () => Promise<RlsHardeningStatus>;
|
|
81
|
+
/** Appends audit events (postgresStorage supplies one; ADR 0042). */
|
|
82
|
+
audit?: AuditSink;
|
|
78
83
|
/**
|
|
79
84
|
* One turn at a time per conversation across instances (postgresStorage
|
|
80
85
|
* supplies an advisory lock). Default: a lock in this process.
|
|
@@ -162,6 +167,15 @@ export interface A2AAppOptions {
|
|
|
162
167
|
streamText?: boolean;
|
|
163
168
|
/** Concurrent tasks across all agents. 0 = unlimited. */
|
|
164
169
|
maxConcurrentTasks?: number;
|
|
170
|
+
/**
|
|
171
|
+
* Where audit events go: failed authentications, task outcomes, erasures
|
|
172
|
+
* (ADR 0042). Default: the storage's sink when it has one, else none.
|
|
173
|
+
*/
|
|
174
|
+
audit?: AuditSink;
|
|
175
|
+
/** Concurrent tasks for one scope (one end user). Default 4; 0 = unlimited. */
|
|
176
|
+
maxConcurrentPerScope?: number;
|
|
177
|
+
/** Concurrent tasks for one authenticated caller. Default 0 (unlimited). */
|
|
178
|
+
maxConcurrentPerCaller?: number;
|
|
165
179
|
/** Task submissions (POST) per window per client IP. */
|
|
166
180
|
rateLimit?: {
|
|
167
181
|
windowMs: number;
|
|
@@ -232,6 +246,12 @@ export interface A2AApp {
|
|
|
232
246
|
/** Stop admitting tasks, wait up to `graceMs` for running ones, cancel the rest.
|
|
233
247
|
* Resolves with the number of tasks that had to be canceled. */
|
|
234
248
|
shutdown(graceMs: number): Promise<number>;
|
|
249
|
+
/**
|
|
250
|
+
* Fail `/readyz` from now on while still serving every request, so a load
|
|
251
|
+
* balancer stops routing here before the listener closes. The first step of
|
|
252
|
+
* a graceful stop; `shutdown()` is the last.
|
|
253
|
+
*/
|
|
254
|
+
markUnready(): void;
|
|
235
255
|
}
|
|
236
256
|
/**
|
|
237
257
|
* Build the agent card for one syndicate at one route prefix, in the A2A 1.0
|