Five deploy/supply-chain items. Land I1 first — it is the gate every other issue's PRs will run through, so it pays back across the whole backlog.
Line numbers current as of origin/main @ b7edcbc (2026-07-30).
Suggested order: I1 → I2 → I3 → I4 → I5.
PR I1 — GitHub-side CI + full local gate (small — includes the coverage ratchet)
No .github/ directory exists (re-confirmed 2026-07-30). PR #17 added only a local, bypassable (git push --no-verify) pre-push hook at .git/hooks/pre-push running scripts/ci.sh, which lints only tests/ (not src/, by explicit choice at ci.sh:8-10) and runs pytest with a COV_MIN=35 floor (:25) — but no type check and no alembic upgrade head gate. Fix: a GitHub Actions workflow running ruff (including src/) + pytest + a type check + alembic upgrade head against a scratch DB, on every PR.
Folded in — the coverage ratchet (was tracked separately as T1): bash scripts/ci.sh was run end-to-end on 2026-07-30 and passed (exit 0): 523 passed in 41.44 s, total coverage 38.99 %. (Commit 10c240c reports 39.08 %; the ~0.1 pt delta is environmental — py3.12 host venv vs py3.11 container.) Up from 487/37 % at the original audit baseline as the six post-9548c08 commits added 36 test functions (433 → 469). scripts/ci.sh pins COV_MIN=35 (ci.sh:25), ~4 points below actual. Concrete task in this PR: raise COV_MIN to 38 and keep it a one-way ratchet.
Confirmed 0 % in that run: worker/main.py, agent/pi_handler.py, agent/main.py, cli.py. Measured low: profile_export.py 8 %, agent_page.py 10 %, tools.py 10 %, admin.py 11 %, grantbot.py 12 %, email_notifications.py 13 %, invite.py 17 %, slack_provisioning.py 18 %, email_inbound.py 19 %, simulation.py 30 %. The integration harness (conftest.py + testcontainers-PG + TestClient) already exists from PR #17 and the test venv is at .venv-test/, so no test-infra build-out is needed.
This is the definition of done for every issue in this backlog: each PR ships a test that covers its defect line and fails against the pre-fix code. Issues #20, #21 and #23 touch the largest 0 %/low-coverage files and therefore carry most of the ratchet.
PR I2 — Real /api/health DB check + a migrate step (small — live failure observed)
main.py:144-147 /api/health returns an unconditional {"status":"ok"} with no SELECT 1, and there is no alembic upgrade in the build/deploy path. The prod compose adds a healthcheck plus nginx depends_on: service_healthy, which makes this worse — it marks an unmigrated app healthy and sends it traffic.
This was observed live on 2026-07-30, not predicted. The local dev database is at alembic_version = 0018 while the code requires 0021. In the same process, at the same moment:
GET /api/health -> HTTP 200 {"status":"ok"}
first ORM read of agent_messages -> ProgrammingError:
<class 'asyncpg.exceptions.UndefinedColumnError'>: column agent_messages.content does not exist
So the endpoint reports healthy on an app for which every read of the primary conversation table fails.
Fix: a migrate step gated before app traffic + a SELECT 1 in the health route. (Separately from this PR, that dev DB needs alembic upgrade head — alembic upgrade head was verified to apply cleanly 0001→0021 on a fresh database.)
PR I3 — Dockerfile hardening (medium — verified against the built image)
Verified on 2026-07-30 by building and inspecting copiscience-app:
| claim |
observed |
no .dockerignore |
file absent |
| runs as root |
docker compose exec app id → uid=0(root) gid=0(root); image Config.User empty |
| single-stage |
11 layers, one FROM |
COPY src/ src/ before pip install . |
busts the dependency layer cache on every source change |
COPY . . bakes junk |
/app/.git = 5.9 MB, /app/tests, /app/.notes |
The .notes entry is the important one: COPY does not honour .gitignore, so the gitignored .notes/ directory — this backlog, the issue drafts and every prior audit document — ships inside the production image. That, not cache efficiency or image size, is the reason to do this.
Fix: add .dockerignore (at minimum .git, .notes, tests, .venv-test, logs); split dep-install from source copy; non-root USER; multi-stage build.
PR I4 — Lockfile + dependency pinning (medium)
All 17 runtime deps are >= floors with no upper caps and no lockfile (pyproject.toml:11-29); anthropic>=0.26.0 floats across majors; plotly>=5.20.0 is a runtime dep but imported only in scripts/. Fix: a hash-pinned lockfile installed in the Dockerfile; raise floors past known CVEs; cap pre-1.0/major SDKs; move plotly to dev/scripts.
PR I5 — Reconcile nginx.conf + add resource limits (small-medium)
nginx/nginx.conf is a stale Next.js template: "Next.js app on port 3000" comments at :14/:37/:69/:127/:166 (the upstream is actually app:8000), proxy_read_timeout 120s (:124/:145/:237), no CSP add_header, and a dead proxy_cache_valid (:169) with no proxy_cache_path anywhere in the file. No mem_limit/cpus on any prod service; prod bind-mounts ./prompts over the baked copy (docker-compose.prod.yml:41/97/126). (SEC-15 rate-limit zones were already added.) Fix: reconcile the nginx template; add container resource limits.
Five deploy/supply-chain items. Land I1 first — it is the gate every other issue's PRs will run through, so it pays back across the whole backlog.
Line numbers current as of
origin/main@b7edcbc(2026-07-30).Suggested order: I1 → I2 → I3 → I4 → I5.
PR I1 — GitHub-side CI + full local gate (small — includes the coverage ratchet)
No
.github/directory exists (re-confirmed 2026-07-30). PR #17 added only a local, bypassable (git push --no-verify) pre-push hook at.git/hooks/pre-pushrunningscripts/ci.sh, which lints onlytests/(notsrc/, by explicit choice atci.sh:8-10) and runs pytest with aCOV_MIN=35floor (:25) — but no type check and noalembic upgrade headgate. Fix: a GitHub Actions workflow running ruff (includingsrc/) + pytest + a type check +alembic upgrade headagainst a scratch DB, on every PR.Folded in — the coverage ratchet (was tracked separately as T1):
bash scripts/ci.shwas run end-to-end on 2026-07-30 and passed (exit 0): 523 passed in 41.44 s, total coverage 38.99 %. (Commit10c240creports 39.08 %; the ~0.1 pt delta is environmental — py3.12 host venv vs py3.11 container.) Up from 487/37 % at the original audit baseline as the six post-9548c08commits added 36 test functions (433 → 469).scripts/ci.shpinsCOV_MIN=35(ci.sh:25), ~4 points below actual. Concrete task in this PR: raiseCOV_MINto 38 and keep it a one-way ratchet.Confirmed 0 % in that run:
worker/main.py,agent/pi_handler.py,agent/main.py,cli.py. Measured low:profile_export.py8 %,agent_page.py10 %,tools.py10 %,admin.py11 %,grantbot.py12 %,email_notifications.py13 %,invite.py17 %,slack_provisioning.py18 %,email_inbound.py19 %,simulation.py30 %. The integration harness (conftest.py+ testcontainers-PG +TestClient) already exists from PR #17 and the test venv is at.venv-test/, so no test-infra build-out is needed.PR I2 — Real
/api/healthDB check + a migrate step (small — live failure observed)main.py:144-147/api/healthreturns an unconditional{"status":"ok"}with noSELECT 1, and there is noalembic upgradein the build/deploy path. The prod compose adds a healthcheck plus nginxdepends_on: service_healthy, which makes this worse — it marks an unmigrated app healthy and sends it traffic.This was observed live on 2026-07-30, not predicted. The local dev database is at
alembic_version = 0018while the code requires0021. In the same process, at the same moment:So the endpoint reports healthy on an app for which every read of the primary conversation table fails.
Fix: a migrate step gated before app traffic + a
SELECT 1in the health route. (Separately from this PR, that dev DB needsalembic upgrade head—alembic upgrade headwas verified to apply cleanly 0001→0021 on a fresh database.)PR I3 — Dockerfile hardening (medium — verified against the built image)
Verified on 2026-07-30 by building and inspecting
copiscience-app:.dockerignoredocker compose exec app id→uid=0(root) gid=0(root); imageConfig.UseremptyFROMCOPY src/ src/beforepip install .COPY . .bakes junk/app/.git= 5.9 MB,/app/tests,/app/.notesThe
.notesentry is the important one:COPYdoes not honour.gitignore, so the gitignored.notes/directory — this backlog, the issue drafts and every prior audit document — ships inside the production image. That, not cache efficiency or image size, is the reason to do this.Fix: add
.dockerignore(at minimum.git,.notes,tests,.venv-test,logs); split dep-install from source copy; non-rootUSER; multi-stage build.PR I4 — Lockfile + dependency pinning (medium)
All 17 runtime deps are
>=floors with no upper caps and no lockfile (pyproject.toml:11-29);anthropic>=0.26.0floats across majors;plotly>=5.20.0is a runtime dep but imported only inscripts/. Fix: a hash-pinned lockfile installed in the Dockerfile; raise floors past known CVEs; cap pre-1.0/major SDKs; moveplotlyto dev/scripts.PR I5 — Reconcile nginx.conf + add resource limits (small-medium)
nginx/nginx.confis a stale Next.js template: "Next.js app on port 3000" comments at:14/:37/:69/:127/:166(the upstream is actuallyapp:8000),proxy_read_timeout 120s(:124/:145/:237), no CSPadd_header, and a deadproxy_cache_valid(:169) with noproxy_cache_pathanywhere in the file. Nomem_limit/cpuson any prod service; prod bind-mounts./promptsover the baked copy (docker-compose.prod.yml:41/97/126). (SEC-15 rate-limit zones were already added.) Fix: reconcile the nginx template; add container resource limits.