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.
90 lines
4.6 KiB
Markdown
90 lines
4.6 KiB
Markdown
# 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
|
||
```
|