Skip to content

feat(install): collapse Caddy to TLS-only, drop PUBLIC_PATHS, add --no-tls (ADR-0001 Child B — refs #54)#59

Merged
thinmintdev merged 1 commit into
mainfrom
feat/adr-0001-b-caddy-reduction
May 17, 2026
Merged

feat(install): collapse Caddy to TLS-only, drop PUBLIC_PATHS, add --no-tls (ADR-0001 Child B — refs #54)#59
thinmintdev merged 1 commit into
mainfrom
feat/adr-0001-b-caddy-reduction

Conversation

@thinmintdev
Copy link
Copy Markdown
Contributor

Wave 2 of ADR-0001 (refs #54, lands the breaking change Child A unblocked).

ADR: docs/adr/0001-collapse-edge-auth-into-fastapi.md. Parent issue: #54. Spec: #56.

Summary

  • Demote Caddy to a dumb TLS terminator + reverse proxy. All auth (password + session cookies + Bearer tokens) lives in FastAPI, where Child A put it.
  • Delete the PUBLIC_PATHS frozenset + require_token_unless_public dependency. Route publicness is now declared by NOT attaching an auth dep at include_router(...) time — no allowlist, no two-source drift class of bugs (Caddy basic_auth swallows PUBLIC_PATHS allowlist — first-run wizard unbootstrappable #28, PUBLIC_PATHS duplicated in Python + Caddyfile — needs single source of truth #51).
  • Drop --auth=basic + admin-credential prompts from installer/install.sh. Add --no-tls which skips Caddy entirely and binds FastAPI on 0.0.0.0:8080 (right path for hosts behind an existing reverse proxy).
  • Wire the wizard's password step. Real integration in ui/src/views/FirstRun.vue — probes /api/auth/status on mount, auto-skips when a password is already set, otherwise renders a "Set up password (optional)" step that POSTs to /api/auth/password (Child A's first-run public path).

Files touched, by concern

Concern Files
Caddyfile reduction packaging/caddy/Caddyfile.template (-89 / +24, 107 → 42 lines — the directive block itself is the 10-line ADR §1 form; remainder is doc header + commented-import seam)
PUBLIC_PATHS deletion + router rewiring src/hal0/api/middleware/auth.py, src/hal0/api/__init__.py, src/hal0/api/routes/v1.py (split into public_router + router), src/hal0/api/routes/health.py (doc comment)
Installer flag surface installer/install.sh (drop --auth=basic, add --no-tls, rename HAL0_HOSTNAMEHAL0_PUBLIC_HOST, auto-pick tls internal vs ACME based on hostname)
Wizard UI hook ui/src/views/FirstRun.vue (new step 1 = password; picker/license/install/done renumber to 2/3/4/5; auto-skip when password_set)
Harness rows tests/harness/installer-test.sh (drop auth-basic; add tls-default + no-tls), scripts/harness.sh (rename HAL0_HARNESS_AUTHHAL0_HARNESS_TLS)
Tests tests/api/test_no_public_paths.py (new, 15 tests), tests/api/test_auth_middleware.py (drop /api/install/curated-models from the protected-routes parametrize — wizard endpoint now public by design)

git grep PUBLIC_PATHS returns zero hits in src/ and packaging/. Remaining mentions live in docs/adr/0001-..., tests/harness/FINDINGS.md, and a couple of historical comments — Child C will sweep those.

Caddyfile LoC delta

packaging/caddy/Caddyfile.template: 107 → 42 lines (-65 net). The actual Caddy directives went from basicauth + @public path matcher + 3 handle blocks + handle_path /chat* (~70 effective lines) down to:

{$HAL0_PUBLIC_HOST:hal0.local} {
    tls {$HAL0_TLS_DIRECTIVE:internal}
    encode zstd gzip
    reverse_proxy 127.0.0.1:8080
}

Wizard UI: real or scaffolded?

Real integration. ui/src/views/FirstRun.vue got a new step 1 (password) inserted before the picker. It probes /api/auth/status on mount; if password_set is already true the step auto-skips to the picker. Submit calls the public-on-first-run POST /api/auth/password. Skip advances without setting one. Step renumber: picker 1→2, license 2→3, install 3→4, done 4→5. Build verified with vite build.

Test results

PYTHONPATH=<worktree>/src uv run --no-sync pytest tests/
869 passed, 3 skipped, 4 deselected in 31.85s

15 new tests in tests/api/test_no_public_paths.py all green. The auth + installer regression suite (211 tests across test_auth_* + test_password_auth + test_installer_routes) all green.

Out of scope — Child C lands these

Per the ADR's three-PR plan:

Do NOT close #54 on merge — Child C still needs to land.

CI heads-up

Hal0ai org Actions are billing-blocked; PR CI will fail in ~2s with no logs. Verified locally with the PYTHONPATH=<worktree>/src uv run --no-sync pytest invocation above. User will admin-merge.

🤖 Generated with Claude Code

…o-tls (ADR-0001 Child B — refs #54)

Wave 2 of ADR-0001: demote Caddy to a dumb TLS terminator + reverse
proxy. All auth now lives in FastAPI (password + session cookies +
Bearer — Child A surface). Two-source drift around PUBLIC_PATHS goes
away because there is no second source anymore.

Caddyfile
---------
packaging/caddy/Caddyfile.template collapses from 107 → 42 lines (the
directive block itself is the 10-line ADR §1 form; remainder is doc
header + commented-import seam). Drops the global basicauth block, the
/chat handle_path, the /v1 handle, the @public path matcher, and the
default basicauth handler. Renders into a single TLS-terminating
reverse_proxy 127.0.0.1:8080.

API
---
src/hal0/api/middleware/auth.py: delete PUBLIC_PATHS frozenset +
require_token_unless_public dependency. git grep PUBLIC_PATHS returns
zero hits in src/ (remaining mentions are doc / changelog comments).
src/hal0/api/__init__.py: rewire routers — wizard endpoints + auth
endpoints + /v1/models mount auth-free; admin routers still carry
Depends(require_token); /v1 splits into v1.public_router (models
probe) + v1.router (inference, auth-required).

Installer
---------
installer/install.sh: remove --auth=basic and the admin-credential
prompts. Add --no-tls (skip Caddy entirely; bind 0.0.0.0:8080). The
default path installs Caddy with the new minimal template; --no-tls
binds the API directly. Env vars rename HAL0_HOSTNAME → HAL0_PUBLIC_HOST
to match the template. Auth self-test removed (no edge auth to round-
trip against). HAL0_TLS_DIRECTIVE auto-resolves to "internal" for
*.local / localhost and to the ACME contact email otherwise.

Wizard UI (real integration)
----------------------------
ui/src/views/FirstRun.vue: insert a new "Set up password (optional)"
step before the model picker. Probes /api/auth/status on mount and
auto-skips when a password is already set. Submit POSTs to
/api/auth/password (the public first-run path from Child A); Skip
advances without setting one (open-mode posture, surfaced in the
install summary). UI builds cleanly with vite.

Harness
-------
tests/harness/installer-test.sh: drop the auth-basic row. Add
tls-default (asserts the rendered Caddyfile is the minimal form — no
basicauth / @public, reverse_proxy line present) and no-tls (asserts
Caddy unit absent, hal0-api binds 0.0.0.0, /api/auth/status reports
auth_mode=open). HAL0_HARNESS_AUTH env knob renamed to HAL0_HARNESS_TLS.

Tests
-----
tests/api/test_no_public_paths.py: new — asserts the PUBLIC_PATHS
symbol is gone, asserts every formerly-public path stays reachable
without credentials, asserts a writer route still 401s without auth.
15 new tests, all green. tests/api/test_auth_middleware.py: drop the
/api/install/curated-models case from the protected-routes parametrize
(wizard endpoint is now public by design).

Out of scope (Child C will land):
  - installer/README.md, PLAN.md, FINDINGS.md edits
  - closing #43 / #51

CI: Hal0ai org Actions are billing-blocked; verified locally with
PYTHONPATH=<worktree>/src uv run --no-sync pytest — 869 passed, 3
skipped, 4 deselected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@thinmintdev thinmintdev merged commit 635cf5a into main May 17, 2026
0 of 6 checks passed
thinmintdev added a commit that referenced this pull request May 17, 2026
…d C — refs #54) (#60)

Wave 2, Child C of ADR-0001. Documentation pass for the auth collapse
shipped in PRs #58 (Child A — FastAPI password + session cookies +
dual cookie/Bearer middleware) and #59 (Child B — Caddyfile reduction
+ PUBLIC_PATHS deletion + --no-tls flag + HAL0_PUBLIC_HOST /
HAL0_HARNESS_TLS rename).

installer/README.md
-------------------
Rewrites the auth section to describe the single FastAPI auth layer.
Drops every reference to --auth=basic / HAL0_ADMIN_USER /
HAL0_ADMIN_PASSWORD / HAL0_HOSTNAME / Caddy basic_auth / htpasswd.
Documents the --no-tls flag (FastAPI binds 0.0.0.0:8080, reachable at
http://<host>:8080/). Renames HAL0_HOSTNAME to HAL0_PUBLIC_HOST in the
env-var table. Adds an "Upgrade notes (pre-v1)" subsection explaining
that existing --auth=basic installs lose edge auth on upgrade and the
two mitigations — set a password in the wizard, or --no-tls behind your
own reverse proxy. Calls out the wizard's password-setup step (POST
/api/auth/password, public on first run per Child A).

PLAN.md
-------
§1 "Auth + reverse proxy" rewritten to reflect the single FastAPI layer
and a "Trust posture" subsection: hal0 defaults to open on the LAN;
password auth is opt-in via the dashboard wizard; programmatic clients
use Bearer tokens unchanged from #29. Drops the Caddy basic_auth /
PUBLIC_PATHS prose; narrows Caddy's scope to TLS termination + reverse
proxy. §10 (harness flags) renames HAL0_HARNESS_AUTH → HAL0_HARNESS_TLS
to match what the harness scripts actually look for.

docs/api-errors.md
------------------
Adds a brief note in the 401 section linking to ADR-0001 / PR #58 and
naming the new endpoints (POST /api/auth/login, /api/auth/logout,
/api/auth/password). The envelope shapes themselves are unchanged.

tests/harness/FINDINGS.md
-------------------------
Prepends "FIXED BY ARCHITECTURE REMOVAL (ADR-0001)" notes to the three
historical entries that the auth collapse renders structurally
unrepeatable: §10 (Caddy basic_auth swallows PUBLIC_PATHS — the
original #28 critical bug, fixed in PR #49 and now historical because
Caddy no longer has matchers or basicauth per PR #59), §16 (basic_auth
password unrecoverable post-install — source of the #43 HITL decision,
fixed by deletion because credential capture moved into the wizard per
PRs #58 + #59), and §21 (/api/metrics/prometheus orphan in
PUBLIC_PATHS — fixed by deletion because PUBLIC_PATHS is gone). The
original report bodies are preserved verbatim below each note for
historical reference. Re-run instructions updated to the new
HAL0_HARNESS_TLS knob.

README.md, tests/harness/README.md
----------------------------------
Sync the auth-posture summary in the repo root README and the harness
opt-in flags table to match the new single-FastAPI model and the
HAL0_HARNESS_TLS rename. These weren't called out in the spec but were
left stale after Child B; updating them keeps the docs internally
consistent.

Closes #43 and #51 (per the parent ADR plan). Issue close comments
follow the merge via the gh CLI; this PR body is the GH-semantics
hook in case the manual close doesn't land.

closes #43
closes #51
refs #54
refs #57

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
thinmintdev added a commit that referenced this pull request May 21, 2026
ADR-0001 (Collapse edge auth into FastAPI) is implemented. Child A
(#58 — FastAPI password auth), Child B (#59 — Caddyfile reduction +
--no-tls), and Child C (#60 — docs pass) all landed. Flips the
header from Proposed → Accepted, records proposal/acceptance dates
separately, names the implementing PRs, and appends an Outcome
section summarizing what shipped against the original Decision.

Adds #28 (the critical basic_auth ordering bug) to the closed-on-land
list per tests/harness/FINDINGS.md §10. README.md and installer/README.md
were already brought into line with the v1 single-FastAPI-layer reality
in PR #60 — no further changes needed there.
@thinmintdev thinmintdev deleted the feat/adr-0001-b-caddy-reduction branch May 21, 2026 20:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ADR-0001] Collapse edge auth into FastAPI — parent

1 participant