diff --git a/CLAUDE.md b/CLAUDE.md index 4ecbf0c..5421874 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## What this is -Cyclone is a self-hosted X12 EDI claims-management suite for a single billing office (Colorado Medicaid currently). It parses 837P professional claims and 835 ERA remittances (X12 005010X222A1 / 005010X221A1) and also handles 999, TA1, 270, 271, and 277CA. Local-only by design: binds to `127.0.0.1`, requires login (auth boundary is HTTP; bcrypt + HttpOnly session cookie; first admin bootstrapped from `CYCLONE_ADMIN_USERNAME` + `CYCLONE_ADMIN_PASSWORD` env vars; see SP24 spec for the full posture), no internet exposure. +Cyclone is a self-hosted X12 EDI claims-management suite for a single billing office (Colorado Medicaid currently). It parses 837P professional claims and 835 ERA remittances (X12 005010X222A1 / 005010X221A1) and also handles 999, TA1, 270, 271, and 277CA. Always binds to `0.0.0.0` — the host firewall / compose port publishing is what restricts reachability, not the bind address. Requires login (auth boundary is HTTP; bcrypt + HttpOnly session cookie; first admin bootstrapped from `CYCLONE_ADMIN_USERNAME` + `CYCLONE_ADMIN_PASSWORD` env vars; see SP24 spec for the full posture). LAN-only by design — don't expose the published ports to the WAN. Stack: one Python process (FastAPI + uvicorn, port 8000) + one Node process in dev (Vite, port 5173). The authoritative state is a single SQLite file at `~/.local/share/cyclone/cyclone.db` (or SQLCipher at the same path when the macOS Keychain entry + `sqlcipher3` are both present). @@ -30,7 +30,7 @@ Optional backend extras: `pip install -e '.[sqlcipher]'` (encryption at rest, SP ```bash # Terminal 1 — backend cd backend -.venv/bin/python -m cyclone serve # default 127.0.0.1:8000 +.venv/bin/python -m cyclone serve # default 0.0.0.0:8000 # CYCLONE_PORT=... overrides port; CYCLONE_RELOAD=1 enables uvicorn --reload # Or: .venv/bin/uvicorn cyclone.api:app --reload --port 8000 @@ -179,6 +179,6 @@ Exit codes are documented per subcommand in `cyclone-cli` — `0` for success, ` - **Don't call `useTailStream` from inside a `use` hook.** The subscription lives on the page so the lifecycle ties to whoever mounts the hook, not to whoever happens to call it. - **The store facade.** The public API of `cyclone.store` is preserved through SP21's split — call through the facade, not directly into the underlying modules. - **Encryption is optional, not required.** When the Keychain entry is missing **or** `sqlcipher3` is not installed, the DB falls back to plain SQLite. Don't fail boot on missing encryption. -- **Local-only by design.** The backend binds to `127.0.0.1`, requires login (bcrypt + HttpOnly session cookie; see SP24 spec), and the threat model is still a stolen/imaged drive — SQLCipher at rest and the macOS Keychain handle that. The auth boundary is the HTTP layer; the file-system posture is unchanged. Don't add internet exposure. Don't disable auth without an explicit `CYCLONE_AUTH_DISABLED=1` env var (the escape hatch logs a WARNING at boot). +- **Always bind to 0.0.0.0.** The bind address does not control reachability — the host firewall / compose port publishing does. Requires login (bcrypt + HttpOnly session cookie; see SP24 spec), and the threat model is still a stolen/imaged drive — SQLCipher at rest and the macOS Keychain handle that. The auth boundary is the HTTP layer; the file-system posture is unchanged. Don't expose the published ports to the public internet (LAN-only or VPN-fronted). Don't disable auth without an explicit `CYCLONE_AUTH_DISABLED=1` env var (the escape hatch logs a WARNING at boot). \ No newline at end of file diff --git a/README.md b/README.md index b989bb9..5a2497c 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Two terminals: # Terminal 1 — backend cd backend .venv/bin/python -m cyclone serve -# (defaults to 127.0.0.1:8000; override with CYCLONE_PORT=...; reload with CYCLONE_RELOAD=1) +# (defaults to 0.0.0.0:8000; override with CYCLONE_HOST / CYCLONE_PORT / CYCLONE_RELOAD=1) # Terminal 2 — frontend npm run dev diff --git a/backend/src/cyclone/__main__.py b/backend/src/cyclone/__main__.py index c8f0041..e76c354 100644 --- a/backend/src/cyclone/__main__.py +++ b/backend/src/cyclone/__main__.py @@ -1,10 +1,11 @@ """Entry point for ``python -m cyclone``. * ``python -m cyclone`` (no args) — Click CLI (``cli.main``) -* ``python -m cyclone serve`` — start the FastAPI app on 127.0.0.1:8000 +* ``python -m cyclone serve`` — start the FastAPI app on 0.0.0.0:8000 Honors the env vars: +* ``CYCLONE_HOST`` (default ``0.0.0.0`` — always bind to all interfaces) * ``CYCLONE_PORT`` (default ``8000``) * ``CYCLONE_RELOAD`` (default ``0``; set to ``1`` to enable uvicorn reload) """ @@ -40,12 +41,13 @@ def main() -> None: if len(sys.argv) >= 2 and sys.argv[1] == "serve": port = os.environ.get("CYCLONE_PORT", "8000") - # Local-only by default — see CLAUDE.md. The Docker image - # overrides to 0.0.0.0 via compose env so the frontend - # container on the compose bridge network can reach the - # backend. Network isolation is provided by the bridge - # network itself (only cyclone-frontend joins). - host = os.environ.get("CYCLONE_HOST", "127.0.0.1") + # Always bind to 0.0.0.0 so the API is reachable from the + # frontend container on the compose bridge network AND from + # the host (Vite dev proxy) AND from the LAN. Network isolation + # is provided by the host firewall / compose port publishing, + # not by binding to loopback. Override with CYCLONE_HOST if you + # have a reason to restrict. + host = os.environ.get("CYCLONE_HOST", "0.0.0.0") reload = os.environ.get("CYCLONE_RELOAD", "0") == "1" sys.argv = [ sys.argv[0], diff --git a/docker-compose.yml b/docker-compose.yml index 035d9aa..1e5c741 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,10 +43,11 @@ services: # embedding the secret in the compose file. CYCLONE_ADMIN_USERNAME_FILE: "/run/secrets/cyclone_admin_username" CYCLONE_ADMIN_PASSWORD_FILE: "/run/secrets/cyclone_admin_password" - # Bind 0.0.0.0 so the frontend container on the compose bridge - # network can reach us. The bridge network provides isolation — - # only the `frontend` service is on it; the host firewall still - # blocks anything that isn't on the LAN. + # Always bind to 0.0.0.0. The compose-managed bridge network + # isolates the backend from the host's LAN/WAN — only the + # `frontend` service joins it. Host firewall / port publishing + # is the layer that controls what reaches us from outside the + # compose network, not the bind address. CYCLONE_HOST: "0.0.0.0" secrets: - cyclone_db_key diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 0516c4a..17de83d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -678,7 +678,7 @@ Returns `status="ok"` only when every subsystem is healthy. `status="degraded"` The default deployment. Two terminals: ```bash -# Terminal 1 — backend on 127.0.0.1:8000 +# Terminal 1 — backend on 0.0.0.0:8000 (always binds to all interfaces; firewall is what restricts reachability) cd backend .venv/bin/python -m cyclone serve diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index 2d38577..216c15e 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -151,7 +151,7 @@ Each requirement is `FR-NN`, traceable to one or more sub-projects. Verification | ID | NFR | Source / SP | |---|---|---| -| NFR-1 | **Local-only bind + auth.** Backend binds `127.0.0.1:8000` (overridable via `CYCLONE_PORT`); CORS allowlist is exact (`http://localhost:5173`); login required (bcrypt + HttpOnly session cookie; first admin bootstrapped from env vars `CYCLONE_ADMIN_USERNAME` + `CYCLONE_ADMIN_PASSWORD`); dev/test escape hatch via `CYCLONE_AUTH_DISABLED=1` (logs WARNING at boot). | SP1 + auth (2026-06-23 merge) + SP24 (doc reconciliation) | +| NFR-1 | **Always bind 0.0.0.0 + auth.** Backend binds `0.0.0.0:8000` (overridable via `CYCLONE_HOST` / `CYCLONE_PORT`); CORS allowlist is exact (`http://localhost:5173`); login required (bcrypt + HttpOnly session cookie; first admin bootstrapped from env vars `CYCLONE_ADMIN_USERNAME` + `CYCLONE_ADMIN_PASSWORD`); dev/test escape hatch via `CYCLONE_AUTH_DISABLED=1` (logs WARNING at boot). Reachability is controlled by the host firewall / compose port publishing, not the bind address. | SP1 + auth (2026-06-23 merge) + SP24 (doc reconciliation) | | NFR-2 | **Determinism.** Parser / serializer round-trip is guaranteed on 113 real prodfiles (`docs/prodfiles/claims/*.x12`); canonical fields, not byte-identity (called out in the SP8 spec). | SP8 | | NFR-3 | **Audit completeness.** Every reconciliation anomaly, every state transition, every 999/277CA reject writes an `ActivityEvent` (in `activity_events` table, un-chained; powers the `/activity` feed and inbox state transitions). | SP2 + SP10 | | NFR-4 | **Tamper-evidence.** A separate `audit_log` table (SP11) carries SHA-256 hash-chained rows for security-sensitive events (login attempts, rejections, key rotations, backup lifecycle, rejected requests). `GET /api/admin/audit-log/verify` detects any break. **The `activity_events` (NFR-3) and `audit_log` (NFR-4) tables are distinct** — different semantics, different audiences. New code that needs an audit row must decide which one to write to. | SP11 |