diff --git a/CHANGELOG.md b/CHANGELOG.md index e6187cfd..6e30ecaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to AgentFlow are documented in this file. ## [Unreleased] +### Fixed — single-container demo deploys pin the DuckDB serving backend (G2 S7, 2026-07-06) + +- **`SERVING_BACKEND=duckdb` is now pinned** in `deploy/hf-space/Dockerfile` + (`ENV`), `deploy/fly/fly.toml` (`[env]`), and the `deploy/fly/README.md` + local docker-run example. `config/serving.yaml` has defaulted to ClickHouse + since the ADR 0006 Phase 1 cutover and ships inside the demo image, so any + demo built from post-cutover main crashed on boot (`BackendExecutionError: + connection refused` — no ClickHouse runs beside a single-container demo). + Caught live on the first three-node HF Space bring-up; the standalone demo + Space never showed it only because it still ran a pre-cutover image. + ### Changed — spec/seed number consistency: daily rate, GTIN check digits, band centering, FX honesty (G2 S3, 2026-07-06) - **Seed daily rate now matches §1 (audit m5).** `satellite_seed*.sql` order diff --git a/deploy/fly/README.md b/deploy/fly/README.md index 3b230eb1..4d99cc35 100644 --- a/deploy/fly/README.md +++ b/deploy/fly/README.md @@ -70,6 +70,7 @@ curl -i -X POST \ python -c "import tomllib, pathlib; tomllib.loads(pathlib.Path('deploy/fly/fly.toml').read_text(encoding='utf-8'))" docker build -t agentflow-demo -f Dockerfile.api . docker run --rm -p 8000:8000 \ + -e SERVING_BACKEND=duckdb \ -e DUCKDB_PATH=/data/agentflow-demo.duckdb \ -e AGENTFLOW_USAGE_DB_PATH=/data/agentflow-demo-api.duckdb \ -e AGENTFLOW_DEMO_MODE=true \ diff --git a/deploy/fly/fly.toml b/deploy/fly/fly.toml index 3aae8a3a..1b8085aa 100644 --- a/deploy/fly/fly.toml +++ b/deploy/fly/fly.toml @@ -5,6 +5,8 @@ primary_region = "fra" dockerfile = "../../Dockerfile.api" [env] + # config/serving.yaml defaults to ClickHouse; the demo VM runs none. + SERVING_BACKEND = "duckdb" DUCKDB_PATH = "/data/agentflow-demo.duckdb" AGENTFLOW_USAGE_DB_PATH = "/data/agentflow-demo-api.duckdb" AGENTFLOW_DEMO_MODE = "true" diff --git a/deploy/hf-space/Dockerfile b/deploy/hf-space/Dockerfile index 475bec16..5af3e217 100644 --- a/deploy/hf-space/Dockerfile +++ b/deploy/hf-space/Dockerfile @@ -38,6 +38,9 @@ ENV AGENTFLOW_ENTITY_CONTRACTS_DIR=/app/contracts/entities # Demo mode (mirrors deploy/fly/README.md): injected public key, admin routes # return 404, mutating routes blocked except POST /v1/query[/explain], DuckDB # seeded on first boot. Data lives under the demo user's writable home. +# config/serving.yaml defaults to ClickHouse (ADR 0006 Phase 1); the +# single-container demo runs none, so pin the embedded DuckDB backend. +ENV SERVING_BACKEND=duckdb ENV AGENTFLOW_DEMO_MODE=true ENV AGENTFLOW_SEED_ON_BOOT=true ENV DEMO_API_KEY=demo-key diff --git a/deploy/hf-space/three-node/DEPLOY.md b/deploy/hf-space/three-node/DEPLOY.md index c8be1326..d9bcada1 100644 --- a/deploy/hf-space/three-node/DEPLOY.md +++ b/deploy/hf-space/three-node/DEPLOY.md @@ -68,7 +68,8 @@ TOKEN=... # the shared node token, from your secret store — never commit it # 1. All three healthy curl -fsS $CENTER/v1/health && curl -fsS $SPB/v1/health && curl -fsS $EKB/v1/health -# 2. Public demo-key cannot push (403 — demo-guard holds) +# 2. Public demo-key cannot push (401 — the ingest wants its own bearer, +# not the public key; a wrong bearer gets 403) curl -i -X POST -H "X-API-Key: demo-key" -H "Content-Type: application/json" \ -d '{"origin_branch":"spb","events":[]}' $CENTER/v1/node/events diff --git a/deploy/hf-space/three-node/center/README.md b/deploy/hf-space/three-node/center/README.md index 1d676ca4..e04b8ce9 100644 --- a/deploy/hf-space/three-node/center/README.md +++ b/deploy/hf-space/three-node/center/README.md @@ -70,7 +70,7 @@ curl -fsS $SPACE/v1/health # Cross-branch view (public demo key) curl -fsS -H "X-API-Key: demo-key" $SPACE/v1/node/branches -# Public callers cannot push events — the node token is required (403) +# Public callers cannot push events — the node token is required (401) curl -i -X POST -H "X-API-Key: demo-key" -H "Content-Type: application/json" \ -d '{"origin_branch":"spb","events":[]}' $SPACE/v1/node/events ``` diff --git a/docs/decisions/0012-three-node-demo-topology.md b/docs/decisions/0012-three-node-demo-topology.md index 1137fee2..e05b29d7 100644 --- a/docs/decisions/0012-three-node-demo-topology.md +++ b/docs/decisions/0012-three-node-demo-topology.md @@ -150,8 +150,10 @@ choreography below. No second account, no paid tier, no organization required. exists (ADR 0006 Phase 1). - The endpoint is **allow-listed in the demo-mode guard** exactly like `/v1/query` (`src/serving/api/main.py:286-299`), so public callers with the - `demo-key` still get `403` on it; only the node token authorizes it. It is - mounted **only** in center role; in edge/standalone role it does not exist. + `demo-key` are still rejected on it by the endpoint's own bearer ladder + (`401` with no bearer, `403` with a wrong one); only the node token + authorizes it. It is mounted **only** in center role; in edge/standalone + role it does not exist. - **Push, not pull:** edges emit on their own activity (the existing background generator tick, reused). This is what makes sleep choreography work. diff --git a/docs/three-node-demo-topology.md b/docs/three-node-demo-topology.md index 4ebe3fc8..92f206ac 100644 --- a/docs/three-node-demo-topology.md +++ b/docs/three-node-demo-topology.md @@ -69,8 +69,9 @@ Content-Type: application/json } 200 OK -> { "accepted": N, "applied": N, "dead_lettered": M } -401 -> missing/malformed bearer -403 -> wrong token, OR demo-key/public caller (demo-guard), OR role != center +401 -> missing/malformed bearer (a public demo-key caller sends none, so it lands here) +403 -> bearer present but wrong (including the demo-key offered as a bearer) +404 -> role != center (N2 - the role gate runs before the auth ladder) 422 -> body shape invalid (not the per-event schema - that dead-letters, see below) ``` @@ -78,8 +79,9 @@ Content-Type: application/json (reuse the pattern in `src/serving/api/auth/`); **not** the `demo-key` path. - The demo-guard (`main.py:286-299`) blocks `POST` for the public key on every path except the allow-list; adding `/v1/node/events` to that set lets the - **token-authenticated** node call through while the public `demo-key` still - gets `403` (N3). + **token-authenticated** node call through, and the endpoint's own bearer + ladder still rejects the public `demo-key` caller — `401` without a bearer, + `403` with a wrong one (N3). - Apply each event via `local_pipeline._process_event(conn, event, clickhouse_sink=...)` on the center's serving connection - **no new serving logic**. A per-event schema failure dead-letters exactly as in-process events do (that path already writes @@ -199,7 +201,8 @@ Bring-up order: **center first** (edges need its URL live), then the two edges. ## 12. Verify-live checklist (F2 done-gate) - `curl {center}/v1/health`, `{edge-spb}/v1/health`, `{edge-ekb}/v1/health` -> all 200. -- Public demo-key `POST {center}/v1/node/events` -> `403` (demo-guard holds, N3). +- Public demo-key `POST {center}/v1/node/events` -> `401` (it carries no bearer; + a wrong bearer gets `403` — the ingest never honors the public key, N3). - `POST {center}/v1/node/events` with the node token, one seeded `spb` order -> `200 applied:1`; then a center cross-branch read shows `spb` last-seen set and the metric moved (N4). @@ -215,8 +218,9 @@ Machine-checkable; each is a unit/integration test F2 must add (mirrors - **N1** Standalone role (no `AGENTFLOW_NODE_ROLE`): `/v1/node/events` is not mounted (`404`), no emitter task; behavior byte-identical to today's demo. - **N2** `/v1/node/events` mounted iff role=`center`; edge/standalone => `404`. -- **N3** Ingest rejects the public `demo-key` (`403`, demo-guard) and missing/wrong - bearer (`401`/`403`); accepts the correct `AGENTFLOW_NODE_TOKEN`. +- **N3** Ingest rejects the public `demo-key` (`401` — it sends no bearer; `403` if + offered as one) and missing/wrong bearer (`401`/`403`); accepts the correct + `AGENTFLOW_NODE_TOKEN`. - **N4** A valid POSTed event is applied via `_process_event`, lands in `pipeline_events` tagged `branch=`, and moves the matching center metric. - **N5** Idempotency: the same `event_id` POSTed twice does not double-count.