Files
atheos-blog-lxc/aidlc-docs/design-artifacts/unit-of-work.md
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

4.6 KiB
Raw Permalink Blame History

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