Adds the branch-based publishing model: the editor writes to a long-lived
`atheos-writer` branch, and merging into `master` stays in Gitea.
Six macros installed into Atheos's macro store (a copy is kept in
files/macros.db.php so a rebuild reproduces them), invoked by right-clicking
the `blog` folder:
1. Start/Join Writer Branch idempotent create-or-switch + push -u
2. Commit + Push Writer Branch identity guard -> add -A -> commit (only
if something is staged) -> push
3. DESTRUCTIVE: Reset Writer to Master fetch -> reset --hard origin/master ->
push --force-with-lease
4. Status + Recent Log read-only
5. Refresh Master (ff-only) pull master down, return to writer
6. Merge Master Into Writer Branch when master has moved ahead
Atheos has no terminal and codegit exposes no branch-switching, so macros are
the only way to run arbitrary git from the UI. Verified: codegit's `checkout`
action takes a FILE argument - it is per-file revert, not branch switching.
Identity handling: the commit macros deliberately do NOT hardcode an identity.
php-fpm runs with HOME=/home/atheos (verified), the same global gitconfig the
operator edits over SSH, so hardcoding `git -c user.name=...` would OVERRIDE the
operator's own identity. Instead macro 2 guards on an identity existing and fails
with an actionable message - which is what design decision D1 intends.
Verified by exercising each command through the web server as the php-fpm user:
- macro 1, 4, 5 return rc=0 with correct output
- macro 2's identity guard fires with the exact RUNBOOK STEP 1 message
- macro 3 was tested with real divergence: committed on the writer branch,
reset --hard to master, force-pushed; origin/atheos-writer then matched
origin/master exactly (0 ahead, 0 behind)
Security note recorded in the docs and README: this model only holds if branch
protection is enabled on armistace/blog's master. It is currently NOT enabled,
so the deploy key could force-push master directly - the operator needs to turn
it on (repo-owner action).
Also documents two hazards found while testing:
- macro 2's `git add -A` stages everything, and a subsequent `reset --hard`
deletes any untracked file that was staged. Keep scratch files outside
workspace/blog/.
- recovery procedure for a file lost that way (git fsck --lost-found ->
ls-tree -> cat-file), which was used to restore a file during testing.
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.