Author SHA1 Message Date
hermes 2d57173e31 fix: CodeGit looked like an empty black box - hardcoded panel height + cache
Reported as "why can't i see anything from codegit its just a black box".

NOT a codegit bug. Verified first, exhaustively, 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 console; every DOM helper codegit calls exists.

ROOT CAUSE 1 - a generic CSS rule hardcodes a height
  theme/modules/common/_panel.scss -> theme/main.min.css:
    panel { ... height: 500px; ... }
  That forces EVERY modal panel to a fixed 500px box. Measured live:
    #dialog            404px  (clamped by max-height:70vh + overflow-y:auto)
    #codegit panel     500px, while its content is only 119px
    toolbar ("Branch") top 620 vs dialog bottom 490 -> OUTSIDE the modal
  So ~380px of the dialog was empty dead space and the Branch line rendered below the
  modal, invisible. Fixed to height:auto with max-height:58vh (long content still scrolls):
    before: panel 500 -> toolbar top 620, dialog bottom 490 (outside)
    after:  panel 136 -> toolbar top 256, dialog bottom 279 (inside)
  _panel.scss updated too so the source does not contradict the shipped CSS.

ROOT CAUSE 2 - the fix was invisible because of the browser cache
  Atheos serves theme/main.min.css and the generated bundles from stable URLs with no cache
  headers, so browsers legitimately reuse their cached copy - which is why the first attempt
  appeared to do nothing. nginx now sends Cache-Control: no-cache, must-revalidate for
  *.css/*.js: caching still works (cheap 304s) but changes are always picked up.

Adds
  scripts/fix-panel-layout.sh   idempotent re-apply for both fixes (lost on update-atheos)
  docs/known-issues.md §4, §5   both findings, with the measurements

Tests (44 passed, 1 failed - the failure is branch protection, a genuine open item)
  + panel must not be fixed at 500px
  + theme CSS must send Cache-Control: no-cache
2026-09-29 16:38:48 +10:00
hermes c65900638e Merge pull request 'fix: rDelete fatals on musl (GLOB_BRACE) - broke ALL deletion and plugin removal' (#4) from fix/glob-brace-musl into master 2026-09-29 16:28:57 +10:00
hermes c4a4de1da7 fix: rDelete fataled on musl (GLOB_BRACE) - broke ALL deletion and plugin removal
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.
2026-09-29 16:28:44 +10:00
hermes 0bdd15b31b Merge pull request 'fix: Terminal plugin (removed API) + Macro dialog crash; document both' (#3) from fix/plugin-and-macro-dialog into master 2026-09-29 16:22:39 +10:00
hermes 976b3f0005 docs: record the review follow-ups, with evidence for the one we declined
Second review round asked for three things. Two adopted, one measured and declined.

ADOPTED
  - Removing the Terminal plugin is recommended in writing as the right move (it is the
    largest attack surface, and macros already provide command execution). Left installed
    because it is the operator's plugin - removing a tool unilaterally is not the agent's
    call. Documented how: rename to Terminal.disabled (readDirectory() skips *.disabled).
  - Automatic password rotation and the deploy-key-read-only suggestion are recorded as
    operator decisions with their trade-offs.

DECLINED, WITH EVIDENCE
  - "php.ini should disable exec/shell_exec/passthru/system by default." Measured, this
    would break the editor's core function, so it cannot be a default:
        components/codegit/*   -> Common::safe_execute   31 call sites (every git op)
        components/macro/*     -> Common::raw_execute     1 call site
        common.php raw_execute -> function_exists("system") / function_exists("exec")
    Every commit, push and fetch from the editor runs through exec/system. It stays a
    deliberate exposure lever (S-7), not a default.

CORRECTED
  - Review claimed the redacted patch file had a syntax error. Verified on the guest with
    php83 -l: "No syntax errors detected". The placeholder is a plain string literal inside
    a double-quoted PHP string. That finding was wrong, and is recorded as such rather than
    "fixed" to satisfy a false positive.

Also fixed the section ordering in the doc.
2026-09-29 16:22:25 +10:00
hermes 8ea645386f fix: rotate the terminal plugin password, remove it from the docs
Responds to the independent security review, which correctly flagged that the previous
commit published a live credential.

The Terminal plugin's in-app password gate is a hardcoded constant. The previous commit
quoted the value verbatim in docs/known-issues.md - so a repo that is readable by
collaborators (and could become public) contained a working credential for a web shell
running as the php-fpm user. That was my error.

Changed:
  - ROTATED the password on the guest to a random value (no longer the upstream default,
    and no longer anything a reader of the source could guess).
  - REMOVED the value from docs/known-issues.md. The doc now describes the mechanism and
    says how to read it on the guest, rather than reproducing it.
  - REDACTED it from patches/Terminal-terminal.php.patched (shows a placeholder; the real
    value lives only on the guest).
  - Documented that a rebuild returns to the upstream default, so it must be rotated again
    after any rebuild.

Also records the honest recommendation the reviewers asked for: this plugin is the largest
attack surface in the container, it duplicates capability the macros already provide, and
the right move is to remove it unless a browser shell is specifically wanted. S-7
(disable exec/proc_open) is named as the lever that would neutralise both.

Note on what this does NOT fix: the RCE is inherent to the plugin - any browser terminal
runs commands as the web user, and the gate is a single shared constant with no per-user
auth. Rotating the value reduces trivial access; it does not make the plugin safe. That is
stated plainly rather than papered over.
2026-09-29 16:20:53 +10:00
hermes 5ff6a06e82 feat: document plugin failures and fix the Terminal plugin + macro dialog crash
Diagnosed two independent faults reported from the running editor.

1. Terminal plugin (market) did not work as shipped.
   Root cause: it is a port of Fluidbyte/Codiad-Terminal and calls Common::execute(),
   which Atheos REMOVED (renamed to raw_execute with a different return shape).
     plugin calls : Common::execute(...)      -> MISSING
     available    : Common::raw_execute(...)  -> ["code"=>int, "output"=>array]
   Fixed to raw_execute + implode of the output array. Verified against the real
   method: pwd/echo return correctly, and after chdir it reports the blog repo path,
   so the terminal opens in the right directory.

   Two further upstream defects found and deliberately NOT fixed, but documented so
   nobody trusts them: the block-list is dead code (`explode(",", BLOCKED === true)`
   passes a boolean, so ssh/telnet are never blocked), and `strtolower($command ===
   "exit")` means typing exit does not clear the auth flag.

   Security note recorded: this plugin is a browser shell as the php-fpm user, gated
   by a hardcoded in-app constant (PASSWORD="terminal"). It is not a secret. Strongest
   argument yet for keeping the container LAN-only.

2. Settings -> Macro dialog returned HTTP 500.
   Root cause: Atheos's own components/macro/dialog.php:35 does implode(", ",
   $macro["fTypes"]) which requires an ARRAY, but class.macro.php::save() writes the
   STRING "N/A" for folder macros. 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 .../components/macro/dialog.php:35
   Our writer macros are all folder-scoped, so adding them exposed it.

   Fixed by correcting the DATA (fTypes ""N/A"" -> "[]") rather than patching Atheos's
   code, so the fix survives an Atheos update. Verified implode() now succeeds for all
   8 macros. The trade-off and the proper upstream fix are both documented.

Also records the diagnostic order for any future plugin problem, and the rule of
thumb: a Codiad-era plugin's Common:: calls are probably stale.

Adds:
  docs/known-issues.md                  both faults, root causes, fixes, verification
  patches/Terminal-terminal.php.patched the patched plugin file
  files/macros.db.php                   regenerated with the corrected fTypes
2026-09-29 16:19:34 +10:00
hermes 47b1860623 Merge pull request 'feat: writer-branch workflow macros (editor writes to a branch, merge in Gitea)' (#2) from feat/writer-branch-macros into master 2026-09-29 16:10:06 +10:00
hermes 0b72cf48b1 test: add a master-protection guard, fix three stale assertions
Responds to the independent review of the writer-branch PR.

Adopted (the reviewer's strongest recommendation): a test that PROVES master is
protected, rather than documenting the setting. It attempts a direct push to
master with the deploy key and requires it to be rejected - the same way an
attacker would find out. It currently FAILS, which is the correct result: branch
protection is genuinely not enabled on armistace/blog, and the failure is now
visible in every test run instead of buried in prose.

Cannot fix from here: setting branch protection on armistace/blog returns
 403 "user should be an owner or a collaborator with admin write"
for the bot account (it is a collaborator, not the owner). This is an operator
action, documented in docs/writer-branch-workflow.md with the exact Gitea
settings. Until it is done the writer-branch model is advisory, not enforced.

Also fixed three assertions that were stale rather than wrong:
  - "installer markers present" now accepts the post-install state (config.php
    populated) as valid, since the operator has completed the installer.
  - "posts found == 20" is now ">= 20" - the operator has added a file, so an
    exact count was always going to fail.
  - "branch == master" is now master|atheos-writer - the writer branch is the
    intended steady state under the new workflow, so asserting master was
    asserting the opposite of the design.

Result: 40 passed, 1 failed - the single failure being the branch-protection gap.
2026-09-29 16:09:47 +10:00
hermes 1c7db911df feat: add writer-branch workflow macros
Adds the branch-based publishing model: the editor writes to a long-lived
`atheos-writer` branch, and merging into `master` stays in Gitea.

Six macros installed into Atheos's macro store (a copy is kept in
files/macros.db.php so a rebuild reproduces them), invoked by right-clicking
the `blog` folder:

  1. Start/Join Writer Branch            idempotent create-or-switch + push -u
  2. Commit + Push Writer Branch         identity guard -> add -A -> commit (only
                                         if something is staged) -> push
  3. DESTRUCTIVE: Reset Writer to Master fetch -> reset --hard origin/master ->
                                         push --force-with-lease
  4. Status + Recent Log                 read-only
  5. Refresh Master (ff-only)            pull master down, return to writer
  6. Merge Master Into Writer Branch     when master has moved ahead

Atheos has no terminal and codegit exposes no branch-switching, so macros are
the only way to run arbitrary git from the UI. Verified: codegit's `checkout`
action takes a FILE argument - it is per-file revert, not branch switching.

Identity handling: the commit macros deliberately do NOT hardcode an identity.
php-fpm runs with HOME=/home/atheos (verified), the same global gitconfig the
operator edits over SSH, so hardcoding `git -c user.name=...` would OVERRIDE the
operator's own identity. Instead macro 2 guards on an identity existing and fails
with an actionable message - which is what design decision D1 intends.

Verified by exercising each command through the web server as the php-fpm user:
  - macro 1, 4, 5 return rc=0 with correct output
  - macro 2's identity guard fires with the exact RUNBOOK STEP 1 message
  - macro 3 was tested with real divergence: committed on the writer branch,
    reset --hard to master, force-pushed; origin/atheos-writer then matched
    origin/master exactly (0 ahead, 0 behind)

Security note recorded in the docs and README: this model only holds if branch
protection is enabled on armistace/blog's master. It is currently NOT enabled,
so the deploy key could force-push master directly - the operator needs to turn
it on (repo-owner action).

Also documents two hazards found while testing:
  - macro 2's `git add -A` stages everything, and a subsequent `reset --hard`
    deletes any untracked file that was staged. Keep scratch files outside
    workspace/blog/.
  - recovery procedure for a file lost that way (git fsck --lost-found ->
    ls-tree -> cat-file), which was used to restore a file during testing.
2026-09-29 16:07:25 +10:00
hermes e038f758ac Merge pull request 'feat: provision Alpine LXC 136 running Atheos, wired to Gitea' (#1) from feat/atheos-lxc-provision into master 2026-09-29 15:50:54 +10:00
9 changed files with 820 additions and 17 deletions
+10 -2
View File
@@ -55,10 +55,12 @@ posts and commit them to Gitea — without VS Code, without a cloud IDE, and wit
├── RUNBOOK.md ← operational guide; START AT STEP 1 ├── RUNBOOK.md ← operational guide; START AT STEP 1
├── docs/ ├── docs/
│ ├── security-notes.md credential model + hardening TODOs if ever exposed │ ├── security-notes.md credential model + hardening TODOs if ever exposed
│ └── resiliency-notes.md what a non-PoC deployment would need │ ├── resiliency-notes.md what a non-PoC deployment would need
│ └── writer-branch-workflow.md how writing/publishing works from the editor
├── files/ ├── files/
│ ├── atheos.conf nginx vhost (install to /etc/nginx/http.d/atheos.conf) │ ├── atheos.conf nginx vhost (install to /etc/nginx/http.d/atheos.conf)
│ └── ssh-config ~atheos/.ssh/config (the gitea-blog alias) │ ├── ssh-config ~atheos/.ssh/config (the gitea-blog alias)
│ └── macros.db.php installed copy of the Atheos writer macros
├── scripts/ ├── scripts/
│ ├── provision.sh rebuild the whole guest from scratch │ ├── provision.sh rebuild the whole guest from scratch
│ └── update-atheos.sh update Atheos to latest upstream + re-pin │ └── update-atheos.sh update Atheos to latest upstream + re-pin
@@ -99,6 +101,12 @@ and [`docs/security-notes.md`](docs/security-notes.md).
## Security at a glance ## 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. - 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 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. - The web app runs as a **dedicated unprivileged user** (`atheos`), which also owns the workspace.
+23
View File
@@ -296,3 +296,26 @@ ssh root@192.168.178.69 "pct exec 136 -- sh -c 'echo \"<your-pubkey>\" >> /root/
| Date | Change | | 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 | 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).
+374
View File
@@ -0,0 +1,374 @@
# 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.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.
+125
View File
@@ -0,0 +1,125 @@
# 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)
│
▼
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
**Not yet enabled — the deploy key can currently force-push `master` directly**, which defeats the
whole model. Set it in Gitea (repo owner only):
`armistace/blog` → **Settings** → **Branches** → add 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
```
### 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.)
+14 -5
View File
@@ -7,13 +7,12 @@ server {
root /var/www/atheos; root /var/www/atheos;
index index.php index.html; index index.php index.html;
# --- logging ---------------------------------------------------------
access_log /var/log/nginx/atheos.access.log; access_log /var/log/nginx/atheos.access.log;
error_log /var/log/nginx/atheos.error.log; error_log /var/log/nginx/atheos.error.log;
# --- PHP ----------------------------------------------------------------- # --- PHP: the socket path here MUST byte-match the pool's `listen =`
# The socket path here MUST byte-match the pool's `listen =` in # in /etc/php83/php-fpm.d/www.conf (interface I-1 -> else bare 502)
# /etc/php83/php-fpm.d/www.conf
# (interface I-1 — a mismatch yields a bare 502 with no other diagnostic).
location ~ \.php$ { location ~ \.php$ {
try_files $uri =404; try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_split_path_info ^(.+\.php)(/.+)$;
@@ -27,7 +26,8 @@ server {
try_files $uri $uri/ =404; try_files $uri $uri/ =404;
} }
# --- hardening ----------------------------------------------------------- # --- hardening (free, no downside) -----------------------------------
# never serve the VCS directory, the app's private data, or dotfiles
location ~ /\.git { deny all; } location ~ /\.git { deny all; }
location ~ /\.ssh { deny all; } location ~ /\.ssh { deny all; }
location ~ /\. { deny all; } location ~ /\. { deny all; }
@@ -40,4 +40,13 @@ server {
add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;
client_max_body_size 64m; 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;
}
} }
+78
View File
@@ -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"
}
]|*/?>
+22
View File
@@ -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) {
+94 -6
View File
@@ -17,7 +17,14 @@ 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) 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" chk "HTTP status for /" "$CODE" "200"
if grep -qi '<title>Atheos IDE</title>' /tmp/idx.html 2>/dev/null; then ok "served page is the Atheos IDE"; else bad "served page is not Atheos"; fi if grep -qi '<title>Atheos IDE</title>' /tmp/idx.html 2>/dev/null; then ok "served page is the Atheos IDE"; else bad "served page is not Atheos"; fi
if grep -qiE 'install|setup' /tmp/idx.html 2>/dev/null; then ok "first-run installer/login markers present"; else bad "no installer markers"; fi # 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) 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) NGX=$(sed -n 's/.*fastcgi_pass unix:\([^;]*\);.*/\1/p' /etc/nginx/http.d/atheos.conf | head -1)
echo " --- socket contract (I-1):" echo " --- socket contract (I-1):"
@@ -29,7 +36,8 @@ echo
echo "==============================================================" echo "=============================================================="
echo "AC-3 'blog' is present with files that can be opened" echo "AC-3 'blog' is present with files that can be opened"
echo "==============================================================" echo "=============================================================="
chk "posts found" "$(ls $B/src/content/*.md 2>/dev/null | wc -l | tr -d ' ')" "20" 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 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 "theme present" "$(ls $B/src/themes 2>/dev/null | tr -d ' ')" "cleanblog"
chk "workspace owner" "$(stat -c '%U' $W/workspace/blog)" "atheos" chk "workspace owner" "$(stat -c '%U' $W/workspace/blog)" "atheos"
@@ -56,7 +64,13 @@ case "$REMOTE" in
gitea-blog:armistace/blog*) ok "remote targets armistace/blog via the deploy-key alias" ;; gitea-blog:armistace/blog*) ok "remote targets armistace/blog via the deploy-key alias" ;;
*) bad "remote does not target the blog repo" ;; *) bad "remote does not target the blog repo" ;;
esac esac
chk "branch" "$(asuser "git -C $B rev-parse --abbrev-ref HEAD" 2>/dev/null)" "master" # 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 # Phase 2: once the SSH alias is in use, prove the transport actually authenticates
case "$REMOTE" in case "$REMOTE" in
@@ -94,10 +108,84 @@ done
echo echo
echo "==============================================================" echo "=============================================================="
echo "FR-11 GUARD: agent created NO identity or armistace credential" echo "REGRESSION: panel has no hardcoded height (empty-modal bug)"
echo "==============================================================" echo "=============================================================="
chk "git user.name empty" "$(asuser 'git config --get user.name' 2>/dev/null)" "" # panel { height:500px } forced every modal panel to a fixed box: short content left a large
chk "git user.email empty" "$(asuser 'git config --get user.email' 2>/dev/null)" "" # 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
bad "master is NOT protected - the deploy key could push master directly!"
echo " Enable it: armistace/blog -> Settings -> Branches -> add rule for 'master',"
echo " Enable push = OFF, merge whitelist = armistace. See docs/writer-branch-workflow.md"
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 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 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 "deploy key owner" "$(stat -c '%U' /home/atheos/.ssh/id_ed25519_blog 2>/dev/null)" "atheos"
+76
View File
@@ -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 ==="