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:
2026-09-29 15:50:34 +10:00
parent 27272e6f6f
commit a6fb61d0ea
26 changed files with 2420 additions and 1 deletions
+64
View File
@@ -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)
+132
View File
@@ -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.
+33
View File
@@ -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.
+62
View File
@@ -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).
+149
View File
@@ -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
+32
View File
@@ -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.
+129
View File
@@ -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.