Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,23 @@ home installs, OpenAI-compatible inference, bundled OpenWebUI chat.
(shipped ahead of schedule via Team K, 2026-05-15 — `1a8a480`, `76b7f8b`)
- External-LLM upstreams (OpenRouter, Anthropic, OpenAI, custom
OpenAI-compatible)
- **Auth + reverse proxy** (shipped ahead of schedule via Team J,
2026-05-15 — `ba79427`, `f62902c`)
- Default install posture `--auth=off`; opt-in `--auth=basic` brings
up a Caddy reverse proxy with basic_auth on the dashboard, bearer
tokens on `/v1/*`, OpenWebUI SSO via trusted-header
- **Auth + reverse proxy** (per [ADR-0001](docs/adr/0001-collapse-edge-auth-into-fastapi.md),
collapsed to a single FastAPI layer in PRs #58 + #59; original dual-layer
shipped ahead of schedule via Team J, 2026-05-15 — `ba79427`, `f62902c`)
- All auth lives in FastAPI: Bearer tokens for programmatic clients,
password + session cookie for browser dashboard. Caddy is a dumb TLS
terminator + reverse proxy (no edge auth, no path allowlist).
- **Trust posture:** a fresh install starts **open on the LAN** — no
password set, dashboard + `/v1/*` reachable without credentials.
Password auth is **opt-in via the dashboard wizard** (Set up password
step → `POST /api/auth/password`); once set, writer routes require
login (reads stay open per the wizard's choice). Programmatic clients
use Bearer tokens minted under #29 — unchanged.
- `--no-tls` install flag skips Caddy entirely; FastAPI binds
`0.0.0.0:8080` for hosts behind an existing reverse proxy
(Traefik / nginx / Cloudflare Tunnel / etc.).
- `hal0.local` reachable on the LAN (mDNS via avahi); HTTPS via
Caddy's internal CA or Let's Encrypt when a public hostname is set
Caddy's internal CA or Let's Encrypt when a public hostname is set.
- **Dashboard UI** (Vue 3 + Pinia + Tailwind 4)
- 9 views: Dashboard, Slots, Models, Hardware, Logs, Settings,
Providers, FirstRun, plus a not-found / error shell
Expand Down Expand Up @@ -497,10 +507,10 @@ or operator touches on a real host — installer, every CLI subcommand,
slot lifecycle, uninstall — and emits one structured JSON row per
scenario. A fail flags one specific surface, not the whole pipeline.

- `bash scripts/harness.sh` — non-mutating defaults (skips prod install + auth path)
- `bash scripts/harness.sh` — non-mutating defaults (skips prod install + TLS path)
- `HAL0_HARNESS_PROD=1 bash scripts/harness.sh` — also exercises sudo `/opt/hal0` install
- `HAL0_HARNESS_AUTH=1 HAL0_HARNESS_PROD=1 bash scripts/harness.sh` — adds the Caddy
`--auth=basic` install path
- `HAL0_HARNESS_TLS=1 HAL0_HARNESS_PROD=1 bash scripts/harness.sh` — adds the
TLS-default install row (installs Caddy + renders the Caddyfile per ADR-0001)
- `python3 scripts/harness-report.py tests/harness/reports/harness.json` — pretty-printer

Status vocabulary, scenario layout, JSON schema, and the "how to add a
Expand Down
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,18 @@ docker / disk / ports).

### Auth posture

The installer defaults to `--auth=off` — API on `:8080`, OpenWebUI on
`:3001`, no auth in front. For LAN-only boxes that's fine; for anything
exposed beyond the LAN, install with `--auth=basic` to bring up the
Caddy reverse proxy (basic_auth at the edge for the dashboard, bearer
tokens for the OpenAI-compatible API, OpenWebUI SSO via trusted
header). See [`installer/README.md`](./installer/README.md) for the
full flow.
Per [ADR-0001](./docs/adr/0001-collapse-edge-auth-into-fastapi.md), all
auth lives in FastAPI. A fresh install is **open on the LAN** — no
password, no Bearer required for the dashboard or `/v1/*`. The
dashboard wizard's password-setup step (`POST /api/auth/password`,
public on first run) opts in to login. Programmatic clients use Bearer
tokens unchanged.

