# Deployment trust & reproducibility (D7, 2026-07) Deep revision **D7** from [`2026-07-full-repo-review.md`](2026-07-full-repo-review.md). Addresses finding #5 ("the trust model no longer matches deployment") and the deployment gaps in dimension 12: desktop-era filesystem RPCs shipping unauthenticated on the public Space, and a continuous, ungated, force-pushed deploy with no rollback primitive. The security-critical half — the **lockdown profile** — shipped in full. The reproducibility tail (a pinned Python closure) is a documented follow-up because it must be resolved on the deployment's interpreter. ## The lockdown profile The filesystem RPCs assume the caller is the local operator: | Endpoint | Desktop intent | Risk on the public Space | |---|---|---| | `POST /api/config-file-path` | point the app at a custom config JSON | read any JSON on the container | | `POST /api/save-session` | save a study to a chosen folder | write anywhere on the container | | `GET /api/list-sessions` | browse saved studies | enumerate any directory | | `POST /api/load-session` | reload a saved study | read any `session.json` | | `GET /api/pick-path` | native OS file/dir picker | n/a headless (already a no-op) | On the Space (anonymous visitors, Game Mode) none of these are needed — studies come from the bundled presets. So when `COSTUDY4GRID_LOCKDOWN` is truthy (`1`/`true`/`yes`/`on`), each is **disabled**: ``` 403 {"detail": "This operation is disabled on the hosted deployment.", "code": "LOCKED_DOWN"} ``` - The flag is read once at import into `main._LOCKDOWN`; the guard `_reject_when_locked_down()` is the first statement of each locked handler. `LOCKED_DOWN` is a stable code in the D2 error envelope (`services/api_errors.py`, mapped from status 403) and the frontend `ApiErrorCode` union. - The **read-only** app config (`GET /api/user-config`, `GET /api/config-file-path`) stays available so the SPA still boots and reads its bundled `config.json`. - The `Dockerfile` sets `COSTUDY4GRID_LOCKDOWN=1`. Local dev and the test suite leave it unset, so behaviour is unchanged off the Space — `TestLockdownProfile` in `test_api_endpoints.py` exercises both states (403 + `LOCKED_DOWN` when on; normal 200 when off; read-only config reachable either way). Disabling (rather than confining to a fixed directory) is the conservative choice for a single-player ephemeral container; confinement is a possible future refinement if the Space ever needs session persistence. ## Test-gated deploy + rollback `.github/workflows/deploy-huggingface.yml` previously fired on every push to `main` and force-pushed a history-free snapshot to the Space — no test gate, and (because the Space push is force-pushed + squashed) no rollback trail. D7 tightens both: - **Test gate**: the deploy now triggers on `workflow_run` of the **Tests** workflow completing *successfully* on `main`. A red build no longer ships. `workflow_dispatch` still runs unconditionally (the rollback path). - **Rollback pointer**: every successful deploy tags the exact commit it shipped on origin as `space-deploy--`. Since the Space's own history is squashed away, this origin tag is the only durable record of what shipped. Roll back by re-dispatching the workflow from a prior `space-deploy-*` tag (`workflow_dispatch` is not gated). Full steps in [`deploy/huggingface/SETUP.md`](../../deploy/huggingface/SETUP.md) → "Rolling back a bad deploy". ## Reproducible Python closure (tracked follow-up) The image resolves the dependency tree at build time (version *floors* in `pyproject.toml` plus the `expert_op4grid_recommender` floor), so a zero-change rebuild can still pick up newer transitive releases — the Dockerfile's "mirrors CI" claim isn't literally true. The fix is a committed lockfile consumed by both the `Dockerfile` and the Tests workflow: ```bash # on Python 3.10 (the image base — NOT a dev 3.11, or it pins wrong wheels): pip install pip-tools pip-compile --output-file requirements.lock pyproject.toml ``` This is left as a follow-up rather than generated in-repo precisely because it must be resolved on the deployment's 3.10 interpreter; a lockfile pinned on the wrong Python is worse than none. See [`SETUP.md`](../../deploy/huggingface/SETUP.md) → "Reproducible Python closure". ## Remaining (tracked) - Generate + wire the `requirements.lock` (above). - Confinement (vs. outright disabling) of the session RPCs if the Space ever needs persistence. - `HEALTHCHECK` in the Dockerfile and dropping the image's dead weight (dimension-12 items, independent of the trust model).