feat(config): PINKY_DEPLOYMENT_MODE env + cached app.state.deployment_mode (#434) - #472
feat(config): PINKY_DEPLOYMENT_MODE env + cached app.state.deployment_mode (#434)#472olegbrok wants to merge 1 commit into
Conversation
…_mode (#434) PR-1 of the #433 web-exposed hardening track. Establishes the single env-var switch downstream middleware will branch on (#436 LAN filter, #437 headers, #439 admin elevation, #441 boot guards, etc.) instead of each feature carrying its own knob. What ---- * New `pinky_daemon.config` module: - `DeploymentMode` enum: `trusted` (default, current Mac Mini), `lan`, `web`. - `resolve_deployment_mode()` reads `PINKY_DEPLOYMENT_MODE`, normalises case + whitespace, **fails closed** on invalid values (`InvalidDeploymentModeError`) — a typo like `prod` aborts startup rather than silently falling back to `trusted`. - `default_bind_host(mode)`: `0.0.0.0` for trusted/lan, `127.0.0.1` for web (assumes a reverse proxy in front). - `warn_if_insecure_exposure(mode, host)`: loud stderr warning when `trusted` + `0.0.0.0`, so accidental VPS exposure is obvious in logs while back-compat is preserved. * `pinky_daemon.__main__`: - `--host` default flipped to `None` so we can tell "operator picked 0.0.0.0" from "old hardcoded default" — needed by #436/#441. - On startup: resolve mode → resolve host → warn → log mode + host. - `InvalidDeploymentModeError` → `sys.exit(2)` with a useful message. * `pinky_daemon.api.create_api`: - New `deployment_mode` kwarg; if `None`, resolved from env so embedded callers and tests don't have to wire it through. - Cached on `app.state.deployment_mode` — single source of truth for the rest of #433. - `/api` payload exposes `deployment_mode` so VPS operators can confirm posture without scraping logs. Tests (26 new) -------------- * Resolver: empty/unset → trusted; each canonical value; case-insensitive; whitespace stripped; invalid value raises with a useful message; aliases like "production" / "dev" don't silently pass. * `default_bind_host`: trusted/lan → 0.0.0.0, web → 127.0.0.1. * `warn_if_insecure_exposure`: warns only on trusted + 0.0.0.0; quiet on lan/web and on specific bind addresses. * `create_api`: explicit mode cached on `app.state`; defaults resolve from env; `/api` exposes the mode for each posture. Existing test_api.py: 286 tests still green. Closes part of #433. Refs #434. 🤖 Authored by Barsik
| def test_api_endpoint_exposes_mode_for_each_value(self, tmp_db_path): | ||
| from pinky_daemon.api import create_api | ||
|
|
||
| for mode in DeploymentMode: |
| from typing import TYPE_CHECKING, Literal | ||
|
|
||
| if TYPE_CHECKING: | ||
| from pinky_daemon.config import DeploymentMode |
CodeQL findings (overnight triage)CodeQL surfaced 1 error + 1 note on this PR. Both reviewed: 1. ❗ Real bug:
|
Update: full-stack CI passes cleanOpened #480 as a CI-validation draft on the cumulative diff of all 8 stack PRs targeting `main`. Result: all 11 checks green, including CodeQL. So the CodeQL findings I flagged earlier dissolve:
The latent test bug I called out still stands though: `tests/test_deployment_mode.py:210-214` has the `assert` outside the for-loop, so the test only verifies the last enum value. Not a CI failure — the assertion passes on the last iteration — but worth fixing for correctness. Easy nit, doesn't need to block merge. Net: this stack should be safe to merge as-is once Murzik signs off. — Barsik 🤖 |
Correction on the latent test bugLooked closer: the assert-outside-loop bug is not in #472. On this branch, `tests/test_deployment_mode.py:205-209` has the assert correctly inside the for-loop. The bug got introduced in #474 (`feat/lan-filter-stacked`) — when that PR wrapped the test in `patch.dict()` to bypass the new LAN filter, the rewrap moved the assert one indent level out of the loop. Filing the correction comment on #474. This PR (#472) is clean on its own; the CodeQL fail it surfaced was both false positives. — Barsik 🤖 |
Summary
PR-1 of the #433 web-exposed hardening track. Establishes the single env-var switch that downstream middleware (#436 LAN filter, #437 headers, #439 admin elevation, #441 boot guards, etc.) will branch on, instead of each feature carrying its own knob.
Closes part of #433. Refs #434.
What
pinky_daemon/config.py(new):DeploymentModeenum:trusted(default, current Mac Mini),lan,webresolve_deployment_mode()— readsPINKY_DEPLOYMENT_MODE, normalises case + whitespace, fails closed on invalid values (InvalidDeploymentModeError). A typo likeprodaborts startup, not silently fall back totrusted.default_bind_host(mode)—0.0.0.0for trusted/lan,127.0.0.1for web (assumes reverse proxy in front).warn_if_insecure_exposure(mode, host)— loud stderr warning whentrusted+0.0.0.0, so accidental VPS exposure shows up in logs while back-compat is preserved.pinky_daemon/__main__.py:--hostdefault flipped toNoneso we can tell operator picked 0.0.0.0 from old hardcoded default. Needed by Hardening 3/8: LAN-only mode + private-CIDR middleware + bind defaults #436 / Hardening 8/8: Refuse-to-boot guards for insecure web-mode configs #441 — they have to distinguish operator intent from inherited default.InvalidDeploymentModeError→sys.exit(2)with a useful message.pinky_daemon/api.pycreate_api:deployment_modekwarg; ifNone, resolved from env so embedded callers and tests don't have to wire it through.app.state.deployment_mode— single source of truth for the rest of Hardening: PinkyBot for web-exposed deployments (tracking) #433./apipayload exposesdeployment_modeso VPS operators can confirm posture without scraping logs.Murzik review followups (from #434) — addressed
InvalidDeploymentModeError, daemon exits 2--hostdefaults toNoneinternally — argparse default isNone; mode-appropriate default applied after resolution/api— addeddeployment_modefieldtrusted+0.0.0.0—warn_if_insecure_exposure()printed to stderr at startupproduction/dev/local/stagingall rejectapp.state.deployment_mode— established as the single cache pointTests
26 new tests in
tests/test_deployment_mode.pycovering the resolver, the bind-host helper, the insecure-exposure warning, and thecreate_apiwiring (app.state+/apiexposure).Existing
test_api.py: 286 tests still green (verified locally).Test plan
pytest tests/test_deployment_mode.py -v(26 passing locally)pytest tests/test_api.py -v(286 passing locally; no regression)ruff check src/pinky_daemon/config.py src/pinky_daemon/__main__.py src/pinky_daemon/api.py tests/test_deployment_mode.py(clean)PINKY_DEPLOYMENT_MODE=prod→ confirm exit 2 + clear errorPINKY_DEPLOYMENT_MODE=webwithout--host→ confirm bind to 127.0.0.1curl /apiconfirmsdeployment_modefieldOut of scope (other PRs in the track)
The middleware that acts on
deployment_modelives in subsequent PRs: #442 client-IP resolver, #436 LAN filter + bind enforcement, #437 headers, #435 bearer auth, #443 CSRF, #440 audit, #438 rate limits, #439 elevated admin, #441 refuse-to-boot.🤖 Opened by Barsik