Files
atheos-blog-lxc/aidlc-docs/design-artifacts/unit-of-work.md
T
hermes a6fb61d0ea feat: provision Alpine LXC 136 running Atheos, wired to Gitea
Provisions a LAN-only browser code editor (Atheos) in an Alpine 3.23 LXC on the
scotty PVE node, with armistace/blog cloned and ready to edit. Built via the
AI-DLC workflow; the full process trail is in aidlc-docs/.

Deliverables
- scripts/provision.sh          rebuild the guest from scratch (re-runnable)
- scripts/update-atheos.sh      move Atheos to latest upstream and RE-PIN
- scripts/build-and-test.sh     captures evidence for every acceptance criterion
- files/atheos.conf             nginx vhost (socket path byte-matches the FPM pool)
- files/ssh-config              ~atheos/.ssh/config deploy-key alias
- RUNBOOK.md                    operational guide (START AT STEP 1)
- docs/security-notes.md        credential model + wider-web hardening TODOs
- docs/resiliency-notes.md      what a non-PoC deployment would need

Design decisions
- D1 no git identity is configured by the agent; the operator sets it. Git will
  refuse the first commit until then, which is deliberate (requirement FR-11).
- D2 Atheos pinned to commit f90cd18 (build 611) with a verified tarball sha256,
  plus update-atheos.sh as the deliberate escape hatch.
- D3 nginx :80, web root /var/www/atheos, php-fpm over a unix socket.
- D6 git auth is a per-repository DEPLOY KEY scoped to armistace/blog, not a
  personal access token (a PAT's write:repository scope reaches every repo the
  account can write).

Verified on the live guest (40 checks, 0 failures)
- AC-2  Atheos served over HTTP 200 by nginx+php-fpm; socket paths byte-match
- AC-3  20 posts + themes/cleanblog present; owned by the php-fpm user
- AC-4  all five writable paths writable as the service user
- AC-5  remote targets armistace/blog; deploy key authenticates over SSH
- AC-6  survival proven with a real container restart, marker byte-identical
- AC-8  no python/pelican/pip/make and no DB engine - scope boundary enforced
- FR-11 git identity is empty; deploy key mode 600 owned by the service user

Traps documented in the RUNBOOK (each cost real debugging time)
- The Gitea SSH user is `gitea`, NOT `git` (app.ini SSH_USER=gitea). With `git`,
  sshd answers "Invalid user git" and rejects before reading authorized_keys,
  which is indistinguishable from a bad key.
- The Gitea SSH endpoint is the LAN address (:22 is not forwarded publicly).
- `uptime` and `free` report the PVE HOST inside an LXC without lxcfs, so a
  container that just restarted can claim days of uptime.
2026-09-29 15:50:34 +10:00

