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.
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
# keys and credentials — never commit
|
||||||
|
*.pem
|
||||||
|
id_ed25519*
|
||||||
|
id_rsa*
|
||||||
|
*.key
|
||||||
|
.env
|
||||||
|
|
||||||
|
# AI-DLC scratch
|
||||||
|
aidlc-docs/**/*.tmp
|
||||||
@@ -1,3 +1,119 @@
|
|||||||
# atheos-blog-lxc
|
# atheos-blog-lxc
|
||||||
|
|
||||||
Alpine LXC running Atheos (browser editor) wired to Gitea + armistace/blog. LAN-only. Built with AI-DLC.
|
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` |
|
||||||
|
|||||||
+298
@@ -0,0 +1,298 @@
|
|||||||
|
# RUNBOOK — atheos-blog-lxc
|
||||||
|
|
||||||
|
Atheos browser editor for `armistace/blog`, running in an Alpine LXC on Proxmox.
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Guest** | LXC **136** on node `scotty` (192.168.178.69) |
|
||||||
|
| **IP** | **http://192.168.178.57/** |
|
||||||
|
| **OS** | Alpine 3.23.2 · unprivileged · 2 vCPU · 1 GB RAM · 20 GB root · `onboot=1` |
|
||||||
|
| **App** | Atheos IDE, pinned to commit `f90cd1806dbe0e25673a77e27390e8b1a240c3f6` (build 611) |
|
||||||
|
| **Repo** | `git.aridgwayweb.com/hermes/atheos-blog-lxc` (this provisioning repo) |
|
||||||
|
| **Workspace** | `/var/www/atheos/workspace/blog` → clone of `armistace/blog` |
|
||||||
|
| **Access** | LAN only. No NPM proxy host, no DNS record, no port-forward. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ STEP 1 — DO THIS FIRST: set your git identity
|
||||||
|
|
||||||
|
**The agent deliberately did not configure a git identity** (design decision D1, requirement FR-11).
|
||||||
|
Git will **refuse to commit** until you set one. This is your first action after SSH-ing in.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ssh root@192.168.178.57
|
||||||
|
|
||||||
|
# become the user that runs PHP and owns the workspace
|
||||||
|
su -s /bin/sh atheos
|
||||||
|
|
||||||
|
# set YOUR identity (these values are placeholders - use your real ones)
|
||||||
|
git config --global user.name "Andrew Ridgway"
|
||||||
|
git config --global user.email "ar17787@gmail.com"
|
||||||
|
|
||||||
|
# verify
|
||||||
|
git config --global --get user.name
|
||||||
|
git config --global --get user.email
|
||||||
|
```
|
||||||
|
|
||||||
|
> Why you and not the agent: you own the `armistace` identity. Configuring it under your name
|
||||||
|
> programmatically would impersonate you and blur commit attribution.
|
||||||
|
|
||||||
|
The same identity must be set for the deploy key's user, which is the **same account** (`atheos`),
|
||||||
|
so the single `su` above covers both the editor's commits and anything you do by hand.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STEP 2 — install the deploy key (Phase 2 prerequisite)
|
||||||
|
|
||||||
|
The guest already has a per-repo deploy keypair. Only the **public** half is shared:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# print it again if you need it
|
||||||
|
cat /home/atheos/.ssh/id_ed25519_blog.pub
|
||||||
|
```
|
||||||
|
|
||||||
|
Add it in Gitea:
|
||||||
|
|
||||||
|
1. Open `armistace/blog` → **Settings** → **Deploy Keys** → **Add Deploy Key**
|
||||||
|
2. Title: `atheos-blog-lxc`
|
||||||
|
3. Paste the public key
|
||||||
|
4. ✅ **TICK "Allow write access"** — required, otherwise push is rejected
|
||||||
|
|
||||||
|
Public key (as generated 2026-09-29):
|
||||||
|
|
||||||
|
```
|
||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINj03Tep9pULTvVDCC3q1ZtocKTZSmDhPsB61Zic1SPk atheos-blog-lxc deploy key (armistace/blog)
|
||||||
|
```
|
||||||
|
|
||||||
|
Fingerprint to confirm you pasted the right one:
|
||||||
|
|
||||||
|
```
|
||||||
|
SHA256:qHvsjjSX79aPZRoNTRI4j+HH3hArAuINIRu9ewxx7t8 (ED25519)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STEP 3 — switch the remote to SSH (Phase 2)
|
||||||
|
|
||||||
|
Once the key is installed, tell the agent (or run it yourself):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
su -s /bin/sh atheos -c \
|
||||||
|
'git -C /var/www/atheos/workspace/blog remote set-url origin gitea-blog:armistace/blog.git'
|
||||||
|
|
||||||
|
# verify the transport actually authenticates
|
||||||
|
su -s /bin/sh atheos -c 'ssh -T gitea-blog' # expect a Gitea greeting, not "Permission denied"
|
||||||
|
su -s /bin/sh atheos -c 'git -C /var/www/atheos/workspace/blog fetch --dry-run'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why the remote uses `gitea-blog` and not a hostname
|
||||||
|
|
||||||
|
`gitea-blog` is an SSH config alias defined in `/home/atheos/.ssh/config`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Host gitea-blog
|
||||||
|
HostName 192.168.178.155 # LAN address
|
||||||
|
User gitea # <-- NOT `git`! see the trap below
|
||||||
|
IdentityFile ~/.ssh/id_ed25519_blog
|
||||||
|
IdentitiesOnly yes
|
||||||
|
Port 22
|
||||||
|
```
|
||||||
|
|
||||||
|
> ### ⚠️ TRAP: the SSH user is `gitea`, not `git`
|
||||||
|
> Every GitHub/Gitea tutorial uses `git@host`. **This instance does not.** Its `app.ini` sets
|
||||||
|
> `SSH_USER = gitea`.
|
||||||
|
>
|
||||||
|
> If you use `User git`, sshd replies **`Invalid user git`** and rejects the connection
|
||||||
|
> **before it ever reads `authorized_keys`** — so the error looks exactly like a rejected key
|
||||||
|
> and sends you hunting for a problem that isn't there. Confirmed in the sshd log on LXC 109:
|
||||||
|
> ```
|
||||||
|
> Invalid user git from 192.168.178.57 port 39780
|
||||||
|
> Connection closed by invalid user git 192.168.178.57 [preauth]
|
||||||
|
> ```
|
||||||
|
> Correct user → `Hi there! You've successfully authenticated with the deploy key named atheos-blog-lxc`.
|
||||||
|
|
||||||
|
Two reasons for the alias:
|
||||||
|
|
||||||
|
1. **`IdentitiesOnly yes`** means no other key is ever offered — a stray `git push` cannot
|
||||||
|
authenticate with a different (possibly broader) identity.
|
||||||
|
2. **It's the LAN address because that's the only path that works.** Verified 2026-09-29:
|
||||||
|
|
||||||
|
| Endpoint | Result |
|
||||||
|
|---|---|
|
||||||
|
| `git.aridgwayweb.com:22` | **unreachable** — the public path does not forward :22 (only :443 via NPM) |
|
||||||
|
| `192.168.178.155:22` | **OPEN** — Gitea's `app.ini` sets `SSH_DOMAIN = 192.168.178.155`, `SSH_PORT = 22`, `START_SSH_SERVER = false` |
|
||||||
|
|
||||||
|
So Gitea serves SSH from the LXC's own `sshd` and advertises the LAN address. Since this guest is
|
||||||
|
LAN-only, LAN transport is also the correct choice — no internet round-trip.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STEP 4 — complete the Atheos installer
|
||||||
|
|
||||||
|
Open **http://192.168.178.57/** in a browser. The Atheos first-run installer appears and creates
|
||||||
|
the **admin user** — this is yours to own; the agent seeded no accounts.
|
||||||
|
|
||||||
|
Verify afterwards:
|
||||||
|
|
||||||
|
- you can open the **Workspace** and see a `blog` project
|
||||||
|
- opening `src/content/*.md` shows your posts
|
||||||
|
- saving persists (the files are at `/var/www/atheos/workspace/blog`)
|
||||||
|
|
||||||
|
### Atheos git settings
|
||||||
|
|
||||||
|
Open Atheos → (user menu) → **Settings** to confirm the git integration. The `codegit` component
|
||||||
|
handles commit/push. If you see *"detected dubious ownership"*, the `safe.directory` entries are
|
||||||
|
missing — restore them with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
su -s /bin/sh atheos -c 'git config --global --add safe.directory /var/www/atheos/workspace'
|
||||||
|
su -s /bin/sh atheos -c 'git config --global --add safe.directory /var/www/atheos/workspace/blog'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Day-to-day operations
|
||||||
|
|
||||||
|
### Edit and commit
|
||||||
|
|
||||||
|
Do everything in the browser at `http://192.168.178.57/`. Commit via Atheos's git panel, or by hand:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ssh root@192.168.178.57
|
||||||
|
su -s /bin/sh atheos
|
||||||
|
cd /var/www/atheos/workspace/blog
|
||||||
|
git status
|
||||||
|
git add -A && git commit -m "post: <what changed>"
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
### The build is NOT here
|
||||||
|
|
||||||
|
This box **only writes markdown**. Building/publishing the Pelican site happens in the existing
|
||||||
|
pipeline elsewhere. There is deliberately **no Python, no pelican, no `make`** on this guest.
|
||||||
|
|
||||||
|
### Services (OpenRC — not systemd)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rc-service nginx status
|
||||||
|
rc-service php-fpm83 status
|
||||||
|
rc-update show | grep -E 'nginx|php-fpm83' # both must be in 'default'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
```sh
|
||||||
|
tail -f /var/log/nginx/atheos.error.log # PHP fatals surface here
|
||||||
|
tail -f /var/log/nginx/atheos.access.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reboot / recovery
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pct reboot 136 # from scotty, or
|
||||||
|
pct stop 136 && pct start 136
|
||||||
|
```
|
||||||
|
|
||||||
|
Services are enabled at the OpenRC `default` runlevel, so they return automatically
|
||||||
|
(verified — see `aidlc-docs` acceptance criteria AC-6).
|
||||||
|
|
||||||
|
### Rebuild from scratch
|
||||||
|
|
||||||
|
The whole guest is reproducible:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# on scotty (192.168.178.69), as root
|
||||||
|
pct destroy 136
|
||||||
|
sh scripts/provision.sh # from this repo, on the node
|
||||||
|
```
|
||||||
|
|
||||||
|
Nothing unique lives on the guest: the blog's source of truth is Gitea. Only **uncommitted edits**
|
||||||
|
would be lost — commit often.
|
||||||
|
|
||||||
|
### Update Atheos (and re-pin)
|
||||||
|
|
||||||
|
Atheos has no releases, so this install is pinned to a commit. To move to the latest upstream commit,
|
||||||
|
install it, and re-pin:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh scripts/update-atheos.sh # on the guest; preserves workspace/, backs up, verifies, re-pins
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security model (summary — full detail in `docs/security-notes.md`)
|
||||||
|
|
||||||
|
- **Git auth = a per-repository deploy key**, scoped to `armistace/blog`, write-enabled.
|
||||||
|
Not a PAT (whose `write:repository` scope reaches **every** repo the account can write).
|
||||||
|
- Private key: `/home/atheos/.ssh/id_ed25519_blog`, mode `600`, owned by the php-fpm user.
|
||||||
|
- The web app and the key share one unprivileged user, so a compromised Atheos **can** write `blog`.
|
||||||
|
That is the accepted floor — one repo, and (with branch protection) one branch.
|
||||||
|
- **LAN-only.** If this is ever exposed: keep branch protection on `blog`'s `master`, terminate TLS
|
||||||
|
at NPM, and put authentication *in front* of Atheos. See `docs/security-notes.md` §2 (S-1…S-14).
|
||||||
|
|
||||||
|
### Container resource reporting (a real trap — cost debugging time)
|
||||||
|
|
||||||
|
`uptime` and `free` on Alpine are **busybox** applets. Busybox reads via the **`sysinfo()` syscall**,
|
||||||
|
which lxcfs **cannot intercept** — so inside this container they report the **PVE host**, not the
|
||||||
|
container. A container that restarted seconds ago will cheerfully claim *"up 2 days"*.
|
||||||
|
|
||||||
|
lxcfs **is** working correctly here — the virtualised files are right:
|
||||||
|
|
||||||
|
```
|
||||||
|
/proc/meminfo → MemTotal: 1048576 kB ← our 1024 MB limit
|
||||||
|
/proc/uptime → 105 s ← container
|
||||||
|
/proc/cpuinfo → 2 processors ← container
|
||||||
|
```
|
||||||
|
|
||||||
|
The container is correct; **the tool is wrong**. Fix = `apk add procps-ng`, whose `/usr/bin/free`
|
||||||
|
and `/usr/bin/uptime` read those virtualised `/proc` files:
|
||||||
|
|
||||||
|
| Tool | Reports | |
|
||||||
|
|---|---|---|
|
||||||
|
| `procps-ng` `free` | `1024 MB` | ✅ correct |
|
||||||
|
| busybox `free` | `15701 MB` | ❌ host |
|
||||||
|
| `procps-ng` `uptime` | `up 2 min` | ✅ correct |
|
||||||
|
| busybox `uptime` | `up 2 days` | ❌ host |
|
||||||
|
|
||||||
|
> **Note:** `apk add lxcfs` inside the guest is **not** a fix — lxcfs is a host-provided FUSE
|
||||||
|
> filesystem, not a guest package. `provision.sh` installs `procps-ng` for this reason.
|
||||||
|
> (`df` is unaffected: it stats the filesystem directly, so it was always accurate.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reaching the guest when SSH from your laptop fails
|
||||||
|
|
||||||
|
The guest has **root SSH enabled** with your fleet keys installed:
|
||||||
|
|
||||||
|
- `ssh-rsa ... aridgway@strategenics-25`
|
||||||
|
- `ssh-ed25519 ... oct2025`
|
||||||
|
|
||||||
|
If those keys aren't what you're using, add/push a key via the PVE node (which always works):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ssh root@192.168.178.69 "pct exec 136 -- sh -c 'echo \"<your-pubkey>\" >> /root/.ssh/authorized_keys'"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Fix |
|
||||||
|
|---|---|---|
|
||||||
|
| **502 Bad Gateway** | nginx `fastcgi_pass` and the FPM pool `listen` disagree | `grep -E '^listen' /etc/php83/php-fpm.d/www.conf` and compare with the `fastcgi_pass` line in `/etc/nginx/http.d/atheos.conf`. Both must read `/run/php-fpm83/php-fpm.sock`. Then restart both services. |
|
||||||
|
| **"detected dubious ownership"** | `safe.directory` missing or the repo is owned by a different user | Re-add the two `safe.directory` entries (STEP 4) |
|
||||||
|
| **git commit refuses: "please tell me who you are"** | identity not set — **expected on a fresh build** | STEP 1 |
|
||||||
|
| **git push: Permission denied (publickey)** | deploy key not installed, **or the SSH user is wrong**, or "Allow write access" not ticked | Check all three. Especially: `grep User /home/atheos/.ssh/config` must say **`gitea`**, not `git` (see STEP 3 trap). On the Gitea host, confirm with `journalctl -u ssh \| tail` — `Invalid user git` means the username is wrong, not the key. |
|
||||||
|
| **push times out** | remote still pointing at `git.aridgwayweb.com` (:22 not forwarded) | STEP 3 — use the `gitea-blog` alias |
|
||||||
|
| **Atheos shows a blank page / 500** | PHP fatal | `tail -50 /var/log/nginx/atheos.error.log` |
|
||||||
|
| **Atheos installer says a path isn't writable** | ownership broken | `chown -R atheos:atheos /var/www/atheos` |
|
||||||
|
| **`free` / `uptime` show the PVE host's memory or days of uptime** | on Alpine these are **busybox** applets using the `sysinfo()` **syscall**, which lxcfs cannot intercept | `apk add procps-ng` — its `/usr/bin/free` and `/usr/bin/uptime` read the lxcfs-virtualised `/proc` files. Already installed by `provision.sh`; this bites if you rebuild by hand. |
|
||||||
|
| **PHP session errors** | rootfs full | `df -h /` (the rootfs is 20 GB and the app is tiny, so this indicates something else) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Change log
|
||||||
|
|
||||||
|
| Date | Change |
|
||||||
|
|---|---|
|
||||||
|
| 2026-09-29 | Initial build: LXC 136 on scotty, Alpine 3.23.2, nginx + php83-fpm (socket), Atheos pinned at `f90cd18` (build 611), `armistace/blog` cloned over HTTPS, per-repo deploy key generated. LAN-only. |
|
||||||
@@ -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@<pve-node>` + `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)
|
||||||
@@ -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
|
||||||
@@ -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://<ip>/ ────┼──► 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:<FPM_SOCKET>` 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)
|
||||||
@@ -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 |
|
||||||
@@ -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:<FPM_SOCKET>; ... }
|
||||||
|
location ~ \.php$ { fastcgi_pass unix:<FPM_SOCKET>; ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Contract**: `<FPM_SOCKET>` 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.
|
||||||
@@ -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.
|
||||||
@@ -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 | `~<fpmuser>/.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 |
|
||||||
@@ -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://<ip>/` 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.
|
||||||
@@ -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://<lxc-ip>/
|
||||||
|
▼
|
||||||
|
nginx (C2, :80)
|
||||||
|
│ location ~ \.php$ → fastcgi_pass unix:<FPM_SOCKET> [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.
|
||||||
@@ -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 |
|
||||||
@@ -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://<ip>/` returns the Atheos installer (HTTP 200, no PHP fatal in the error log); a write test inside `workspace/` succeeds as the FPM user |
|
||||||
|
| **ACs** | AC-2, AC-3 |
|
||||||
|
| **Notes** | **The genuinely uncertain unit** (risk C-1). If Atheos fights Alpine's PHP layout, stop and report rather than papering over it |
|
||||||
|
|
||||||
|
## U4 — Git + clone + deploy key
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Goal** | The operator can open `blog`, edit it, and push from here once they add the key |
|
||||||
|
| **Components** | C6, C7 |
|
||||||
|
| **Produces** | `git` installed; `safe.directory` set for the workspace; clone of `armistace/blog` (branch `master`) at `workspace/blog`; **ed25519 deploy keypair generated on the guest** (private key owned by the FPM user, mode 600); `~/.ssh/config` host alias `gitea-blog` with `IdentitiesOnly yes`; `user.name`/`user.email` **deliberately left unset** |
|
||||||
|
| **Preconditions** | U1 (git), U3 (workspace exists) |
|
||||||
|
| **Verification** | `blog` appears as a project and its files open; `git -C workspace/blog remote -v` shows Gitea; the **public** key is emitted for the operator; `git config --get user.name` returns **empty** (proving D1) |
|
||||||
|
| **ACs** | AC-3, AC-4, AC-5 |
|
||||||
|
| **Notes** | Two phases by necessity: this unit leaves the remote on **HTTPS**; the SSH switch happens *after* the operator installs the deploy key (D6). The agent must not install it |
|
||||||
|
|
||||||
|
## U5 — Docs, scripts, handoff
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Goal** | A different agent or a rebuild needs no prior context |
|
||||||
|
| **Components** | all |
|
||||||
|
| **Produces** | `README.md`, `RUNBOOK.md` (incl. **"set your git identity first"** prompt, the deploy-key install steps, and the exact `git remote set-url` command), `docs/security-notes.md`, `docs/resiliency-notes.md`, `scripts/provision.sh`, `scripts/update-atheos.sh` |
|
||||||
|
| **Preconditions** | U1–U4 |
|
||||||
|
| **Verification** | README + RUNBOOK exist and are linked; `provision.sh` is the exact sequence that was actually run; `update-atheos.sh` re-clones Atheos at latest and **re-pins the SHA** (D2) |
|
||||||
|
| **ACs** | AC-7 |
|
||||||
|
| **Notes** | The RUNBOOK is the operator-facing artifact: it must open with their required first action |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deferred / not a unit
|
||||||
|
|
||||||
|
- **The SSH remote switch** — operator-owned (D6). Documented in RUNBOOK, not built.
|
||||||
|
- **Atheos admin user creation** — operator completes the installer (D5).
|
||||||
|
- **Pelican build/publish** — out of scope entirely (Q1=A, AC-8).
|
||||||
|
|
||||||
|
## Greenfield code-organization strategy
|
||||||
|
|
||||||
|
No application code is generated, so the repo holds **only** provisioning + docs:
|
||||||
|
|
||||||
|
```
|
||||||
|
atheos-blog-lxc/
|
||||||
|
├── README.md
|
||||||
|
├── RUNBOOK.md
|
||||||
|
├── docs/{security-notes.md,resiliency-notes.md}
|
||||||
|
├── scripts/{provision.sh,update-atheos.sh}
|
||||||
|
└── aidlc-docs/ ← process trail only; never code
|
||||||
|
```
|
||||||
@@ -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 <atheos@atheos.local>`, 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 <ar17787@gmail.com>`) 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://<ip>/`), 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).
|
||||||
@@ -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<br/><b>COMPLETED</b>"]
|
||||||
|
RE["Reverse Engineering<br/><b>SKIPPED</b>"]
|
||||||
|
RA["Requirements Analysis<br/><b>COMPLETED</b>"]
|
||||||
|
US["User Stories<br/><b>SKIPPED</b>"]
|
||||||
|
WP["Workflow Planning<br/><b>IN PROGRESS</b>"]
|
||||||
|
AD["Application Design<br/><b>EXECUTE</b>"]
|
||||||
|
UG["Units Generation<br/><b>EXECUTE</b>"]
|
||||||
|
end
|
||||||
|
subgraph CONSTRUCTION["🟢 CONSTRUCTION PHASE"]
|
||||||
|
FD["Functional Design<br/><b>SKIP</b>"]
|
||||||
|
NFRA["NFR Requirements<br/><b>SKIP</b>"]
|
||||||
|
NFRD["NFR Design<br/><b>SKIP</b>"]
|
||||||
|
ID["Infrastructure Design<br/><b>EXECUTE</b>"]
|
||||||
|
CG["Code Generation<br/><b>EXECUTE</b>"]
|
||||||
|
BT["Build and Test<br/><b>EXECUTE</b>"]
|
||||||
|
end
|
||||||
|
subgraph OPERATIONS["🟡 OPERATIONS PHASE"]
|
||||||
|
OPS["Operations<br/><b>PLACEHOLDER</b>"]
|
||||||
|
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
|
||||||
@@ -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)*
|
||||||
@@ -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://<lxc-ip>/`. 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.
|
||||||
@@ -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://<lxc-ip>/` 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.
|
||||||
@@ -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 |
|
||||||
@@ -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 ~/<fpmuser>/.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 | `~/<fpmuser>/.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 |
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
#!/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 '<title>Atheos IDE</title>' /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")"}')"
|
||||||
|
# NOTE: on Alpine, `free`/`uptime` are BUSYBOX applets that use the sysinfo() syscall, which
|
||||||
|
# lxcfs cannot intercept - so they report the PVE HOST, not this container. procps-ng (installed
|
||||||
|
# by provision.sh) replaces them with binaries that read the lxcfs-virtualised /proc files.
|
||||||
|
# Detect via apk, which is the reliable signal (the binaries are plain files either way).
|
||||||
|
if apk info -e procps-ng >/dev/null 2>&1; then
|
||||||
|
echo " memory: $(/usr/bin/free -m 2>/dev/null | awk 'NR==2{print $3"MB used of "$2"MB"}')"
|
||||||
|
echo " uptime: $(/usr/bin/uptime 2>/dev/null | sed 's/.*up /up /;s/,.*//') (container-accurate)"
|
||||||
|
else
|
||||||
|
echo " memory: (procps-ng NOT installed - busybox free/uptime would report the HOST)"
|
||||||
|
fi
|
||||||
|
echo " lxcfs virtualises /proc: meminfo=$(grep MemTotal /proc/meminfo | awk '{print $2"kB"}') uptime=$(cut -d. -f1 /proc/uptime)s"
|
||||||
|
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
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
#!/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 procps-ng >/dev/null 2>&1
|
||||||
|
# procps-ng (NOT lxcfs) is what makes free/uptime report the CONTAINER rather than the host.
|
||||||
|
# busybox applets use the sysinfo() syscall, which lxcfs cannot intercept; procps-ng reads the
|
||||||
|
# lxcfs-virtualised /proc/meminfo and /proc/uptime. Verified: procps free -> 1024MB, busybox -> host.
|
||||||
|
# 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
|
||||||
|
'
|
||||||
|
|
||||||
|
# --- nginx vhost: installed BY THE SCRIPT so provisioning is repeatable end-to-end -----------
|
||||||
|
# (review finding: leaving this as a manual "see README" step broke the project's own
|
||||||
|
# repeatability requirement FR-9 — a rebuild from scratch would serve the distro default page)
|
||||||
|
pct exec "$VMID" -- sh -c "cat > /etc/nginx/http.d/atheos.conf" <<'NGINXCONF'
|
||||||
|
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;
|
||||||
|
|
||||||
|
# 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).
|
||||||
|
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; }
|
||||||
|
|
||||||
|
# never serve the VCS dir, the app's private data, or dotfiles
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
NGINXCONF
|
||||||
|
# remove the distro default vhost so ours is authoritative, then start the stack
|
||||||
|
pct exec "$VMID" -- sh -c '
|
||||||
|
rm -f /etc/nginx/http.d/default.conf
|
||||||
|
rc-service php-fpm83 restart >/dev/null 2>&1
|
||||||
|
rc-service nginx restart >/dev/null 2>&1
|
||||||
|
sleep 2
|
||||||
|
# prove the socket contract holds BEFORE Atheos is installed, so a 502 can only ever
|
||||||
|
# mean an nginx/php-fpm problem and never be confused with an Atheos problem
|
||||||
|
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)
|
||||||
|
if [ "$SOCK" = "$NGX" ]; then echo "socket contract OK: $SOCK"; else echo "SOCKET MISMATCH: pool=$SOCK nginx=$NGX"; exit 1; fi
|
||||||
|
'
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
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://<guest-ip>/ 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."
|
||||||
@@ -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."
|
||||||
Reference in New Issue
Block a user