Reported as "deleting terminal just cycles in the ui". Root cause is much larger than the
plugin: it was a platform bug that broke deletion everywhere.
CAUSE
traits/helpers.php::rDelete() did:
glob($target . "{*,.[!.]*,..?*}", GLOB_BRACE|GLOB_MARK);
GLOB_BRACE is a GLIBC extension. Alpine is musl, where the constant is undefined,
and referencing an undefined constant is a FATAL in PHP 8:
PHP Fatal error: Uncaught Error: Undefined constant "GLOB_BRACE"
in traits/helpers.php:138
#0 components/market/class.market.php(178): Common::rDelete()
#1 components/market/controller.php(46): Market->remove()
The fatal kills the request before Common::send(200), so the front end never gets a
settle response and the UI spins forever. A hang, not an error - which is why it
presented as "cycles in the ui".
BLAST RADIUS (measured, two call sites)
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 functionality, so this was never a plugin bug - installing the
Terminal plugin is just what made it visible.
FIX
patches/helpers-rDelete-GLOB_BRACE.diff - expand the brace pattern by hand when
GLOB_BRACE is undefined, keeping upstream behaviour where it is defined. Verified:
nested dirs + dotfiles + single files all delete correctly now.
Upstream has NOT fixed this (checked main) so it cannot be resolved by updating.
PLUGIN REMOVED
Once deletion worked, removal via the market succeeded and the plugin is now gone:
{"text":"Plugin successfully removed.","status":200}
That is the right end state - a browser shell was the largest attack surface, and
macros already provide command execution. Removed through the market's own code path
so the plugin list, addons cache and bundle stayed consistent.
TESTS (now 42 passed, 1 failed - the failure is branch protection, a genuine open item)
+ a GLOB_BRACE regression guard that fails loudly if the patch is lost to an update
+ the identity guard is INVERTED: FR-11 required an EMPTY identity so the agent could
not impersonate the operator, but the operator has since asked for it to be set, so
the test asserting emptiness was asserting the opposite of the current design. It
now requires the identity to be present and to NOT look agent-generated.
docs/known-issues.md: new section 1 documents the GLOB_BRACE fault in full, including the
honest note that this is the real cost of choosing Alpine over a glibc base image.
atheos-blog-lxc
Provisioning for a browser-based code editor (Atheos) in an Alpine LXC, wired to Gitea, with the
armistace/blog repository cloned and ready to edit.
Built with the AI-DLC workflow — the full process trail (requirements, design, units, decisions,
audit log) lives in aidlc-docs/.
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 — 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
makehere — 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:
- Open http://192.168.178.57/ and complete the Atheos first-run installer.
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.- Add the deploy key to
armistace/blogand switch the remote to SSH — RUNBOOK.md STEP 2 & 3.
👉 The full operational guide is 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:
# 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
and docs/security-notes.md.
Security at a glance
- Writing happens on a branch, never on
master. The editor commits to a long-livedatheos-writerbranch and you merge via PR in Gitea — seedocs/writer-branch-workflow.md. ⚠️ This only holds if branch protection is enabled onarmistace/blog'smaster— without it the deploy key could force-push master directly. Confirm with thebranch_protectionsAPI call in that document. - Git auth: deploy key scoped to
armistace/blog, private key mode600, 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 onblog'smaster. - 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 |