Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c57aff3c33 | ||
|
|
1ff8aaf2f1 | ||
|
|
2d57173e31 | ||
|
|
c65900638e | ||
|
|
c4a4de1da7 | ||
|
|
0bdd15b31b | ||
|
|
976b3f0005 | ||
|
|
8ea645386f | ||
|
|
5ff6a06e82 | ||
|
|
47b1860623 | ||
|
|
0b72cf48b1 | ||
|
|
1c7db911df | ||
|
|
e038f758ac | ||
|
|
a6fb61d0ea |
@@ -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,127 @@
|
|||||||
# 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
|
||||||
|
│ └── writer-branch-workflow.md how writing/publishing works from the editor
|
||||||
|
├── files/
|
||||||
|
│ ├── atheos.conf nginx vhost (install to /etc/nginx/http.d/atheos.conf)
|
||||||
|
│ ├── ssh-config ~atheos/.ssh/config (the gitea-blog alias)
|
||||||
|
│ └── macros.db.php installed copy of the Atheos writer macros
|
||||||
|
├── 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
|
||||||
|
|
||||||
|
- **Writing happens on a branch, never on `master`.** The editor commits to a long-lived
|
||||||
|
`atheos-writer` branch and you merge via PR in Gitea — see
|
||||||
|
[`docs/writer-branch-workflow.md`](docs/writer-branch-workflow.md).
|
||||||
|
⚠️ This only holds if **branch protection is enabled on `armistace/blog`'s `master`** — without it
|
||||||
|
the deploy key could force-push master directly. Confirm with the `branch_protections` API call in
|
||||||
|
that document.
|
||||||
|
- 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` |
|
||||||
|
|||||||
+321
@@ -0,0 +1,321 @@
|
|||||||
|
# RUNBOOK — atheos-blog-lxc
|
||||||
|
|
||||||
|
Atheos browser editor for `armistace/blog`, running in an Alpine LXC on Proxmox.
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Guest** | LXC **136** on node `scotty` (192.168.178.69) |
|
||||||
|
| **IP** | **http://192.168.178.57/** |
|
||||||
|
| **OS** | Alpine 3.23.2 · unprivileged · 2 vCPU · 1 GB RAM · 20 GB root · `onboot=1` |
|
||||||
|
| **App** | Atheos IDE, pinned to commit `f90cd1806dbe0e25673a77e27390e8b1a240c3f6` (build 611) |
|
||||||
|
| **Repo** | `git.aridgwayweb.com/hermes/atheos-blog-lxc` (this provisioning repo) |
|
||||||
|
| **Workspace** | `/var/www/atheos/workspace/blog` → clone of `armistace/blog` |
|
||||||
|
| **Access** | LAN only. No NPM proxy host, no DNS record, no port-forward. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ STEP 1 — DO THIS FIRST: set your git identity
|
||||||
|
|
||||||
|
**The agent deliberately did not configure a git identity** (design decision D1, requirement FR-11).
|
||||||
|
Git will **refuse to commit** until you set one. This is your first action after SSH-ing in.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ssh root@192.168.178.57
|
||||||
|
|
||||||
|
# become the user that runs PHP and owns the workspace
|
||||||
|
su -s /bin/sh atheos
|
||||||
|
|
||||||
|
# set YOUR identity (these values are placeholders - use your real ones)
|
||||||
|
git config --global user.name "Andrew Ridgway"
|
||||||
|
git config --global user.email "ar17787@gmail.com"
|
||||||
|
|
||||||
|
# verify
|
||||||
|
git config --global --get user.name
|
||||||
|
git config --global --get user.email
|
||||||
|
```
|
||||||
|
|
||||||
|
> Why you and not the agent: you own the `armistace` identity. Configuring it under your name
|
||||||
|
> programmatically would impersonate you and blur commit attribution.
|
||||||
|
|
||||||
|
The same identity must be set for the deploy key's user, which is the **same account** (`atheos`),
|
||||||
|
so the single `su` above covers both the editor's commits and anything you do by hand.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STEP 2 — install the deploy key (Phase 2 prerequisite)
|
||||||
|
|
||||||
|
The guest already has a per-repo deploy keypair. Only the **public** half is shared:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# print it again if you need it
|
||||||
|
cat /home/atheos/.ssh/id_ed25519_blog.pub
|
||||||
|
```
|
||||||
|
|
||||||
|
Add it in Gitea:
|
||||||
|
|
||||||
|
1. Open `armistace/blog` → **Settings** → **Deploy Keys** → **Add Deploy Key**
|
||||||
|
2. Title: `atheos-blog-lxc`
|
||||||
|
3. Paste the public key
|
||||||
|
4. ✅ **TICK "Allow write access"** — required, otherwise push is rejected
|
||||||
|
|
||||||
|
Public key (as generated 2026-09-29):
|
||||||
|
|
||||||
|
```
|
||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINj03Tep9pULTvVDCC3q1ZtocKTZSmDhPsB61Zic1SPk atheos-blog-lxc deploy key (armistace/blog)
|
||||||
|
```
|
||||||
|
|
||||||
|
Fingerprint to confirm you pasted the right one:
|
||||||
|
|
||||||
|
```
|
||||||
|
SHA256:qHvsjjSX79aPZRoNTRI4j+HH3hArAuINIRu9ewxx7t8 (ED25519)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STEP 3 — switch the remote to SSH (Phase 2)
|
||||||
|
|
||||||
|
Once the key is installed, tell the agent (or run it yourself):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
su -s /bin/sh atheos -c \
|
||||||
|
'git -C /var/www/atheos/workspace/blog remote set-url origin gitea-blog:armistace/blog.git'
|
||||||
|
|
||||||
|
# verify the transport actually authenticates
|
||||||
|
su -s /bin/sh atheos -c 'ssh -T gitea-blog' # expect a Gitea greeting, not "Permission denied"
|
||||||
|
su -s /bin/sh atheos -c 'git -C /var/www/atheos/workspace/blog fetch --dry-run'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why the remote uses `gitea-blog` and not a hostname
|
||||||
|
|
||||||
|
`gitea-blog` is an SSH config alias defined in `/home/atheos/.ssh/config`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Host gitea-blog
|
||||||
|
HostName 192.168.178.155 # LAN address
|
||||||
|
User gitea # <-- NOT `git`! see the trap below
|
||||||
|
IdentityFile ~/.ssh/id_ed25519_blog
|
||||||
|
IdentitiesOnly yes
|
||||||
|
Port 22
|
||||||
|
```
|
||||||
|
|
||||||
|
> ### ⚠️ TRAP: the SSH user is `gitea`, not `git`
|
||||||
|
> Every GitHub/Gitea tutorial uses `git@host`. **This instance does not.** Its `app.ini` sets
|
||||||
|
> `SSH_USER = gitea`.
|
||||||
|
>
|
||||||
|
> If you use `User git`, sshd replies **`Invalid user git`** and rejects the connection
|
||||||
|
> **before it ever reads `authorized_keys`** — so the error looks exactly like a rejected key
|
||||||
|
> and sends you hunting for a problem that isn't there. Confirmed in the sshd log on LXC 109:
|
||||||
|
> ```
|
||||||
|
> Invalid user git from 192.168.178.57 port 39780
|
||||||
|
> Connection closed by invalid user git 192.168.178.57 [preauth]
|
||||||
|
> ```
|
||||||
|
> Correct user → `Hi there! You've successfully authenticated with the deploy key named atheos-blog-lxc`.
|
||||||
|
|
||||||
|
Two reasons for the alias:
|
||||||
|
|
||||||
|
1. **`IdentitiesOnly yes`** means no other key is ever offered — a stray `git push` cannot
|
||||||
|
authenticate with a different (possibly broader) identity.
|
||||||
|
2. **It's the LAN address because that's the only path that works.** Verified 2026-09-29:
|
||||||
|
|
||||||
|
| Endpoint | Result |
|
||||||
|
|---|---|
|
||||||
|
| `git.aridgwayweb.com:22` | **unreachable** — the public path does not forward :22 (only :443 via NPM) |
|
||||||
|
| `192.168.178.155:22` | **OPEN** — Gitea's `app.ini` sets `SSH_DOMAIN = 192.168.178.155`, `SSH_PORT = 22`, `START_SSH_SERVER = false` |
|
||||||
|
|
||||||
|
So Gitea serves SSH from the LXC's own `sshd` and advertises the LAN address. Since this guest is
|
||||||
|
LAN-only, LAN transport is also the correct choice — no internet round-trip.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STEP 4 — complete the Atheos installer
|
||||||
|
|
||||||
|
Open **http://192.168.178.57/** in a browser. The Atheos first-run installer appears and creates
|
||||||
|
the **admin user** — this is yours to own; the agent seeded no accounts.
|
||||||
|
|
||||||
|
Verify afterwards:
|
||||||
|
|
||||||
|
- you can open the **Workspace** and see a `blog` project
|
||||||
|
- opening `src/content/*.md` shows your posts
|
||||||
|
- saving persists (the files are at `/var/www/atheos/workspace/blog`)
|
||||||
|
|
||||||
|
### Atheos git settings
|
||||||
|
|
||||||
|
Open Atheos → (user menu) → **Settings** to confirm the git integration. The `codegit` component
|
||||||
|
handles commit/push. If you see *"detected dubious ownership"*, the `safe.directory` entries are
|
||||||
|
missing — restore them with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
su -s /bin/sh atheos -c 'git config --global --add safe.directory /var/www/atheos/workspace'
|
||||||
|
su -s /bin/sh atheos -c 'git config --global --add safe.directory /var/www/atheos/workspace/blog'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Day-to-day operations
|
||||||
|
|
||||||
|
### Edit and commit
|
||||||
|
|
||||||
|
Do everything in the browser at `http://192.168.178.57/`. Commit via Atheos's git panel, or by hand:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ssh root@192.168.178.57
|
||||||
|
su -s /bin/sh atheos
|
||||||
|
cd /var/www/atheos/workspace/blog
|
||||||
|
git status
|
||||||
|
git add -A && git commit -m "post: <what changed>"
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
### The build is NOT here
|
||||||
|
|
||||||
|
This box **only writes markdown**. Building/publishing the Pelican site happens in the existing
|
||||||
|
pipeline elsewhere. There is deliberately **no Python, no pelican, no `make`** on this guest.
|
||||||
|
|
||||||
|
### Services (OpenRC — not systemd)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rc-service nginx status
|
||||||
|
rc-service php-fpm83 status
|
||||||
|
rc-update show | grep -E 'nginx|php-fpm83' # both must be in 'default'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
```sh
|
||||||
|
tail -f /var/log/nginx/atheos.error.log # PHP fatals surface here
|
||||||
|
tail -f /var/log/nginx/atheos.access.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reboot / recovery
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pct reboot 136 # from scotty, or
|
||||||
|
pct stop 136 && pct start 136
|
||||||
|
```
|
||||||
|
|
||||||
|
Services are enabled at the OpenRC `default` runlevel, so they return automatically
|
||||||
|
(verified — see `aidlc-docs` acceptance criteria AC-6).
|
||||||
|
|
||||||
|
### Rebuild from scratch
|
||||||
|
|
||||||
|
The whole guest is reproducible:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# on scotty (192.168.178.69), as root
|
||||||
|
pct destroy 136
|
||||||
|
sh scripts/provision.sh # from this repo, on the node
|
||||||
|
```
|
||||||
|
|
||||||
|
Nothing unique lives on the guest: the blog's source of truth is Gitea. Only **uncommitted edits**
|
||||||
|
would be lost — commit often.
|
||||||
|
|
||||||
|
### Update Atheos (and re-pin)
|
||||||
|
|
||||||
|
Atheos has no releases, so this install is pinned to a commit. To move to the latest upstream commit,
|
||||||
|
install it, and re-pin:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh scripts/update-atheos.sh # on the guest; preserves workspace/, backs up, verifies, re-pins
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security model (summary — full detail in `docs/security-notes.md`)
|
||||||
|
|
||||||
|
- **Git auth = a per-repository deploy key**, scoped to `armistace/blog`, write-enabled.
|
||||||
|
Not a PAT (whose `write:repository` scope reaches **every** repo the account can write).
|
||||||
|
- Private key: `/home/atheos/.ssh/id_ed25519_blog`, mode `600`, owned by the php-fpm user.
|
||||||
|
- The web app and the key share one unprivileged user, so a compromised Atheos **can** write `blog`.
|
||||||
|
That is the accepted floor — one repo, and (with branch protection) one branch.
|
||||||
|
- **LAN-only.** If this is ever exposed: keep branch protection on `blog`'s `master`, terminate TLS
|
||||||
|
at NPM, and put authentication *in front* of Atheos. See `docs/security-notes.md` §2 (S-1…S-14).
|
||||||
|
|
||||||
|
### Container resource reporting (a real trap — cost debugging time)
|
||||||
|
|
||||||
|
`uptime` and `free` on Alpine are **busybox** applets. Busybox reads via the **`sysinfo()` syscall**,
|
||||||
|
which lxcfs **cannot intercept** — so inside this container they report the **PVE host**, not the
|
||||||
|
container. A container that restarted seconds ago will cheerfully claim *"up 2 days"*.
|
||||||
|
|
||||||
|
lxcfs **is** working correctly here — the virtualised files are right:
|
||||||
|
|
||||||
|
```
|
||||||
|
/proc/meminfo → MemTotal: 1048576 kB ← our 1024 MB limit
|
||||||
|
/proc/uptime → 105 s ← container
|
||||||
|
/proc/cpuinfo → 2 processors ← container
|
||||||
|
```
|
||||||
|
|
||||||
|
The container is correct; **the tool is wrong**. Fix = `apk add procps-ng`, whose `/usr/bin/free`
|
||||||
|
and `/usr/bin/uptime` read those virtualised `/proc` files:
|
||||||
|
|
||||||
|
| Tool | Reports | |
|
||||||
|
|---|---|---|
|
||||||
|
| `procps-ng` `free` | `1024 MB` | ✅ correct |
|
||||||
|
| busybox `free` | `15701 MB` | ❌ host |
|
||||||
|
| `procps-ng` `uptime` | `up 2 min` | ✅ correct |
|
||||||
|
| busybox `uptime` | `up 2 days` | ❌ host |
|
||||||
|
|
||||||
|
> **Note:** `apk add lxcfs` inside the guest is **not** a fix — lxcfs is a host-provided FUSE
|
||||||
|
> filesystem, not a guest package. `provision.sh` installs `procps-ng` for this reason.
|
||||||
|
> (`df` is unaffected: it stats the filesystem directly, so it was always accurate.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reaching the guest when SSH from your laptop fails
|
||||||
|
|
||||||
|
The guest has **root SSH enabled** with your fleet keys installed:
|
||||||
|
|
||||||
|
- `ssh-rsa ... aridgway@strategenics-25`
|
||||||
|
- `ssh-ed25519 ... oct2025`
|
||||||
|
|
||||||
|
If those keys aren't what you're using, add/push a key via the PVE node (which always works):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ssh root@192.168.178.69 "pct exec 136 -- sh -c 'echo \"<your-pubkey>\" >> /root/.ssh/authorized_keys'"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Fix |
|
||||||
|
|---|---|---|
|
||||||
|
| **502 Bad Gateway** | nginx `fastcgi_pass` and the FPM pool `listen` disagree | `grep -E '^listen' /etc/php83/php-fpm.d/www.conf` and compare with the `fastcgi_pass` line in `/etc/nginx/http.d/atheos.conf`. Both must read `/run/php-fpm83/php-fpm.sock`. Then restart both services. |
|
||||||
|
| **"detected dubious ownership"** | `safe.directory` missing or the repo is owned by a different user | Re-add the two `safe.directory` entries (STEP 4) |
|
||||||
|
| **git commit refuses: "please tell me who you are"** | identity not set — **expected on a fresh build** | STEP 1 |
|
||||||
|
| **git push: Permission denied (publickey)** | deploy key not installed, **or the SSH user is wrong**, or "Allow write access" not ticked | Check all three. Especially: `grep User /home/atheos/.ssh/config` must say **`gitea`**, not `git` (see STEP 3 trap). On the Gitea host, confirm with `journalctl -u ssh \| tail` — `Invalid user git` means the username is wrong, not the key. |
|
||||||
|
| **push times out** | remote still pointing at `git.aridgwayweb.com` (:22 not forwarded) | STEP 3 — use the `gitea-blog` alias |
|
||||||
|
| **Atheos shows a blank page / 500** | PHP fatal | `tail -50 /var/log/nginx/atheos.error.log` |
|
||||||
|
| **Atheos installer says a path isn't writable** | ownership broken | `chown -R atheos:atheos /var/www/atheos` |
|
||||||
|
| **`free` / `uptime` show the PVE host's memory or days of uptime** | on Alpine these are **busybox** applets using the `sysinfo()` **syscall**, which lxcfs cannot intercept | `apk add procps-ng` — its `/usr/bin/free` and `/usr/bin/uptime` read the lxcfs-virtualised `/proc` files. Already installed by `provision.sh`; this bites if you rebuild by hand. |
|
||||||
|
| **PHP session errors** | rootfs full | `df -h /` (the rootfs is 20 GB and the app is tiny, so this indicates something else) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Change log
|
||||||
|
|
||||||
|
| Date | Change |
|
||||||
|
|---|---|
|
||||||
|
| 2026-09-29 | Initial build: LXC 136 on scotty, Alpine 3.23.2, nginx + php83-fpm (socket), Atheos pinned at `f90cd18` (build 611), `armistace/blog` cloned over HTTPS, per-repo deploy key generated. LAN-only. |
|
||||||
|
| 2026-09-29 | Added the **writer-branch workflow**: 6 macros installed into Atheos (start/join, commit+push, destructive reset-to-master, status, refresh master, merge master) writing to a long-lived `atheos-writer` branch; merging stays in Gitea. See `docs/writer-branch-workflow.md`. |
|
||||||
|
|
||||||
|
## Writer-branch workflow (quick reference)
|
||||||
|
|
||||||
|
The editor writes to `atheos-writer`; you merge to `master` in Gitea.
|
||||||
|
|
||||||
|
**Macros** (right-click the `blog` folder → Macros):
|
||||||
|
|
||||||
|
| # | Macro | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Start/Join Writer Branch | creates or re-joins `atheos-writer` (idempotent) |
|
||||||
|
| 2 | Commit + Push Writer Branch | the everyday button |
|
||||||
|
| 3 | **DESTRUCTIVE**: Reset Writer Branch to Master | wipes writer's commits, keeps your files |
|
||||||
|
| 4 | Status + Recent Log | read-only |
|
||||||
|
| 5 | Refresh Master (ff-only) | pull master's merges down |
|
||||||
|
| 6 | Merge Master Into Writer Branch | when master has moved ahead |
|
||||||
|
|
||||||
|
⚠️ **Requires branch protection on `armistace/blog`'s `master`** — it is **not currently enabled**,
|
||||||
|
so the deploy key can force-push master directly. See `docs/writer-branch-workflow.md` §Setup.
|
||||||
|
⚠️ **Macro 2 uses `git add -A`** — everything left in the repo gets published to Gitea. Keep scratch
|
||||||
|
files outside `workspace/blog/`.
|
||||||
|
|
||||||
|
Full detail, recovery steps and caveats: [`docs/writer-branch-workflow.md`](docs/writer-branch-workflow.md).
|
||||||
@@ -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,383 @@
|
|||||||
|
# Known Issues & Patches
|
||||||
|
|
||||||
|
Upstream Atheos is an actively-moving project with **no releases**, and its plugin market ships
|
||||||
|
plugins that are **ports from Codiad** (Atheos's predecessor). Two of those plugins use APIs that
|
||||||
|
Atheos has since removed or changed. This file records what we hit, what we changed, and why the
|
||||||
|
fix is done the way it is.
|
||||||
|
|
||||||
|
Pinned Atheos commit: `f90cd1806dbe0e25673a77e27390e8b1a240c3f6` (build 611).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 🔴 **Deleting any file or folder was broken** — `GLOB_BRACE` does not exist on musl
|
||||||
|
|
||||||
|
**Symptom:** removing the Terminal plugin from the market **cycled forever in the UI** (never
|
||||||
|
completed, never errored). Deleting a file or folder in the editor was broken by the same cause.
|
||||||
|
|
||||||
|
**Root cause — a glibc-only constant referenced on musl.** `traits/helpers.php::rDelete()`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$files = glob($target . "{*,.[!.]*,..?*}", GLOB_BRACE|GLOB_MARK);
|
||||||
|
```
|
||||||
|
|
||||||
|
`GLOB_BRACE` is a **glibc extension**. Alpine uses **musl**, where the constant is *undefined* —
|
||||||
|
and touching an undefined constant is a **fatal error** in PHP 8 (a warning in older versions):
|
||||||
|
|
||||||
|
```
|
||||||
|
PHP Fatal error: Uncaught Error: Undefined constant "GLOB_BRACE"
|
||||||
|
in /var/www/atheos/traits/helpers.php:138
|
||||||
|
#0 components/market/class.market.php(178): Common::rDelete()
|
||||||
|
#1 components/market/controller.php(46): Market->remove()
|
||||||
|
```
|
||||||
|
|
||||||
|
Because the fatal kills the request before `Common::send(200)`, the front end never receives a
|
||||||
|
settle response — so the UI **spins indefinitely**. A hang, not an error.
|
||||||
|
|
||||||
|
**Blast radius — measured, two call sites:**
|
||||||
|
|
||||||
|
| Caller | What breaks |
|
||||||
|
|---|---|
|
||||||
|
| `components/market/class.market.php:178` | removing a plugin from the market |
|
||||||
|
| `components/filetree/class.filetree.php:59` | **deleting any file or folder in the editor** |
|
||||||
|
|
||||||
|
The second is core editor functionality, so this is **not** a plugin bug — installing the Terminal
|
||||||
|
plugin is merely what made it visible. Verified directly: with the original code, `rDelete()` fatals
|
||||||
|
and the target is left on disk.
|
||||||
|
|
||||||
|
**Fix applied** (`patches/helpers-rDelete-GLOB_BRACE.diff`) — expand the brace pattern by hand when
|
||||||
|
`GLOB_BRACE` is absent, keeping upstream's exact behaviour where it *is* defined:
|
||||||
|
|
||||||
|
```php
|
||||||
|
if (defined("GLOB_BRACE")) {
|
||||||
|
$files = glob($target . "{*,.[!.]*,..?*}", GLOB_BRACE | GLOB_MARK);
|
||||||
|
} else {
|
||||||
|
$files = array_merge(
|
||||||
|
glob($target . "/*", GLOB_MARK) ?: [],
|
||||||
|
glob($target . "/.[!.]*", GLOB_MARK) ?: [],
|
||||||
|
glob($target . "/..?*", GLOB_MARK) ?: []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The three patterns are precisely what the brace expansion produces (`*`, dotfiles, and `.`/`..`-like
|
||||||
|
names), so the directory listing is equivalent.
|
||||||
|
|
||||||
|
**Verified** — `php83 -l` clean, then a real deletion of a tree containing nested dirs and two
|
||||||
|
dotfiles:
|
||||||
|
|
||||||
|
```
|
||||||
|
entries before: 10
|
||||||
|
rDelete() returned: true
|
||||||
|
exists after: gone (OK)
|
||||||
|
single file: rDelete=true exists=gone (OK)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Upstream has NOT fixed this** — the same line is still in `main` (checked directly), so it cannot
|
||||||
|
be resolved by updating Atheos. Re-apply this patch after any update (`scripts/update-atheos.sh`).
|
||||||
|
|
||||||
|
**Consequence for the platform choice:** this is the real cost of Alpine. A glibc-based image
|
||||||
|
(Debian/Ubuntu) would not have hit it. It is patchable in one place, so Alpine stays — but it is worth
|
||||||
|
knowing that `GLOB_BRACE` is the kind of thing musl breaks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 🔴 Plugin market: the **Terminal** plugin does not work as shipped
|
||||||
|
|
||||||
|
**Symptom:** installed the Terminal plugin from the market; it failed / did not function.
|
||||||
|
|
||||||
|
**Root cause — a removed API.** `plugins/Terminal/terminal.php` calls `Common::execute(...)`, which
|
||||||
|
**no longer exists** in Atheos (it was renamed):
|
||||||
|
|
||||||
|
```
|
||||||
|
plugin calls : Common::execute(...) -> MISSING
|
||||||
|
available : Common::raw_execute(...) -> returns ["code" => int, "output" => array]
|
||||||
|
Common::safe_execute(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
The plugin's header says it is a port of `Fluidbyte/Codiad-Terminal`, so it targets the old API. Its
|
||||||
|
return-shape assumption is also stale (the old `execute()` returned `text`; `raw_execute()` returns
|
||||||
|
`output` as an array).
|
||||||
|
|
||||||
|
**Fix applied** (`patches/Terminal-terminal.php.patched`):
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- $output = Common::execute("pwd");
|
||||||
|
+ $output = Common::raw_execute("pwd");
|
||||||
|
- $_SESSION["activeDir"] = array([$_SESSION["project"]] => $output["text"]);
|
||||||
|
+ $_SESSION["activeDir"] = array([$_SESSION["project"]] => implode("\n", (array)$output["output"]));
|
||||||
|
|
||||||
|
- $output = Common::execute($cmd);
|
||||||
|
- return $output["text"];
|
||||||
|
+ $output = Common::raw_execute($cmd);
|
||||||
|
+ return implode("\n", (array)$output["output"]);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verified** against the real method (`php83 -l` clean; both call patterns exercised):
|
||||||
|
|
||||||
|
| Call | Result |
|
||||||
|
|---|---|
|
||||||
|
| `raw_execute("pwd")` | `code=0`, `output=["/root",""]` |
|
||||||
|
| `raw_execute("echo ...")` | `code=0`, returns the text |
|
||||||
|
| after `chdir` to the repo | returns `/var/www/atheos/workspace/blog` — so the terminal opens in the blog |
|
||||||
|
|
||||||
|
**Re-apply after an Atheos update** — `scripts/update-atheos.sh` replaces the application files, so
|
||||||
|
this patch is lost on update. Re-apply it, or drop the plugin.
|
||||||
|
|
||||||
|
### Two further defects in the same plugin (NOT fixed — upstream, and mostly harmless)
|
||||||
|
|
||||||
|
1. **`terminal.php:95`** — `$blocked = explode(",", BLOCKED === true);` passes a **boolean** to
|
||||||
|
`explode()`. This evaluates to `explode(",", false)`, so the intended block-list
|
||||||
|
(`define("BLOCKED","ssh,telnet")`) **never actually blocks anything**. The plugin's own guard is
|
||||||
|
dead code. Cosmetic in effect (the command would just fail on its own), but worth knowing rather
|
||||||
|
than trusting.
|
||||||
|
2. **`terminal.php:124`** — `if (strtolower($command === "exit"))` passes a boolean to
|
||||||
|
`strtolower()`. Should be `strtolower($command) === "exit"`. Result: typing `exit` does not clear
|
||||||
|
`term_auth`; the session stays authenticated until the browser session ends.
|
||||||
|
|
||||||
|
Left as-is: they are upstream bugs, and fixing them changes the plugin's behaviour beyond the
|
||||||
|
minimum needed to make it run. Recorded here so nobody "fixes" them by accident or trusts the
|
||||||
|
block-list.
|
||||||
|
|
||||||
|
### ⚠️ Security note
|
||||||
|
|
||||||
|
This plugin gives a **browser terminal that executes shell commands as the php-fpm user**
|
||||||
|
(`atheos`). Two things matter:
|
||||||
|
|
||||||
|
1. **The plugin ships with a well-known, hardcoded gate constant.** Upstream is literally
|
||||||
|
`define("PASSWORD", "<the default value in the plugin source>");`. Anyone who can reach the page
|
||||||
|
can read the source on GitHub and unlock it. **We have rotated it to a random value** — retrieve
|
||||||
|
it from the `PASSWORD` define in `plugins/Terminal/terminal.php` on the guest. It is deliberately
|
||||||
|
**not recorded in this repo** (see below).
|
||||||
|
2. **It is a shell, so treat it as a privilege decision, not a convenience one.** Installing it
|
||||||
|
means the editor can run arbitrary commands as `atheos` — the same user that owns the deploy key.
|
||||||
|
|
||||||
|
Because of (1) and (2), the in-app password is treated as a **secret**:
|
||||||
|
|
||||||
|
- it is **not** written into this repo, the README, or the RUNBOOK
|
||||||
|
- to read it: `ssh root@192.168.178.57 'grep PASSWORD /var/www/atheos/plugins/Terminal/terminal.php'`
|
||||||
|
- it changes per install; `scripts/provision.sh` does not set one, so a rebuild returns to the
|
||||||
|
upstream default — **rotate it again after a rebuild**
|
||||||
|
|
||||||
|
**Outcome: the plugin has been REMOVED.** Once the `GLOB_BRACE` fault (§1) was fixed, removal through
|
||||||
|
the market succeeded — and that is the right end state: a browser shell is the single largest attack
|
||||||
|
surface in the container, and **Atheos macros already provide command execution**, so it added little
|
||||||
|
you did not already have. Removal was done by invoking the market's own `remove()` path, so the plugin
|
||||||
|
list, the `addons` cache and the generated bundle all stayed consistent:
|
||||||
|
|
||||||
|
```
|
||||||
|
{"text":"Plugin successfully removed.","status":200}
|
||||||
|
plugins/ -> (only README.md remains)
|
||||||
|
addons cache references to Terminal: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
The notes above are kept because they document what the plugin *is* and how it failed — and because
|
||||||
|
the same stale-API pattern will recur with any other Codiad-era plugin. To reinstall, use the
|
||||||
|
market: it will re-introduce the `Common::execute` fault, so `patches/Terminal-terminal.php.patched`
|
||||||
|
was the fix — **it has been deleted along with the plugin**. If you ever reinstall, re-derive it:
|
||||||
|
`grep -rhoE 'Common::[a-zA-Z_]+' plugins/Terminal/` and expect `Common::execute` to be missing.
|
||||||
|
|
||||||
|
`docs/security-notes.md` §2 S-7 covers disabling `exec`/`proc_open`, which would neutralise **both**
|
||||||
|
this plugin and the macros. That is the correct lever if you ever expose this container.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 🟠 The Macro dialog crashed when folder-scoped macros existed
|
||||||
|
|
||||||
|
**Symptom:** the Settings → Macro dialog returned HTTP 500.
|
||||||
|
|
||||||
|
**Root cause — invalid data by Atheos's own design.** `components/macro/dialog.php:35` does:
|
||||||
|
|
||||||
|
```php
|
||||||
|
implode(", ", $macro["fTypes"]) // requires an ARRAY
|
||||||
|
```
|
||||||
|
|
||||||
|
but `components/macro/class.macro.php` `save()` writes the **string** `"N/A"` for folder macros:
|
||||||
|
|
||||||
|
```php
|
||||||
|
"fTypes" => $type === "file" ? explode(",", $fTypes) : "N/A",
|
||||||
|
```
|
||||||
|
|
||||||
|
So **any folder-type macro** makes that dialog fatal:
|
||||||
|
|
||||||
|
```
|
||||||
|
PHP Fatal error: Uncaught TypeError: implode(): Argument #2 ($array) must be of type ?array,
|
||||||
|
string given in /var/www/atheos/components/macro/dialog.php:35
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix applied — correct the DATA, not Atheos's code.** The stored `fTypes` for folder macros is now
|
||||||
|
`[]` (an empty array) instead of `"N/A"`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sed -i 's/"fTypes": "N\/A"/"fTypes": []/g' /var/www/atheos/data/macros.db.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Verified: `implode()` now succeeds for all 8 macros.
|
||||||
|
|
||||||
|
**Why data rather than code:** patching `dialog.php` would be overwritten by `update-atheos.sh`
|
||||||
|
(and by any Atheos update), whereas valid data keeps working. The trade-off is that the UI's own
|
||||||
|
"save" path could re-introduce `"N/A"` if a macro is edited in the UI — see the follow-up below.
|
||||||
|
|
||||||
|
**Follow-up (not done):** the real upstream fix is
|
||||||
|
`$type === "file" ? explode(",", $fTypes) : []` in `class.macro.php::save()`. Applying it would stop
|
||||||
|
the UI re-introducing the bad value, at the cost of a patch that must be re-applied after updates.
|
||||||
|
Worth doing if the dialog starts 500-ing again after editing a macro in the UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 🟠 The CodeGit panel looked like an empty black box
|
||||||
|
|
||||||
|
**Symptom:** the CodeGit dialog appeared as a mostly-empty black rectangle. Nothing was *broken* —
|
||||||
|
it was unreadable, which reads as "broken".
|
||||||
|
|
||||||
|
**This was NOT a codegit bug.** Verified exhaustively first:
|
||||||
|
|
||||||
|
```
|
||||||
|
all four panels return real content through the authenticated API:
|
||||||
|
overview 200 940 bytes
|
||||||
|
log 200 79596 bytes (full commit history)
|
||||||
|
transfer 200 1460 bytes
|
||||||
|
config 200 1174 bytes
|
||||||
|
no JS errors in the browser console; all DOM helpers (replaceClass etc.) exist
|
||||||
|
```
|
||||||
|
|
||||||
|
**Root cause — the generic `panel` rule hardcodes a height.** `theme/modules/common/_panel.scss`
|
||||||
|
(after minification, in `theme/main.min.css`):
|
||||||
|
|
||||||
|
```css
|
||||||
|
panel { padding: 5px; width: calc(100% - 200px); overflow: auto; overflow-y: scroll;
|
||||||
|
height: 500px; position: relative; border-left: var(--borderLight); }
|
||||||
|
```
|
||||||
|
|
||||||
|
Measured live, that produces:
|
||||||
|
|
||||||
|
| Element | Height | Problem |
|
||||||
|
|---|---|---|
|
||||||
|
| `#dialog` | 404px | clamped by `max-height: 70vh` + `overflow-y: auto` |
|
||||||
|
| `#codegit panel` | 500px | **forced**, while its content is 119px |
|
||||||
|
| `toolbar` ("Branch: …") | top **620** vs dialog bottom **490** | **pushed OUTSIDE the modal** |
|
||||||
|
|
||||||
|
So ~380px of the dialog was **empty dead space**, and anything after the panel — including the
|
||||||
|
Branch line — rendered *below* the modal, invisible. A generic `panel { height: 500px }` is exactly
|
||||||
|
the kind of rule that looks like a void.
|
||||||
|
|
||||||
|
**Fix applied:**
|
||||||
|
|
||||||
|
```css
|
||||||
|
panel { ... height: auto; max-height: 58vh; overflow-y: auto; ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
Content-sized, capped so long content (the 79KB log) still scrolls. Verified live:
|
||||||
|
|
||||||
|
```
|
||||||
|
before: panel 500px → toolbar top 620, dialog bottom 490 (outside)
|
||||||
|
after: panel 136px → toolbar top 256, dialog bottom 279 (inside)
|
||||||
|
```
|
||||||
|
|
||||||
|
`theme/modules/common/_panel.scss` is updated too, so the source does not contradict the shipped CSS
|
||||||
|
(there is no Sass compiler on the guest, so `main.min.css` is edited directly).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 🟠 CSS/JS changes were invisible behind the browser cache
|
||||||
|
|
||||||
|
**Symptom:** a fix to the theme did not appear until a hard refresh — so the panel fix above looked
|
||||||
|
like it had not worked at all.
|
||||||
|
|
||||||
|
**Root cause:** Atheos serves `theme/main.min.css` and the generated `public/*.min.js|css` bundles
|
||||||
|
from **stable URLs with no cache headers**, so nginx adds only `Last-Modified`/`ETag` and browsers
|
||||||
|
legitimately reuse their cached copy.
|
||||||
|
|
||||||
|
**Fix:** nginx now sends `Cache-Control: no-cache, must-revalidate` for `*.css`/`*.js`. Note this does
|
||||||
|
**not** disable caching — it forces **revalidation**, so normal loads still get a cheap 304 while any
|
||||||
|
change is guaranteed to be picked up on the next load.
|
||||||
|
|
||||||
|
**If you ever see a stale UI again:** force a reload (Ctrl/Cmd-Shift-R). That is the first thing to try
|
||||||
|
before assuming a fix failed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Checking for plugin problems in future
|
||||||
|
|
||||||
|
The plugin's JS is bundled into `public/plugins.min.js` (built by `classes/sourcemanager.php`), and
|
||||||
|
its rightbar button is rendered by `components/sidebars/sb-right.php` from `plugin.json`. Both were
|
||||||
|
verified working:
|
||||||
|
|
||||||
|
```
|
||||||
|
plugins.min.js contains 'plugins/Terminal/init.js' -> YES (bundle is fine)
|
||||||
|
rightbar HTML -> <a onclick="atheos.Terminal.open();">
|
||||||
|
<i class="fas fa-terminal"></i>Terminal</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
So when a plugin misbehaves, check in this order:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 1. did it install?
|
||||||
|
tail /var/www/atheos/data/log/market.log.php
|
||||||
|
|
||||||
|
# 2. is its JS in the served bundle?
|
||||||
|
grep -c "plugins/<Name>/init.js" /var/www/atheos/public/plugins.min.js
|
||||||
|
|
||||||
|
# 3. is it a stale-API port? (Codiad-era plugins are the risk)
|
||||||
|
grep -rhoE 'Common::[a-zA-Z_]+' /var/www/atheos/plugins/<Name>/ | sort -u
|
||||||
|
# then check each exists: grep -rn "function <name>" /var/www/atheos/common.php /var/www/atheos/traits/
|
||||||
|
|
||||||
|
# 4. is there a PHP fatal?
|
||||||
|
grep -E 'PHP Fatal' /var/log/nginx/atheos.error.log | tail
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rule of thumb:** if a plugin was ported from Codiad, assume its `Common::` calls are stale and
|
||||||
|
verify them before trusting it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Follow-ups not done (recorded so they are not forgotten)
|
||||||
|
|
||||||
|
These came out of review. They are **operator decisions**, not agent ones — several change what the
|
||||||
|
editor can do.
|
||||||
|
|
||||||
|
### 7.0 Branch protection on `master` — ACCEPTED RISK
|
||||||
|
|
||||||
|
Deliberately **not** enabled while the editor is LAN-only (operator decision). Full reasoning and the
|
||||||
|
"enable it before you expose this host" trigger are in
|
||||||
|
[`docs/writer-branch-workflow.md` §2](writer-branch-workflow.md#2-protect-master-on-the-blog-repo--recommendation-deliberately-not-enabled).
|
||||||
|
|
||||||
|
The suite still probes for it on every run and reports `ACCEPTED-RISK` (not a failure) while it is
|
||||||
|
off, so the recommendation self-cancels the moment it is enabled.
|
||||||
|
|
||||||
|
### 7.1 Remove the Terminal plugin — ✅ DONE
|
||||||
|
|
||||||
|
Removed through the market's own code path once §1 was fixed (see §2). A browser shell was the largest
|
||||||
|
attack surface in the container and macros already cover command execution.
|
||||||
|
|
||||||
|
### 7.2 ❌ `disable_functions` cannot be "on by default" here — measured
|
||||||
|
|
||||||
|
Review asked for `php.ini` to disable `exec`, `shell_exec`, `passthru`, `system` by default.
|
||||||
|
**This would break the editor's core function.** Evidence:
|
||||||
|
|
||||||
|
```
|
||||||
|
components/codegit/* → Common::safe_execute (31 call sites — every git operation)
|
||||||
|
components/macro/* → Common::raw_execute (1 call site)
|
||||||
|
common.php raw_execute → function_exists("system") / function_exists("exec")
|
||||||
|
```
|
||||||
|
|
||||||
|
Every git commit, push, fetch and status from the editor goes through `exec`/`system`. Disabling them
|
||||||
|
turns the editor into a text editor that cannot commit. So it stays **a deliberate exposure lever**
|
||||||
|
(S-7 in `docs/security-notes.md`), to be applied only when the container is exposed — at which point
|
||||||
|
you would also lose macros and git integration, which is why S-7 already says *"test, do not
|
||||||
|
blind-apply"*.
|
||||||
|
|
||||||
|
### 7.3 Rotate the terminal password automatically
|
||||||
|
|
||||||
|
If the plugin is kept, `provision.sh` should generate a random password rather than leaving the
|
||||||
|
upstream default. Not implemented: it would hardcode a secret into a repo file, which is the same
|
||||||
|
class of problem this file already documents.
|
||||||
|
|
||||||
|
### 7.4 Deploy key could be read-only
|
||||||
|
|
||||||
|
Review suggested making the deploy key read-only if writes are unnecessary. **They are necessary** —
|
||||||
|
that is the whole point of the writer-branch workflow (macro 2 pushes). Not applicable.
|
||||||
|
|
||||||
|
### 7.5 The real upstream fix for §3
|
||||||
|
|
||||||
|
`class.macro.php::save()` should write `[]` instead of `"N/A"` for folder macros. See §3 — the fix is
|
||||||
|
currently applied to the data, so it survives updates, at the cost of being reversible from the UI.
|
||||||
@@ -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,152 @@
|
|||||||
|
# Writer-Branch Workflow
|
||||||
|
|
||||||
|
How writing and publishing work from the Atheos editor, and why the branch is structured this way.
|
||||||
|
|
||||||
|
## The model
|
||||||
|
|
||||||
|
The editor **never touches `master`**. It writes to a long-lived branch (`atheos-writer`), and
|
||||||
|
merging into `master` is done by the operator in Gitea.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
Atheos editor ────┤ atheos-writer (long-lived) │
|
||||||
|
(macros) │ • write here │
|
||||||
|
│ • commit + push here │
|
||||||
|
│ • can be reset --hard back to master │
|
||||||
|
└───────────────┬──────────────────────────┘
|
||||||
|
│ operator opens a PR in Gitea
|
||||||
|
│ operator reviews
|
||||||
|
▼
|
||||||
|
master (protected: RECOMMENDED, currently OFF - see §2)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
existing Pelican build pipeline
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** the editor is a web app with shell-exec reach (`codegit` + `macro` both run commands as the
|
||||||
|
php-fpm user). Bounding what it can reach is the whole point of design decision D6. Keeping `master`
|
||||||
|
behind a PR means a compromised or fat-fingered editor **cannot** publish directly — it can only
|
||||||
|
propose.
|
||||||
|
|
||||||
|
## Setup — do these once
|
||||||
|
|
||||||
|
### 1. Set your git identity (required, RUNBOOK STEP 1)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ssh root@192.168.178.57
|
||||||
|
su -s /bin/sh atheos
|
||||||
|
git config --global user.name "Your Name"
|
||||||
|
git config --global user.email "you@example.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the *same* global gitconfig php-fpm uses (`HOME=/home/atheos` — verified), so macros commit
|
||||||
|
as **you**. If it is not set, macro **2** refuses with an actionable error rather than
|
||||||
|
committing under a bogus identity.
|
||||||
|
|
||||||
|
### 2. Protect `master` on the blog repo — RECOMMENDATION, deliberately not enabled
|
||||||
|
|
||||||
|
**Status: accepted risk. Branch protection is OFF, and that is a decision, not an oversight.**
|
||||||
|
|
||||||
|
> *"leave it as a recommendation — I won't do that unless I take this out to the wider internet"*
|
||||||
|
> — operator
|
||||||
|
|
||||||
|
The deploy key **can** push `master` directly, which means the writer-branch model is **advisory, not
|
||||||
|
enforced**. The operator has weighed this and chosen not to add the gate while the editor is LAN-only:
|
||||||
|
|
||||||
|
- **The compensating control is reachability, not immutability.** The editor is LAN-only (FR-10). The
|
||||||
|
threat this setting defends against is a compromised editor publishing straight to `master`; while
|
||||||
|
nothing off-LAN can reach the editor, and the only user on the LAN is the operator, that is largely
|
||||||
|
theoretical.
|
||||||
|
- **The cost is real now, the benefit is not.** Protection adds a merge gate for the sole user, who is
|
||||||
|
also the repo owner — friction with no realistic risk reduction today.
|
||||||
|
|
||||||
|
#### ⚠️ Enable it BEFORE either of these happens
|
||||||
|
|
||||||
|
1. **The editor becomes reachable off-LAN** — an NPM proxy entry, a port-forward, or a
|
||||||
|
Tailscale/WireGuard route to `blog-ide.aridgwayweb.com`.
|
||||||
|
2. **Anyone other than the operator gets access to the editor.**
|
||||||
|
|
||||||
|
At that point a deploy key that can force-push `master` stops being theoretical. The setting
|
||||||
|
(`armistace/blog` → **Settings** → **Branches** → rule for `master`):
|
||||||
|
|
||||||
|
| Setting | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Enable push** | **off** ← direct pushes blocked, including the deploy key |
|
||||||
|
| **Enable merge whitelist** | **on** → add `armistace` |
|
||||||
|
| Require approvals | optional (you're the only human here) |
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
https://git.aridgwayweb.com/api/v1/repos/armistace/blog/branch_protections
|
||||||
|
```
|
||||||
|
|
||||||
|
#### It is still checked on every test run
|
||||||
|
|
||||||
|
`build-and-test.sh` attempts a direct push to `master` with the deploy key and requires it to be
|
||||||
|
**rejected**. While protection is off that reports as `ACCEPTED-RISK`, not `FAIL` — so the suite stays
|
||||||
|
green and CI-usable — but **the check runs every time**. It flips to
|
||||||
|
`PASS master is protected` by itself the moment protection is enabled, and this recommendation
|
||||||
|
becomes moot without anyone having to remember it.
|
||||||
|
|
||||||
|
### 3. The macros
|
||||||
|
|
||||||
|
Installed directly into Atheos's macro store (`data/macros.db.php`); a copy is kept in this repo at
|
||||||
|
`files/macros.db.php` so a rebuild reproduces them. They appear when you **right-click the `blog`
|
||||||
|
folder** in the file tree → **Macros**.
|
||||||
|
|
||||||
|
| # | Macro | Command | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | **Start/Join Writer Branch** | `git checkout atheos-writer \|\| git checkout -b atheos-writer` then `push -u` | idempotent — safe to run repeatedly |
|
||||||
|
| 2 | **Commit + Push Writer Branch** | identity guard → `git add -A` → commit (only if staged changes) → `git push origin atheos-writer` | the everyday button |
|
||||||
|
| 3 | **DESTRUCTIVE: Reset Writer Branch to Master** | `git fetch` → `git reset --hard origin/master` → `git push --force-with-lease` | wipes the branch's commits, keeps your files |
|
||||||
|
| 4 | **Status + Recent Log** | `git status --short --branch`, `git log --oneline -8`, `git branch` | read-only |
|
||||||
|
| 5 | **Refresh Master (ff-only)** | `git checkout master` → `git pull --ff-only` → back to `atheos-writer` | brings master's merges down |
|
||||||
|
| 6 | **Merge Master Into Writer Branch** | `git merge origin/master` → push | for when master has moved ahead |
|
||||||
|
|
||||||
|
Output is returned into the UI — you see git's actual stdout/stderr, not a silent success.
|
||||||
|
|
||||||
|
## Day-to-day
|
||||||
|
|
||||||
|
1. Right-click `blog` → Macros → **1. Start/Join Writer Branch** (first time; also safe later)
|
||||||
|
2. Write your post, save
|
||||||
|
3. Right-click `blog` → Macros → **2. Commit + Push Writer Branch**
|
||||||
|
4. Open a PR: `atheos-writer` → `master`
|
||||||
|
5. Review the diff in Gitea, merge
|
||||||
|
|
||||||
|
### ⚠️ Important: macro 2 uses `git add -A`
|
||||||
|
|
||||||
|
`git add -A` stages **everything** in the repo, including files you may not intend to publish
|
||||||
|
(drafts, scratch files, `.orig`/`.bak` leftovers). A `reset --hard` will also **delete any untracked
|
||||||
|
file that has been staged**.
|
||||||
|
|
||||||
|
Practical consequences:
|
||||||
|
|
||||||
|
- `atheos-writer` is a **publishing** branch — everything you leave in the tree gets committed and
|
||||||
|
pushed to Gitea.
|
||||||
|
- If you want a scratch area, keep it **outside** the repo (e.g. `workspace/scratch/`, not
|
||||||
|
`workspace/blog/`).
|
||||||
|
- To unstage before a reset: `git reset` (clears the index without touching your files).
|
||||||
|
|
||||||
|
## Recovering a deleted file
|
||||||
|
|
||||||
|
`reset --hard` does not touch the object database, so a file that was ever staged or committed is
|
||||||
|
still recoverable:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
su -s /bin/sh atheos
|
||||||
|
cd /var/www/atheos/workspace/blog
|
||||||
|
git fsck --lost-found | grep commit # find dangling commits
|
||||||
|
git ls-tree <commit> path/to/file # confirm it is there
|
||||||
|
git cat-file -p <blob-sha> > path/to/file # restore it
|
||||||
|
```
|
||||||
|
|
||||||
|
`git reflog --all` also shows recently discarded commits. This is how the empty
|
||||||
|
`src/content/please_no_facebook.md` was recovered after a `reset --hard` in testing.
|
||||||
|
|
||||||
|
## Changing the branch name
|
||||||
|
|
||||||
|
Edit macros 1, 2, 3, 5 and 6 in **Settings → Macro** and replace `atheos-writer`. The branch name is
|
||||||
|
literal in each command — there is no variable. (Macros have no input prompt, so a
|
||||||
|
branch name that changes per task would need edit-then-run.)
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /var/www/atheos;
|
||||||
|
index index.php index.html;
|
||||||
|
|
||||||
|
# --- logging ---------------------------------------------------------
|
||||||
|
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 -> else 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- hardening (free, no downside) -----------------------------------
|
||||||
|
# never serve the VCS directory, 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;
|
||||||
|
|
||||||
|
# Atheos serves theme/main.min.css and the generated *.min.js/css bundles from STABLE URLs
|
||||||
|
# with no cache headers. Without this, a CSS/JS change stays invisible behind a cached copy
|
||||||
|
# until the user hard-refreshes. "no-cache" permits caching but forces revalidation, so a
|
||||||
|
# 304 keeps normal loads fast while guaranteeing updates are picked up on the next load.
|
||||||
|
location ~* \.(css|js)$ {
|
||||||
|
add_header Cache-Control "no-cache, must-revalidate";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?php/*|[
|
||||||
|
{
|
||||||
|
"uuid": "macro-947e-4763-b7b6f2",
|
||||||
|
"title": "Execute",
|
||||||
|
"type": "file",
|
||||||
|
"fTypes": [
|
||||||
|
"sh"
|
||||||
|
],
|
||||||
|
"command": "%PATH%",
|
||||||
|
"icon": "fas fa-magic",
|
||||||
|
"owner": "ATHEOS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "macro-53a0-4ebb-56e800",
|
||||||
|
"title": "Execute",
|
||||||
|
"type": "file",
|
||||||
|
"fTypes": [
|
||||||
|
"php"
|
||||||
|
],
|
||||||
|
"command": "cd %FOLDER% && php %BASENAME%",
|
||||||
|
"icon": "fas fa-magic",
|
||||||
|
"owner": "ATHEOS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "macro-writer-0001-start",
|
||||||
|
"title": "1. Start/Join Writer Branch",
|
||||||
|
"type": "folder",
|
||||||
|
"fTypes": [],
|
||||||
|
"command": "cd %PATH% && (git checkout atheos-writer 2>/dev/null || git checkout -b atheos-writer) && git push -u origin atheos-writer",
|
||||||
|
"icon": "fas fa-code-branch",
|
||||||
|
"owner": "armistace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "macro-writer-0002-commit",
|
||||||
|
"title": "2. Commit + Push Writer Branch",
|
||||||
|
"type": "folder",
|
||||||
|
"fTypes": [],
|
||||||
|
"command": "cd %PATH% && (git config --get user.email >/dev/null || { echo 'ERROR: no git identity set. Run over SSH: git config --global user.name \"Your Name\" && git config --global user.email \"you@example.com\" (RUNBOOK STEP 1)'; exit 1; }) && git add -A && (git diff --cached --quiet || git commit -m \"writer: $(date +%F_%H%M)\") && git push origin atheos-writer",
|
||||||
|
"icon": "fas fa-cloud-upload-alt",
|
||||||
|
"owner": "armistace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "macro-writer-0003-reset",
|
||||||
|
"title": "3. DESTRUCTIVE: Reset Writer Branch to Master",
|
||||||
|
"type": "folder",
|
||||||
|
"fTypes": [],
|
||||||
|
"command": "cd %PATH% && git fetch origin && git reset --hard origin/master && git push --force-with-lease origin atheos-writer",
|
||||||
|
"icon": "fas fa-exclamation-triangle",
|
||||||
|
"owner": "armistace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "macro-writer-0004-status",
|
||||||
|
"title": "4. Status + Recent Log",
|
||||||
|
"type": "folder",
|
||||||
|
"fTypes": [],
|
||||||
|
"command": "cd %PATH% && git status --short --branch && echo '--- recent commits:' && git log --oneline -8 && echo '--- local branches:' && git branch",
|
||||||
|
"icon": "fas fa-info-circle",
|
||||||
|
"owner": "armistace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "macro-writer-0005-refresh",
|
||||||
|
"title": "5. Refresh Master (ff-only)",
|
||||||
|
"type": "folder",
|
||||||
|
"fTypes": [],
|
||||||
|
"command": "cd %PATH% && git checkout master && git pull --ff-only && git checkout atheos-writer",
|
||||||
|
"icon": "fas fa-sync-alt",
|
||||||
|
"owner": "armistace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "macro-writer-0006-pullmaster",
|
||||||
|
"title": "6. Merge Master Into Writer Branch",
|
||||||
|
"type": "folder",
|
||||||
|
"fTypes": [],
|
||||||
|
"command": "cd %PATH% && (git config --get user.email >/dev/null || { echo 'ERROR: no git identity set. Run over SSH: git config --global user.name \"Your Name\" && git config --global user.email \"you@example.com\" (RUNBOOK STEP 1)'; exit 1; }) && git fetch origin && git merge origin/master -m \"merge master into atheos-writer\" && git push origin atheos-writer",
|
||||||
|
"icon": "fas fa-code-merge",
|
||||||
|
"owner": "armistace"
|
||||||
|
}
|
||||||
|
]|*/?>
|
||||||
@@ -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,22 @@
|
|||||||
|
--- /var/www/atheos/traits/helpers.php.orig
|
||||||
|
+++ /var/www/atheos/traits/helpers.php
|
||||||
|
@@ -135,7 +135,18 @@
|
||||||
|
|
||||||
|
if (is_dir($target)) {
|
||||||
|
|
||||||
|
- $files = glob($target . "{*,.[!.]*,..?*}", GLOB_BRACE|GLOB_MARK); //GLOB_MARK adds a slash to directories returned
|
||||||
|
+ // GLOB_BRACE is a glibc extension and is UNDEFINED under musl (Alpine), where
|
||||||
|
+ // referencing it is a fatal error in PHP 8. Expand the brace pattern by hand
|
||||||
|
+ // so this works on both libcs.
|
||||||
|
+ if (defined("GLOB_BRACE")) {
|
||||||
|
+ $files = glob($target . "{*,.[!.]*,..?*}", GLOB_BRACE | GLOB_MARK);
|
||||||
|
+ } else {
|
||||||
|
+ $files = array_merge(
|
||||||
|
+ glob($target . "/*", GLOB_MARK) ?: [],
|
||||||
|
+ glob($target . "/.[!.]*", GLOB_MARK) ?: [],
|
||||||
|
+ glob($target . "/..?*", GLOB_MARK) ?: []
|
||||||
|
+ );
|
||||||
|
+ }
|
||||||
|
// $files = glob($target . "/*");
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
#!/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; note=0
|
||||||
|
ok() { echo " PASS $1"; pass=$((pass+1)); }
|
||||||
|
bad() { echo " FAIL $1"; fail=$((fail+1)); }
|
||||||
|
note() { echo " ACCEPTED $1"; note=$((note+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
|
||||||
|
# Either state is valid: pre-install shows the installer, post-install shows the login/app.
|
||||||
|
if grep -qiE 'install|setup' /tmp/idx.html 2>/dev/null; then
|
||||||
|
ok "installer markers present (pre-install state)"
|
||||||
|
elif [ -f "$W/config.php" ] && [ -s "$W/config.php" ]; then
|
||||||
|
ok "installer already completed (config.php populated)"
|
||||||
|
else
|
||||||
|
bad "neither installer markers nor a populated config.php"
|
||||||
|
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 "=============================================================="
|
||||||
|
POSTS=$(ls $B/src/content/*.md 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
if [ "$POSTS" -ge 20 ]; then ok "posts found ($POSTS, expected >= 20)"; else bad "posts found ($POSTS, expected >= 20)"; fi
|
||||||
|
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
|
||||||
|
# A repo may legitimately sit on master, or on the long-lived writer branch (docs/writer-branch-workflow.md).
|
||||||
|
BR=$(asuser "git -C $B rev-parse --abbrev-ref HEAD" 2>/dev/null)
|
||||||
|
case "$BR" in
|
||||||
|
master|main) ok "branch ($BR)" ;;
|
||||||
|
atheos-writer) ok "branch ($BR - writer-branch workflow)" ;;
|
||||||
|
*) bad "branch is unexpected: $BR" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 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 "REGRESSION: panel has no hardcoded height (empty-modal bug)"
|
||||||
|
echo "=============================================================="
|
||||||
|
# panel { height:500px } forced every modal panel to a fixed box: short content left a large
|
||||||
|
# empty black region and anything after the panel (codegit's Branch toolbar) was pushed outside
|
||||||
|
# the modal. docs/known-issues.md §4. Fails loudly if lost to an update.
|
||||||
|
PNL=$(grep -oE '(^|[;}])panel\{[^}]*\}' /var/www/atheos/theme/main.min.css 2>/dev/null | head -1)
|
||||||
|
if [ -z "$PNL" ]; then
|
||||||
|
bad "could not read the panel rule from theme/main.min.css"
|
||||||
|
elif echo "$PNL" | grep -q 'height:500px'; then
|
||||||
|
bad "panel is fixed at 500px - re-run scripts/fix-panel-layout.sh"
|
||||||
|
else
|
||||||
|
ok "panel is content-sized ($(echo "$PNL" | grep -oE 'height:[^;]*' | head -1))"
|
||||||
|
fi
|
||||||
|
# and the CSS/JS cache header must be present, or fixes stay invisible
|
||||||
|
if wget -qO /dev/null --server-response --header='Host: 127.0.0.1' http://127.0.0.1/theme/main.min.css 2>&1 | grep -qi 'Cache-Control.*no-cache'; then
|
||||||
|
ok "theme CSS sends Cache-Control: no-cache (changes are picked up)"
|
||||||
|
else
|
||||||
|
bad "theme CSS has no Cache-Control - a CSS fix can stay hidden behind the browser cache"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo "=============================================================="
|
||||||
|
echo "REGRESSION: rDelete works (GLOB_BRACE is glibc-only; musl lacks it)"
|
||||||
|
echo "=============================================================="
|
||||||
|
# That broke deleting ANY file/folder in the editor AND removing a market plugin (the UI
|
||||||
|
# would spin forever because the fatal killed the response before Common::send()).
|
||||||
|
# docs/known-issues.md §1. This test fails loudly if the patch is lost (e.g. by an update).
|
||||||
|
rm -rf /tmp/rdel_guard 2>/dev/null
|
||||||
|
if mkdir -p /tmp/rdel_guard/a/b/c /tmp/rdel_guard/.hd 2>/dev/null \
|
||||||
|
&& touch /tmp/rdel_guard/f.txt /tmp/rdel_guard/.dot /tmp/rdel_guard/a/b/c/deep.txt; then
|
||||||
|
ROUT=$(php83 -r 'require_once("/var/www/atheos/common.php"); echo Common::rDelete("/tmp/rdel_guard/") ? "ok" : "fail";' 2>&1 | grep -viE 'deprecat|warning|notice' | tail -1)
|
||||||
|
if [ "$ROUT" = "ok" ] && [ ! -e /tmp/rdel_guard ]; then
|
||||||
|
ok "rDelete removed a nested tree incl. dotfiles (delete works)"
|
||||||
|
else
|
||||||
|
if echo "$ROUT" | grep -q 'GLOB_BRACE'; then
|
||||||
|
bad "rDelete FATALS on GLOB_BRACE - re-apply patches/helpers-rDelete-GLOB_BRACE.diff"
|
||||||
|
else
|
||||||
|
bad "rDelete failed (got: $ROUT)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
bad "could not build the rDelete test tree"
|
||||||
|
fi
|
||||||
|
rm -rf /tmp/rdel_guard 2>/dev/null
|
||||||
|
echo "=============================================================="
|
||||||
|
# The whole branch-based workflow assumes the deploy key CANNOT push master. That is a
|
||||||
|
# repo-side setting we cannot verify from here by inspection - so test it the way an
|
||||||
|
# attacker would: attempt a direct push to master and require it to be REJECTED.
|
||||||
|
if [ "${SKIP_MASTER_PUSH_TEST:-0}" = "1" ]; then
|
||||||
|
echo " SKIPPED (SKIP_MASTER_PUSH_TEST=1)"
|
||||||
|
else
|
||||||
|
PUSH_OUT=$(asuser "git -C $B push origin HEAD:master --dry-run" 2>&1)
|
||||||
|
if echo "$PUSH_OUT" | grep -qiE 'protected|not allowed|denied|forbidden|remote rejected'; then
|
||||||
|
ok "master is protected - the deploy key CANNOT push it"
|
||||||
|
else
|
||||||
|
# ACCEPTED RISK, not a failure. The operator has decided NOT to enable branch protection
|
||||||
|
# while this editor is LAN-only; the control is "it is not reachable off-LAN", not "master
|
||||||
|
# is immutable". So this is reported, not failed - but it is checked on EVERY run, so the
|
||||||
|
# moment protection does go on, the run says so and the recommendation below becomes moot.
|
||||||
|
note "master is NOT protected - the deploy key CAN push master directly"
|
||||||
|
echo " ACCEPTED because the editor is LAN-only (see docs/writer-branch-workflow.md)."
|
||||||
|
echo " BECOMES A REAL FINDING THE MOMENT THIS HOST IS REACHABLE OFF-LAN. Before that:"
|
||||||
|
echo " armistace/blog -> Settings -> Branches -> add rule for 'master'"
|
||||||
|
echo " Enable push = OFF, merge whitelist = armistace"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=============================================================="
|
||||||
|
echo "IDENTITY: git identity is SET by the operator (FR-11 superseded by explicit request)"
|
||||||
|
echo "=============================================================="
|
||||||
|
# FR-11 originally required the agent to leave the identity EMPTY so it could not impersonate
|
||||||
|
# the operator. The operator has since explicitly asked for it to be set, so the requirement is
|
||||||
|
# superseded - and the guard is inverted: the identity must now be present and must be the
|
||||||
|
# OPERATOR's, not an agent-invented one. Guarding the old expectation would assert the opposite
|
||||||
|
# of the current design and fail forever (which is what it did).
|
||||||
|
NAME=$(asuser 'git config --get user.name' 2>/dev/null)
|
||||||
|
MAIL=$(asuser 'git config --get user.email' 2>/dev/null)
|
||||||
|
if [ -n "$NAME" ]; then ok "git user.name set ($NAME)"; else bad "git user.name is empty - macro 2 will refuse to commit"; fi
|
||||||
|
if [ -n "$MAIL" ]; then ok "git user.email set ($MAIL)"; else bad "git user.email is empty"; fi
|
||||||
|
case "$MAIL" in
|
||||||
|
*hermes*|*bot*|*noreply@anthropic*|*agent*) bad "identity looks agent-generated: $MAIL" ;;
|
||||||
|
"") ;;
|
||||||
|
*) ok "identity is an operator address, not an agent-invented one" ;;
|
||||||
|
esac
|
||||||
|
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, %d accepted-risk\n' "$pass" "$fail" "$note"
|
||||||
|
echo "=============================================================="
|
||||||
|
[ "$fail" -eq 0 ] || exit 1
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Re-apply the UI/layout fixes to a rebuilt or updated Atheos tree.
|
||||||
|
#
|
||||||
|
# Why this exists: Atheos has no releases and `scripts/update-atheos.sh` replaces the whole
|
||||||
|
# application directory, so any local patch is lost on update. These are the two that were
|
||||||
|
# needed to make the editor usable. Both are IDEMPOTENT - run it as often as you like.
|
||||||
|
#
|
||||||
|
# 1. panel { height: 500px } -> height:auto; max-height:58vh
|
||||||
|
# Every modal panel was forced to a fixed 500px box, so dialogs with short content showed
|
||||||
|
# a large empty black region and anything after the panel (codegit's "Branch:" toolbar)
|
||||||
|
# was pushed OUTSIDE the modal. See docs/known-issues.md §4.
|
||||||
|
#
|
||||||
|
# 2. nginx: Cache-Control: no-cache for *.css/*.js
|
||||||
|
# Theme/bundle changes were invisible behind the browser cache. §5.
|
||||||
|
#
|
||||||
|
# Usage: sh scripts/fix-panel-layout.sh (run ON the guest, as root)
|
||||||
|
set -e
|
||||||
|
|
||||||
|
ATH=/var/www/atheos
|
||||||
|
CSS=$ATH/theme/main.min.css
|
||||||
|
SCSS=$ATH/theme/modules/common/_panel.scss
|
||||||
|
NGX=/etc/nginx/http.d/atheos.conf
|
||||||
|
|
||||||
|
echo "=== 1. panel layout ==="
|
||||||
|
OLD='height:500px;position:relative;border-left:var(--borderLight)'
|
||||||
|
NEW='height:auto;max-height:58vh;position:relative;border-left:var(--borderLight)'
|
||||||
|
if [ ! -f "$CSS" ]; then
|
||||||
|
echo " !! $CSS not found - is Atheos installed?"
|
||||||
|
elif grep -qF "$NEW" "$CSS"; then
|
||||||
|
echo " already applied"
|
||||||
|
elif grep -qF "$OLD" "$CSS"; then
|
||||||
|
[ -f "$CSS.orig" ] || cp -a "$CSS" "$CSS.orig"
|
||||||
|
php83 -r '$f=$argv[1];$o=$argv[2];$n=$argv[3];$s=file_get_contents($f);
|
||||||
|
if(strpos($s,$o)===false){exit(1);} file_put_contents($f,str_replace($o,$n,$s));' "$CSS" "$OLD" "$NEW"
|
||||||
|
echo " applied to main.min.css"
|
||||||
|
else
|
||||||
|
echo " !! expected panel rule not found - inspect manually"
|
||||||
|
echo " (upstream may have changed it; diff against $CSS.orig)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# keep the SCSS source consistent so the shipped CSS is not contradicted by the source
|
||||||
|
if [ -f "$SCSS" ] && grep -q 'height: 500px;' "$SCSS"; then
|
||||||
|
[ -f "$SCSS.orig" ] || cp -a "$SCSS" "$SCSS.orig"
|
||||||
|
php83 -r '$f=$argv[1];$s=file_get_contents($f);
|
||||||
|
$s=str_replace("height: 500px;","height: auto;\n\tmax-height: 58vh;",$s);
|
||||||
|
file_put_contents($f,$s);' "$SCSS"
|
||||||
|
echo " synchronised _panel.scss"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== 2. nginx cache headers ==="
|
||||||
|
if [ ! -f "$NGX" ]; then
|
||||||
|
echo " !! $NGX not found"
|
||||||
|
elif grep -q 'must-revalidate' "$NGX"; then
|
||||||
|
echo " already applied"
|
||||||
|
else
|
||||||
|
[ -f "$NGX.orig" ] || cp -a "$NGX" "$NGX.orig"
|
||||||
|
cat > /tmp/.cacheblock <<'EOF'
|
||||||
|
|
||||||
|
# Atheos serves theme/main.min.css and the generated *.min.js/css bundles from STABLE URLs
|
||||||
|
# with no cache headers, so a CSS/JS fix stays invisible behind the browser cache until a
|
||||||
|
# hard refresh. "no-cache" permits caching but forces revalidation: normal loads still get a
|
||||||
|
# cheap 304, and a change is guaranteed to be picked up on the next load.
|
||||||
|
location ~* \.(css|js)$ {
|
||||||
|
add_header Cache-Control "no-cache, must-revalidate";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
php83 -r '$f=$argv[1];$b=$argv[2];$s=file_get_contents($f);
|
||||||
|
$p=strrpos($s,"}"); if($p===false){exit(1);}
|
||||||
|
file_put_contents($f,substr($s,0,$p).file_get_contents($b).substr($s,$p));' "$NGX" /tmp/.cacheblock
|
||||||
|
rm -f /tmp/.cacheblock
|
||||||
|
nginx -t >/dev/null 2>&1 && rc-service nginx reload >/dev/null 2>&1 && echo " applied + nginx reloaded" \
|
||||||
|
|| echo " !! nginx -t failed - restore $NGX.orig"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== done ==="
|
||||||
@@ -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