Files
atheos-blog-lxc/RUNBOOK.md
hermes 1c7db911df feat: add writer-branch workflow macros
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.
2026-09-29 16:07:25 +10:00

322 lines
13 KiB
Markdown

# RUNBOOK — atheos-blog-lxc
Atheos browser editor for `armistace/blog`, running in an Alpine LXC on Proxmox.
| | |
|---|---|
| **Guest** | LXC **136** on node `scotty` (192.168.178.69) |
| **IP** | **http://192.168.178.57/** |
| **OS** | Alpine 3.23.2 · unprivileged · 2 vCPU · 1 GB RAM · 20 GB root · `onboot=1` |
| **App** | Atheos IDE, pinned to commit `f90cd1806dbe0e25673a77e27390e8b1a240c3f6` (build 611) |
| **Repo** | `git.aridgwayweb.com/hermes/atheos-blog-lxc` (this provisioning repo) |
| **Workspace** | `/var/www/atheos/workspace/blog` → clone of `armistace/blog` |
| **Access** | LAN only. No NPM proxy host, no DNS record, no port-forward. |
---
## ⚠️ STEP 1 — DO THIS FIRST: set your git identity
**The agent deliberately did not configure a git identity** (design decision D1, requirement FR-11).
Git will **refuse to commit** until you set one. This is your first action after SSH-ing in.
```sh
ssh root@192.168.178.57
# become the user that runs PHP and owns the workspace
su -s /bin/sh atheos
# set YOUR identity (these values are placeholders - use your real ones)
git config --global user.name "Andrew Ridgway"
git config --global user.email "ar17787@gmail.com"
# verify
git config --global --get user.name
git config --global --get user.email
```
> Why you and not the agent: you own the `armistace` identity. Configuring it under your name
> programmatically would impersonate you and blur commit attribution.
The same identity must be set for the deploy key's user, which is the **same account** (`atheos`),
so the single `su` above covers both the editor's commits and anything you do by hand.
---
## STEP 2 — install the deploy key (Phase 2 prerequisite)
The guest already has a per-repo deploy keypair. Only the **public** half is shared:
```sh
# print it again if you need it
cat /home/atheos/.ssh/id_ed25519_blog.pub
```
Add it in Gitea:
1. Open `armistace/blog` → **Settings** → **Deploy Keys** → **Add Deploy Key**
2. Title: `atheos-blog-lxc`
3. Paste the public key
4. ✅ **TICK "Allow write access"** — required, otherwise push is rejected
Public key (as generated 2026-09-29):
```
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINj03Tep9pULTvVDCC3q1ZtocKTZSmDhPsB61Zic1SPk atheos-blog-lxc deploy key (armistace/blog)
```
Fingerprint to confirm you pasted the right one:
```
SHA256:qHvsjjSX79aPZRoNTRI4j+HH3hArAuINIRu9ewxx7t8 (ED25519)
```
---
## STEP 3 — switch the remote to SSH (Phase 2)
Once the key is installed, tell the agent (or run it yourself):
```sh
su -s /bin/sh atheos -c \
'git -C /var/www/atheos/workspace/blog remote set-url origin gitea-blog:armistace/blog.git'
# verify the transport actually authenticates
su -s /bin/sh atheos -c 'ssh -T gitea-blog' # expect a Gitea greeting, not "Permission denied"
su -s /bin/sh atheos -c 'git -C /var/www/atheos/workspace/blog fetch --dry-run'
```
### Why the remote uses `gitea-blog` and not a hostname
`gitea-blog` is an SSH config alias defined in `/home/atheos/.ssh/config`:
```
Host gitea-blog
HostName 192.168.178.155 # LAN address
User gitea # <-- NOT `git`! see the trap below
IdentityFile ~/.ssh/id_ed25519_blog
IdentitiesOnly yes
Port 22
```
> ### ⚠️ TRAP: the SSH user is `gitea`, not `git`
> Every GitHub/Gitea tutorial uses `git@host`. **This instance does not.** Its `app.ini` sets
> `SSH_USER = gitea`.
>
> If you use `User git`, sshd replies **`Invalid user git`** and rejects the connection
> **before it ever reads `authorized_keys`** — so the error looks exactly like a rejected key
> and sends you hunting for a problem that isn't there. Confirmed in the sshd log on LXC 109:
> ```
> Invalid user git from 192.168.178.57 port 39780
> Connection closed by invalid user git 192.168.178.57 [preauth]
> ```
> Correct user → `Hi there! You've successfully authenticated with the deploy key named atheos-blog-lxc`.
Two reasons for the alias:
1. **`IdentitiesOnly yes`** means no other key is ever offered — a stray `git push` cannot
authenticate with a different (possibly broader) identity.
2. **It's the LAN address because that's the only path that works.** Verified 2026-09-29:
| Endpoint | Result |
|---|---|
| `git.aridgwayweb.com:22` | **unreachable** — the public path does not forward :22 (only :443 via NPM) |
| `192.168.178.155:22` | **OPEN** — Gitea's `app.ini` sets `SSH_DOMAIN = 192.168.178.155`, `SSH_PORT = 22`, `START_SSH_SERVER = false` |
So Gitea serves SSH from the LXC's own `sshd` and advertises the LAN address. Since this guest is
LAN-only, LAN transport is also the correct choice — no internet round-trip.
---
## STEP 4 — complete the Atheos installer
Open **http://192.168.178.57/** in a browser. The Atheos first-run installer appears and creates
the **admin user** — this is yours to own; the agent seeded no accounts.
Verify afterwards:
- you can open the **Workspace** and see a `blog` project
- opening `src/content/*.md` shows your posts
- saving persists (the files are at `/var/www/atheos/workspace/blog`)
### Atheos git settings
Open Atheos → (user menu) → **Settings** to confirm the git integration. The `codegit` component
handles commit/push. If you see *"detected dubious ownership"*, the `safe.directory` entries are
missing — restore them with:
```sh
su -s /bin/sh atheos -c 'git config --global --add safe.directory /var/www/atheos/workspace'
su -s /bin/sh atheos -c 'git config --global --add safe.directory /var/www/atheos/workspace/blog'
```
---
## Day-to-day operations
### Edit and commit
Do everything in the browser at `http://192.168.178.57/`. Commit via Atheos's git panel, or by hand:
```sh
ssh root@192.168.178.57
su -s /bin/sh atheos
cd /var/www/atheos/workspace/blog
git status
git add -A && git commit -m "post: <what changed>"
git push
```
### The build is NOT here
This box **only writes markdown**. Building/publishing the Pelican site happens in the existing
pipeline elsewhere. There is deliberately **no Python, no pelican, no `make`** on this guest.
### Services (OpenRC — not systemd)
```sh
rc-service nginx status
rc-service php-fpm83 status
rc-update show | grep -E 'nginx|php-fpm83' # both must be in 'default'
```
### Logs
```sh
tail -f /var/log/nginx/atheos.error.log # PHP fatals surface here
tail -f /var/log/nginx/atheos.access.log
```
### Reboot / recovery
```sh
pct reboot 136 # from scotty, or
pct stop 136 && pct start 136
```
Services are enabled at the OpenRC `default` runlevel, so they return automatically
(verified — see `aidlc-docs` acceptance criteria AC-6).
### Rebuild from scratch
The whole guest is reproducible:
```sh
# on scotty (192.168.178.69), as root
pct destroy 136
sh scripts/provision.sh # from this repo, on the node
```
Nothing unique lives on the guest: the blog's source of truth is Gitea. Only **uncommitted edits**
would be lost — commit often.
### Update Atheos (and re-pin)
Atheos has no releases, so this install is pinned to a commit. To move to the latest upstream commit,
install it, and re-pin:
```sh
sh scripts/update-atheos.sh # on the guest; preserves workspace/, backs up, verifies, re-pins
```
---
## Security model (summary — full detail in `docs/security-notes.md`)
- **Git auth = a per-repository deploy key**, scoped to `armistace/blog`, write-enabled.
Not a PAT (whose `write:repository` scope reaches **every** repo the account can write).
- Private key: `/home/atheos/.ssh/id_ed25519_blog`, mode `600`, owned by the php-fpm user.
- The web app and the key share one unprivileged user, so a compromised Atheos **can** write `blog`.
That is the accepted floor — one repo, and (with branch protection) one branch.
- **LAN-only.** If this is ever exposed: keep branch protection on `blog`'s `master`, terminate TLS
at NPM, and put authentication *in front* of Atheos. See `docs/security-notes.md` §2 (S-1…S-14).
### Container resource reporting (a real trap — cost debugging time)
`uptime` and `free` on Alpine are **busybox** applets. Busybox reads via the **`sysinfo()` syscall**,
which lxcfs **cannot intercept** — so inside this container they report the **PVE host**, not the
container. A container that restarted seconds ago will cheerfully claim *"up 2 days"*.
lxcfs **is** working correctly here — the virtualised files are right:
```
/proc/meminfo → MemTotal: 1048576 kB ← our 1024 MB limit
/proc/uptime → 105 s ← container
/proc/cpuinfo → 2 processors ← container
```
The container is correct; **the tool is wrong**. Fix = `apk add procps-ng`, whose `/usr/bin/free`
and `/usr/bin/uptime` read those virtualised `/proc` files:
| Tool | Reports | |
|---|---|---|
| `procps-ng` `free` | `1024 MB` | ✅ correct |
| busybox `free` | `15701 MB` | ❌ host |
| `procps-ng` `uptime` | `up 2 min` | ✅ correct |
| busybox `uptime` | `up 2 days` | ❌ host |
> **Note:** `apk add lxcfs` inside the guest is **not** a fix — lxcfs is a host-provided FUSE
> filesystem, not a guest package. `provision.sh` installs `procps-ng` for this reason.
> (`df` is unaffected: it stats the filesystem directly, so it was always accurate.)
---
## Reaching the guest when SSH from your laptop fails
The guest has **root SSH enabled** with your fleet keys installed:
- `ssh-rsa ... aridgway@strategenics-25`
- `ssh-ed25519 ... oct2025`
If those keys aren't what you're using, add/push a key via the PVE node (which always works):
```sh
ssh root@192.168.178.69 "pct exec 136 -- sh -c 'echo \"<your-pubkey>\" >> /root/.ssh/authorized_keys'"
```
---
## Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| **502 Bad Gateway** | nginx `fastcgi_pass` and the FPM pool `listen` disagree | `grep -E '^listen' /etc/php83/php-fpm.d/www.conf` and compare with the `fastcgi_pass` line in `/etc/nginx/http.d/atheos.conf`. Both must read `/run/php-fpm83/php-fpm.sock`. Then restart both services. |
| **"detected dubious ownership"** | `safe.directory` missing or the repo is owned by a different user | Re-add the two `safe.directory` entries (STEP 4) |
| **git commit refuses: "please tell me who you are"** | identity not set — **expected on a fresh build** | STEP 1 |
| **git push: Permission denied (publickey)** | deploy key not installed, **or the SSH user is wrong**, or "Allow write access" not ticked | Check all three. Especially: `grep User /home/atheos/.ssh/config` must say **`gitea`**, not `git` (see STEP 3 trap). On the Gitea host, confirm with `journalctl -u ssh \| tail` — `Invalid user git` means the username is wrong, not the key. |
| **push times out** | remote still pointing at `git.aridgwayweb.com` (:22 not forwarded) | STEP 3 — use the `gitea-blog` alias |
| **Atheos shows a blank page / 500** | PHP fatal | `tail -50 /var/log/nginx/atheos.error.log` |
| **Atheos installer says a path isn't writable** | ownership broken | `chown -R atheos:atheos /var/www/atheos` |
| **`free` / `uptime` show the PVE host's memory or days of uptime** | on Alpine these are **busybox** applets using the `sysinfo()` **syscall**, which lxcfs cannot intercept | `apk add procps-ng` — its `/usr/bin/free` and `/usr/bin/uptime` read the lxcfs-virtualised `/proc` files. Already installed by `provision.sh`; this bites if you rebuild by hand. |
| **PHP session errors** | rootfs full | `df -h /` (the rootfs is 20 GB and the app is tiny, so this indicates something else) |
---
## Change log
| Date | Change |
|---|---|
| 2026-09-29 | Initial build: LXC 136 on scotty, Alpine 3.23.2, nginx + php83-fpm (socket), Atheos pinned at `f90cd18` (build 611), `armistace/blog` cloned over HTTPS, per-repo deploy key generated. LAN-only. |
| 2026-09-29 | Added the **writer-branch workflow**: 6 macros installed into Atheos (start/join, commit+push, destructive reset-to-master, status, refresh master, merge master) writing to a long-lived `atheos-writer` branch; merging stays in Gitea. See `docs/writer-branch-workflow.md`. |
## Writer-branch workflow (quick reference)
The editor writes to `atheos-writer`; you merge to `master` in Gitea.
**Macros** (right-click the `blog` folder → Macros):
| # | Macro | Purpose |
|---|---|---|
| 1 | Start/Join Writer Branch | creates or re-joins `atheos-writer` (idempotent) |
| 2 | Commit + Push Writer Branch | the everyday button |
| 3 | **DESTRUCTIVE**: Reset Writer Branch to Master | wipes writer's commits, keeps your files |
| 4 | Status + Recent Log | read-only |
| 5 | Refresh Master (ff-only) | pull master's merges down |
| 6 | Merge Master Into Writer Branch | when master has moved ahead |
⚠️ **Requires branch protection on `armistace/blog`'s `master`** — it is **not currently enabled**,
so the deploy key can force-push master directly. See `docs/writer-branch-workflow.md` §Setup.
⚠️ **Macro 2 uses `git add -A`** — everything left in the repo gets published to Gitea. Keep scratch
files outside `workspace/blog/`.
Full detail, recovery steps and caveats: [`docs/writer-branch-workflow.md`](docs/writer-branch-workflow.md).