Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions deploy/fly/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
2 changes: 2 additions & 0 deletions deploy/fly/fly.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions deploy/hf-space/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion deploy/hf-space/three-node/DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion deploy/hf-space/three-node/center/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
6 changes: 4 additions & 2 deletions docs/decisions/0012-three-node-demo-topology.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
18 changes: 11 additions & 7 deletions docs/three-node-demo-topology.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,19 @@ 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)
```

- Auth: constant-time compare of the bearer against `AGENTFLOW_NODE_TOKEN`
(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
Expand Down Expand Up @@ -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).
Expand All @@ -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=<origin>`, and moves the matching center metric.
- **N5** Idempotency: the same `event_id` POSTed twice does not double-count.
Expand Down