The default install runs Caddy in front for TLS termination (Caddy's
internal CA on `.local` hosts, Let's Encrypt for real DNS-resolvable
hostnames). Use `--no-tls` to skip Caddy and front hal0 with your own
reverse proxy. See [`installer/README.md`](./installer/README.md) for
the full flow.

## License

Expand Down
9 changes: 9 additions & 0 deletions docs/api-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,15 @@ malformed body:
Same status, hal0 envelope on both surfaces, because the auth
middleware refuses the request before it can reach an upstream.

Per [ADR-0001](./adr/0001-collapse-edge-auth-into-fastapi.md) (PR #58),
the auth surface is a single FastAPI layer — `POST /api/auth/login`
issues a `hal0_session` cookie, `POST /api/auth/logout` clears it, and
`POST /api/auth/password` sets or rotates the owner password (public
when no password is yet set; writer-scoped otherwise). The middleware
accepts either the session cookie or a Bearer token against the same
`require_token` / `require_writer` deps, so 401 envelopes are identical
across both auth paths.

### 404 — not found

`/api/slots/nope` (hal0 envelope):
Expand Down
105 changes: 64 additions & 41 deletions installer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,8 @@ These are the variables `installer/install.sh` actually reads:
| `HAL0_NO_PROBE` | _(unset)_ | Set to `1` to skip the hardware probe at the end |
| `HAL0_TOOLBOX_IMAGE_VULKAN` | _(unset)_ | Override the Vulkan toolbox image ref written into `api.env` |
| `HAL0_TOOLBOX_IMAGE_ROCM` | _(unset)_ | Override the ROCm toolbox image ref written into `api.env` |
| `HAL0_HOSTNAME` | `hal0.local` | `--auth=basic` only: public hostname used in the Caddyfile |
| `HAL0_TLS_EMAIL` | `admin@$HAL0_HOSTNAME` | `--auth=basic` only: contact email for Let's Encrypt (when not `tls internal`) |
| `HAL0_ADMIN_USER` | _(prompted)_ | `--auth=basic` only: admin username for Caddy basic_auth |
| `HAL0_ADMIN_PASSWORD` | _(prompted)_ | `--auth=basic` only: admin password (hashed by `caddy hash-password`) |
| `HAL0_PUBLIC_HOST` | `hal0.local` | Public hostname rendered into the Caddyfile (TLS default install path) |
| `HAL0_TLS_EMAIL` | `admin@$HAL0_PUBLIC_HOST` | Contact email for Let's Encrypt (when not `tls internal`) |
| `HAL0_OPENWEBUI_PORT` † | `3001` | OpenWebUI host port — **dev mode only** |

† `HAL0_OPENWEBUI_PORT` is honored by `scripts/dev-bootstrap.sh` (the dev-mode launcher). The installed `hal0-openwebui.service` hardcodes `:3001`; to change it post-install, edit `/etc/systemd/system/hal0-openwebui.service` and reload.
Expand All @@ -53,42 +51,31 @@ Example:
HAL0_PORT=9090 sudo bash installer/install.sh
```

## Authentication (`--auth=basic`)
## Authentication

The default install posture is `--auth=off` — the API binds `:8080` and OpenWebUI binds `:3001` directly, with no auth in front. That's safe on a fully-trusted home LAN; it is **not** safe to expose to the internet.
Per [ADR-0001](../docs/adr/0001-collapse-edge-auth-into-fastapi.md), **all auth
now lives in FastAPI** — there is no edge-auth layer in Caddy. The Caddyfile is
a dumb TLS terminator + reverse proxy (`packaging/caddy/Caddyfile.template`,
~42 lines, no `basicauth`, no path matchers, no allowlist).

`--auth=basic` brings up the v0.2 auth POC: a Caddy reverse proxy in front of both services, with HTTP basic_auth at the edge for the dashboard and bearer-token auth for the OpenAI-compatible API.
A fresh install starts **open on the LAN** — no password set, no token
required for the dashboard or `/v1/*`. The dashboard's first-run wizard
includes a **password-setup step** (Set up password) that calls
`POST /api/auth/password` (a public endpoint when no password is yet
set, per ADR-0001 Child A). Once a password is set:

```sh
# Interactive (prompts for admin user/password):
sudo bash installer/install.sh --auth=basic

# Non-interactive:
HAL0_ADMIN_USER=alex HAL0_ADMIN_PASSWORD='hunter2' \
HAL0_HOSTNAME=hal0.local \
sudo bash installer/install.sh --auth=basic
```

What the flag does:

1. Installs Caddy (`apt install caddy` on Debian/Ubuntu, `pacman -S caddy` on Arch/CachyOS). Other distros require a manual install; the script surfaces a clear error and a docs link.
2. Prompts for or accepts via env: `HAL0_ADMIN_USER`, `HAL0_ADMIN_PASSWORD`, `HAL0_HOSTNAME` (default `hal0.local`), `HAL0_TLS_EMAIL` (default `admin@<hostname>`).
3. Hashes the password via `caddy hash-password`, renders `/etc/hal0/Caddyfile` from `packaging/caddy/Caddyfile.template`.
4. Drops `/etc/systemd/system/hal0-caddy.service` and starts it.
5. Sets `HAL0_AUTH_ENABLED=1` in `/etc/hal0/api.env` and re-renders `/etc/hal0/openwebui.env` with `WEBUI_AUTH=True` + `WEBUI_AUTH_TRUSTED_EMAIL_HEADER=X-Forwarded-Email` so OpenWebUI auto-provisions a user from the Caddy-forwarded identity (no second login).
6. Restarts `hal0-api` and `hal0-openwebui` so the new env takes effect.
7. If avahi-daemon is running, drops `/etc/avahi/services/hal0.service` so `hal0.local` resolves on the LAN. Without avahi, add a static `/etc/hosts` entry on each client: `<hal0-ip> hal0.local`.
- Browsers authenticate via `POST /api/auth/login`, which issues a signed
`hal0_session` cookie (HttpOnly, SameSite=Lax, Secure-when-TLS).
`POST /api/auth/logout` clears it.
- Programmatic clients keep using Bearer tokens (`Authorization: Bearer hal0_...`)
unchanged — the FastAPI middleware accepts both the session cookie and
Bearer tokens against the same `require_token` / `require_writer` deps.

After install:

- Dashboard: `https://hal0.local/` — basic_auth prompt → admin user → SPA.
- Chat: `https://hal0.local/chat/` — single-sign-on via the same Caddy basic_auth identity.
- OpenAI API: `https://hal0.local/v1/models` (no auth), `https://hal0.local/v1/chat/completions -H 'Authorization: Bearer hal0_...'` (token required).

Mint a token via the Settings UI (Authentication panel → Create token) or via:
Mint a Bearer token via the Settings UI (Authentication panel → Create token)
or directly:

```sh
curl -k -u 'admin:hunter2' \
curl -k -H 'Authorization: Bearer hal0_<admin-token>' \
https://hal0.local/api/auth/tokens \
-H 'Content-Type: application/json' \
-d '{"label": "openwebui-bridge", "scope": "all"}'
Expand All @@ -97,21 +84,57 @@ curl -k -u 'admin:hunter2' \
The raw token is in the response **once** — copy it immediately. To revoke:

```sh
curl -k -u 'admin:hunter2' -X DELETE \
curl -k -H 'Authorization: Bearer hal0_<admin-token>' -X DELETE \
https://hal0.local/api/auth/tokens/<token-id>
```

The `tls internal` directive in the rendered Caddyfile mints a self-signed certificate via Caddy's internal CA. For a real (DNS-resolvable) hostname, edit `/etc/hal0/Caddyfile` to remove `tls internal` and Caddy will provision a Let's Encrypt cert on the next reload.
### TLS

The default install path runs Caddy in front of FastAPI for TLS termination.
The `tls internal` directive in the rendered Caddyfile mints a self-signed
certificate via Caddy's internal CA — picked automatically when
`HAL0_PUBLIC_HOST` ends in `.local` or is `localhost`. For a real
DNS-resolvable hostname, set `HAL0_PUBLIC_HOST` and `HAL0_TLS_EMAIL`; Caddy
provisions a Let's Encrypt cert on the first reload.

To roll back:
If avahi-daemon is running, the installer drops `/etc/avahi/services/hal0.service`
so `hal0.local` resolves on the LAN. Without avahi, add a static `/etc/hosts`
entry on each client: `<hal0-ip> hal0.local`.

### Skip Caddy entirely (`--no-tls`)

```sh
sudo systemctl disable --now hal0-caddy
sudo sed -i 's|^HAL0_AUTH_ENABLED=.*|HAL0_AUTH_ENABLED=0|' /etc/hal0/api.env
sudo HAL0_AUTH_ENABLED=0 /opt/hal0/.venv/bin/python -m hal0.openwebui.env_writer
sudo systemctl restart hal0-api hal0-openwebui
sudo bash installer/install.sh --no-tls
```

`--no-tls` skips the Caddy install and Caddyfile render. FastAPI binds
`0.0.0.0:8080` and is reachable directly at `http://<host>:8080/`. This is
the right path when hal0 sits behind an existing reverse proxy (for example,
the staging deployment behind Traefik) — front it with whatever TLS and auth
layer your edge already provides.

`--dev` implies `--no-tls`; there is no system Caddy install in a dev tree.

### Upgrade notes (pre-v1)

hal0 is pre-v1. Existing installs that used the old `--auth=basic` path lose
**edge auth** on next install upgrade — the Caddyfile no longer carries
`basicauth`. The dashboard and `/v1/*` are reachable without credentials
until you do one of:

- **Set a password in the dashboard wizard.** On first load after upgrade,
the wizard's password-setup step calls `POST /api/auth/password` and
writes the bcrypt hash into the FastAPI auth store. Writer routes then
require login; reads stay open (configurable per the same wizard step).
- **Install with `--no-tls`** and front hal0 with your own reverse proxy
(Traefik, nginx, Caddy you manage outside hal0, Cloudflare Tunnel, etc.).
Your edge owns auth.

Bearer tokens minted under the prior install continue to work — token storage
moved with the rest of auth into the FastAPI store (no migration required).
The Caddy `basicauth` credentials themselves are not migrated; re-entry via
the wizard's password-setup step is the supported path.

## Dev mode (`--dev`)

Runs the full installer logic but lays everything under `$PWD/.hal0ai/` instead of FHS paths. systemd units are **not** installed or enabled — they are written to `$PWD/.hal0ai/etc/systemd/system/` for inspection only.
Expand Down
43 changes: 36 additions & 7 deletions tests/harness/FINDINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,17 @@ The round added four parallel teammates beyond the δ-harness:
δ-tier). Their per-team scratch reports remain under
`tests/harness/_in-progress/` until cleaned up.

## 10. Caddy basic_auth swallows the PUBLIC_PATHS allowlist — **critical / bug**
## 10. Caddy basic_auth swallows the PUBLIC_PATHS allowlist — **critical / bug** · ✅ FIXED BY ARCHITECTURE REMOVAL (ADR-0001)

> **FIXED BY ARCHITECTURE REMOVAL (ADR-0001).** The original fix in PR
> #49 (issue #28) is now historical. Per
> [ADR-0001](../../docs/adr/0001-collapse-edge-auth-into-fastapi.md), the
> Caddyfile no longer carries `basicauth` or a `@public path` matcher
> — Caddy is a dumb TLS terminator + reverse proxy (PR #59), and all
> auth lives in FastAPI (PR #58). The ordering bug cannot recur because
> there is no edge-auth layer to mis-order. Original report preserved
> below.


The dashboard `handle {}` block in `/etc/hal0/Caddyfile` applies
`basicauth` to every path that doesn't match `/chat*` or `/v1/*` —
Expand Down Expand Up @@ -469,7 +479,16 @@ because callers will route to it.
/v1/models non-empty`. Task #10's lifecycle work covered most
cases but missed model-less containers.

## 16. basic_auth password is unrecoverable post-install — **medium / gap**
## 16. basic_auth password is unrecoverable post-install — **medium / gap** · ✅ FIXED BY ARCHITECTURE REMOVAL (ADR-0001)

> **FIXED BY ARCHITECTURE REMOVAL (ADR-0001).** This was the source of
> issue #43 (the HITL credential-capture decision). The installer no
> longer prompts for or renders a basic_auth password — credential
> capture moved into the dashboard wizard's password-setup step
> (`POST /api/auth/password`, PR #58 + #59). Password rotation is a
> wizard interaction, not a file rewrite + Caddy reload. Original
> report preserved below for history.


The installer renders the Caddyfile with the bcrypt hash inline and
discards the plaintext. There is no on-host record of the original
Expand Down Expand Up @@ -563,7 +582,17 @@ in `error_codes.install()`'s handler set
..., "details":{"fields":[...]}}}` shape. Likely affects every
validated query/path/body parameter across all routes.

## 21. `/api/metrics/prometheus` is in PUBLIC_PATHS but the route is unimplemented — **low / dead config**
## 21. `/api/metrics/prometheus` is in PUBLIC_PATHS but the route is unimplemented — **low / dead config** · ✅ FIXED BY ARCHITECTURE REMOVAL (ADR-0001)

> **FIXED BY ARCHITECTURE REMOVAL (ADR-0001).** The `PUBLIC_PATHS`
> frozenset was deleted by PR #59 — every route's auth requirement is
> now declared in code via `dependencies=[Depends(require_token)]` (or
> by omitting the dep for public routes). The `/api/metrics/prometheus`
> orphan is documented in `src/hal0/api/routes/health.py`'s module
> docstring as a placeholder until a real exporter ships; the
> allowlist-vs-route drift cannot recur because the allowlist no longer
> exists. Original report preserved below.


`src/hal0/api/middleware/auth.py:103` lists
`"/api/metrics/prometheus"` in `PUBLIC_PATHS`, but
Expand Down Expand Up @@ -625,7 +654,7 @@ different schema for proxied vs. originated errors.
| Path | Reason | How to enable |
|---|---|---|
| `install.sh` prod install (touches `/etc`, `/var/lib`, `/usr/lib`) | mutates the real host | `HAL0_HARNESS_PROD=1 bash scripts/harness.sh` |
| `--auth=basic` Caddy install | needs prod mode + caddy installable | `HAL0_HARNESS_AUTH=1 HAL0_HARNESS_PROD=1 …` |
| TLS-default Caddy install | needs prod mode + caddy installable | `HAL0_HARNESS_TLS=1 HAL0_HARNESS_PROD=1 …` (renamed from `HAL0_HARNESS_AUTH` per ADR-0001) |
| ROCm / FLM-NPU / Moonshine / Kokoro real-model rounds | `hal0-test` LXC owns these | `make release-test` (existing γ tier) |
| Settings GET/PUT round-trip | route exists but not driven; planned next harness iteration | extend `cli-test.sh` |
| First-run wizard endpoints | currently mostly stub (PLAN §7) | wait for Team-B model-pull integration |
Expand All @@ -636,14 +665,14 @@ different schema for proxied vs. originated errors.
## How to re-run

```
# full harness (no prod, no auth):
# full harness (no prod, no TLS):
bash scripts/harness.sh

# include sudo /opt/hal0 install + uninstall:
HAL0_HARNESS_PROD=1 bash scripts/harness.sh

# include --auth=basic install path too:
HAL0_HARNESS_AUTH=1 HAL0_HARNESS_PROD=1 bash scripts/harness.sh
# include the TLS-default install path too (installs Caddy per ADR-0001):
HAL0_HARNESS_TLS=1 HAL0_HARNESS_PROD=1 bash scripts/harness.sh
```

The aggregate JSON lands at `tests/harness/reports/harness.json` and
Expand Down
Loading
Loading