90 lines
4.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Unit of Work — atheos-blog-lxc
**Stage**: INCEPTION — Units Generation (Part 2: Generation)
**Date**: 2026-09-29
A unit = a logical grouping of work that ends in a **verifiable state**.
---
## U1 — Guest provisioning
| | |
|---|---|
| **Goal** | An Alpine 3.23 LXC exists, reachable, correctly sized, and boot-persistent |
| **Components** | C1 |
| **Produces** | VMID **136** on node `scotty`, 20 GB root, 2 vCPU, 1 GB RAM, unprivileged, `onboot=1`, TZ `Australia/Brisbane`, package index updated |
| **Preconditions** | Alpine 3.23 template downloaded on scotty (`pveam update` + `download`) |
| **Verification** | `pct config 136` shows Alpine + 20 GB root; the guest answers `ping`/`ssh`; date is Brisbane |
| **ACs** | AC-1 |
| **Notes** | Template download is part of this unit — it does not exist on scotty yet (risk C-3) |
## U2 — Web stack
| | |
|---|---|
| **Goal** | nginx serves PHP, and the two sides agree on the socket |
| **Components** | C2, C3 |
| **Produces** | `nginx`, `php83-fpm`, `php83-{session,zip,openssl,mbstring}` installed; pool + vhost configured; `allow_url_fopen=On`; **both services enabled at the OpenRC `default` runlevel** |
| **Preconditions** | U1 |
| **Verification** | A `phpinfo()`-style probe renders over HTTP (**not** 502/404); the nginx `fastcgi_pass` socket path **string-matches** the pool's `listen` (interface I-1); both services are in `rc-update show` |
| **ACs** | AC-2 |
| **Notes** | Resolve the actual FPM socket from the installed pool config, not from upstream docs (risk C-1). OpenRC idioms only — no systemd (risk C-2) |
## U3 — Atheos install
| | |
|---|---|
| **Goal** | Atheos runs and its installer page loads |
| **Components** | C4, C5 |
| **Produces** | Atheos at `/var/www/atheos` pinned to a resolved commit SHA; `config.php` present; `data/`, `workspace/`, `plugins/`, `themes/` writable by the FPM user |
| **Preconditions** | U2 |
| **Verification** | `http://<ip>/` returns the Atheos installer (HTTP 200, no PHP fatal in the error log); a write test inside `workspace/` succeeds as the FPM user |
| **ACs** | AC-2, AC-3 |
| **Notes** | **The genuinely uncertain unit** (risk C-1). If Atheos fights Alpine's PHP layout, stop and report rather than papering over it |
## U4 — Git + clone + deploy key
| | |
|---|---|
| **Goal** | The operator can open `blog`, edit it, and push from here once they add the key |
| **Components** | C6, C7 |
| **Produces** | `git` installed; `safe.directory` set for the workspace; clone of `armistace/blog` (branch `master`) at `workspace/blog`; **ed25519 deploy keypair generated on the guest** (private key owned by the FPM user, mode 600); `~/.ssh/config` host alias `gitea-blog` with `IdentitiesOnly yes`; `user.name`/`user.email` **deliberately left unset** |
| **Preconditions** | U1 (git), U3 (workspace exists) |
| **Verification** | `blog` appears as a project and its files open; `git -C workspace/blog remote -v` shows Gitea; the **public** key is emitted for the operator; `git config --get user.name` returns **empty** (proving D1) |
| **ACs** | AC-3, AC-4, AC-5 |
| **Notes** | Two phases by necessity: this unit leaves the remote on **HTTPS**; the SSH switch happens *after* the operator installs the deploy key (D6). The agent must not install it |
## U5 — Docs, scripts, handoff
| | |
|---|---|
| **Goal** | A different agent or a rebuild needs no prior context |
| **Components** | all |
| **Produces** | `README.md`, `RUNBOOK.md` (incl. **"set your git identity first"** prompt, the deploy-key install steps, and the exact `git remote set-url` command), `docs/security-notes.md`, `docs/resiliency-notes.md`, `scripts/provision.sh`, `scripts/update-atheos.sh` |
| **Preconditions** | U1–U4 |
| **Verification** | README + RUNBOOK exist and are linked; `provision.sh` is the exact sequence that was actually run; `update-atheos.sh` re-clones Atheos at latest and **re-pins the SHA** (D2) |
| **ACs** | AC-7 |
| **Notes** | The RUNBOOK is the operator-facing artifact: it must open with their required first action |
---
## Deferred / not a unit
- **The SSH remote switch** — operator-owned (D6). Documented in RUNBOOK, not built.
- **Atheos admin user creation** — operator completes the installer (D5).
- **Pelican build/publish** — out of scope entirely (Q1=A, AC-8).
## Greenfield code-organization strategy
No application code is generated, so the repo holds **only** provisioning + docs:
```
atheos-blog-lxc/
├── README.md
├── RUNBOOK.md
├── docs/{security-notes.md,resiliency-notes.md}
├── scripts/{provision.sh,update-atheos.sh}
└── aidlc-docs/ ← process trail only; never code
```