The auth-merge that landed on 2026-06-23 (14 commits, a25504b..39ae988) adds real auth: User/Session models, bcrypt, matrix_gate, AUTH_DISABLED, a login router, RBAC on every existing endpoint, and a RequireAuth guard on the React shell. But CLAUDE.md, docs/REQUIREMENTS.md, and docs/ARCHITECTURE.md (all added/updated on the css-reduction branch) still describe the pre-auth "no auth, local-only, single operator, single host" posture. The doc/code divergence is real and would confuse anyone reading the docs. This is the spec for SP24 — the alignment. It is intentionally a docs/skills-only SP: no schema, no role, no gate changes. The plan section enumerates the doc lines to update; the implementation step sweeps the markdown for residual "no auth" claims. Draft, awaiting user sign-off per the SP-N spec flow.
10 KiB
Cyclone Auth Posture Alignment — Design
Date: 2026-06-23
Status: Draft, awaiting user sign-off
Branch: main (auth code already landed; this SP aligns docs + decisions)
Depends on: The auth work that landed in main via the merge of origin/main on 2026-06-23 (commits a25504b..39ae988, 14 commits, the feat(auth):/fix(auth):/etc. series on auth-rebased).
Replaces: The "no auth, no LAN-bind, single operator" claim in CLAUDE.md, docs/REQUIREMENTS.md, and docs/ARCHITECTURE.md — the codebase no longer matches the doc and the divergence needs to be resolved one way or the other.
1. Overview
The auth work that landed in main is materially more than a stub. The codebase now has:
- A
usersandsessionsSQLAlchemy model (migrations 0013 + 0014) - A
User.password_hashfield using bcrypt - An
auth_routerexposing/api/auth/login,/api/auth/logout,/api/auth/me - A
matrix_gateFastAPI dependency and a permissions matrix that gates every existing endpoint by role (admin / user / viewer) - An
AUTH_DISABLEDflag (module-level incyclone.auth.deps, also settable viaCYCLONE_AUTH_DISABLED=1at boot) that the conftest autouse fixture flips toTrueso the test suite can run without a login round-trip per request - A
RateLimitMiddleware(SP19) that the conftest bypasses viaapp.state.testing - A
RequireAuthroute guard and anAuthProviderReact context on the frontend - A sidebar user indicator + logout button on the shell
- A
cyclone adminCLI for creating users and rotating passwords
But CLAUDE.md, docs/REQUIREMENTS.md, and docs/ARCHITECTURE.md (all three added or updated on the css-reduction branch in commit 12311bf / a39bfb2) still say:
CLAUDE.md:7— "Local-only by design: binds to127.0.0.1, no auth, no internet exposure."CLAUDE.md:182— "Local-only by design. The backend binds to127.0.0.1, has no auth, and the threat model is a stolen/imaged drive, not a remote attacker. Don't add internet exposure or auth without an explicit spec."docs/REQUIREMENTS.md:36— "Local-only on purpose: binds127.0.0.1, no auth, no internet exposure, single operator, single machine, one trading partner"docs/REQUIREMENTS.md:39— "Threat model: a process on the same host. No second party to authenticate; no second host to harden against."docs/REQUIREMENTS.md:154— NFR-1: "no auth, no API key"docs/ARCHITECTURE.md:16— "has no authentication because the threat model is a stolen or imaged drive, not a remote attacker"docs/ARCHITECTURE.md:703— "Multi-user / multi-host — no auth, no LAN-bind, no Docker, no reverse proxy. The operator is one person, the host is one machine. (SP23 fork if this changes.)"
The pre-auth claims in the docs are now false. The threat model is no longer "a process on the same host" — there is at minimum a password boundary that requires a deliberate authentication step before any data is returned.
2. Goals
- Decide the canonical auth posture. Is this v1 with a single local operator who has to log in, or is it the first step toward SP23 (Ubuntu + Docker + RBAC + LAN-bind)? Pick one and write it down; the docs and the code must agree.
- Update the three stale top-level docs (
CLAUDE.md,docs/REQUIREMENTS.md,docs/ARCHITECTURE.md) to reflect the chosen posture. Every "no auth" claim is replaced with the truth; every "single operator, single host" claim is qualified by the new auth boundary. - Update the relevant Cyclone superpowers skills that still describe the pre-auth system: at minimum
cyclone-tests(conftest'sAUTH_DISABLEDflag is now mandatory context for any new test file),cyclone-api-router(thematrix_gatedependency is now part of every router), andcyclone-spec(the SP-N spec template's "threat model" section needs an auth-aware example). - Make
AUTH_DISABLEDdiscoverable in the conftest. The conftest autouse fixture flipscyclone.auth.deps.AUTH_DISABLED = Truefor every test, and the test suite relies on this. A new test file that importscyclone.auth.depsand reads the flag directly will see it asTrueand may make wrong assumptions. Acyclone.auth.testinghelper that documents "this is what the test suite does to the auth flag, and how to opt out per-test" would make the dependency explicit. - Tighten the migration sequence. Migrations 0013 and 0014 (auth_users_and_sessions, audit_log_user_id) are auth-related. They currently have no docstring beyond the filename. The migrations table in
docs/ARCHITECTURE.mdshould list them and explain the dependency (theaudit_log.user_idFK in 0014 is a forward reference tousers.idfrom 0013, so the renumbering on theclaims-unique-fixbranch — 0013/0014 → 0015/0016 — was necessary to keep the auth migrations at the front of the chain).
3. Non-goals
- Reverting the auth work. It ships in main, it's tested, and the operator can no longer use Cyclone without logging in. Going back is a separate decision the user would have to ask for explicitly; this SP assumes the auth work stays.
- Shipping SP23 (Ubuntu + Docker + LAN-bind). SP23 is its own design (
docs/superpowers/specs/2026-06-22-cyclone-ubuntu-docker-deployment-design.md) and is still "awaiting user decision." This SP only reconciles the doc/code divergence that the auth-merge created; it does not advance the LAN-bind / Docker conversation. - Adding new auth features. No SSO, no MFA, no per-claim ACL, no audit log viewer UI. The auth in main is what's documented; this SP just describes it.
- Changing the data model. The
users/sessions/audit_log.user_idschema stays. The bcrypt-hashed password and HttpOnly session cookie stay. Theadmin/user/viewerrole enum stays.
4. Decisions to make (questions for the user)
These are the points that the spec can't answer on its own. They each have a default that matches the auth work in main, but the user may want to override.
- What is the v1 posture? The auth work in main binds to
127.0.0.1(still) and gates every endpoint behindmatrix_gate(now). The default reading is "single-operator local-only, but with a login screen." Alternatives: revert the auth (separate decision), or treat this as a stepping stone to SP23.- Default: v1 = single-operator local-only with auth. SP23 is a future decision.
- How is the first admin created?
cyclone.auth.bootstrapreadsCYCLONE_ADMIN_USERNAME+CYCLONE_ADMIN_PASSWORDfrom the env and creates the row on first boot (no users exist) — seebackend/src/cyclone/auth/bootstrap.py:45-65. If the env vars are unset and no users exist,cyclone serveerrors out with a message telling the operator to set them. Thecyclone admin create-userCLI is a follow-up convenience for adding more users after the first one.- Default: the env-var bootstrap stays. Document the env vars in
CLAUDE.mdunder the existingInstall/Devsections.
- Default: the env-var bootstrap stays. Document the env vars in
- Does the
AUTH_DISABLEDescape hatch ship as a real production knob? It's acyclone.auth.deps.AUTH_DISABLEDmodule-level boolean that the conftest flips toTruefor the test suite, ANDCYCLONE_AUTH_DISABLED=1sets it at boot viacyclone.auth.bootstrap:37-43. The current behavior is "synthetic admin, no role check" when the flag is set.- Default: keep the env-var escape hatch (it makes local dev + tests possible), but add a startup log line in
cyclone servethat screamsWARNING: CYCLONE_AUTH_DISABLED=1 — all requests treated as admin, dev onlyso a misconfigured production deploy fails loudly. Make the conftest path explicit incyclone-tests.
- Default: keep the env-var escape hatch (it makes local dev + tests possible), but add a startup log line in
- Should the docs be explicit about what the auth boundary is and isn't? The default reading is "the auth boundary is the HTTP layer; the file-system and the SQLite file are still bound by the local-only threat model (stolen/imaged drive, SQLCipher at rest, etc.)."
- Default: the auth boundary is HTTP-layer only. SQLCipher, Keychain, and the 127.0.0.1 bind are unchanged.
5. Plan
The plan lives in docs/superpowers/plans/2026-06-23-cyclone-auth-posture-alignment.md once the spec is approved. The shape of the plan is:
CLAUDE.md— replace lines 7 and 182 with the v1 posture ("local-only with auth; auth boundary is HTTP; default bootstrap on first run"). Update theFrontend at a glance/Backend at a glancesections to mentioncyclone.authandmatrix_gate. Note theAUTH_DISABLEDtest flag incyclone-tests.docs/REQUIREMENTS.md— replace the "Local-only on purpose" line 36 and the "Threat model" line 39 with the v1 posture. Update NFR-1 (line 154) to reflect that auth IS now part of the contract. Update §6 (FRs) to add an "Auth boundary" subsection. Update §11 (Roadmap) to note that SP24 lands the auth posture and SP23 is a separate future decision.docs/ARCHITECTURE.md— replace line 16 and line 703. Add an "Auth" section to the module map (cyclone.authpackage, thematrix_gatedependency, the conftest bypass). Update the migrations table to list 0013 and 0014 and explain why the renumbering on the SP22 branch was necessary.- The three Cyclone superpowers skills under
.superpowers/skills/that describe tests, routing, and spec shape: each gets a one-paragraph addendum. cyclone servestartup log: emit aWARNING: AUTH_DISABLEDline ifcyclone.auth.deps.AUTH_DISABLEDisTrueat boot. The flag is off by default; only the conftest flips it.
6. Verification
- The three top-level docs no longer contain "no auth" / "no authentication" / "no second party to authenticate" claims that are false.
- The Cyclone skills addenda are present and self-consistent.
- The startup warning fires when
AUTH_DISABLED = Trueand is silent otherwise. - The existing test suite still passes (the auth changes are in main; this SP only touches docs + the startup warning).
7. Out of scope
- Any new feature beyond docs + skills addenda + startup warning.
- Any change to the
users/sessions/audit_logschema. - Any change to the role matrix or the per-endpoint gate.
- Any change to SP23 (separate spec, separate decision).
- The CLAUDE.md contradiction is the canary; the rest of the doc set may have other places where the pre-auth claim leaked in. The implementation step sweeps
docs/and*.mdfiles under.superpowers/skills/for the strings "no auth", "no authentication", "single operator, single host" and either fixes them or explicitly marks them as historical context.