commit c2897ed660bee1f0d47e7fa44236b3e34438c353 Author: hermes Date: Tue Sep 29 15:44:11 2026 +1000 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. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f4b09c --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# keys and credentials — never commit +*.pem +id_ed25519* +id_rsa* +*.key +.env + +# AI-DLC scratch +aidlc-docs/**/*.tmp diff --git a/README.md b/README.md new file mode 100644 index 0000000..889ae9d --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# atheos-blog-lxc + +Provisioning for a **browser-based code editor** (Atheos) in an Alpine LXC, wired to Gitea, with the +`armistace/blog` repository cloned and ready to edit. + +Built with the **AI-DLC** workflow — the full process trail (requirements, design, units, decisions, +audit log) lives in [`aidlc-docs/`](aidlc-docs/). + +--- + +## What this is + +A small, single-purpose Proxmox container that puts an editor in your browser so you can write blog +posts and commit them to Gitea — without VS Code, without a cloud IDE, and without a build toolchain. + +| | | +|---|---| +| **Guest** | Proxmox LXC **136** on node `scotty` | +| **OS** | Alpine 3.23.2 (unprivileged, OpenRC) | +| **URL** | `http://192.168.178.57/` — **LAN only** | +| **Stack** | nginx → php-fpm 8.3 (unix socket) → Atheos IDE | +| **Sizes** | 2 vCPU · 1 GB RAM · 20 GB rootfs · `onboot=1` | +| **Editor** | [Atheos](https://github.com/Atheos/Atheos) — PHP, no database, real vim keybindings | +| **Project** | `armistace/blog` cloned to `/var/www/atheos/workspace/blog` | +| **Git auth** | per-repository **deploy key** (write-enabled) | + +### What it deliberately is NOT + +- **Not VS Code / code-server / Coder.** Atheos is an independent, lightweight PHP editor. +- **Not a build environment.** The blog is a Pelican site, but building/publishing happens in your + existing pipeline. There is intentionally **no Python, no pelican, no `make`** here — the presence + of Python would be a defect (verified by acceptance criterion AC-8). +- **Not internet-facing.** No reverse proxy host, no DNS record, no port-forward is created. + +--- + +## Quick start + +**If you just want to use it:** + +1. Open **http://192.168.178.57/** and complete the Atheos first-run installer. +2. `ssh root@192.168.178.57`, then **set your git identity** — see **RUNBOOK.md STEP 1**. Git will + not let you commit until you do, and that is deliberate: the agent does not create your identity. +3. Add the deploy key to `armistace/blog` and switch the remote to SSH — **RUNBOOK.md STEP 2 & 3**. + +👉 **The full operational guide is [RUNBOOK.md](RUNBOOK.md).** Start at STEP 1. + +--- + +## Repo layout + +``` +. +├── README.md this file +├── RUNBOOK.md ← operational guide; START AT STEP 1 +├── docs/ +│ ├── security-notes.md credential model + hardening TODOs if ever exposed +│ └── resiliency-notes.md what a non-PoC deployment would need +├── files/ +│ ├── atheos.conf nginx vhost (install to /etc/nginx/http.d/atheos.conf) +│ └── ssh-config ~atheos/.ssh/config (the gitea-blog alias) +├── scripts/ +│ ├── provision.sh rebuild the whole guest from scratch +│ └── update-atheos.sh update Atheos to latest upstream + re-pin +└── aidlc-docs/ AI-DLC process trail (requirements → design → units → audit) +``` + +--- + +## Rebuilding from scratch + +The guest holds nothing unique — the blog's source of truth is Gitea — so a rebuild is cheap: + +```sh +# on scotty (192.168.178.69), as root +pct destroy 136 +sh scripts/provision.sh +``` + +`scripts/provision.sh` is written to be re-runnable and reproduces the documented build exactly, +including the pinned Atheos commit and its verified tarball checksum. + +--- + +## Design decisions worth knowing + +| # | Decision | Why | +|---|---|---| +| **D1** | **No git identity is configured by the agent.** You set it (RUNBOOK STEP 1). | You own the `armistace` identity; setting it programmatically would impersonate you and blur attribution (requirement FR-11). | +| **D2** | **Atheos is pinned to a commit SHA**, with `update-atheos.sh` to deliberately move it. | Atheos has no releases; a moving branch is not reproducible. | +| **D3** | nginx :80, web root `/var/www/atheos`, php-fpm over a **unix socket**. | Smallest network surface — nothing extra listens on the LAN. | +| **D6** | **Per-repository deploy key**, not a personal access token. | A Gitea PAT's scopes are *route groups* matched against your whole account, so a stolen token reaches **every repo you can write**. A deploy key reaches exactly one. | + +Full rationale, including the alternatives that were rejected and the verification behind them, is in +[`aidlc-docs/design-artifacts/application-design.md`](aidlc-docs/design-artifacts/application-design.md) +and [`docs/security-notes.md`](docs/security-notes.md). + +--- + +## Security at a glance + +- Git auth: **deploy key scoped to `armistace/blog`**, private key mode `600`, owned by the php-fpm user. +- The SSH alias uses `IdentitiesOnly yes`, so no other key is ever offered. +- The web app runs as a **dedicated unprivileged user** (`atheos`), which also owns the workspace. +- ⚠️ **Accepted risk:** Atheos runs as a web app with shell-exec reach, so a compromise can use the + deploy key to write `blog`. That is the intended floor (one repo, one branch) rather than the + ceiling. Keep **branch protection on `blog`'s `master`**. +- If this is ever exposed to the wider web, work through `docs/security-notes.md` §2 first. + +--- + +## Provenance + +| | | +|---|---| +| Built | 2026-09-29 | +| AI-DLC stages executed | Workspace Detection, Requirements, Workflow Planning, Application Design, Units Generation, Infrastructure Design, Code Generation, Build & Test | +| Stages skipped (with rationale) | Reverse Engineering, User Stories, Functional Design, NFR Requirements, NFR Design — see `aidlc-docs/plans/execution-plan.md` | +| Atheos pin | `f90cd1806dbe0e25673a77e27390e8b1a240c3f6` (build 611), tarball sha256 `03ebcb1479b59f1898b5fa50808b181750b42211698f7f7b4042cf2514d78b20` | diff --git a/RUNBOOK.md b/RUNBOOK.md new file mode 100644 index 0000000..05e2669 --- /dev/null +++ b/RUNBOOK.md @@ -0,0 +1,269 @@ +# 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: " +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). + +--- + +## 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 \"\" >> /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` | +| **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. | diff --git a/aidlc-docs/aidlc-state.md b/aidlc-docs/aidlc-state.md new file mode 100644 index 0000000..74d6bf5 --- /dev/null +++ b/aidlc-docs/aidlc-state.md @@ -0,0 +1,64 @@ +# AI-DLC State Tracking + +## Project Information +- **Project**: atheos-blog-lxc +- **Start Date**: 2026-09-29T05:05:05Z +- **Current Stage**: INCEPTION — Workspace Detection (complete) → Requirements Analysis +- **Deploy Target**: Proxmox LXC (Alpine 3.23) — NOT the K3s cluster. Deploy-target flavour: + "self-hosted LXC", so `k3s-flavor.md` does NOT apply and no CI/registry pipeline is wired. + +## Workspace State +- **Existing Code**: No (greenfield repo; the blog being edited is a separate existing repo) +- **Programming Languages**: none yet (deliverable is provisioning/docs; target app is PHP) +- **Build System**: none (Alpine + nginx/php-fpm provisioning; Makefile-driven install script) +- **Reverse Engineering Needed**: N/A for the repo; brownfield recon done on the *estate* + (blog repo, Gitea, PVE templates) — see audit.md Workspace Detection entry. +- **Workspace Root**: /home/hermes/dev/atheos-blog-lxc + +## Existing Estate Facts (load-bearing — verify before trusting) +- **Atheos** (Atheos/Atheos, 677★, MIT, active): PHP web IDE, **no database**. Install = drop + files in a web root + make `config.php`, `data/`, `workspace/`, `plugins/`, `themes/` writable. + Optional PHP extensions for the plugin marketplace: **ZIP, OPENSSL, MBSTRING**, and + `allow_url_fopen=On`. Has git integration via its **codegit** component. +- **blog repo** = `armistace/blog` (public, default branch `master`). It is a **Pelican static + site**, not plain content: + - `src/content/*.md` (~20 posts) + `src/content/images/` + - `src/themes/cleanblog/` + - `src/pelicanconf.py`, `src/devpelconf.py`, `src/publishconf.py`, `src/Makefile`, `src/tasks.py` + - `requirements.txt`: `pelican[markdown]`, `markdown-markup-emoji` + - Root: `Dockerfile` (python:slim → `make html` → `make serve-global`), `kube/`, `.gitea/workflows/build_push.yml` + - **Implication**: to *regenerate/publish* the site you need Python+pelican (`make html`, `make github`). + Atheos alone edits the markdown + git and cannot build it. +- **PVE**: template `alpine-3.23-default_20260116_amd64.tar.xz` available (must download first). + Node `janeway` (.225) = 15.9 GB RAM (3.3 GB free), storage `extra` 874 GB free. + Node `data` (.42) = 7.9 GB RAM (only ~0.95 GB free) — avoid heavy guests. +- **Gitea**: LXC 109 on data (.42), guest .155, **1.27.3** (post-CVE-2026-60004 remediation). + `.151` = LXC 110 **nginxpm (NPM)** which fronts it; runners are on k3s nodes .161/.162. +- **Access**: `root@` + `pct exec`. Gitea token in `~/.hermes/credentials/gitea-api.env`. + +## Code Location Rules +- **Application Code**: Workspace root (NEVER in aidlc-docs/) +- **Documentation**: aidlc-docs/ only +- **Structure patterns**: See code-generation.md Critical Rules + +## Extension Configuration +| Extension | Enabled | Decided At | +|---|---|---| +| security-baseline | ❌ No (opt-out; TODOs documented in docs/security-notes.md) | Requirements Analysis | +| resiliency-baseline | ❌ No (opt-out; TODOs documented in docs/resiliency-notes.md) | Requirements Analysis | +| property-based-testing | ❌ No | Requirements Analysis | + +## Stage Progress +### 🔵 INCEPTION PHASE +- [x] Workspace Detection +- [ ] Reverse Engineering (brownfield only — N/A; estate recon done inline) +- [x] Requirements Analysis +- [x] User Stories (SKIPPED — user-confirmed; substituted with a requirement→unit map) +- [x] Workflow Planning +- [x] Application Design (minimal depth — 5 design artifacts; D6 deploy-key model) +- [x] Units Generation (5 units; requirement→unit map substitutes the story map) +### 🟢 CONSTRUCTION PHASE +- [ ] Per-Unit Loop +- [ ] Build and Test +### 🟡 OPERATIONS PHASE +- [ ] Operations (placeholder) diff --git a/aidlc-docs/audit.md b/aidlc-docs/audit.md new file mode 100644 index 0000000..72cb408 --- /dev/null +++ b/aidlc-docs/audit.md @@ -0,0 +1,132 @@ +# AI-DLC Audit Log + +> APPEND-ONLY. Every user input captured verbatim, never summarized or paraphrased, +> with ISO-8601 timestamps. NEVER overwrite this file — always append new sections. + +## Workspace Detection +**Timestamp**: 2026-09-29T05:05:05Z +**User Input**: "ok back to the plan. We need to engage AIDLC I want an alpine lxc that +1. install atheos (nginx php etc) +2. sets up git integrations (if required) +3. downloads https://git.aridgwayweb.com/armistace/blog ready for me to use +4. I will set upt he armistace ssh keys myself +5. is backed by a 20gb root (tiny its basically going to be used to write blog)" +**AI Response**: "Workspace Detection executed. New project (no existing aidlc-docs/). +Created workspace scaffold at ~/dev/atheos-blog-lxc. Conducted pre-kickoff reconnaissance: +(A) blog repo armistace/blog is a PELICAN STATIC SITE (Python), not plain markdown — it has +src/{content,pelicanconf.py,devpelconf.py,publishconf.py,Makefile,tasks.py,themes/cleanblog}, +builds with `make html` and publishes with `make github`/publishconf.py. Atheos alone CANNOT +build/regenerate it; it edits the .md sources + git only. +(B) Atheos requires: PHP + web server (nginx+php-fpm), NO database; writable config.php, data, +workspace, plugins, themes; optional extensions ZIP, OPENSSL, MBSTRING (needed for plugin +marketplace); optionally allow_url_fopen=On. Ships git integration (codegit component). +(C) Alpine 3.23 (alpine-3.23-default_20260116_amd64.tar.xz) is available as a PVE template +but NOT yet downloaded on any node. +(D) CORRECTION to the 2026-09-29 Gitea incident record: 192.168.178.151 is LXC 110 (nginxpm / +NPM), NOT the Gitea Actions runner. The .151 traffic observed against Gitea was NPM proxying +Gitea's own web traffic; the runners run on the k3s nodes (k3s-3 .161, k3s-2 .162). Memory +corrected accordingly. +(E) git.aridgwayweb.com resolves to a PUBLIC IP (115.70.15.16) — Gitea is internet-facing via NPM." +**Context**: INCEPTION — Workspace Detection + +## Requirements Analysis +**Timestamp**: 2026-09-29T05:05:05Z +**User Input**: (none yet — awaiting answers) +**AI Response**: "Created aidlc-docs scaffold (aidlc-state.md, audit.md, plans/, requirements/, +story-artifacts/, design-artifacts/, incidents/). Generated +aidlc-docs/requirements/requirement-verification-questions.md with 5 questions + stated +assumptions. Key gate: Q1 asks whether the box must also BUILD the Pelican site (Python) or is +an editor only (Atheos/PHP), because the blog repo is a Pelican static site, not plain markdown. +Presented the questions inline in Matrix (Andrew reviews in-chat, not in .md files)." +**Context**: INCEPTION — Requirements Analysis (Step 6 gate: awaiting user answers) + +## Requirements Analysis — Gate Answers +**Timestamp**: 2026-09-29T05:10:00Z +**User Input** (verbatim): +"1. A - It doesn't need to build the pipeline is well known just writing markdown is more than enough +2. I'll supply them, give me the ip address and I'll ssh in and set up my credentials in gitea, you can't do this and I want it under armistace +3. scotty +4. LAN only +5. A +6. clone it but leave instructions to get it over to ssh once I add the key" +**AI Response**: "Answers recorded: Q1=A (editor only; Pelican build explicitly out of scope — the +existing pipeline handles it, 'just writing markdown is more than enough'); Q2=Andrew supplies +credentials himself over SSH, under the `armistace` identity (agent must NOT set an armistace git +identity or credentials); Q3=PVE node **scotty** (.69) — verified: 15.7 GB RAM (12.1 GB available), +8 GB swap, scotty-local-lvm thin pool 771 GB free, in-use VMIDs 104-107/119/125/126/133-135 so +**136 is free**; Alpine template not yet downloaded on scotty; Q4=LAN only (no NPM/DNS/forward); +Q5=A (opt out of security+resiliency, document the TODOs); Q6=clone the repo but leave RUNBOOK +instructions for switching the remote to SSH once the key is added." +**Context**: INCEPTION — Requirements Analysis (Step 7: generating requirements.md) + +## Requirements Analysis — Approval +**Timestamp**: 2026-09-29T05:12:00Z +**User Input** (verbatim): "no I'm good to skip lets do it" +**AI Response**: "Requirements APPROVED. User Stories stage explicitly SKIPPED (user confirmed). +Per the AI-DLC rule for skipped stories, a **requirement→unit map** will be substituted for +unit-of-work-story-map.md. Proceeding to Workflow Planning." +**Context**: INCEPTION — Requirements Analysis complete → Workflow Planning + +## Workflow Planning — Approval +**Timestamp**: 2026-09-29T05:15:00Z +**AI Prompt**: "Approve this execution plan? 6 stages to execute (Application Design, Units Generation, Infrastructure Design, Code Generation, Build and Test), 5 to skip (Reverse Engineering, User Stories, Functional Design, NFR Requirements, NFR Design)." +**User Response** (verbatim): "continue looks good" +**Status**: APPROVED +**Context**: INCEPTION — Workflow Planning complete → Application Design + +## Application Design — Gate Answers +**Timestamp**: 2026-09-29T05:18:00Z +**User Input** (verbatim): +"1. B jI'll set it in there, remind me in the runbbok +2. good idea for now, but leave an update script that will reclone at latest and pin +3. looks good to me" +**AI Response**: "Answers recorded. Q1=B — the agent configures **NO git identity** (user.name/ +user.email left unset); the RUNBOOK must explicitly prompt the operator to set them as their first +action after SSH. Q2 — pin Atheos to a resolved commit SHA **and** additionally ship an +**update/re-pin script** that re-clones Atheos at latest and re-pins the SHA. Q3 — layout accepted +(web root /var/www/atheos, workspace /var/www/atheos/workspace, nginx :80, php-fpm unix socket). +Generated the five application-design artifacts (components.md, component-methods.md, services.md, +component-dependency.md, application-design.md) with design decisions D1-D5 recorded." +**Context**: INCEPTION — Application Design complete (awaiting approval) + +## Application Design — Security Model Decision (D6) +**Timestamp**: 2026-09-29T05:22:00Z +**User Input** (verbatim): "would it be safer to have an atheos user or use a application key? lets get this security model for the key right incase I want to expose to the wider web" +**AI Response**: "Researched against official sources + the live 1.27.3 instance rather than opining. +Verified: (a) Gitea PAT scopes are ROUTE GROUPS (e.g. write:repository) matched against the USER's +access, NOT per-repository — so a stolen armistace PAT reaches every repo the account can write; +the instance's DB shows armistace holding 8 tokens, one with scope `all`. (b) Deploy keys are +genuinely per-repository: swagger confirms /repos/{owner}/{repo}/keys (get,post,delete) and the +DeployKey schema carries read_only. (c) CVE-2026-28744 (fixed 1.26.2) confirms the scope mechanism +is real — 'repository-scoped access-token permissions on repository operations'. +RECOMMENDED and user ACCEPTED a per-repo deploy key over both a PAT and a dedicated Gitea user. +Rationale: a dedicated user still owns/needs access to `blog`, so it reduces nothing while adding a +credential and muddying attribution; the deploy key bounds blast radius to ONE repo natively. +Recorded as design decision **D6**. Wrote docs/security-notes.md (S-1..S-14 for wider-web exposure) +and docs/resiliency-notes.md (R-1..R-8). Agent confirmed it CANNOT add the deploy key itself (API +returns 'user should be an owner or a collaborator with admin write') — operator will add the public +half in the repo." +**Context**: INCEPTION — Application Design (security model finalised) + +## Application Design — Approval +**Timestamp**: 2026-09-29T05:23:00Z +**User Input** (verbatim): "I like the model lets go with that. When you give me the public key (in here) I'll get it in the repo" +**AI Response**: "Design APPROVED including D6. Proceeding to Units Generation, then CONSTRUCTION." +**Context**: INCEPTION — Application Design complete → Units Generation + +## Units Generation — Approval +**Timestamp**: 2026-09-29T05:26:00Z +**AI Prompt**: "Approve these 5 units and the U4 two-phase approach (clone over HTTPS now, deploy key generated + public half handed to operator, SSH switch as Phase 2 once the key is installed)?" +**User Response** (verbatim): "phase 2 is all you I'll give you the go ahead and you can switch it over. lets get building" +**Status**: APPROVED — units approved; U4 two-phase approved; **Phase 2 (SSH remote switch) is agent-owned, to be executed once the operator signals the deploy key is installed**. Construction to run AUTONOMOUSLY per the user's standing preference (no per-unit gates; stop only for hard blockers). +**Context**: INCEPTION complete → CONSTRUCTION PHASE (Infrastructure Design → Code Generation → Build and Test) + +## OPERATIONAL NOTE — pre-existing Gitea CVE remediation (executed 2026-09-29, pre-construction) +**Timestamp**: 2026-09-29T05:26:00Z +**AI Response**: "Recorded for trail integrity: earlier in this same session (before this AI-DLC workflow +was engaged) the Gitea instance was found compromised via CVE-2026-60004 and remediated + upgraded +1.23.8 → 1.27.3. This deviates from the stage order — it happened before Inception. It is NOT an +output of any stage of this workflow. Full record: spine partition hermes-ops, source_ref +`gitea-cve-2026-60004-incident`. Also corrected a factual error in that record during this session: +192.168.178.151 is LXC 110 (nginxpm), NOT the Actions runner (user correction: '161 is the runner')." +**Context**: OUT-OF-STAGE — recorded for audit honesty diff --git a/aidlc-docs/design-artifacts/application-design.md b/aidlc-docs/design-artifacts/application-design.md new file mode 100644 index 0000000..34dbfa8 --- /dev/null +++ b/aidlc-docs/design-artifacts/application-design.md @@ -0,0 +1,76 @@ +# Application Design (consolidated) — atheos-blog-lxc + +**Stage**: INCEPTION — Application Design (minimal depth) · **Date**: 2026-09-29 +**Deploy target**: Proxmox LXC `136` on node `scotty` (.69) — Alpine 3.23 · LAN-only + +--- + +## 1. What this system is + +A small, single-purpose guest whose only job is to put a **browser editor** in front of the +`armistace/blog` working copy. It is deliberately **not** a build environment. + +``` + ┌───────────────────────── LXC 136 (scotty) ─────────────────────────┐ + Browser (LAN) │ │ + http:/// ────┼──► nginx :80 ──unix socket──► php-fpm (php83) ──► Atheos (PHP) │ + │ │ │ + │ ├──► workspace/blog/** (rw) │ + │ └──► git CLI ──► local commit│ + └────────────────────────────────────────────────────────────────────┘ + │ + (operator, later) SSH identity + SSH remote + ▼ + Gitea git.aridgwayweb.com +``` + +## 2. Components + +| ID | Component | Role | +|---|---|---| +| C1 | LXC `136` on scotty, Alpine 3.23 | isolated runtime, OpenRC, 20 GB root, onboot | +| C2 | nginx :80 | HTTP front end; sends `*.php` to C3 | +| C3 | php-fpm `php83` | PHP runtime; ZIP/OPENSSL/MBSTRING; `allow_url_fopen=On` | +| C4 | Atheos (pinned SHA) | the editor; git via its `codegit` component | +| C5 | `/var/www/atheos/workspace` | project storage (writable) | +| C6 | git CLI | VCS; `safe.directory` set; **no identity** (operator-owned) | +| C7 | `blog` working copy | clone of `armistace/blog` on `master` | +| C8 | Gitea (external) | clone source; agent never pushes | +| C9 | Operator (external) | owns SSH key, git identity, Atheos admin, remote switch | + +## 3. Key interfaces + +1. **nginx → php-fpm**: `fastcgi_pass unix:` must equal the pool's `listen =`. Mismatch ⇒ **502**. +2. **Atheos → filesystem**: writable `config.php`, `data/`, `workspace/`, `plugins/`, `themes/`. Failure ⇒ installer dependency error. +3. **Atheos → git**: needs `git` on PATH **and** `safe.directory` for the workspace (else *dubious ownership*). Identity intentionally unset. +4. **git → Gitea**: HTTPS clone (public); operator later switches to SSH. +5. **OpenRC**: `nginx` + `php-fpm83` enabled at `default` runlevel — otherwise first reboot breaks service (AC-6). + +## 4. Design decisions (from the gate) + +| # | Decision | Source | +|---|---|---| +| D1 | **No git identity configured by the agent.** `user.name`/`user.email` left unset; RUNBOOK instructs the operator to set them first. | Q1=B | +| D2 | **Atheos pinned to a resolved commit SHA**, with an additional **update script** that re-clones at latest and re-pins. | Q2 | +| D3 | Web root `/var/www/atheos`; workspace `/var/www/atheos/workspace`; nginx :80; php-fpm via unix socket. | Q3 | +| D4 | Atheos served at the **site root**. | assumed, accepted | +| D5 | Operator completes Atheos's first-run installer; **agent seeds no users**. | FR-11 / Q2 of requirements | +| **D6** | **Credential = a per-repository Gitea deploy key scoped to `armistace/blog`, write-enabled** (`read_only:false`), owned by the php-fpm user, aliased via `~/.ssh/config` with `IdentitiesOnly yes`. **Not** a PAT (account-wide scopes) and **not** a dedicated Gitea user (no blast-radius reduction for `blog`). Private key generated on the guest; public half handed to the operator to add in-repo. | Andrew, 2026-09-29 (security review) | + +## 5. Scope guards (each is a testable negative) + +- No database installed +- **No Python / pelican / `make`** — the presence of Python is a defect (AC-8) +- No NPM proxy host / DNS / port-forward (FR-10) +- No `armistace` credential, SSH key, or git identity created by the agent (FR-11) + +## 6. Design-level risks handed to Infrastructure Design + +- **C-1** Atheos on Alpine is under-documented → resolve the FPM socket and extension package names from the *installed* system, not from upstream docs. +- **C-2** OpenRC, not systemd → all service work uses `rc-service` / `rc-update`. +- **C-3** Alpine template must be downloaded on scotty first (`pveam update` + `download`). +- **C-4** The installer needs a writable web root before it will run. + +## 7. Unit mapping + +U1 guest (C1) · U2 web stack (C2,C3) · U3 Atheos (C4,C5) · U4 git + clone (C6,C7) · U5 docs + script (all) diff --git a/aidlc-docs/design-artifacts/component-dependency.md b/aidlc-docs/design-artifacts/component-dependency.md new file mode 100644 index 0000000..4f8836b --- /dev/null +++ b/aidlc-docs/design-artifacts/component-dependency.md @@ -0,0 +1,49 @@ +# Component Dependencies — atheos-blog-lxc + +**Stage**: INCEPTION — Application Design (minimal depth) + +## Dependency matrix + +Rows *depend on* columns. `→` = depends on, `—` = none. + +| | C1 LXC | C2 nginx | C3 php-fpm | C4 Atheos | C5 workspace | C6 git | C7 blog | +|---|---|---|---|---|---|---|---| +| **C1 LXC** | — | — | — | — | — | — | — | +| **C2 nginx** | ✔ | — | ✔ (I-1) | — | — | — | — | +| **C3 php-fpm** | ✔ | — | — | — | ✔ (I-2) | — | — | +| **C4 Atheos** | — | — | ✔ | — | ✔ | ✔ (I-3) | — | +| **C5 workspace** | — | — | — | — | — | — | ✔ | +| **C6 git** | ✔ | — | — | — | — | — | — | +| **C7 blog** | — | — | — | — | ✔ | — | — | + +**Reading it**: nothing works before C1; nginx is useless without php-fpm; Atheos depends on three +things simultaneously; the clone is a leaf. + +## Build / provisioning order (topological) + +``` +C1 LXC + ├─► C2 nginx ──► (needs) C3 php-fpm ──► C4 Atheos ──► C5 workspace ──► C7 blog (clone) + └─► C6 git ──────────────────────────────────────────────┘ +``` + +- **Critical path**: C1 → C2/C3 → C4 → C5 → C7 +- **Parallelisable**: nginx install and git install are independent + +## External dependencies (version-pinned at build time) + +| Dependency | Source | Pin strategy | +|---|---|---| +| Alpine rootfs | PVE template `alpine-3.23-default_20260116_amd64.tar.xz` | template filename is versioned | +| `nginx`, `php83*`, `git` | Alpine `main`/`community` repos | record installed versions in the RUNBOOK at build time | +| **Atheos** | `github.com/Atheos/Atheos` (master, no releases) | **pin to a resolved commit SHA** (design Q2); record it | +| `armistace/blog` | Gitea, branch `master` | clone `master`; note the commit for the record | + +## Coupling risks (where the design is tight) + +| Coupling | Why it's tight | Mitigation | +|---|---|---| +| nginx ↔ php-fpm socket path | a string must match exactly; failure is a 502 with no other signal | resolve from the installed pool config, then assert both sides match | +| FPM user ↔ workspace ownership | a UID mismatch yields "dubious ownership" or unwritable dirs | set ownership once from the actual FPM user, verify by writing a file | +| template filename ↔ Alpine version | a wrong/renamed template fails `pct create` | confirm the exact filename with `pveam available` before creating | +| Atheos master ↔ PHP 8.3 | upstream targets older PHP; 8.3 may warn | smoke-test the installer page and check the error log for PHP fatals | diff --git a/aidlc-docs/design-artifacts/component-methods.md b/aidlc-docs/design-artifacts/component-methods.md new file mode 100644 index 0000000..92c9acd --- /dev/null +++ b/aidlc-docs/design-artifacts/component-methods.md @@ -0,0 +1,65 @@ +# Component Methods / Interfaces — atheos-blog-lxc + +**Stage**: INCEPTION — Application Design (minimal depth) +**Note**: No business logic exists to specify. These are the **contracts between components** — +the things that must match or the stack silently fails (the classic 502 / permission-denied class). + +## I-1 · nginx → php-fpm (the interface most likely to break) + +``` +nginx server block: + listen 80; + root /var/www/atheos; + index index.php index.html; + + location = /index.php { fastcgi_pass unix:; ... } + location ~ \.php$ { fastcgi_pass unix:; ... } +``` + +**Contract**: `` in `nginx.conf` **must equal** the `listen =` path in the php-fpm pool. +Alpine's `php83-fpm` default differs between packages — resolve it from the installed +`/etc/php83/php-fpm.d/www.conf` rather than assuming. + +- **Input**: HTTP request; **Output**: `text/html` from the PHP process +- **Failure mode if mismatched**: `502 Bad Gateway` (socket missing) — AC-2 fails + +## I-2 · Atheos → filesystem (the write contract) + +Atheos's installer requires **write access** to exactly these paths, relative to `/var/www/atheos`: + +| Path | Why | Mode | +|---|---|---| +| `config.php` | installer writes the config; file must exist | writable by FPM user | +| `data/` | user accounts, settings | writable | +| `workspace/` | projects (incl. the `blog` clone) | writable | +| `plugins/` | plugin install/marketplace | writable | +| `themes/` | theme install | writable | + +- **Owner/group**: the php-fpm run user (resolved from the pool config) +- **Failure mode**: installer shows a dependency error; AC-3 fails + +## I-3 · Atheos (`codegit`) → git CLI + +- **Requires**: `git` on `PATH` +- **Requires**: `safe.directory` for the workspace — otherwise git refuses with + `detected dubious ownership` because the FPM user differs from the filesystem owner +- **Identity**: `user.name` / `user.email` are **deliberately UNSET** (design Q1=B). + The operator sets them first thing after SSH (see RUNBOOK). +- **Failure mode**: "dubious ownership" errors, or commit refused for missing identity + +## I-4 · git CLI → Gitea + +- **Remote (initial)**: `https://git.aridgwayweb.com/armistace/blog.git` (public ⇒ no credential) +- **Remote (operator, later)**: `git@git.aridgwayweb.com:armistace/blog.git` — the RUNBOOK gives the + exact `git remote set-url` command for after the key is added +- **Contract**: the agent must **not** alter the remote to SSH itself (FR-6/FR-11) + +## I-5 · OpenRC service contract (not systemd) + +| Service | Enable | Purpose | +|---|---|---| +| `nginx` | `rc-update add nginx default` | HTTP | +| `php-fpm83` | `rc-update add php-fpm83 default` | FastCGI (verify exact rc name on 3.23) | + +- **Failure mode if not enabled**: works until first reboot, then AC-6 fails. This is why NFR-2 is + verified with an actual reboot rather than a service-status check. diff --git a/aidlc-docs/design-artifacts/components.md b/aidlc-docs/design-artifacts/components.md new file mode 100644 index 0000000..afe5cb1 --- /dev/null +++ b/aidlc-docs/design-artifacts/components.md @@ -0,0 +1,33 @@ +# Components — atheos-blog-lxc + +**Stage**: INCEPTION — Application Design (minimal depth) +**Note**: This project has no application code to design. "Components" here are the **runtime +pieces of the provisioned guest and their interfaces**, recorded so unit boundaries are defensible +and a later agent inherits the map. + +## Provisioned by the agent + +| ID | Component | Purpose | Responsibilities | +|---|---|---|---| +| **C1** | **LXC guest** (`136` on scotty, Alpine 3.23) | The isolated runtime | OpenRC init, network (`eth0`, DHCP/static), 20 GB rootfs, timezone | +| **C2** | **nginx** | HTTP front end | Listen `:80`; serve static assets; dispatch `*.php` to C3 over a unix socket | +| **C3** | **php-fpm (`php83`)** | PHP FastCGI runtime | Execute Atheos PHP as a dedicated unprivileged user; extensions ZIP/OPENSSL/MBSTRING; `allow_url_fopen=On` | +| **C4** | **Atheos** (upstream PHP app, pinned SHA) | The browser editor | File tree, editor, and git operations via its `codegit` component | +| **C5** | **Atheos workspace** (`/var/www/atheos/workspace`) | Project storage | Holds working copies Atheos opens as projects; must be writable by C3 | +| **C6** | **git CLI** | VCS client | Invoked by C4's `codegit`; needs `safe.directory`, has **no configured identity** (operator sets it) | +| **C7** | **`blog` working copy** | The thing the operator edits | A clone of `armistace/blog` at `C5/blog`, on `master` | + +## External / not provisioned by the agent + +| ID | Component | Relationship | +|---|---|---| +| **C8** | **Gitea** (`git.aridgwayweb.com` → LXC 109, .155) | Clone source. **Read-only** from this project's perspective — the agent never pushes | +| **C9** | **Operator (human)** | SSHes in; **owns**: SSH key, git identity, Atheos admin account, and switching the remote to SSH | + +## Explicit non-components (scope guards) + +- **No database.** Atheos is file-backed; nothing is installed for it. +- **No Python / pelican / `make html`.** The build pipeline is out of scope (FR-1 / AC-8). The + presence of Python would be a **defect**, not a bonus. +- **No NPM proxy host, no DNS, no port-forward** (LAN-only, FR-10). +- **No Atheos user seeded by the agent** — C9 creates the first account via the installer. diff --git a/aidlc-docs/design-artifacts/infrastructure-design.md b/aidlc-docs/design-artifacts/infrastructure-design.md new file mode 100644 index 0000000..22596d6 --- /dev/null +++ b/aidlc-docs/design-artifacts/infrastructure-design.md @@ -0,0 +1,150 @@ +# Infrastructure Design — atheos-blog-lxc + +**Stage**: CONSTRUCTION — Infrastructure Design +**Date**: 2026-09-29 · **Deploy target**: Proxmox LXC (Alpine 3.23) on node `scotty` (.69) + +> **No open questions.** Every infrastructure decision was settled at the Requirements and Application +> Design gates (Q1–Q6, D1–D6). Re-asking them would be ceremony. This artifact maps the approved +> design onto concrete infrastructure, and records the *provenance* of each choice. + +--- + +## 1. Guest specification (U1) + +| Property | Value | Provenance / rationale | +|---|---|---| +| Node | **scotty** (192.168.178.69) | user Q3 | +| VMID | **136** | verified free — in-use on scotty: 104-107, 119, 125, 126, 133-135 | +| OS | **Alpine 3.23** (`alpine-3.23-default_20260116_amd64.tar.xz`) | user requirement ("alpine lxc") | +| Rootfs | **20 GB** | user requirement (point 5) | +| Storage | `scotty-local-lvm` (thin) | 771 GB free at design time; thin keeps 20 GB nominal, minimal actual | +| vCPU | 2 | idle PHP app; 1 risks slow composer-less PHP ops | +| RAM | 1024 MB | nginx + php-fpm is trivial; scotty has 12.1 GB available | +| Swap | 256 MB | PHP can spike on large files; cheap insurance | +| Privileged? | **unprivileged** | default-safe; nothing here needs device access | +| `onboot` | **1** | NFR-2 | +| Network | `eth0`, `bridge=vmbr0`, DHCP | matches all existing guests; LAN-only (FR-10) | +| DNS | router-provided | LAN-only | +| Timezone | `Australia/Brisbane` | NFR-5 | +| Features | **none required** | ⚠️ see note below | + +### ⚠️ Note on LXC `features` +The observability project hit `status=226/NAMESPACE` because a container lacked **`nesting`**. +That was **systemd**. This guest is **OpenRC** on Alpine, which does not need `nesting`. +So: `features` left **unset**, and if a service fails to start we revisit `nesting` rather than +pre-emptively enabling a feature that widens the container's escape surface. + +### Template provenance +The Alpine 3.23 template is **not yet present on scotty** (risk C-3). U1 therefore begins with +`pveam update` + `pveam download local alpine-3.23-default_...`, and the download is verified before +`pct create` runs. + +--- + +## 2. Runtime stack (U2) + +| Layer | Choice | Notes | +|---|---|---| +| HTTP | `nginx` (Alpine `main`) | listens `:80`, `root /var/www/atheos` | +| PHP | `php83-fpm` + `php83` CLI | Alpine 3.23's PHP is **8.3** — confirmed available (`php83` present in the v3.23 index) | +| Extensions | `php83-session`, `php83-zip`, `php83-openssl`, `php83-mbstring` | Atheos: ZIP/OPENSSL/MBSTRING for the plugin marketplace; `session` is not optional | +| PHP ini | `allow_url_fopen=On` | Atheos marketplace requirement (recorded as accepted risk; turn off if unused — S-14) | +| Socket | **unix socket** | nothing extra listens on the LAN; path resolved from the *installed* pool config, not assumed | + +### The socket contract (interface I-1 — the 502 risk) +nginx's `fastcgi_pass` and the php-fpm pool's `listen` are two strings that must be **byte-identical**. +The design does not assume a path: U2 **reads the pool config after install** and writes nginx to +match, then asserts both sides agree. A mismatch produces a bare 502 with no other diagnostic. + +--- + +## 3. Application layer (U3) + +| Item | Value | +|---|---| +| Web root | `/var/www/atheos` | +| Source | `github.com/Atheos/Atheos`, **pinned to a resolved commit SHA** (D2) | +| Database | **none** — Atheos is file-backed | +| Writable paths | `config.php`, `data/`, `workspace/`, `plugins/`, `themes/` | +| Ownership | owned by the **php-fpm run user** (resolved from the pool) | +| Run user | resolved at U2; **U2/U3/U4 must agree on the same user** (cross-unit risk) | + +--- + +## 4. VCS layer (U4) + +| Item | Value | Provenance | +|---|---|---| +| `git` | Alpine package | user point 2 | +| `safe.directory` | workspace path | prevents *dubious ownership* (I-3) | +| Committer identity | **deliberately UNSET** | **D1 / Q1=B** — operator sets it first | +| Clone | `armistace/blog` branch `master` → `workspace/blog` | user point 3, Q6 | +| Remote (initial) | `https://git.aridgwayweb.com/armistace/blog.git` → **HTTPS** (public repo) | U4 phase 1 | +| Credential | **ed25519 deploy key**, per-repo, write-enabled | **D6** | +| Private key | `~/.ssh/id_ed25519_blog`, mode `600`, owned by FPM user | D6 | +| SSH alias | `Host gitea-blog` + `IdentitiesOnly yes` | D6 — a stray push cannot hit another repo | +| Remote (final) | `gitea-blog:armistace/blog.git` | U4 **phase 2**, after operator installs the key | + +--- + +## 5. Networking & exposure + +- **LAN-only** (FR-10). nginx binds `:80` on the guest; nothing else is configured. +- **No NPM proxy host, no DNS record, no router forward** — deliberately not created. +- php-fpm on a unix socket ⇒ **only `:80` and `:22` listen**. +- `git.aridgwayweb.com` resolves to a **public IP** — the *clone* traverses the internet, but the + guest itself is not exposed. Noted because it makes Gitea-side scoping (D6) the meaningful control. + +--- + +## 6. Operations & persistence (OpenRC, not systemd) + +| Service | Enable command | Failure if missed | +|---|---|---| +| `nginx` | `rc-update add nginx default` | serves until reboot, then **AC-6 fails** | +| `php-fpm83` | `rc-update add php-fpm83 default` | same | + +- Logs: `/var/log/nginx/`, php-fpm error log (path resolved at install) +- **Verification uses an actual reboot**, not a service-status check — a service can be "started" + and still not be enabled. +- Monitoring/alerting: **out of scope** (R-3, S-12). Observability stack is a separate project. + +--- + +## 7. Deployment architecture + +``` + LAN 192.168.178.0/24 + │ + ┌────────▼─────────┐ + │ scotty (.69) │ PVE node + │ ┌────────────┐ │ + │ │ LXC 136 │ │ Alpine 3.23 · 2 vCPU · 1 GB · 20 GB root + │ │ │ │ + :80 ─────────┼──┤ nginx │ │ unprivileged · onboot=1 + │ │ │ unix │ │ + │ │ ▼ socket │ │ + │ │ php-fpm83 │ │ + │ │ │ │ │ + │ │ ├─► /var/www/atheos{,/workspace/blog} + │ │ └─► git ─┼──┼──► gitea-blog:armistace/blog.git + │ └────────────┘ │ (deploy key, D6) + └──────────────────┘ + │ + internet (clone only) + ▼ + git.aridgwayweb.com (Gitea LXC 109) +``` + +--- + +## 8. Infrastructure decisions summary + +| # | Decision | Why | +|---|---|---| +| I-1 | Unprivileged, no `features` | default-safe; `nesting` is a systemd need, not ours | +| I-2 | Thin-provisioned `scotty-local-lvm` | 20 GB nominal on 771 GB free; bounded blast radius | +| I-3 | php-fpm over **unix socket** | smallest network surface | +| I-4 | Same user for FPM, file ownership, and key | three-way identity agreement — the top cross-unit risk | +| I-5 | **No** monitoring/alerting | out of scope, recorded (R-3/S-12) | +| I-6 | Reboot-based verification | a "started" service is not an "enabled" one | diff --git a/aidlc-docs/design-artifacts/requirement-to-unit-map.md b/aidlc-docs/design-artifacts/requirement-to-unit-map.md new file mode 100644 index 0000000..1ac9d57 --- /dev/null +++ b/aidlc-docs/design-artifacts/requirement-to-unit-map.md @@ -0,0 +1,61 @@ +# Requirement → Unit Map — atheos-blog-lxc + +**Stage**: INCEPTION — Units Generation (Part 2) + +> **Deliberate substitution.** The AI-DLC rules mandate `unit-of-work-story-map.md`, which maps +> *user stories* to units. The **User Stories stage was skipped** (user-confirmed at the Requirements +> gate: "no I'm good to skip"). With no stories to map, this document maps **requirements and +> acceptance criteria** to units instead — the same assurance (nothing is silently dropped) applied +> to the artifacts that actually exist. + +## Functional requirements + +| FR | Requirement (short) | Unit | +|---|---|---| +| FR-1 | Alpine 3.23 LXC on scotty, 20 GB root, 2 vCPU/1 GB, onboot, unprivileged | **U1** | +| FR-2 | nginx + PHP-FPM serving Atheos; ZIP/OPENSSL/MBSTRING; `allow_url_fopen=On` | **U2** | +| FR-3 | Atheos installed, pinned, 5 writable paths, no DB | **U3** | +| FR-4 | git integration configured (git installed, `safe.directory`, identity handling) | **U4** | +| FR-5 | `armistace/blog` cloned into `workspace/` on `master` | **U4** | +| FR-6 | Remote reachable; instructions to switch to SSH; agent doesn't switch it | **U4** (build) + **U5** (document) | +| FR-7 | RUNBOOK.md with IP, SSH, workspace path, remote-switch steps, credential setup | **U5** | +| FR-8 | README.md | **U5** | +| FR-9 | Repeatable provisioning script in the repo | **U5** | +| FR-10 | LAN-only; no NPM/DNS/forward | **U2** (bind + no proxy config) | +| FR-11 | Agent creates no `armistace` credentials or git identity | **U4** (verification asserts identity is empty) | +| FR-12 | `docs/security-notes.md` + `docs/resiliency-notes.md` | **U5** | + +## Non-functional requirements + +| NFR | Requirement (short) | Unit | +|---|---|---| +| NFR-1 | Fits comfortably in 20 GB | **U1** (sizing) | +| NFR-2 | Survives reboot with clone + edits intact | **U1** (onboot) + **U2** (services enabled) + **U3/U4** (persistence) → verified at Build & Test | +| NFR-3 | Repeatable / idempotent-friendly provisioning | **U5** | +| NFR-4 | No K3s dependency | all (negative) | +| NFR-5 | Timezone Australia/Brisbane | **U1** | + +## Acceptance criteria + +| AC | Short | Unit | +|---|---|---| +| AC-1 | `pct config 136` shows Alpine + 20 GB root + onboot | **U1** | +| AC-2 | `http:///` serves Atheos (not 502/404) | **U2**+**U3** | +| AC-3 | `blog` present as a project whose files open | **U3**+**U4** | +| AC-4 | Edit + save persists to disk | **U3**+**U4** | +| AC-5 | `git remote -v` points at Gitea; RUNBOOK gives the SSH switch | **U4**+**U5** | +| AC-6 | Reboot → Atheos serves again, clone + edits intact | Build & Test | +| AC-7 | RUNBOOK lets another agent re-provision/hand off | **U5** | +| AC-8 | **No Python/pelican present** (scope respected) | Build & Test | + +## Coverage check + +| | Count | Assigned to exactly one unit | Unassigned | +|---|---|---|---| +| Functional requirements | 12 | 12 ✔ | 0 | +| Non-functional requirements | 5 | 5 ✔ | 0 | +| Acceptance criteria | 8 | 8 ✔ | 0 | + +**Every FR and NFR is assigned to exactly one unit.** ACs that require two units to build but one +moment to verify (AC-2, AC-3, AC-4) list both; AC-6 and AC-8 are whole-system properties verified +once at Build & Test rather than owned by a single unit — noted explicitly rather than left implicit. diff --git a/aidlc-docs/design-artifacts/services.md b/aidlc-docs/design-artifacts/services.md new file mode 100644 index 0000000..d0f1183 --- /dev/null +++ b/aidlc-docs/design-artifacts/services.md @@ -0,0 +1,62 @@ +# Services / Request Flow — atheos-blog-lxc + +**Stage**: INCEPTION — Application Design (minimal depth) + +There is no service layer to design (no application code). What matters is the **two flows** the +guest supports, and where each can break. + +## Flow 1 — Operator edits and commits + +``` +Browser (LAN) + │ http:/// + ▼ +nginx (C2, :80) + │ location ~ \.php$ → fastcgi_pass unix: [I-1] + ▼ +php-fpm (C3, php83) + │ executes as the FPM user + ▼ +Atheos (C4, /var/www/atheos) + ├── reads/writes workspace/blog/** [I-2 write contract] + └── on Commit: git (C6) + │ safe.directory must include workspace/ [I-3] + │ identity UNSET by design (Q1=B) + ▼ + commit created in the local clone +``` + +**Break points**: FPM socket mismatch (502) · unwritable `workspace/` (installer/editor errors) · +dubious-ownership (git refuses) · missing identity (commit refused until operator sets it). + +## Flow 2 — Operator pushes (operator-owned path) + +``` +Atheos Commit → operator adds SSH key + sets git identity → remote switched to SSH (operator) + │ + ▼ + Gitea (C8) receives the push +``` + +The **agent builds the path but does not walk it**: it clones over HTTPS, leaves the remote as-is, +and documents the switch. Pushing is the operator's first hands-on act. + +## Flow 3 — Build/publish (OUT OF SCOPE — happens elsewhere) + +``` +Gitea (C8) → existing pipeline (NOT on this guest) → make html → published site +``` + +Explicitly excluded (Q1=A). No Python, no pelican, no `make` on this guest. If this flow were +expected here, **C-1's risk profile would change materially** — it is not. + +## Provisioning order (what must exist before what) + +``` +U1 guest → U2 nginx+php-fpm → U3 Atheos → U4 git + clone → U5 docs + script + ▲ + U4 also depends on U1 (git) ─────────────────┘ +``` + +Each arrow is a real dependency: installing Atheos before php-fpm leaves nothing to serve it; +cloning before Atheos exists has no workspace to clone into. diff --git a/aidlc-docs/design-artifacts/unit-of-work-dependency.md b/aidlc-docs/design-artifacts/unit-of-work-dependency.md new file mode 100644 index 0000000..b80d160 --- /dev/null +++ b/aidlc-docs/design-artifacts/unit-of-work-dependency.md @@ -0,0 +1,48 @@ +# Unit of Work Dependency — atheos-blog-lxc + +**Stage**: INCEPTION — Units Generation (Part 2) + +## Dependency matrix + +Rows *depend on* columns. + +| | U1 Guest | U2 Web stack | U3 Atheos | U4 Git+clone | U5 Docs | +|---|---|---|---|---|---| +| **U1 Guest** | — | — | — | — | — | +| **U2 Web stack** | ✔ | — | — | — | — | +| **U3 Atheos** | ✔ | ✔ | — | — | — | +| **U4 Git+clone** | ✔ (git) | — | ✔ (workspace) | — | — | +| **U5 Docs** | ✔ | ✔ | ✔ | ✔ | — | + +## Build order + +``` +U1 ──► U2 ──► U3 ──► U4 ──► U5 + │ ▲ │ + └─────────────────┴────────┘ + (U4 needs git from U1) +``` + +**Critical path**: U1 → U2 → U3 → U4 → U5. Strictly sequential — no parallelism worth the +coordination cost at this size. Each unit already includes a verification step, so a failure is +caught at unit granularity rather than at the end. + +## Why these boundaries + +| Boundary | Reason | +|---|---| +| U1 before everything | nothing can be installed without the guest | +| U2 (nginx+php) is one unit, not two | the **socket contract (I-1)** spans both — splitting them would create a boundary through a single interface, which is exactly where integration bugs live | +| U3 after U2 | Atheos has nothing to run on until PHP exists | +| U4 separate from U3 | **different failure class**: U3 is PHP-runtime risk (C-1); U4 is VCS/permissions risk. Separating them keeps a U3 failure from disguising itself as "git is broken" | +| U4 before U5 | the RUNBOOK must document the real remote command, which only exists after the clone | +| U5 last | docs describe what was actually built | + +## Cross-unit risks + +| Risk | Units | Note | +|---|---|---| +| FPM user identity | U2 → U3 → U4 | U2 resolves the run user; U3 sets ownership; U4 owns the key. **All three must agree on the same user** — a mismatch yields "dubious ownership" (I-3) | +| FPM socket path | U2 (internal) | must match across nginx + pool | +| Pinned Atheos SHA | U3 → U5 | U5's update script must re-pin the same mechanism | +| Remote URL | U4 → U5 | U5 documents it; U4 must not change it to SSH | diff --git a/aidlc-docs/design-artifacts/unit-of-work.md b/aidlc-docs/design-artifacts/unit-of-work.md new file mode 100644 index 0000000..37c12ba --- /dev/null +++ b/aidlc-docs/design-artifacts/unit-of-work.md @@ -0,0 +1,89 @@ +# 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:///` 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 +``` diff --git a/aidlc-docs/plans/application-design-plan.md b/aidlc-docs/plans/application-design-plan.md new file mode 100644 index 0000000..172de9d --- /dev/null +++ b/aidlc-docs/plans/application-design-plan.md @@ -0,0 +1,75 @@ +# Application Design Plan — atheos-blog-lxc + +**Stage**: INCEPTION — Application Design (**minimal depth**) +**Date**: 2026-09-29 + +## Rationale for minimal depth + +There is no application code to design — this project provisions a guest and installs an existing +PHP application. "Application Design" here means recording the **runtime component map and its +interfaces**, so that (a) the unit boundaries are defensible and (b) a later agent inheriting the +repo can see how the pieces talk to each other without re-deriving it. + +## Plan (checkboxes) + +- [ ] Generate `components.md` — component definitions + responsibilities +- [ ] Generate `component-methods.md` — the *interfaces* between components (not business logic) +- [ ] Generate `services.md` — orchestration / request flow +- [ ] Generate `component-dependency.md` — dependency matrix + data flow +- [ ] Generate `application-design.md` — consolidated design +- [ ] Validate design completeness and consistency + +## Design questions + +Most design decisions were already fixed in requirements.md. Below is the one that is genuinely +open, plus two where I state my decision so you can veto it rather than having to choose blind. + +--- + +### Q1 (genuine — blocks U4) — what git committer identity should Atheos use? + +Atheos's git integration commits as a configured user. You told me you'll own the `armistace` +credentials (**FR-11** — I must not create them). But `git` will refuse to commit at all without +*some* identity, so leaving it entirely unset risks a broken first commit for you. + +- **A)** Set a **neutral non-armistace** identity, e.g. `Atheos `, purely so + commits don't hard-fail; **you** override it with your own identity when you set up credentials. + *(Recommended — leaves the tool working without impersonating you.)* +- **B)** Set **nothing at all** — leave `user.name`/`user.email` unset, and the RUNBOOK tells you to + set them as your first action after SSH-ing in. *(Purest reading of FR-11; first commit fails until you do.)* +- **C)** Use a **placeholder under your name** (e.g. `armistace `) so commits are + attributed to you out of the box. *(Note: this creates an armistace identity, which is what FR-11 + says I should not do — only pick this if you want to override that.)* +- **X)** Other (describe after the tag) + +`[Answer]:` + +--- + +### Q2 (decision stated — veto if wrong) — pin Atheos to a specific upstream commit + +Upstream Atheos is an actively-moving master branch with no releases. For a reproducible +re-provision (FR-9), I propose pinning to a **specific verified commit SHA** resolved at build +time and recorded in the script + RUNBOOK, rather than tracking `master`. + +`[Answer]:` *(leave blank to accept)* + +--- + +### Q3 (decision stated — veto if wrong) — where things live + +- Web root: `/var/www/atheos` (Atheos app) +- Atheos workspace: `/var/www/atheos/workspace` → this is where `blog` is cloned +- nginx listens on **:80**, `root /var/www/atheos`, `index index.php` +- php-fpm via **unix socket** (`/run/php-fpm83/...`) — not a TCP port, so nothing extra listens on the LAN + +`[Answer]:` *(leave blank to accept)* + +--- + +## Assumptions carried + +- Atheos is served at the **site root** (`http:///`), not a subdirectory. +- The operator completes Atheos's first-run installer in the browser (creates the admin user); + the agent seeds neither the admin nor any Atheos user. +- `blog` is pre-cloned into `workspace/` so it appears as a ready-to-open project (your Q6). diff --git a/aidlc-docs/plans/execution-plan.md b/aidlc-docs/plans/execution-plan.md new file mode 100644 index 0000000..39cb1ea --- /dev/null +++ b/aidlc-docs/plans/execution-plan.md @@ -0,0 +1,149 @@ +# Execution Plan — atheos-blog-lxc + +**Stage**: INCEPTION — Workflow Planning +**Date**: 2026-09-29 +**Deploy Target**: Proxmox LXC (Alpine 3.23) on node **scotty** (.69). No K3s flavour, no CI. + +--- + +## Detailed Analysis Summary + +### Change Impact Assessment +- **User-facing changes**: Yes — a new web UI (Atheos) becomes reachable on the LAN. +- **Structural changes**: Yes — one new Proxmox guest (VMID 136). +- **Data model changes**: No — Atheos is database-less; it stores config in files. +- **API changes**: No. +- **NFR impact**: Low — a small single-purpose guest; NFRs already captured in requirements.md. +- **Existing systems touched**: `scotty` gains a guest + a template download. `armistace/blog` is + **read** (cloned) only — never pushed to by the agent. + +### Risk Assessment +- **Risk Level**: **Medium** — the steps are well understood, but Atheos-on-Alpine is under-documented + (C-1) and the install has a browser-driven first-run step. +- **Rollback Complexity**: **Easy** — `pct destroy 136` removes the guest; nothing else is mutated. +- **Testing Complexity**: **Simple-to-Moderate** — all acceptance criteria are observable + (served page, file on disk, git remote, survives reboot). + +### Component Relationships +- **Primary component**: LXC 136 on scotty. +- **Internal**: nginx → php-fpm (unix socket) → Atheos PHP app → workspace/ filesystem. +- **External (read-only)**: `git.aridgwayweb.com/armistace/blog` (public clone). +- **Operator-owned**: SSH key, git identity, Atheos login — the agent creates none of these. + +--- + +## Workflow Visualization + +```mermaid +flowchart TD + Start(["User Request"]) + subgraph INCEPTION["🔵 INCEPTION PHASE"] + WD["Workspace Detection
COMPLETED"] + RE["Reverse Engineering
SKIPPED"] + RA["Requirements Analysis
COMPLETED"] + US["User Stories
SKIPPED"] + WP["Workflow Planning
IN PROGRESS"] + AD["Application Design
EXECUTE"] + UG["Units Generation
EXECUTE"] + end + subgraph CONSTRUCTION["🟢 CONSTRUCTION PHASE"] + FD["Functional Design
SKIP"] + NFRA["NFR Requirements
SKIP"] + NFRD["NFR Design
SKIP"] + ID["Infrastructure Design
EXECUTE"] + CG["Code Generation
EXECUTE"] + BT["Build and Test
EXECUTE"] + end + subgraph OPERATIONS["🟡 OPERATIONS PHASE"] + OPS["Operations
PLACEHOLDER"] + end + Start --> WD --> RA --> WP --> AD --> UG --> ID --> CG --> BT --> End(["Complete"]) +style WD fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff +style RA fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff +style WP fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff +style CG fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff +style BT fill:#4CAF50,stroke:#1B5E20,stroke-width:3px,color:#fff +style RE fill:#BDBDBD,stroke:#424242,stroke-width:2px,stroke-dasharray: 5 5,color:#000 +style US fill:#BDBDBD,stroke:#424242,stroke-width:2px,stroke-dasharray: 5 5,color:#000 +style FD fill:#BDBDBD,stroke:#424242,stroke-width:2px,stroke-dasharray: 5 5,color:#000 +style NFRA fill:#BDBDBD,stroke:#424242,stroke-width:2px,stroke-dasharray: 5 5,color:#000 +style NFRD fill:#BDBDBD,stroke:#424242,stroke-width:2px,stroke-dasharray: 5 5,color:#000 +style AD fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray: 5 5,color:#000 +style UG fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray: 5 5,color:#000 +style ID fill:#FFA726,stroke:#E65100,stroke-width:3px,stroke-dasharray: 5 5,color:#000 +style OPS fill:#FFF59D,stroke:#F9A825,stroke-width:2px,color:#000 +linkStyle default stroke:#333,stroke-width:2px +``` + +*(Plain-text equivalent for Matrix: WD ✅ → RA ✅ → WP ✅ → AD ▶ → UG ▶ → ID ▶ → CG ▶ → BT ▶ → Done. +Skipped: RE, US, FD, NFRA, NFRD.)* + +--- + +## Phases to Execute + +### 🔵 INCEPTION PHASE +- [x] Workspace Detection — COMPLETED +- [x] Reverse Engineering — **SKIP** (greenfield repo; the *estate* recon was folded into Workspace Detection and recorded in `aidlc-state.md` → Existing Estate Facts) +- [x] Requirements Analysis — COMPLETED +- [x] User Stories — **SKIP** (user-confirmed; single operator, no app code. A **requirement→unit map** substitutes for `unit-of-work-story-map.md`, per the skipped-stories rule) +- [x] Workflow Planning — COMPLETED +- [ ] **Application Design** — **EXECUTE (minimal depth)** + - **Rationale**: there *are* distinct components with real interfaces (nginx ↔ php-fpm socket, Atheos ↔ workspace, git ↔ remote). Recording them at minimal depth gives the handoff a component map and drives unit boundaries. Minimal, not comprehensive — no app code exists to design. +- [ ] **Units Generation** — **EXECUTE** + - **Rationale**: genuinely multi-unit work (guest, web stack, app, VCS, docs) with an ordering dependency. + +### 🟢 CONSTRUCTION PHASE +- [ ] Functional Design — **SKIP** + - **Rationale**: no business logic to design; the "functions" are package installs and config files already specified in requirements.md. +- [ ] NFR Requirements — **SKIP** + - **Rationale**: all NFRs already captured (requirements.md §3); nothing new surfaces at construction. +- [ ] NFR Design — **SKIP** + - **Rationale**: same — NFRs here are simple resource/reboot-survival constraints, not a design problem. +- [ ] **Infrastructure Design** — **EXECUTE** + - **Rationale**: **this is the real design work for an infra project** — the guest spec, storage choice, template provenance, network mode, resource sizing, and the OpenRC service wiring. Getting this wrong is the main risk (C-1/C-2/C-3). +- [ ] **Code Generation** — EXECUTE (always) + - **Rationale**: the provisioning script + the docs are the deliverables. +- [ ] **Build and Test** — EXECUTE (always) + - **Rationale**: here "build and test" = **apply against the live node and verify**, plus the reboot-survival cycle (AC-6). Not a compiled artifact. + +### 🟡 OPERATIONS PHASE +- [ ] Operations — PLACEHOLDER (future deployment/monitoring workflows) + +--- + +## Unit Plan (provisional — finalised in Units Generation) + +| Unit | Scope | Depends on | Primary ACs | +|---|---|---|---| +| **U1** | LXC creation: download Alpine 3.23 template on scotty, `pct create 136` (20 GB root, 2 vCPU, 1 GB, onboot, unprivileged), base config, timezone, package index | — | AC-1 | +| **U2** | Web stack: `nginx` + `php83-fpm` + `php83-{session,zip,openssl,mbstring}`, `allow_url_fopen`, php-fpm pool, nginx vhost | U1 | AC-2 | +| **U3** | Atheos install: fetch pinned upstream commit into the web root, correct ownership/permissions on `config.php`, `data/`, `workspace/`, `plugins/`, `themes/` | U2 | AC-2, AC-3 | +| **U4** | Git + clone: install git, `safe.directory`, committer identity (non-armistace), clone `armistace/blog` into `workspace/`, leave remote HTTP | U1, U3 | AC-3, AC-4, AC-5 | +| **U5** | Repo + docs: `README.md`, `RUNBOOK.md` (incl. SSH remote-switch + operator credential steps), `docs/security-notes.md`, `docs/resiliency-notes.md`, re-provision script | U1–U4 | AC-7 | + +--- + +## Estimated Timeline + +**No wall-clock estimate offered** — fabricating one would be worse than omitting it. The work is +5 units, several of which are a handful of commands each; the only genuinely uncertain step is +U3 (Atheos-on-Alpine, risk C-1). Each unit ends in a verifiable state, so progress is measurable +even without a time estimate. + +--- + +## Success Criteria + +- **Primary goal**: a LAN-reachable Atheos on Alpine where `armistace/blog` is cloned and editable, + with credentials owned entirely by the operator. +- **Key deliverables**: LXC 136 on scotty; Atheos served by nginx/PHP; the blog clone in the + workspace; README + RUNBOOK + re-provision script; security/resiliency TODO notes. +- **Quality gates**: + - AC-1…AC-8 all demonstrated with real command output (not assertions) + - Atheos served (not 502/404) — verified over HTTP from the LAN + - **Reboot-survival verified** (AC-6), not assumed + - **No Python/pelican present** (AC-8) — proves scope was respected + - **No `armistace` credential or git identity created by the agent** (FR-11) + - Independent review of the generated provisioning script before the Build-and-Test gate + - `audit.md` complete and append-only; `aidlc-state.md` current diff --git a/aidlc-docs/plans/unit-of-work-plan.md b/aidlc-docs/plans/unit-of-work-plan.md new file mode 100644 index 0000000..a826cb3 --- /dev/null +++ b/aidlc-docs/plans/unit-of-work-plan.md @@ -0,0 +1,32 @@ +# Unit of Work Plan — atheos-blog-lxc + +**Stage**: INCEPTION — Units Generation (Part 1: Planning) +**Date**: 2026-09-29 · **Depth**: minimal + +## Rationale + +The unit boundaries were already surfaced in the approved execution plan and the operator approved +them ("continue looks good"). The decomposition is not genuinely in question — it follows the +component dependency chain directly. So this plan carries **no open questions**; asking any would be +ceremony. It is recorded here so the generation step is traceable rather than improvised. + +## Decomposition principle + +Units follow the **topological order of the dependency graph** — each unit ends in a state you can +verify, and no unit depends on a later one. This matters because a failure in U3 (the genuinely +uncertain one, Atheos-on-Alpine) must not leave U1/U2 work wasted or unverifiable. + +## Plan + +- [ ] Generate `unit-of-work.md` — unit definitions + responsibilities +- [ ] Generate `unit-of-work-dependency.md` — dependency matrix + build order +- [ ] Generate `requirement-to-unit-map.md` — **substitutes `unit-of-work-story-map.md`** (stories were skipped); every FR/NFR/AC assigned to exactly one unit +- [ ] Validate unit boundaries and dependencies +- [ ] Verify no requirement is unassigned + +## Approval + +Units were presented in the approved execution plan; the operator's "continue looks good" covers +this decomposition. No blocking questions. + +`[Answer]:` *(leave blank — approval recorded from the execution-plan gate)* diff --git a/aidlc-docs/requirements/requirement-verification-questions.md b/aidlc-docs/requirements/requirement-verification-questions.md new file mode 100644 index 0000000..721288f --- /dev/null +++ b/aidlc-docs/requirements/requirement-verification-questions.md @@ -0,0 +1,98 @@ +# Requirement Verification Questions — atheos-blog-lxc + +**Project**: Alpine LXC running Atheos, wired to Gitea, with `armistace/blog` cloned and ready. +**Stage**: INCEPTION — Requirements Analysis +**Date**: 2026-09-29 + +Answer each question by writing your choice after the `[Answer]:` tag (e.g. `[Answer]: B`). +Option **X) Other** is always available — describe it after the tag. + +--- + +## 0. CRITICAL FINDING — please read before answering Q1 + +Reconnaissance found that **`armistace/blog` is a Pelican static site, not a folder of markdown**: + +``` +src/content/*.md ← ~20 posts (the actual writing) +src/themes/cleanblog/ ← theme +src/pelicanconf.py ← build config +src/Makefile ← `make html`, `make serve-global`, `make github` +requirements.txt ← pelican[markdown], markdown-markup-emoji +``` + +So there are two different needs, and they need different tools: + +- **Atheos** = a browser editor. It edits files and does git (its `codegit` component). + It can edit the `.md`, commit and push — but it **cannot build or publish** the site. +- **Publishing the site** = running `make html` (+ `make github` / `pelican`), which needs + **Python + pelican**, not PHP. + +**Q1. How important is it that this box can also BUILD/PUBLISH the site?** +- A) **Editor only** — I'll build/publish elsewhere (CI or another box). Atheos just edits + commits. +- B) **Also include Pelican** — install Python + pelican + make so I can run `make html` / + `make serve` on this box to preview and regenerate the site. +- C) **Also include Pelican AND a local preview server** — so I can click through the built site + from the browser on this LXC before pushing. +- X) Other (please describe after [Answer]: tag below) + +[Answer]: + +--- + +**Q2. Does Atheos's git integration need to work non-interactively (credentials pre-wired), +or will you supply them when you first push?** +- A) **Bake in the git identity + let me push over HTTPS with a Gitea token** (I'll give you / + you use an existing scoped token) so push works immediately without a prompt. +- B) **Clone read-only / leave push to me** — set up the clone and git config, but leave the + credential to me (e.g. via SSH key I install myself, per my point 4). +- C) **Use SSH from the start** — configure the remote as `git@git.aridgwayweb.com:armistace/blog.git` + and just wait for the key I'll add. +- X) Other (please describe after [Answer]: tag below) + +[Answer]: + +--- + +**Q3. Which PVE node should host the LXC, and on which storage?** +- A) **janeway (.225)** — 3.3 GB RAM free, `extra` storage 874 GB free. (Recommended: roomy.) +- B) **data (.42)** — only ~0.95 GB RAM free and no swap; a 20 GB guest is fine on disk but the + node is tight. (Not recommended.) +- C) **You pick** whatever is healthiest at build time. +- X) Other (please describe after [Answer]: tag below) + +[Answer]: + +--- + +**Q4. Should the container be reachable only on the LAN, or exposed via NPM +(nginx proxy manager) with a hostname like `blog-ide.aridgwayweb.com`?** +- A) **LAN only** — browse to `http:///`. Simplest, nothing new exposed. +- B) **Add an NPM proxy host** — I get a hostname; you add the proxy host + DNS (you own NPM). +- C) **LAN now, NPM later** — build LAN-only, leave a RUNBOOK section for adding NPM. +- X) Other (please describe after [Answer]: tag below) + +[Answer]: + +--- + +**Q5. Extensions (AI-DLC security/resiliency baseline). Your usual choice is to opt OUT for a +LAN-only PoC, and I document the "what to do if this ever moves external" TODOs instead. Confirm?** +- A) **Opt out of both, document the TODOs** (your standing preference). +- B) **Enable security-baseline** too. +- C) **Enable resiliency-baseline** too. +- D) **Enable both.** +- X) Other (please describe after [Answer]: tag below) + +[Answer]: + +--- + +## Notes / assumptions I'll carry unless you say otherwise + +- **Q6 (implicit, shout if wrong):** "downloads … ready for me to use" = the repo is cloned to a + path Atheos can open as a **project**, on the correct branch, with git identity configured — + not that the container does anything to the blog's remote. The blog stays a separate repo; this + project's repo holds the **provisioning**. +- LXC defaults I propose: **2 vCPU, 1 GB RAM, 20 GB root**, Alpine 3.23, `onboot=1`, unprivileged. +- I will **not** touch `armistace` SSH keys — that's your point 4. diff --git a/aidlc-docs/requirements/requirements.md b/aidlc-docs/requirements/requirements.md new file mode 100644 index 0000000..a573f19 --- /dev/null +++ b/aidlc-docs/requirements/requirements.md @@ -0,0 +1,129 @@ +# Requirements — atheos-blog-lxc + +**Project**: Alpine LXC running Atheos, wired to Gitea, with `armistace/blog` cloned and ready to edit. +**Stage**: INCEPTION — Requirements Analysis +**Date**: 2026-09-29 +**Deploy Target**: Proxmox LXC on node **scotty** (.69). Not the K3s cluster. + +--- + +## 1. Intent Analysis Summary + +| | | +|---|---| +| **User request** | An Alpine LXC that (1) installs Atheos with nginx+PHP, (2) sets up git integration, (3) has `git.aridgwayweb.com/armistace/blog` cloned and ready to use, (4) leaves SSH key setup to the operator, (5) is backed by a 20 GB root. | +| **Request type** | New Project (greenfield repo; provisioning a new guest) | +| **Scope estimate** | Multiple components — guest creation, web stack, web app, VCS wiring, docs | +| **Complexity estimate** | Moderate — no application code to write; the risk is in correct provisioning and in not fighting Atheos's install model | + +### The one finding that shaped this + +`armistace/blog` is a **Pelican static site**, not plain markdown: it has `pelicanconf.py`, +`Makefile`, `publishconf.py` and `themes/cleanblog`. Building/publishing needs **Python + pelican**, +which Atheos (PHP) cannot do. Andrew confirmed (**Q1=A**) that the existing pipeline already handles +the build and that *"just writing markdown is more than enough"* — so the **build is explicitly out +of scope**. This box is an **editor + git client**, nothing more. + +--- + +## 2. Functional Requirements + +| ID | Requirement | Source | +|---|---|---| +| **FR-1** | An **Alpine 3.23 LXC** is created on PVE node **scotty** (.69) with a **20 GB rootfs**, 2 vCPU and 1 GB RAM, `onboot=1`, unprivileged. | user point 5 + Q3 | +| **FR-2** | **nginx + PHP-FPM** are installed and configured to serve Atheos; PHP includes the extensions Atheos wants for its plugin marketplace (**ZIP, OPENSSL, MBSTRING**) and `allow_url_fopen=On`. | user point 1 | +| **FR-3** | **Atheos** (github.com/Atheos/Atheos) is installed into the web root, pinned to a specific commit, with `config.php`, `data/`, `workspace/`, `plugins/` and `themes/` writable by the PHP-FPM user. No database is required. | user point 1 | +| **FR-4** | Atheos's **git integration** is configured and usable: git installed, a safe.directory set for the workspace, and Atheos's own git settings present (committer identity, default branch). | user point 2 | +| **FR-5** | `armistace/blog` is **cloned into `workspace/`** on its default branch (`master`) so it appears as an Atheos project, ready to open and edit. | user points 3, 6 | +| **FR-6** | The clone is **read-from-Gitea ready**, and the repo is left with instructions to **switch the remote to SSH** once the operator adds their key. The agent must **not** switch the remote nor install credentials itself. | Q2 + Q6 | +| **FR-7** | A **RUNBOOK.md** documents: the container's IP, how to log in over SSH, where the workspace/project lives, exactly how to move to an SSH remote (`git remote set-url`) after adding the key, plus credential/identity setup and common ops. | Q2 + Q6 + standing preference | +| **FR-8** | A **README.md** documents what this is, the install method, and the one-time provisioning steps. | standing preference | +| **FR-9** | The project repo (`hermes/atheos-blog-lxc`) holds a **repeatable provisioning script** so the whole LXC can be re-created from scratch. | AI-DLC / standing preference | +| **FR-10** | **Reachability is LAN-only**: Atheos served on the LXC's own IP on port 80. No NPM proxy host, no DNS record, no router port-forward is created. | Q4 | +| **FR-11** | The agent **does not create or configure `armistace` credentials or git identity**. The operator performs that over SSH, under their own identity. | Q2 (explicit restriction) | + +--- + +## 3. Non-Functional Requirements + +| ID | Requirement | +|---|---| +| **NFR-1** | Rootfs is 20 GB and must stay comfortably within it (Atheos + nginx + PHP + git + the blog repo is well under 1 GB). | +| **NFR-2** | The container must survive a **reboot** and return serving Atheos with the clone still present and functional (verify, don't assume). | +| **NFR-3** | Provisioning must be **repeatable and idempotent-friendly**: a single documented script plus manual steps. | +| **NFR-4** | The guest must not depend on the K3s cluster; it is a standalone LXC. | +| **NFR-5** | Timezone should match the operator (**Australia/Brisbane**) so Atheos/git timestamps are sane. | + +--- + +## 4. Explicitly Out of Scope (confirmed by the operator) + +- **Building/publishing the Pelican site.** No Python, pelican, `make html`, or preview server on + this box (Q1=A). The existing pipeline handles the build. +- **SSH key creation/installation for `armistace`.** Operator does this (Q2, user point 4). +- **Any NPM / public exposure** (Q4=LAN only). +- **Atheos user accounts beyond the initial install.** The operator will create their own login + during Atheos's first-run installer. + +--- + +## 5. Constraints & Risks + +| ID | Constraint / risk | Mitigation | +|---|---|---| +| **C-1** | 🔴 **Atheos on Alpine is off the beaten path.** Upstream only documents Ubuntu/CentOS/Apache/nginx. Alpine's PHP packaging and paths differ. | Verify the package set on Alpine 3.23 before installing (php83 + php83-fpm + php83-session + php83-zip + php83-openssl + php83-mbstring). Confirm the FPM socket path and nginx `fastcgi_pass` match. | +| **C-2** | 🔴 **Alpine is OpenRC, not systemd.** Service management is `rc-service`/`rc-update`. | Provision with OpenRC idioms; do not emit systemd units. | +| **C-3** | 🔴 **The Alpine template is not yet downloaded on scotty.** | `pveam update` + `pveam download local alpine-3.23-default_...` before `pct create`. | +| **C-4** | 🟠 **Atheos first-run installer needs the web root writable** and will create a config + admin user. | Set ownership/permissions correctly; document that the operator completes the installer in the browser. | +| **C-5** | 🟠 **`git clone` of an internet-facing Gitea from a new guest** — the clone is public (repo is public), so no credential is needed for the *clone*. | Confirm the repo stays public; clone over the public URL. If it ever goes private, the RUNBOOK covers adding a credential. | +| **C-6** | 🟡 Agent must respect the **no-armistace-credentials** restriction. | FR-11; state it plainly in the RUNBOOK so a future agent does not "helpfully" set it. | + +--- + +## 6. Extension Configuration + +| Extension | Enabled | Decided At | +|---|---|---| +| security-baseline | ❌ No | Requirements Analysis (Q5=A) | +| resiliency-baseline | ❌ No | Requirements Analysis (Q5=A) | +| property-based-testing | ❌ No | Requirements Analysis (Q5=A) | + +Per the standing preference, the opted-out concerns are **not dropped** — they are written up as +"what to do if this ever moves external" in `docs/security-notes.md` and `docs/resiliency-notes.md` +(see FR-12 below). + +| ID | Requirement | Source | +|---|---|---| +| **FR-12** | Even with both extensions opted out, produce `docs/security-notes.md` and `docs/resiliency-notes.md` listing the concrete hardening TODOs and their code touch-points, and cross-reference them from the README. | standing preference | + +--- + +## 7. Acceptance Criteria (Given/When/Then) + +- **AC-1** — *Given* the LXC is created, *when* I run `pct config 136` on scotty, *then* it shows + Alpine 3.23, rootfs 20 GB, and it is onboot-enabled. +- **AC-2** — *Given* provisioning is complete, *when* I open `http:///` from the LAN, *then* + Atheos's first-run installer (or login) is served by nginx/PHP — not a 502/404. +- **AC-3** — *Given* I complete the Atheos installer, *when* I open the workspace, *then* `blog` is + present as a project whose files I can open and edit. +- **AC-4** — *Given* the `blog` project is open, *when* I edit a markdown file and save, *then* the + change persists to disk inside the container. +- **AC-5** — *Given* the clone, *when* I run `git -C workspace/blog remote -v`, *then* it points at + Gitea (initially HTTP), and the RUNBOOK gives the exact `git remote set-url` command to move to SSH. +- **AC-6** — *Given* the container is rebooted, *when* it comes back, *then* Atheos serves again and + the clone and any edits are still present. +- **AC-7** — *Given* the RUNBOOK, *when* a different agent picks the repo up, *then* they can + re-provision or hand off without prior context. +- **AC-8** — *Given* the build is out of scope, *when* I search the container, *then* no Python/pelican + build tooling is installed (proving scope was respected). + +--- + +## 8. Key Requirements Summary + +- One small **Alpine LXC (136) on scotty**, 20 GB root, serving **Atheos** over **nginx+PHP** on the LAN. +- **Atheos is an editor + git client** here — deliberately **not** a build environment. +- **`armistace/blog` cloned into Atheos's workspace**, ready to write in. +- **Operator-owned credentials**: the agent sets up the clone and the instructions; the operator adds + the SSH key and, later, switches the remote. +- **RUNBOOK + README + re-provision script**, because a later agent or a re-build must not need me. diff --git a/docs/resiliency-notes.md b/docs/resiliency-notes.md new file mode 100644 index 0000000..bed9752 --- /dev/null +++ b/docs/resiliency-notes.md @@ -0,0 +1,55 @@ +# Resiliency Notes — atheos-blog-lxc + +**Status**: resiliency-baseline extension **opted out** (LAN-only PoC) per Andrew's standing preference. +**Purpose**: record what a non-PoC deployment would need, so the trail survives. + +--- + +## 1. What this guest is, from a resiliency standpoint + +A **single small container that can be destroyed and rebuilt** — that is the design, not a gap. +`armistace/blog` is the durability anchor: the source of truth lives in **Gitea**, not on this guest. +Losing the LXC loses only: +- the Atheos install (reproducible: `scripts/provision.sh`) +- the local clone (re-clonable) +- **uncommitted edits** ← the only genuinely perishable data + +## 2. Current posture + +| Aspect | State | Note | +|---|---|---| +| Rootfs | 20 GB, thin-provisioned on `scotty-local-lvm` | thin pool had 771 GB free at design time | +| `onboot` | enabled | survives a node restart | +| Backups | **none** | acceptable: nothing unique is stored | +| Snapshots | none taken (a `pre-*` snapshot is used during *upgrades* only) | | +| Rebuild path | `scripts/provision.sh` | **this is the resiliency mechanism** | +| Reboot survival | **verified**, not assumed | AC-6 — service returns and the clone persists | + +## 3. If this ever moves beyond a LAN PoC + +| # | Item | Touch-point | +|---|---|---| +| R-1 | **Scheduled snapshot** of the guest (`pct snapshot` / PBS) with retention | PVE / PBS | +| R-2 | **Automated verification of the rebuild path** — run `provision.sh` into a throwaway VMID on a schedule; a backup you have never restored is a claim | CI or cron on the PVE node | +| R-3 | **Alert on guest down** (`pct status` + HTTP probe) | observability stack | +| R-4 | **Push-based durability for in-progress work**: an auto-commit/ahead-of-time branch so uncommitted edits stop being the failure mode | a small hook or Atheos-side job | +| R-5 | **Disk alert at 80%** of the 20 GB rootfs (a full rootfs silently breaks PHP sessions) | observability stack | +| R-6 | **Move the clone's durability to the repo**: rely on Gitea backup/restore rather than guest backups | Gitea | +| R-7 | **Document RTO/RPO**: here RTO ≈ re-run `provision.sh` (minutes); RPO = last commit | RUNBOOK | +| R-8 | **`scotty-local-lvm` thin-pool monitoring** — a thin pool that fills takes down *every* guest on it, not just this one | PVE node | + +## 4. Design choices that already help + +- **Nothing unique is stored on the guest** — the deliberately tiny 20 GB root is a *feature*: it + bounds what can be lost and keeps rebuild cheap. +- **Provisioning is a script**, so rebuild is not a memory exercise (FR-9). +- **The build pipeline is elsewhere** (Q1=A), so this guest is not on the critical path for publishing. +- **OpenRC services are enabled at boot** (AC-6 verifies it), so a reboot is not a repair event. + +## 5. Accepted risks + +| Risk | Why accepted | Revisit trigger | +|---|---|---| +| No backups | no unique data; rebuild is scripted | if the guest starts holding anything unique | +| Single guest on a single node | LAN PoC scope | on any production expectation → R-1/R-3 | +| Uncommitted edits are perishable | normal for a git worktree; operator controls commit cadence | if it bites → R-4 | diff --git a/docs/security-notes.md b/docs/security-notes.md new file mode 100644 index 0000000..bdd7234 --- /dev/null +++ b/docs/security-notes.md @@ -0,0 +1,88 @@ +# Security Notes — atheos-blog-lxc + +**Status**: security-baseline extension **opted out** (LAN-only PoC) per Andrew's standing preference. +**Purpose**: preserve the hardening trail so that *if this ever moves to the wider web*, the work is +already scoped, and nobody has to re-derive it. Every item names its code touch-point. + +**Reviewed**: 2026-09-29. Design decision **D6** (see `application-design.md`). + +--- + +## 1. The credential model (the load-bearing decision) + +**Chosen: a per-repository Gitea _deploy key_, scoped to `armistace/blog`, write-enabled.** + +### Why not the alternatives + +| Option | Scope if stolen | Verdict | +|---|---|---| +| **Deploy key** (chosen) | `armistace/blog` **only** | ✅ per-repo scoping, explicit `read_only` flag | +| `armistace` personal access token | **every repo armistace can write** (scopes are route-groups like `write:repository`, matched against user access — *not* per-repo) | ❌ account-wide blast radius | +| `armistace` personal SSH key | whole account, incl. web/API | ❌ | +| Dedicated "Atheos" Gitea user | whatever it owns/grants — **no reduction** for `blog` | ❌ adds a credential + muddies attribution | + +**Verified, not assumed** (2026-09-29, live instance 1.27.3): +- API schema: `AccessToken.scopes` = `["read:repository","write:repository",…]`; `DeployKey` carries + `read_only: bool` and is bound to one repository. Deploy-key endpoints + `/repos/{owner}/{repo}/keys` exist and are **per-repo**. +- The instance's own token inventory shows the risk concretely: `armistace` holds 8 access tokens, + one with scope **`all`**. + +> **Threat model this addresses.** Atheos is itself the untrusted component: it is a file editor +> with shell-exec reach (`codegit` invokes `git`; Atheos uses `exec`/`system`) and it is reachable by +> anyone on the LAN. The goal is not to make compromise impossible — it is to make the **blast radius +> one repository** when it happens. + +### Implementation (code touch-points) + +| Item | Where | Detail | +|---|---|---| +| Keypair generated **on the guest**, never leaves it | `scripts/provision.sh`, U4 | `ssh-keygen -t ed25519 -f ~//.ssh/id_ed25519_blog -N ''` | +| Key owned **only** by the php-fpm run user, mode `600` | U4 | not readable by nginx or other users | +| `IdentitiesOnly yes` + a named host alias | `~//.ssh/config` | `Host gitea-blog` so a stray push cannot target another repo | +| Remote uses the alias | U4 | `gitea-blog:armistace/blog.git` | +| **Public** half handed to the operator | via Matrix | operator adds it: repo → Settings → Deploy Keys → **Allow write access** | + +⚠️ **Honest limitation.** For push to work, the deploy key must be readable by the web-app user, so a +full Atheos compromise *can* use it to write `blog`. Unavoidable for any push-capable editor. The +deploy key makes that the **floor** (one repo, one protected branch), not the **ceiling** (8 repos). + +--- + +## 2. If this is ever exposed beyond the LAN + +| # | Item | Touch-point | +|---|---|---| +| S-1 | **Terminate TLS** at NPM; never serve Atheos plaintext to the internet | NPM proxy host | +| S-2 | **Authenticate in front of Atheos**, not just inside it — an NPM Access List / SSO so the app is not the only gate | NPM | +| S-3 | **fail2ban** on the guest for failed Atheos logins and nginx 401/403s | guest + `nginx.conf` log format | +| S-4 | **Rate-limit** the login endpoint in nginx (`limit_req_zone`) | `nginx.conf` | +| S-5 | **Force HTTPS** redirect; set `Strict-Transport-Security` | `nginx.conf` | +| S-6 | **Session hardening**: `session.cookie_secure=1`, `session.cookie_httponly=1`, `session.cookie_samesite=Lax` | `php.ini` | +| S-7 | **Disable PHP dangerous functions** Atheos does not need: `exec, shell_exec, system, passthru, proc_open, popen` — *except* git needs `proc_open`/`exec`, so this needs testing before applying | `php.ini` — ⚠️ test, do not blind-apply | +| S-8 | **`open_basedir`** to `/var/www/atheos` + `/tmp` to contain a file-read bug | `php.ini` | +| S-9 | **nginx hardening**: `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, deny dotfiles, deny `*.md`/`*.py`/`.git` serving | `nginx.conf` | +| S-10 | **Protect the deploy key + config** from being served: verify `/.ssh`, `/config.php`, `/data` are not web-reachable | `nginx.conf` — **verify with curl, not by reading config** | +| S-11 | **Gitea-side**: consider an IP allowlist / rate limit on the git endpoint if the box is public | Gitea/NPM | +| S-12 | **Alerting** on: nginx 5xx rate, new git remote, deploy-key use, file changes under `/var/www/atheos` | not built — would need the observability stack | +| S-13 | **Keep Atheos patched** — it is PHP, no releases; re-pin deliberately (see `scripts/update-atheos.sh`) | update script | +| S-14 | **`allow_url_fopen=On`** is currently enabled (needed for the plugin marketplace). Turn it **off** for internet exposure unless the marketplace is genuinely required | `php.ini` | + +## 3. Standing hygiene (applies now, LAN or not) + +- Branch protection on `armistace/blog` **master** — so a compromised editor cannot force-push master, + only propose. *(Repo-side; operator-owned.)* +- The deploy key is **write-enabled** by explicit choice. If push ever becomes unnecessary, flip it to + read-only in the repo settings — zero code change. +- No secrets in git: the private key is generated on the guest and never committed (`.gitignore`). +- Agent must never create `armistace` credentials (FR-11). + +## 4. Accepted risks (conscious, recorded) + +| Risk | Why accepted | Revisit trigger | +|---|---|---| +| Atheos has shell-exec capability | it is a git-integrated editor; unavoidable | if exposed publicly → apply S-2, S-7, S-8 | +| Deploy key readable by the web user | required for push | if push no longer needed → set read-only | +| LAN has no TLS | LAN-only scope (FR-10) | on any external exposure → S-1 | +| `allow_url_fopen=On` | plugin marketplace | if the marketplace is unused → set Off | +| No monitoring/alerting | out of scope; observability stack is separate | if exposed publicly → S-12 | diff --git a/files/atheos.conf b/files/atheos.conf new file mode 100644 index 0000000..fd4b03f --- /dev/null +++ b/files/atheos.conf @@ -0,0 +1,43 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + + server_name _; + + root /var/www/atheos; + index index.php index.html; + + access_log /var/log/nginx/atheos.access.log; + error_log /var/log/nginx/atheos.error.log; + + # --- PHP ----------------------------------------------------------------- + # The socket path here MUST byte-match the pool's `listen =` in + # /etc/php83/php-fpm.d/www.conf + # (interface I-1 — a mismatch yields a bare 502 with no other diagnostic). + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass unix:/run/php-fpm83/php-fpm.sock; + fastcgi_index index.php; + include fastcgi.conf; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + } + + location / { + try_files $uri $uri/ =404; + } + + # --- hardening ----------------------------------------------------------- + location ~ /\.git { deny all; } + location ~ /\.ssh { deny all; } + location ~ /\. { deny all; } + location ~ ^/data/ { deny all; } + location ~ ^/(config\.php|common\.php|controller\.php|traits/) { deny all; } + location ~* \.(md|py|sh|log|conf|ini|sql|bak|orig)$ { deny all; } + + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + client_max_body_size 64m; +} diff --git a/files/ssh-config b/files/ssh-config new file mode 100644 index 0000000..c45fae1 --- /dev/null +++ b/files/ssh-config @@ -0,0 +1,25 @@ +# ~atheos/.ssh/config — install to /home/atheos/.ssh/config (mode 600, owner atheos) +# +# gitea-blog: per-repository deploy key for armistace/blog (design decision D6). +# +# !! THE USER IS `gitea`, NOT `git` !! +# This instance's app.ini sets SSH_USER = gitea. +# With `git` the sshd on that host answers "Invalid user git" and rejects the +# connection BEFORE reading authorized_keys - which looks identical to a +# rejected key. Verified in the sshd log on LXC 109, 2026-09-29. +# +# Why the LAN address rather than git.aridgwayweb.com: +# - app.ini sets SSH_DOMAIN = 192.168.178.155 / SSH_PORT = 22 +# with START_SSH_SERVER = false, i.e. SSH is served by the Gitea LXC's own sshd. +# - the public path does NOT forward :22 (only :443, via NPM). Verified: +# git.aridgwayweb.com:22 -> unreachable +# 192.168.178.155:22 -> OPEN +# - this guest is LAN-only, so LAN transport is correct and avoids an internet round-trip. +# +# IdentitiesOnly yes ensures no other key is ever offered to the server. +Host gitea-blog + HostName 192.168.178.155 + User gitea + IdentityFile ~/.ssh/id_ed25519_blog + IdentitiesOnly yes + Port 22 diff --git a/scripts/build-and-test.sh b/scripts/build-and-test.sh new file mode 100644 index 0000000..a443c16 --- /dev/null +++ b/scripts/build-and-test.sh @@ -0,0 +1,120 @@ +#!/bin/sh +# ============================================================================== +# build-and-test.sh — captures evidence for every acceptance criterion (AC-1..AC-8) +# Run ON THE GUEST: ssh root@192.168.178.57 'sh -s' < build-and-test.sh +# ============================================================================== +W=/var/www/atheos +B=$W/workspace/blog +pass=0; fail=0 +ok() { echo " PASS $1"; pass=$((pass+1)); } +bad() { echo " FAIL $1"; fail=$((fail+1)); } +chk() { if [ "$2" = "$3" ]; then ok "$1 ($2)"; else bad "$1 (got '$2', want '$3')"; fi; } +asuser() { su -s /bin/sh atheos -c "$1"; } + +echo "==============================================================" +echo "AC-2 Atheos is served by nginx+PHP (not 502/404)" +echo "==============================================================" +CODE=$(wget -qO /tmp/idx.html --server-response http://127.0.0.1/ 2>&1 | sed -n 's/.*HTTP\/1\.[01] \([0-9]*\).*/\1/p' | head -1) +chk "HTTP status for /" "$CODE" "200" +if grep -qi 'Atheos IDE' /tmp/idx.html 2>/dev/null; then ok "served page is the Atheos IDE"; else bad "served page is not Atheos"; fi +if grep -qiE 'install|setup' /tmp/idx.html 2>/dev/null; then ok "first-run installer/login markers present"; else bad "no installer markers"; fi +SOCK=$(sed -n 's/^listen = //p' /etc/php83/php-fpm.d/www.conf | head -1) +NGX=$(sed -n 's/.*fastcgi_pass unix:\([^;]*\);.*/\1/p' /etc/nginx/http.d/atheos.conf | head -1) +echo " --- socket contract (I-1):" +echo " php-fpm listen : $SOCK" +echo " nginx fastcgi : $NGX" +chk "socket paths byte-match" "$NGX" "$SOCK" + +echo +echo "==============================================================" +echo "AC-3 'blog' is present with files that can be opened" +echo "==============================================================" +chk "posts found" "$(ls $B/src/content/*.md 2>/dev/null | wc -l | tr -d ' ')" "20" +if [ -d "$W/workspace/blog/.git" ]; then ok "it is a git working copy"; else bad "not a git working copy"; fi +chk "theme present" "$(ls $B/src/themes 2>/dev/null | tr -d ' ')" "cleanblog" +chk "workspace owner" "$(stat -c '%U' $W/workspace/blog)" "atheos" + +echo +echo "==============================================================" +echo "AC-4 An edit persists to disk (as the FPM user)" +echo "==============================================================" +asuser "printf 'x\n' > $B/.ac4-probe" >/dev/null 2>&1 +if [ "$(asuser "cat $B/.ac4-probe" 2>/dev/null)" = "x" ]; then ok "write+read back as 'atheos'"; else bad "cannot write as atheos"; fi +asuser "rm -f $B/.ac4-probe" >/dev/null 2>&1 +for p in config.php data workspace plugins themes; do + if asuser "test -w $W/$p" 2>/dev/null; then ok "writable: $p"; else bad "NOT writable: $p"; fi +done + +echo +echo "==============================================================" +echo "AC-5 Remote points at Gitea; the SSH switch is documented" +echo "==============================================================" +REMOTE=$(asuser "git -C $B remote get-url origin" 2>/dev/null) +echo " remote now: $REMOTE" +case "$REMOTE" in + *aridgwayweb.com*armistace/blog*) ok "remote targets armistace/blog on Gitea (HTTPS)" ;; + gitea-blog:armistace/blog*) ok "remote targets armistace/blog via the deploy-key alias" ;; + *) bad "remote does not target the blog repo" ;; +esac +chk "branch" "$(asuser "git -C $B rev-parse --abbrev-ref HEAD" 2>/dev/null)" "master" + +# Phase 2: once the SSH alias is in use, prove the transport actually authenticates +case "$REMOTE" in + gitea-blog:*) + AUTH=$(asuser 'ssh -o BatchMode=yes -o ConnectTimeout=8 -T gitea-blog 2>&1' | head -1) + case "$AUTH" in + *successfully\ authenticated*) ok "deploy key authenticates over SSH" ;; + *) bad "deploy key does NOT authenticate: $AUTH" ;; + esac + ;; +esac + +echo +echo "==============================================================" +echo "AC-6 Survives reboot (services enabled, data intact)" +echo "==============================================================" +echo " container uptime: $(cut -d. -f1 /proc/uptime)s (reboot was issued during the test)" +for s in nginx php-fpm83 sshd; do + if rc-update show 2>/dev/null | grep -qE "^\s+$s\s+\|\s+default"; then ok "$s enabled at boot"; else bad "$s NOT enabled at boot"; fi + if rc-service "$s" status >/dev/null 2>&1; then ok "$s running"; else bad "$s not running"; fi +done +if [ -S "$SOCK" ]; then ok "fpm socket exists after reboot"; else bad "fpm socket missing"; fi + +echo +echo "==============================================================" +echo "AC-8 SCOPE GUARD: no build toolchain present (must be ABSENT)" +echo "==============================================================" +for t in python python3 pelican pip pip3 make; do + if command -v "$t" >/dev/null 2>&1; then bad "$t IS present - scope violated!"; else ok "$t absent (correct)"; fi +done +echo " --- also check no database engine was installed (Atheos needs none):" +for d in mysqld mariadbd postgres redis-server; do + if command -v "$d" >/dev/null 2>&1; then bad "$d present - unexpected"; else ok "$d absent (correct)"; fi +done + +echo +echo "==============================================================" +echo "FR-11 GUARD: agent created NO identity or armistace credential" +echo "==============================================================" +chk "git user.name empty" "$(asuser 'git config --get user.name' 2>/dev/null)" "" +chk "git user.email empty" "$(asuser 'git config --get user.email' 2>/dev/null)" "" +if [ -f /home/atheos/.ssh/id_ed25519_blog ]; then ok "deploy key present"; else bad "deploy key missing"; fi +chk "deploy key mode" "$(stat -c '%a' /home/atheos/.ssh/id_ed25519_blog 2>/dev/null)" "600" +chk "deploy key owner" "$(stat -c '%U' /home/atheos/.ssh/id_ed25519_blog 2>/dev/null)" "atheos" +chk "ssh config mode" "$(stat -c '%a' /home/atheos/.ssh/config 2>/dev/null)" "600" + +echo +echo "==============================================================" +echo "RESOURCES" +echo "==============================================================" +echo " rootfs: $(df -hP / | tail -1 | awk '{print $2" total, "$3" used, "$4" avail ("$5")"}')" +echo " NOTE: \`free\` and \`uptime\` report the HOST, not this container - lxcfs is not" +echo " installed. The enforced limit is memory=1024MB/swap=256MB from PVE." +echo " Check real usage host-side: cat /sys/fs/cgroup/lxc/136/memory.current" +echo " listening ports: $(netstat -tln 2>/dev/null | awk '/LISTEN/{split($4,a,":"); print a[length(a)]}' | sort -u | tr '\n' ' ')" + +echo +echo "==============================================================" +printf 'RESULT: %d passed, %d failed\n' "$pass" "$fail" +echo "==============================================================" +[ "$fail" -eq 0 ] || exit 1 diff --git a/scripts/provision.sh b/scripts/provision.sh new file mode 100644 index 0000000..a6437cb --- /dev/null +++ b/scripts/provision.sh @@ -0,0 +1,194 @@ +#!/bin/sh +# ============================================================================== +# provision.sh — rebuild the Atheos blog editor LXC from scratch +# Project: atheos-blog-lxc (AI-DLC) +# Target : Proxmox LXC 136 on node scotty (192.168.178.69) +# +# RUN THIS ON THE PVE NODE (scotty), as root. It is written to be re-runnable: +# each step checks state before acting. +# +# This script reproduces exactly what was executed during the initial build on +# 2026-09-29. It deliberately does NOT create any `armistace` credential, git +# identity, or Gitea user (requirement FR-11). +# ============================================================================== +set -eu + +VMID=136 +NODE_HOST="192.168.178.69" +TEMPLATE_STORAGE="LARGE_STORAGE_1" +ROOTFS_STORAGE="scotty-local-lvm" +TEMPLATE="alpine-3.23-default_20260116_amd64.tar.xz" +BRIDGE="vmbr0" +ROOTFS_SIZE=20 +CORES=2 +MEMORY=1024 +SWAP=256 +TZ_NAME="Australia/Brisbane" + +# --- Atheos pin (design decision D2: pinned, not tracking a moving branch) ---- +# Recorded at build time 2026-09-29 from github.com/Atheos/Atheos (default branch `main`) +ATHEOS_SHA="f90cd1806dbe0e25673a77e27390e8b1a240c3f6" +ATHEOS_BUILD=611 +ATHEOS_TARBALL_SHA256="03ebcb1479b59f1898b5fa50808b181750b42211698f7f7b4042cf2514d78b20" + +# --- the blog repo (read-only clone source; agent never pushes) -------------- +BLOG_REPO="https://git.aridgwayweb.com/armistace/blog.git" +GITEA_SSH_HOST="192.168.178.155" # LAN address: Gitea SSH_DOMAIN; public :22 is not forwarded +GITEA_SSH_USER="gitea" # !! app.ini SSH_USER=gitea - NOT the usual `git`. + # Using `git` yields "Invalid user git" from sshd, + # which is rejected BEFORE authorized_keys is read. + +WEBROOT="/var/www/atheos" +FPM_USER="atheos" +FPM_SOCKET="/run/php-fpm83/php-fpm.sock" + +log() { printf '\n=== %s\n' "$*"; } + +# ============================================================================== +log "1/9 verify we are on the PVE node and the VMID is free" +hostname +pct status "$VMID" >/dev/null 2>&1 && { echo "ERROR: VMID $VMID already exists - aborting"; exit 1; } + +# ============================================================================== +log "2/9 ensure the Alpine template is present" +pveam update >/dev/null 2>&1 || true +if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then + pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" +else + echo "template already present" +fi + +# ============================================================================== +log "3/9 create the container (U1)" +pct create "$VMID" "$TEMPLATE_STORAGE:vztmpl/$TEMPLATE" \ + --hostname atheos-blog \ + --cores "$CORES" \ + --memory "$MEMORY" \ + --swap "$SWAP" \ + --rootfs "$ROOTFS_STORAGE:$ROOTFS_SIZE" \ + --net0 "name=eth0,bridge=$BRIDGE,ip=dhcp" \ + --unprivileged 1 \ + --onboot 1 \ + --ostype alpine \ + --features "" \ + --description "Atheos browser editor for armistace/blog (AI-DLC project atheos-blog-lxc). LAN-only. Deploy key auth. See Gitea hermes/atheos-blog-lxc RUNBOOK." + +pct start "$VMID" +sleep 8 +pct status "$VMID" + +# ============================================================================== +log "4/9 base configuration (U1): timezone, ssh, apt/apk index" +pct exec "$VMID" -- sh -c " + apk update >/dev/null 2>&1 + apk add --no-cache tzdata openssh >/dev/null 2>&1 + cp /usr/share/zoneinfo/$TZ_NAME /etc/localtime + echo $TZ_NAME > /etc/timezone + rc-update add sshd default >/dev/null 2>&1 + rc-service sshd start >/dev/null 2>&1 + date +" + +# ============================================================================== +log "5/9 web stack (U2): nginx + php83 + extensions" +pct exec "$VMID" -- sh -c ' + apk add --no-cache nginx php83 php83-fpm \ + php83-session php83-zip php83-openssl php83-mbstring php83-ctype php83-curl \ + php83-dom php83-json php83-iconv php83-fileinfo php83-simplexml php83-xml \ + php83-tokenizer >/dev/null 2>&1 + # dedicated service user (I-4: FPM user == file owner == key owner) + adduser -D -s /sbin/nologin -h /home/atheos atheos 2>/dev/null || true + mkdir -p /var/www/atheos/workspace /run/php-fpm83 /var/log/nginx + chown nginx:nginx /run/php-fpm83 + chmod 755 /run/php-fpm83 + chown -R atheos:atheos /var/www/atheos /home/atheos + # timezone (NFR-5) + sed -i "s|^;\?date.timezone =.*|date.timezone = Australia/Brisbane|" /etc/php83/php.ini + # --- the socket contract (I-1): pool listen MUST byte-match nginx fastcgi_pass --- + cp -n /etc/php83/php-fpm.d/www.conf /etc/php83/php-fpm.d/www.conf.orig 2>/dev/null || true + sed -i "s|^user = .*|user = atheos|" /etc/php83/php-fpm.d/www.conf + sed -i "s|^group = .*|group = atheos|" /etc/php83/php-fpm.d/www.conf + sed -i "s|^listen = .*|listen = '"$FPM_SOCKET"'|" /etc/php83/php-fpm.d/www.conf + grep -q "^listen.owner" /etc/php83/php-fpm.d/www.conf || echo "listen.owner = nginx" >> /etc/php83/php-fpm.d/www.conf + grep -q "^listen.group" /etc/php83/php-fpm.d/www.conf || echo "listen.group = nginx" >> /etc/php83/php-fpm.d/www.conf + grep -q "^listen.mode" /etc/php83/php-fpm.d/www.conf || echo "listen.mode = 0660" >> /etc/php83/php-fpm.d/www.conf + rc-update add nginx default >/dev/null 2>&1 + rc-update add php-fpm83 default >/dev/null 2>&1 + rc-service php-fpm83 restart >/dev/null 2>&1 + rc-service nginx restart >/dev/null 2>&1 +' +echo "NOTE: install the nginx vhost from files/atheos.conf (see README) into /etc/nginx/http.d/atheos.conf" +echo " and remove /etc/nginx/http.d/default.conf, then: rc-service nginx restart" + +# ============================================================================== +log "6/9 Atheos install at the pinned commit (U3)" +pct exec "$VMID" -- sh -c " + cd /tmp + wget -q -O atheos.tar.gz https://codeload.github.com/Atheos/Atheos/tar.gz/$ATHEOS_SHA + echo '$ATHEOS_TARBALL_SHA256 atheos.tar.gz' | sha256sum -c - || { echo 'CHECKSUM MISMATCH - abort'; exit 1; } + rm -rf /tmp/atheos-extract && mkdir -p /tmp/atheos-extract + tar xzf atheos.tar.gz -C /tmp/atheos-extract + cd /tmp/atheos-extract/Atheos-$ATHEOS_SHA + for f in \$(ls -A); do + [ \"\$f\" = workspace ] && continue + cp -a \"\$f\" $WEBROOT/ 2>/dev/null || true + done + cd $WEBROOT + touch config.php && chmod 664 config.php + for d in data workspace plugins themes; do mkdir -p \$d; chmod 775 \$d; done + chown -R atheos:atheos $WEBROOT + echo \"Atheos build: \$(cat .version)\" +" + +# ============================================================================== +log "7/9 git + clone + deploy key (U4)" +pct exec "$VMID" -- sh -c " + apk add --no-cache git openssh-client >/dev/null 2>&1 + cd $WEBROOT/workspace + su -s /bin/sh atheos -c 'git config --global --add safe.directory $WEBROOT/workspace' + su -s /bin/sh atheos -c 'git config --global --add safe.directory $WEBROOT/workspace/blog' + # clone over HTTPS (public repo => no credential at this stage; U4 phase 1) + su -s /bin/sh atheos -c 'test -d blog/.git || git clone $BLOG_REPO blog' + # deploy keypair: private key stays on the guest, owned by the FPM user + mkdir -p /home/atheos/.ssh && chmod 700 /home/atheos/.ssh && chown atheos:atheos /home/atheos/.ssh + su -s /bin/sh atheos -c 'test -f /home/atheos/.ssh/id_ed25519_blog || ssh-keygen -t ed25519 -f /home/atheos/.ssh/id_ed25519_blog -N \"\" -C \"atheos-blog-lxc deploy key (armistace/blog)\"' + chmod 600 /home/atheos/.ssh/id_ed25519_blog + chmod 644 /home/atheos/.ssh/id_ed25519_blog.pub + chown atheos:atheos /home/atheos/.ssh/id_ed25519_blog /home/atheos/.ssh/id_ed25519_blog.pub +" +# NB the ~/.ssh/config alias is written by the install-files step (see files/ssh-config) +# IMPORTANT: its User must be "$GITEA_SSH_USER" (gitea), NOT `git`. +pct exec "$VMID" -- sh -c "chmod 600 /home/atheos/.ssh/config 2>/dev/null; chown atheos:atheos /home/atheos/.ssh/config 2>/dev/null; true" + +# ============================================================================== +log "8/9 PUBLISH the deploy key public half to the operator" +pct exec "$VMID" -- sh -c " + echo + echo ' ----- BEGIN deploy key (public half) -----' + cat /home/atheos/.ssh/id_ed25519_blog.pub + echo ' ----- END deploy key (public half) -----' + ssh-keygen -lf /home/atheos/.ssh/id_ed25519_blog.pub +" +cat <<'INSTRUCTIONS' + + >>> OPERATOR ACTION: add the key above to Gitea + Repo: armistace/blog -> Settings -> Deploy Keys -> Add Deploy Key + Title: atheos-blog-lxc + TICK "Allow write access" (required for push) + Then tell the agent to run phase 2 (switch the remote to SSH): + git -C /var/www/atheos/workspace/blog remote set-url origin gitea-blog:armistace/blog.git + +INSTRUCTIONS + +# ============================================================================== +log "9/9 smoke test" +pct exec "$VMID" -- sh -c ' + ip -4 addr show eth0 | grep inet + rc-service nginx status | head -1 + rc-service php-fpm83 status | head -1 + ls -lad /run/php-fpm83/php-fpm.sock +' +echo +echo "Provisioning complete. Now open http:/// in a browser and complete" +echo "the Atheos first-run installer (creates the admin user - operator owned)." +echo "IMPORTANT: first action after SSH -> set your git identity. See RUNBOOK.md step 1." diff --git a/scripts/update-atheos.sh b/scripts/update-atheos.sh new file mode 100644 index 0000000..1780851 --- /dev/null +++ b/scripts/update-atheos.sh @@ -0,0 +1,99 @@ +#!/bin/sh +# ============================================================================== +# update-atheos.sh — update the Atheos install on LXC 136 and RE-PIN the commit +# Design decision D2: Atheos has no releases, so the install is pinned to a +# resolved commit SHA for reproducibility. This script is the deliberate escape +# hatch: it fetches the latest upstream commit, updates the install, and +# re-pins by rewriting the recorded SHA. +# +# RUN THIS ON THE GUEST (as root), or via: ssh root@192.168.178.57 'sh -s' < update-atheos.sh +# +# It does NOT touch your git identity, your deploy key, the blog clone, or +# anything under workspace/. Only the Atheos application files are replaced. +# ============================================================================== +set -eu + +WEBROOT="/var/www/atheos" +FPM_USER="atheos" +STATE_FILE="/etc/atheos-blog-lxc.pin" + +log() { printf '\n=== %s\n' "$*"; } + +log "current pin" +[ -f "$STATE_FILE" ] && cat "$STATE_FILE" || echo "(no pin recorded - first run)" +echo "installed build: $(cat $WEBROOT/.version 2>/dev/null || echo unknown)" + +log "resolve latest upstream commit (branch: main)" +LATEST_SHA=$(wget -qO- https://api.github.com/repos/Atheos/Atheos/commits/main \ + | sed -n 's/.*"sha": *"\([a-f0-9]\{40\}\)".*/\1/p' | head -1) +if [ -z "$LATEST_SHA" ]; then + echo "ERROR: could not resolve the upstream SHA (network or rate limit). Aborting." + exit 1 +fi +echo "latest: $LATEST_SHA" + +CURRENT_SHA=$(sed -n 's/^SHA=//p' "$STATE_FILE" 2>/dev/null || true) +if [ "$LATEST_SHA" = "$CURRENT_SHA" ]; then + echo "Already at the latest pin ($LATEST_SHA). Nothing to do." + exit 0 +fi +echo "updating: $CURRENT_SHA -> $LATEST_SHA" + +log "download + extract to a staging dir (install is NOT touched until verified)" +rm -rf /tmp/atheos-update && mkdir -p /tmp/atheos-update +cd /tmp/atheos-update +wget -q -O atheos.tar.gz "https://codeload.github.com/Atheos/Atheos/tar.gz/$LATEST_SHA" +sha256sum atheos.tar.gz | sed 's/^/ tarball sha256: /' +tar xzf atheos.tar.gz +SRC="/tmp/atheos-update/Atheos-$LATEST_SHA" +test -d "$SRC" || { echo "ERROR: extract produced no $SRC"; exit 1; } + +log "sanity-check the staged tree looks like Atheos" +for f in index.php common.php controller.php classes; do + test -e "$SRC/$f" || { echo "ERROR: staged tree is missing $f - refusing to install"; exit 1; } +done +echo "staged tree looks sane" + +log "back up the current app files (excluding workspace/)" +BK="/root/atheos-backup-$(date +%Y%m%d-%H%M%S)" +mkdir -p "$BK" +for f in $(ls -A "$WEBROOT"); do + [ "$f" = workspace ] && continue + cp -a "$WEBROOT/$f" "$BK/" 2>/dev/null || true +done +echo "backup: $BK" + +log "install the new files (workspace/ preserved)" +cd "$SRC" +for f in $(ls -A); do + [ "$f" = workspace ] && continue + rm -rf "$WEBROOT/$f" + cp -a "$f" "$WEBROOT/" 2>/dev/null || true +done +chown -R "$FPM_USER:$FPM_USER" "$WEBROOT" +echo "new build: $(cat $WEBROOT/.version 2>/dev/null || echo unknown)" + +log "re-pin" +{ + echo "# atheos-blog-lxc Atheos pin - managed by scripts/update-atheos.sh" + echo "# Updated: $(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "SHA=$LATEST_SHA" +} > "$STATE_FILE" +cat "$STATE_FILE" + +log "restart + smoke test" +rc-service php-fpm83 restart >/dev/null 2>&1 || true +rc-service nginx restart >/dev/null 2>&1 || true +sleep 2 +CODE=$(wget -qO- --server-response http://127.0.0.1/ 2>&1 | sed -n 's/.*HTTP\/1\.[01] \([0-9]*\).*/\1/p' | head -1) +echo "local HTTP status: ${CODE:-unknown}" +if [ "${CODE:-000}" != "200" ]; then + echo + echo "WARNING: the app did not return 200." + echo "Roll back with: rm -rf $WEBROOT/* (except workspace/) && cp -a $BK/* $WEBROOT/" + exit 1 +fi +echo +echo "Update complete and re-pinned to $LATEST_SHA." +echo "NOTE: re-run your verification (open the UI). If Atheos changed its installer/config" +echo " semantics, the web UI will tell you - this script only replaces application files."