docs(grill): adversarial review — 15 challenges, 2 MUST, 4 FIX, 7 ACCEPT

G-101 MUST: GITEA_TOKEN injection chain broken (hookscript on host, token in CT env)
  → Fix: bake token into snippet at staging time
G-102 MUST: PRAXIS_DB_PATH never read by server (db/store.py, db/migrate.py hardcode path)
  → Fix: add os.environ.get('PRAXIS_DB_PATH', 'praxis.db') in 2 files
G-103 FIX: 5 missing env vars in injection list (OLLAMA_CHAT_URL, CARTESIA_VOICE_ID, etc.)
G-104 FIX: health-check timeout 300s→600s (zero margin vs 5min build)
G-105 FIX: Dockerfile pip install runs before source copy (build fails)
G-106 FIX: bats test count 9→10 (MH-26 + Makefile)

Verdict: APPROVE_WITH_NOTES — plan sound with 6 fixes applied in EXECUTE

---ci---
project: praxis
phase: 0
milestone: v0.2
status: grill
---/ci---
This commit is contained in:
Praxis CI
2026-08-01 14:10:15 +00:00
parent 0df1ec391a
commit 2999c5163c
2 changed files with 425 additions and 8 deletions
+18 -7
View File
@@ -1,14 +1,25 @@
{
"phase": 0,
"stage": "plan",
"stage": "grill",
"milestone": "v0.2",
"phase_role": "pre_execution",
"attempts": 0,
"updated_at": "2026-08-01T14:25:00Z",
"plan_summary": {
"slices": 10,
"waves": 4,
"tasks": 34,
"req_coverage": "20/20"
"updated_at": "2026-08-01T14:35:00Z",
"grill_summary": {
"challenges": 15,
"binding_decisions": 13,
"must": 2,
"fix": 4,
"accept": 7,
"escalations": 0,
"verdict": "APPROVE_WITH_NOTES"
},
"binding_actions": {
"G-101": "MUST: bake GITEA_TOKEN into snippet at staging time",
"G-102": "MUST: add PRAXIS_DB_PATH env read to db/store.py + db/migrate.py",
"G-103": "FIX: add 5 missing env vars to injection list",
"G-104": "FIX: bump health-check timeout 300s→600s",
"G-105": "FIX: fix Dockerfile pip install copy ordering",
"G-106": "FIX: fix bats test count 9→10"
}
}
+407 -1
View File
@@ -201,4 +201,410 @@
---
*End of grill report. Verdict: PROCEED at confidence 0.72. 8 binding decisions (G-001..G-008), 0 escalations. Escalations visible via `ciagent audit`. This grill surfaces findings; it does not rewrite PROJECT.md, ROADMAP.md, or REQUIREMENTS.md. Binding decisions that warrant spec changes must be promoted explicitly by the user (e.g., via `ciagent-clarify` or a follow-up CLARIFY stage).*
*End of grill report. Verdict: PROCEED at confidence 0.72. 8 binding decisions (G-001..G-008), 0 escalations. Escalations visible via `ciagent audit`. This grill surfaces findings; it does not rewrite PROJECT.md, ROADMAP.md, or REQUIREMENTS.md. Binding decisions that warrant spec changes must be promoted explicitly by the user (e.g., via `ciagent-clarify` or a follow-up CLARIFY stage).*
---
# Praxis — v0.2 Proxmox LXC Deployment Grill (Red-Team Review)
> **Grill date:** 2026-08-01
> **Griller:** CI Griller (adversarial red-team)
> **Mode:** full autonomy (auto-decide all; 0 escalations expected)
> **Target:** `.ciagent/PLAN.md` — 10 slices, 4 waves, 34 tasks, 20 REQ-IDs (REQ-DEPLOY-01..16, REQ-NFR-DEPLOY-01..04)
> **Artifacts reviewed:** PROJECT.md (D-021..D-030), REQUIREMENTS.md, RESEARCH.md (10 questions, 6 risks), ARCHITECTURE.md, PERSONAS.md (5 active, frontend deactivated), PLAN.md, config.json, coreci source (`/root/coreci/scripts/proxmox/`), praxis codebase (`server/__main__.py`, `db/store.py`, `pyproject.toml`, `.gitignore`, `.env.example`, `client/package.json`)
> **Confidence threshold:** 0.60 (binding); < 0.60 = escalate
---
## Method
Assumed the plan is unfeasible, over-scoped, and too costly. Cross-referenced every plan claim against coreci source and the praxis codebase. Found where the plan is wrong.
---
## Challenges
### C-01: GITEA_TOKEN not available to the firstboot hookscript — secret injection chain is broken
**Axis:** Feasibility / Dependency risk / Security
**Confidence:** 0.85
**Evidence:**
- PLAN.md TASK-05-01 step 3 (line 368): `pct exec "$vmid" -- sh -c 'git clone https://${GITEA_TOKEN}@git.cloudinit.dev/.../praxis.git /opt/praxis'`
- PLAN.md TASK-05-01 (line 372): "GITEA_TOKEN is available via lxc.environment (set by lxc-config.sh in SLICE-03)"
- RESEARCH.md Q5 (line 23): "GITEA_TOKEN is passed via lxc.environment and available inside the CT"
- coreci `firstboot-hook.sh` lines 19-27 comment: "Environment (set on the PVE host when the hookscript runs; for a fully-automated deploy, **stage a version of this snippet with the secrets baked in**)"
- coreci `lxc-config.sh` line 59-61: `lxc.environment: GITEA_TOKEN=...` — writes to `/etc/pve/lxc/<vmid>.conf`, injecting into the **CT's** systemd environment, NOT the PVE host's environment
**The problem:** The hookscript runs on the **PVE host** (not inside the CT). `lxc.environment` injects vars into the CT's init process (systemd PID 1 inside the CT), NOT into the PVE host's environment. The hookscript executing on the host does NOT have `GITEA_TOKEN` in its environment. Coreci's design acknowledges this: it says to "stage a version of this snippet with the secrets baked in" — i.e., the snippet file itself is generated with the token embedded. Praxis's `stage-snippet.sh` (TASK-03-06) fetches the raw file from Gitea (no baking), so the token is NOT in the hookscript.
**Secondary issue — `pct exec` env inheritance:** Even if the hookscript had `GITEA_TOKEN` on the host and passed it via `pct exec -- sh -c '...${GITEA_TOKEN}...'`, the single-quoted `sh -c` body passes `${GITEA_TOKEN}` literally to the CT's shell. The CT's shell would need `GITEA_TOKEN` in its environment. `pct exec` in Proxmox 8 does NOT reliably inherit `lxc.environment` vars — it spawns a process in the CT namespace but starts with a fresh environment, not systemd's inherited env. The plan's claim that `lxc.environment``pct exec` inheritance works is unvalidated and contradicts coreci's own design (which fetches on the host and `pct push`es, specifically to avoid needing the token inside the CT).
**Impact:** The firstboot hook's `git clone` will fail with authentication error → the CT never gets the praxis repo → `install-service.sh` never runs → health-check times out at 300s → rollback fires → deploy fails every time. This is a **ship blocker**.
### C-02: PRAXIS_DB_PATH env var is never read by the server — SQLite volume mount is a no-op
**Axis:** Feasibility / Operability / Completeness
**Confidence:** 0.90
**Evidence:**
- PLAN.md TASK-01-03 (line 117): `PRAXIS_DB_PATH=/app/data/praxis.db` in docker-compose.yml environment
- PLAN.md TASK-03-04 (line 237): `lxc.environment: PRAXIS_DB_PATH=/app/data/praxis.db` in lxc-config.sh
- PLAN.md TASK-06-02 (line 456): `PRAXIS_DB_PATH=${PRAXIS_DB_PATH:-/app/data/praxis.db}` in server.env
- PLAN.md MH-06 (line 898): "SQLite persists across `docker compose restart` via named volume `praxis-db`"
- praxis `db/store.py` line 25: `_DEFAULT_DB_PATH = "praxis.db"` (hardcoded, no env read)
- praxis `db/migrate.py` line 8: `_DEFAULT_DB_PATH = Path("praxis.db")` (hardcoded, no env read)
- `grep -rn "PRAXIS_DB_PATH" /root/praxis/server/ /root/praxis/db/`**0 matches** (only in `.env.example`)
- `PraxisStore.__init__` (store.py:70) takes `db_path` param defaulting to `_DEFAULT_DB_PATH`, but `PraxisStore` is never instantiated in the server code (`grep -rn "PraxisStore(" /root/praxis/server/` → 0 matches). `SessionRecorder` takes a `store: PraxisStore` param but is never instantiated in `pipeline.py`.
**The problem:** The plan sets `PRAXIS_DB_PATH=/app/data/praxis.db` in three places (compose env, lxc.environment, server.env), but the server code never reads `PRAXIS_DB_PATH`. The DB defaults to `./praxis.db` (CWD-relative, which is `/app` in the container). The Docker volume `praxis-db` is mounted at `/app/data`. The server writes to `/app/praxis.db` (container writable layer), NOT `/app/data/praxis.db` (the volume). Data is NOT persisted across container recreation — it's lost on `docker compose down && docker compose up`. The volume mount is dead weight.
Additionally, `PraxisStore` and `SessionRecorder` appear to be defined but never wired into the pipeline — the recorder is not instantiated in `pipeline.py`. This may be a v0.1 gap (recorder defined but not yet connected), but the plan's MH-06 (SQLite persistence verification) will fail because there's no code writing to the DB at the volume path.
**Impact:** Data loss on container restart/recreate. The persistence NFR is claimed but not delivered. MH-06 acceptance criterion will fail.
### C-03: Missing env vars in lxc-config.sh / server.env — server will misconfigure at runtime
**Axis:** Consistency / Completeness
**Confidence:** 0.85
**Evidence:**
- The praxis server reads these env vars (verified by grep):
- `OLLAMA_CHAT_URL` (server/llm/ollama_cloud.py:41) — used for the direct API chat endpoint
- `CARTESIA_VOICE_ID` (server/pipeline.py:127, server/tts/cartesia_tts.py:40) — TTS voice selection
- `DEEPGRAM_REGION`, `DEEPGRAM_LANGUAGE` — referenced in .env.example (lines 36-37), may be read by pipeline
- `PRAXIS_SCENARIO` (server/__main__.py:83) — scenario ID selection
- PLAN.md TASK-03-04 (lines 235-247) lxc-config.sh env var list does NOT include: `OLLAMA_CHAT_URL`, `CARTESIA_VOICE_ID`, `DEEPGRAM_REGION`, `DEEPGRAM_LANGUAGE`, `PRAXIS_SCENARIO`
- PLAN.md TASK-06-02 (lines 453-467) install-service.sh server.env does NOT include the same vars
- praxis `.env.example` (lines 21-40) documents all of these as server config
**The problem:** The plan's env var injection list (TASK-03-04, TASK-06-02) is incomplete. `OLLAMA_CHAT_URL` defaults to `https://ollama.com/api/chat` in code, so it may work without injection — but `CARTESIA_VOICE_ID` and `PRAXIS_SCENARIO` have defaults too. The issue is that the plan claims to wire "all praxis env vars" but the list is missing vars that `.env.example` documents and the code reads. If any of these need to be overridden per-deployment (e.g., a different scenario, a different voice), they can't be without editing the compose file.
**Impact:** Server runs with defaults (may be acceptable for pilot), but the env injection chain is incomplete vs. what the code actually reads. Inconsistency between plan claims and reality.
### C-04: systemd TimeoutStartSec=300 may be insufficient for first-boot build — R-DEPLOY-02 unresolved
**Axis:** Feasibility / Timeline / Operability
**Confidence:** 0.65
**Evidence:**
- RESEARCH.md R-DEPLOY-02 (line 636): "systemd TimeoutStartSec applies to ExecStartPre+ExecStart combined → 300s insufficient for build+up" — confidence 0.65
- RESEARCH.md Q8 (line 278): "the ExecStartPre=docker compose build pattern needs validation (build may exceed systemd's default timeout, may need TimeoutStartSec=300)"
- PLAN.md D-036 (line 974): confidence 0.75, mitigation = "if insufficient, split into praxis-build.service"
- PLAN.md TASK-06-01 (line 424): `TimeoutStartSec=300`
- RESEARCH.md Q2/Q9 estimates: Docker build inside CT = npm ci (~400MB peak) + pip install (~1.2GB peak) + compose up. Estimated 3-5 min total.
- REQ-NFR-DEPLOY-03 target: < 5 min first-boot
**The problem:** `TimeoutStartSec=300` (5 min) is the NFR target ceiling, but it's also the timeout. If the build takes exactly 4.5 min + compose up takes 30s, the total is 5 min — right at the timeout boundary. If `TimeoutStartSec` applies to `ExecStartPre` + `ExecStart` combined (which systemd does in some configurations), 300s is too tight. The plan acknowledges the risk (D-036) but defers mitigation to "monitor and split if needed" — which means the first deploy may fail with a timeout, triggering rollback, and the team discovers the problem only at E2E time (SLICE-10).
**Impact:** First deploy may fail with systemd timeout → rollback → no working CT. Not a design flaw but an estimate risk that should be mitigated proactively, not reactively.
### C-05: Health-check timeout (300s) vs first-boot build time (3-5 min) — zero margin
**Axis:** Feasibility / Timeline
**Confidence:** 0.70
**Evidence:**
- PLAN.md TASK-04-01 (line 333): timeout default 300s
- RESEARCH.md Q7 (line 383): "Docker build inside CT + compose up may take 3-5 min; the default 180s timeout is insufficient. Use PRAXIS_HEALTH_TIMEOUT=300"
- RESEARCH.md Q7 (line 390): "Total: ~3-5 min from CT start to health. 300s timeout covers this with margin" — but 3-5 min = 180-300s, so the upper bound (5 min = 300s) equals the timeout. Zero margin.
- The build includes: apt install Docker (~90s) + git clone (~10s) + docker compose build (~120s) + compose up (~10s) = ~230s best case. But apt install can be slower on a fresh CT, pip install can spike if wheels are missing (R-DEPLOY-01), and network latency adds time.
**The problem:** The health-check timeout (300s) equals the worst-case estimate (5 min). There is no margin. If anything is slower than estimated (network, disk I/O, pip compilation fallback), the health-check fires before the service is up → rollback → deploy fails. The research says "covers this with margin" but 300s = 300s is zero margin.
**Impact:** Intermittent deploy failures under load or slow network conditions. The NFR (REQ-NFR-DEPLOY-03: < 5 min) is set at the same value as the timeout — a deployment that takes 4m59s passes the NFR but leaves 1s of health-check margin.
### C-06: CT internet access is assumed but unvalidated — R-DEPLOY-03
**Axis:** Dependency risk / Feasibility
**Confidence:** 0.60
**Evidence:**
- RESEARCH.md R-DEPLOY-03 (line 637): "CT network can't reach Gitea or apt mirrors (coreci's original concern)" — confidence 0.60
- RESEARCH.md Q2 (line 103): "D-028/D-029 explicitly chose apt-install-inside-CT and clone-from-Gitea, implying the CT DOES have internet in this deployment — different from coreci's original assumption"
- coreci `firstboot-hook.sh` lines 9-14: "The CT's network may not route to the internet (upstream often only routes the host's IP). The PVE host has internet, so this hookscript fetches... on the host... then pushes them into the CT"
- D-029 (PROJECT.md line 98): "CT fetches its own source + builds" — assumes CT has internet
- D-030 (PROJECT.md line 99): "vmbr0 DHCP only" — DHCP gives an IP, but doesn't guarantee internet routing
**The problem:** The entire build-inside-CT approach (D-029) rests on the CT having internet access to reach Debian apt mirrors and `git.cloudinit.dev`. Coreci's original design explicitly assumes the opposite ("CT's network may not route to the internet") and works around it by host-fetching + `pct push`. Praxis reverses this assumption without validation. If the CT's vmbr0 DHCP gives an IP but no default route or no DNS resolution to external hosts, the apt install + git clone both fail. The plan's mitigation (RESEARCH.md: "fallback to host-clone + pct push") is the coreci pattern — but no task in the plan implements this fallback. It's a noted risk with no task.
**Impact:** If CT has no internet, the entire firstboot sequence fails at step 1 (apt install). Deploy is impossible until the network issue is resolved or the fallback is implemented.
### C-07: Docker-in-LXC on ZFS rootfs storage — R-DEPLOY-04 unvalidated
**Axis:** Dependency risk / Feasibility
**Confidence:** 0.55
**Evidence:**
- RESEARCH.md R-DEPLOY-04 (line 638): "Docker-in-LXC on ZFS rootfs storage → overlay2 conflict" — confidence 0.50
- RESEARCH.md Q1 (line 55): "If the PVE host uses ZFS for CT rootfs, Docker's overlay2 may have issues (ZFS CoW + overlay CoW conflict). The coreci .env shows PROXMOX_STORAGE=local which is typically directory/LVM-thin, not ZFS. Verify at deploy time"
- PLAN.md: no task validates the storage type before deploy
**The problem:** If `PROXMOX_STORAGE=local` maps to a ZFS pool (not directory/LVM-thin), Docker's overlay2 driver may fail inside the LXC. The research says "verify at deploy time" but no plan task performs this verification. This is a 0.50 confidence risk (below the binding threshold), but it's a known unknown that could block the deploy with no mitigation task.
**Impact:** Potential build failure if storage is ZFS. Unlikely (coreci uses the same cluster), but unverified.
### C-08: Bats test suite claims 9 unit/integration files but PLAN lists 11 test tasks
**Axis:** Testability / Consistency
**Confidence:** 0.75
**Evidence:**
- PLAN.md SLICE-09 (line 667): 11 tasks (TASK-09-01 through TASK-09-11)
- PLAN.md MH-26 (line 928): "`make test-proxmox-scripts` passes — 9 unit/integration bats files"
- PLAN.md Verification SLICE-09 (line 807): "9 unit/integration bats files"
- TASK-09-10 is `docker-build.bats` (praxis-specific, not from coreci)
- TASK-09-11 is `test_helper.bash` + `Makefile` (not a bats file)
**The problem:** The plan says "9 unit/integration bats files" but SLICE-09 has 11 tasks. TASK-09-10 (docker-build.bats) is the 10th bats file. TASK-09-11 is a helper + Makefile (not a bats file). So there are 10 bats files (9 coreci-derived + 1 docker-build), not 9. The MH-26 and verification claims of "9" are wrong.
**Impact:** Minor — test suite is slightly larger than documented. docker-build.bats may not be included in `make test-proxmox-scripts` if the target only lists 9 files.
### C-09: No task implements the repo update path (code changes after first deploy)
**Axis:** Operability / Completeness
**Confidence:** 0.70
**Evidence:**
- RESEARCH.md Q5 open question 3 (line 648): "Repo update path: When praxis code changes, how is the CT updated? Options: (a) pct exec git pull && systemctl restart praxis, (b) --reconfigure flag, (c) separate lxc-update.sh. Not a v0.2 blocker (first deploy only) but should be designed for"
- PLAN.md: no task creates an update/redeploy script
- PLAN.md SLICE-07 lxc-deploy.sh has `--reconfigure` (re-PUTs config + restarts CT) but this re-runs the firstboot hook which checks `systemctl is-active praxis` → if active, skips. So `--reconfigure` does NOT update the code — it just restarts the CT. The code update path is undefined.
**The problem:** After the first successful deploy, if the praxis code changes (bug fix, v0.2.1), there's no way to update the running CT. `--recreate` destroys + redeploys (works but slow — full rebuild). `--reconfigure` restarts the CT but doesn't pull new code (the hook's idempotency check skips if praxis is active). There's no `git pull && systemctl restart praxis` task or script. The research flags this as "not a v0.2 blocker" but it makes the deployed system a one-shot static snapshot with no update path short of full rebuild.
**Impact:** No code update path without full CT destruction + rebuild. Acceptable for a pilot's first deploy, but operability gap for any post-deploy fix.
### C-10: Pipecat wheel availability for cp312/linux-amd64 — R-DEPLOY-01 untested until SLICE-01
**Axis:** Feasibility / Dependency risk
**Confidence:** 0.60
**Evidence:**
- RESEARCH.md R-DEPLOY-01 (line 635): "Pipecat native-ext wheel missing for cp312/linux-amd64 → source compilation OOMs at 4GB" — confidence 0.70
- RESEARCH.md Q2 (line 101): "Python 3.12 wheels exist for all pipecat-ai extras on linux/amd64 (high probability — pipecat targets CPython 3.11+ and ships manylinux wheels)"
- PLAN.md TASK-01-01 (line 83): Dockerfile uses `python:3.12-slim` + `pip install --no-cache-dir .`
- PLAN.md R-DEPLOY-01 mitigation (line 994): "Pre-test docker build locally (SLICE-01 verification); if compilation needed, bump to 8GB or use --only-binary :all:"
**The problem:** The entire build-inside-CT approach assumes all Pipecat extras (deepgram, cartesia, piper, webrtc) ship cp312 linux/amd64 wheels. If any don't (e.g., `aiortc` Cython extensions, `sounddevice`), pip falls back to source compilation which needs gcc + libasound2-dev (included in the Dockerfile) and may spike memory > 4GB (OOM at the CT's memory limit). The 4GB memory allocation may be insufficient. This is only discoverable at SLICE-01 verification time.
**Impact:** Build may fail if wheels are missing. Mitigation exists (bump to 8GB, `--only-binary :all:`) but is reactive. Caught early at SLICE-01.
### C-11: `scripts/` excluded in .dockerignore but install-service.sh runs from repo clone — consistent
**Axis:** Consistency
**Confidence:** 0.80
**Evidence:**
- PLAN.md TASK-01-02 (line 95): `.dockerignore` excludes `scripts/`
- PLAN.md TASK-05-01 step 4 (line 369): `pct exec "$vmid" -- sh -c 'cd /opt/praxis && sh scripts/install-service.sh'`
- The `.dockerignore` controls the Docker **build context** (the image won't contain `scripts/`). `install-service.sh` runs from the git clone at `/opt/praxis`, NOT from inside the Docker image. No conflict.
**Not a bug** — design is correct. The `.dockerignore` rationale is confusingly worded but the design is sound.
### C-12: `OLLAMA_BASE_URL` injected but `OLLAMA_CHAT_URL` (a different endpoint) is not
**Axis:** Consistency
**Confidence:** 0.70
**Evidence:**
- PLAN.md TASK-03-04 (line 243): `lxc.environment: OLLAMA_BASE_URL=https://ollama.com/v1`
- praxis `server/llm/ollama_cloud.py:41`: reads `OLLAMA_CHAT_URL` (default `https://ollama.com/api/chat`)
- praxis `server/pipeline.py:99`: reads `OLLAMA_BASE_URL` (default `https://ollama.com/v1`)
- PLAN.md env var lists do NOT include `OLLAMA_CHAT_URL`
**The problem:** The server has TWO Ollama env vars: `OLLAMA_BASE_URL` (OpenAI-compatible Pipecat path) and `OLLAMA_CHAT_URL` (direct chat API). The plan injects `OLLAMA_BASE_URL` but not `OLLAMA_CHAT_URL`. Code defaults work, but the injection list is incomplete.
### C-13: No rollback verification for the Docker volume — data loss on rollback
**Axis:** Operability
**Confidence:** 0.65
**Evidence:**
- rollback.sh destroys the CT (`DELETE /nodes/{node}/lxc/{vmid}`), which destroys the CT's rootfs including Docker volumes.
- PLAN.md MH-06: "SQLite persists across `docker compose restart`" — restart ≠ recreate ≠ CT destruction
**The problem:** The Docker named volume `praxis-db` lives inside the CT's Docker daemon. When `rollback.sh` destroys the CT, all Docker volumes are destroyed with it. No volume backup/export step exists in rollback. Data loss on rollback.
**Impact:** Acceptable for pilot (no real users yet), but should be documented.
### C-14: E2E test (SLICE-10) against live cluster — autonomy boundary unclear
**Axis:** Testability / Operability
**Confidence:** 0.60
**Evidence:**
- PLAN.md TASK-10-01: "Requires PROXMOX_* + GITEA_TOKEN + DEEPGRAM_API_KEY env vars"
- config.json: `escalate_external_integration: true` — but E2E is the project's own deployment target
**The problem:** The E2E test creates a real CT on the live cluster, deploys, verifies, and destroys. At full autonomy, this runs without human approval. If the test fails mid-way, a zombie CT may be left. The autonomy/escalation boundary for live-cluster E2E is unclear.
### C-15: Dockerfile `pip install .` runs before source is copied — build will fail
**Axis:** Feasibility / Consistency
**Confidence:** 0.75
**Evidence:**
- PLAN.md TASK-01-01 (line 83): `COPY pyproject.toml`, `RUN pip install --no-cache-dir .`, then `COPY server/ scenarios/ db/`
- `pip install .` installs the PROJECT package, which requires source directories (`server/`, `db/`, `scenarios/`) to exist
- `pyproject.toml` line 9: `readme = "README.md"` — README.md is not copied in the Dockerfile spec
- RESEARCH.md Q4 (line 183): same ordering issue
**The problem:** The Dockerfile copies `pyproject.toml` then runs `pip install .` BEFORE copying `server/`, `scenarios/`, `db/`. With only `pyproject.toml` present, `pip install .` will fail because the packages to install don't exist yet. The standard dep-caching pattern requires either installing deps separately or copying source before project install.
**Impact:** Docker build fails at the `pip install .` step. Spec error in the plan.
---
## Binding Decisions
### G-101: GITEA_TOKEN secret injection chain is broken — MUST fix before execute
- **Challenge:** C-01
- **Axis:** Feasibility / Dependency risk / Security
- **Confidence:** 0.85
- **Verdict:** MUST (blocks ship)
- **Rationale:** The firstboot hookscript runs on the PVE host, but `GITEA_TOKEN` is injected via `lxc.environment` into the CT, not the host. The hook's `git clone` will fail with auth error every time. Coreci's own design acknowledges this ("stage a version of this snippet with the secrets baked in"). The plan's `stage-snippet.sh` fetches a raw file without baking secrets. Additionally, `pct exec` does not reliably inherit `lxc.environment` vars in the CT's exec'd process.
- **Action:** Choose one of:
1. **(Recommended) Bake GITEA_TOKEN into the snippet at staging time:** Modify `stage-snippet.sh` to fetch the hookscript template, `sed`/`envsubst` the `GITEA_TOKEN` into it, then upload the rendered snippet. This matches coreci's documented approach. The token is in the snippet file (stored in Proxmox snippet storage, not git). Minimal change.
2. **Host-side git clone + pct push:** Clone the repo on the PVE host (where `GITEA_TOKEN` can be exported by `lxc-deploy.sh`), then `pct push` the tarball into the CT. This is coreci's original pattern. Reverts D-029's "clone inside CT" but is proven.
3. **Pass GITEA_TOKEN via pct exec explicitly:** `pct exec "$vmid" -- sh -c 'GITEA_TOKEN='"$GITEA_TOKEN"' git clone ...'` — requires `GITEA_TOKEN` in the host env (the hookscript env), which still has the "lxc.environment doesn't reach the host" problem. Doesn't work without baking.
- **Option 1 is the minimal change.** Update TASK-03-06 (stage-snippet.sh) to render the snippet with `GITEA_TOKEN` baked in. Update TASK-05-01 to use the baked-in token. Update RESEARCH.md Q5/Q6.
### G-102: PRAXIS_DB_PATH is never read by the server — MUST fix the code
- **Challenge:** C-02
- **Axis:** Feasibility / Operability / Completeness
- **Confidence:** 0.90
- **Verdict:** MUST (blocks ship)
- **Rationale:** The plan sets `PRAXIS_DB_PATH=/app/data/praxis.db` in 3 places and claims SQLite persistence via Docker volume (MH-06). But `db/store.py` and `db/migrate.py` hardcode `_DEFAULT_DB_PATH = "praxis.db"` with no env read. The server writes to `/app/praxis.db` (container writable layer), NOT the volume at `/app/data/praxis.db`. Data is lost on container recreation. MH-06 will fail.
- **Action:** Add `PRAXIS_DB_PATH` env var reading to `db/store.py` and `db/migrate.py`:
```python
_DEFAULT_DB_PATH = os.environ.get("PRAXIS_DB_PATH", "praxis.db")
```
2-line code change in 2 files. Add as a new task in SLICE-01 or SLICE-02 (data-engineer / backend-engineer territory). Also verify `PraxisStore` is instantiated in the pipeline (if not, recorder is dead code — v0.1 gap, but env var fix is still needed).
### G-103: Incomplete env var injection list — FIX before execute
- **Challenge:** C-03, C-12
- **Axis:** Consistency / Completeness
- **Confidence:** 0.85
- **Verdict:** FIX (must address before execute)
- **Rationale:** The plan's env var injection list (TASK-03-04, TASK-06-02) is missing `OLLAMA_CHAT_URL`, `CARTESIA_VOICE_ID`, `DEEPGRAM_REGION`, `DEEPGRAM_LANGUAGE`, `PRAXIS_SCENARIO` — all of which the server reads from env. Defaults exist, but the plan claims to wire "all praxis env vars" and the list is incomplete.
- **Action:** Add the missing env vars to both TASK-03-04 (lxc-config.sh `lxc.environment` lines) and TASK-06-02 (install-service.sh `server.env` heredoc):
- `OLLAMA_CHAT_URL=https://ollama.com/api/chat`
- `CARTESIA_VOICE_ID=a3536a36-1d18-4efb-a95a-7e44b7b5e384`
- `DEEPGRAM_LANGUAGE=en`
- `DEEPGRAM_REGION=na`
- `PRAXIS_SCENARIO=customer_service_refund_ca_v01`
### G-104: Health-check timeout has zero margin — FIX by bumping to 600s
- **Challenge:** C-04, C-05
- **Axis:** Feasibility / Timeline
- **Confidence:** 0.70
- **Verdict:** FIX (must address before execute)
- **Rationale:** `PRAXIS_HEALTH_TIMEOUT=300` (5 min) equals the worst-case build estimate (5 min). Zero margin. Any slowdown causes timeout → rollback → deploy failure. The NFR target (< 5 min) is a measurement, not a timeout — the timeout should be 2x the target.
- **Action:** Bump `PRAXIS_HEALTH_TIMEOUT` default to `600` (10 min) in TASK-04-01 (health-check.sh) and TASK-08-02 (.env.example). Bump `TimeoutStartSec` in praxis.service (TASK-06-01) to `600` to match (addresses C-04). NFR target stays at < 5 min (measured by timing wrappers).
### G-105: Dockerfile pip install ordering is broken — FIX before execute
- **Challenge:** C-15
- **Axis:** Feasibility / Consistency
- **Confidence:** 0.75
- **Verdict:** FIX (must address before execute)
- **Rationale:** The Dockerfile spec copies `pyproject.toml` then runs `pip install --no-cache-dir .` BEFORE copying `server/`, `scenarios/`, `db/`. `pip install .` installs the project package, which requires source directories. With only `pyproject.toml` present, the install fails. Also `README.md` (referenced by `pyproject.toml`) is not copied.
- **Action:** Fix the Dockerfile in TASK-01-01 to copy source before `pip install .`, OR split into dep install + project install. Add `README.md` to the COPY list. Example fix:
```dockerfile
COPY pyproject.toml README.md ./
COPY server/ ./server/
COPY scenarios/ ./scenarios/
COPY db/ ./db/
RUN pip install --no-cache-dir .
COPY --from=client-builder /app/client/dist ./client/dist
```
### G-106: Bats test count mismatch (9 vs 10) — FIX the count
- **Challenge:** C-08
- **Axis:** Testability / Consistency
- **Confidence:** 0.75
- **Verdict:** FIX (must address before execute)
- **Rationale:** MH-26 and SLICE-09 verification claim "9 unit/integration bats files" but there are 10 (TASK-09-01 through TASK-09-10 are .bats files; TASK-09-11 is a helper + Makefile). The Makefile target must include `docker-build.bats`.
- **Action:** Update MH-26 and SLICE-09 verification to "10 unit/integration bats files." Ensure the Makefile target in TASK-09-11 includes `docker-build.bats`.
### G-107: No repo update path after first deploy — ACCEPT for v0.2
- **Challenge:** C-09
- **Axis:** Operability / Completeness
- **Confidence:** 0.70
- **Verdict:** ACCEPT (acknowledged, no action)
- **Rationale:** No `git pull && systemctl restart` path for code updates. `--reconfigure` restarts but doesn't pull. `--recreate` works (full rebuild) but is slow. Research flags as "not a v0.2 blocker." For a pilot's first deploy, acceptable.
- **Action:** None for v0.2. Document as known limitation: "No in-place code update path; use `--recreate` for code changes."
### G-108: CT internet access unvalidated (R-DEPLOY-03) — ACCEPT with deploy-time check
- **Challenge:** C-06
- **Axis:** Dependency risk / Feasibility
- **Confidence:** 0.60
- **Verdict:** ACCEPT (acknowledged, verify at E2E)
- **Rationale:** Build-inside-CT assumes internet access. Coreci assumed the opposite. At 0.60 confidence, at the binding threshold. E2E test (SLICE-10) will discover this immediately — no silent failure.
- **Action:** No plan change. Add note to SLICE-10: "If firstboot fails at apt install, check CT internet routing. Fallback: host-clone + pct push (D-025 hybrid)."
### G-109: Docker volume data loss on rollback — ACCEPT for pilot
- **Challenge:** C-13
- **Axis:** Operability
- **Confidence:** 0.65
- **Verdict:** ACCEPT (acknowledged, no action)
- **Rationale:** Docker volume destroyed with CT on rollback. Acceptable for pilot (no persistent user data). Should be documented.
- **Action:** Add note to executor notes: "Rollback destroys CT including Docker volumes — all SQLite data lost. Acceptable for pilot."
### G-110: E2E against live cluster — ACCEPT
- **Challenge:** C-14
- **Axis:** Testability / Operability
- **Confidence:** 0.60
- **Verdict:** ACCEPT (acknowledged, no action)
- **Rationale:** E2E runs against live Proxmox at full autonomy. Gated by `PROXMOX_API_URL` (skips if absent). This is the project's own deployment target, not a third-party integration. Consistent with full autonomy.
- **Action:** None. The E2E skip condition handles the no-secrets case.
### G-111: Pipecat wheel risk (R-DEPLOY-01) — ACCEPT with early detection
- **Challenge:** C-10
- **Axis:** Feasibility / Dependency risk
- **Confidence:** 0.60
- **Verdict:** ACCEPT (early detection at SLICE-01)
- **Rationale:** If wheels missing, Docker build fails at SLICE-01 (first task, earliest detection). Mitigation documented (bump to 8GB, `--only-binary :all:`). No silent failure.
- **Action:** None. Executor runs `docker build` locally first.
### G-112: ZFS storage risk (R-DEPLOY-04) — ACCEPT (below threshold)
- **Challenge:** C-07
- **Axis:** Dependency risk
- **Confidence:** 0.55
- **Verdict:** ACCEPT (below binding threshold)
- **Rationale:** At 0.55, below 0.60 threshold. Coreci uses same cluster/storage and works. E2E catches it if it manifests.
- **Action:** None. Informational only.
### G-113: .dockerignore scripts/ exclusion is correct — ACCEPT
- **Challenge:** C-11
- **Axis:** Consistency
- **Confidence:** 0.80
- **Verdict:** ACCEPT (no action)
- **Rationale:** `.dockerignore` excludes `scripts/` from the Docker image. `install-service.sh` runs from the repo clone at `/opt/praxis`, not from the container. Design is correct.
- **Action:** None. Optionally clarify TASK-01-02 rationale.
---
## Escalations
**None.** All 15 challenges are resolved with confidence >= 0.60 (13 binding decisions) or explicitly accepted at full autonomy. No challenge requires human input.
---
## Summary
**Overall assessment: APPROVE_WITH_NOTES**
The v0.2 plan is fundamentally sound — it reuses a battle-tested deployment toolkit (coreci), adapts it with well-researched parameters (4GB/16GB CT sizing, /health:8789 endpoint), and covers all 20 REQ-IDs across 10 coherent slices. The research is thorough (10 questions, 6 risks). The architecture is well-documented. The persona allocation is reasonable.
However, the grill found **2 MUST-fix blockers** and **4 FIX-before-execute issues**:
1. **G-101 (MUST):** GITEA_TOKEN secret injection chain is broken — hookscript runs on PVE host but token is in CT env. Every deploy fails at `git clone`. Fix: bake token into snippet at staging time.
2. **G-102 (MUST):** `PRAXIS_DB_PATH` is never read by server code — Docker volume mount is a no-op, data lost on container recreation. MH-06 fails. Fix: 2-line code change in `db/store.py` + `db/migrate.py`.
3. **G-103 (FIX):** Env var injection list missing 5 vars the server reads.
4. **G-104 (FIX):** Health-check timeout (300s) = worst-case build (5 min) = zero margin. Bump to 600s.
5. **G-105 (FIX):** Dockerfile `pip install .` runs before source copied — build fails. Fix copy ordering.
6. **G-106 (FIX):** Bats test count is 10, not 9 — MH-26 and Makefile need updating.
The remaining 7 challenges (G-107 through G-113) are accepted — known risks with mitigations or pilot-acceptable limitations.
**Verdict:** The plan CANNOT ship as-is. G-101 and G-102 are ship blockers. G-103 through G-106 must be fixed before execute. With these 6 fixes applied, the plan is sound and should proceed.
| Metric | Count |
|--------|-------|
| Total challenges | 15 |
| Binding decisions | 13 |
| MUST (blocks ship) | 2 (G-101, G-102) |
| FIX (before execute) | 4 (G-103, G-104, G-105, G-106) |
| ACCEPT (no action) | 7 (G-107 through G-113) |
| Escalations | 0 |
| Overall | APPROVE_WITH_NOTES — proceed after MUST/FIX addressed |
---
## Per-Axis Scorecard
| Axis | Score | Notes |
|------|-------|-------|
| 1. Feasibility | ⚠️ | 2 blockers (G-101 secret chain, G-102 DB path) + Dockerfile ordering (G-105). Fixable. |
| 2. Scope | ✅ | 20 REQ-IDs, all mapped. Scope is tight (infra-only). Frontend deactivation justified. |
| 3. Cost/effort | ✅ | Reusing coreci verbatim where possible. 34 tasks proportional to a deploy milestone. |
| 4. Dependency risk | ⚠️ | CT internet unvalidated (G-108), Pipecat wheel risk (G-111), ZFS risk (G-112). All have early-detection gates. |
| 5. Security | ⚠️ | Secret chain broken (G-101). `.gitignore` coverage correct. Secrets never committed. |
| 6. Operability | ⚠️ | No update path (G-107, accepted). Data loss on rollback (G-109, accepted). Timeout zero margin (G-104, fix). |
| 7. Testability | ✅ | Bats suite mirrors coreci (10 files). E2E with skip condition. Count mismatch (G-106, fix). |
| 8. Consistency | ⚠️ | Env var list incomplete (G-103). Test count wrong (G-106). Dockerfile spec error (G-105). |
| 9. Completeness | ⚠️ | Missing env vars (G-103). Missing DB path wiring (G-102). No update script (G-107, accepted). REQ coverage 20/20. |
---
*End of v0.2 grill report. Verdict: APPROVE_WITH_NOTES. 13 binding decisions (G-101..G-113), 0 escalations. Escalations visible via `ciagent audit`. This grill surfaces findings; it does not rewrite PROJECT.md, ROADMAP.md, or REQUIREMENTS.md. Binding decisions that warrant spec changes must be promoted explicitly by the user (e.g., via `ciagent-clarify` or a follow-up CLARIFY stage).*