The fatal kills the request beforeCommon::send(200), so the front end never receives a settle
response — hence a hang, which is exactly what "cycles in the UI" looks like.
Blast radius — measured, not guessed
Caller
What was broken
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. Deleting files in the editor was broken this whole time.
Installing the Terminal plugin is merely what made it visible.
Fix
patches/helpers-rDelete-GLOB_BRACE.diff — expand the brace pattern by hand when GLOB_BRACE is
absent, keeping upstream's behaviour where it is defined:
Upstream has NOT fixed this — the line is still in main (checked directly), so updating Atheos
does not resolve it. Hence a regression test in this PR that fails loudly if the patch is lost.
Terminal plugin removed
Once deletion worked, removal through the market succeeded:
This closes the review's strongest recommendation (a browser shell was the largest attack surface, and
macros already provide command execution). Done via the market's own remove() path so the plugin
list, addons cache and generated bundle all stayed consistent.
Test changes
+ GLOB_BRACE regression guard — deletes a nested tree with dotfiles; fails with an explicit
"re-apply the patch" message if the constant is referenced again.
The identity guard is INVERTED. FR-11 required an empty identity so the agent could not
impersonate the operator. The operator has since explicitly asked for it to be set, so the old test
was asserting the opposite of the current design and failing forever. It now requires the
identity to be present and to not look agent-generated.
Result: 42 passed, 1 failed — the single failure being branch protection on armistace/blog,
which is a genuine, still-open operator action and is self-reporting.
Honest note on the platform choice
This is the real cost of Alpine. A glibc base image (Debian/Ubuntu) would not have hit it. It is
patchable in one place, so Alpine stays — but GLOB_BRACE is the kind of thing musl breaks, and it
is now documented as such rather than presented as a plugin quirk.
Test plan
php83 -l clean on the patched file
Real deletion verified: nested dirs, dotfiles, and a single file
Plugin removal verified end-to-end through Market->remove()
Editor still serves HTTP 200 after the patch
Suite: 42 passed, 1 failed (branch protection)
## Summary
**"Deleting Terminal just cycles in the UI" turned out to be a platform bug that broke deletion
everywhere** — not a plugin bug.
### Root cause
`traits/helpers.php::rDelete()` called:
```php
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 receives a settle
response — hence a **hang**, which is exactly what "cycles in the UI" looks like.
### Blast radius — measured, not guessed
| Caller | What was broken |
|---|---|
| `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. **Deleting files in the editor was broken this whole time.**
Installing the Terminal plugin is merely what made it visible.
### Fix
`patches/helpers-rDelete-GLOB_BRACE.diff` — expand the brace pattern by hand when `GLOB_BRACE` is
absent, keeping upstream's 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) ?: []
);
}
```
**Upstream has NOT fixed this** — the line is still in `main` (checked directly), so updating Atheos
does not resolve it. Hence a **regression test** in this PR that fails loudly if the patch is lost.
## Terminal plugin removed
Once deletion worked, removal through the market succeeded:
```
{"text":"Plugin successfully removed.","status":200}
```
This closes the review's strongest recommendation (a browser shell was the largest attack surface, and
macros already provide command execution). Done via the market's **own `remove()` path** so the plugin
list, `addons` cache and generated bundle all stayed consistent.
## Test changes
- **+ GLOB_BRACE regression guard** — deletes a nested tree with dotfiles; fails with an explicit
"re-apply the patch" message if the constant is referenced again.
- **The identity guard is INVERTED.** FR-11 required an **empty** identity so the agent could not
impersonate the operator. The operator has since explicitly asked for it to be set, so the old test
was asserting the **opposite of the current design** and failing forever. It now requires the
identity to be present **and** to not look agent-generated.
**Result: 42 passed, 1 failed** — the single failure being branch protection on `armistace/blog`,
which is a genuine, still-open operator action and is self-reporting.
## Honest note on the platform choice
This is the real cost of Alpine. A glibc base image (Debian/Ubuntu) would not have hit it. It is
patchable in one place, so Alpine stays — but `GLOB_BRACE` is the kind of thing musl breaks, and it
is now documented as such rather than presented as a plugin quirk.
## Test plan
- [x] `php83 -l` clean on the patched file
- [x] Real deletion verified: nested dirs, dotfiles, and a single file
- [x] Plugin removal verified end-to-end through `Market->remove()`
- [x] Editor still serves HTTP 200 after the patch
- [x] Suite: 42 passed, 1 failed (branch protection)
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.
Publishing a credential (my error) - value rotated and removed from the repo in the previous PR.
Remove the Terminal plugin - done in this PR, through the market's own code path.
This PR additionally fixes a defect the reviewers did not see because it only manifests on musl: GLOB_BRACE is undefined there, so rDelete() fataled - which disabled all deletion in the
editor and made plugin removal hang forever. Fixed, with a regression test so it cannot silently
return after an update.
Still open and outside the agent's reach: branch protection on armistace/blog (403 - the bot is
a collaborator, not the owner). The test suite reports it on every run.
## Independent review (round 2 - incremental)
Both prior rounds' blocking items are now closed:
- **Publishing a credential** (my error) - value rotated and removed from the repo in the previous PR.
- **Remove the Terminal plugin** - done in this PR, through the market's own code path.
This PR additionally fixes a defect the reviewers did not see because it only manifests on musl:
`GLOB_BRACE` is undefined there, so `rDelete()` fataled - which disabled **all** deletion in the
editor and made plugin removal hang forever. Fixed, with a regression test so it cannot silently
return after an update.
Still open and outside the agent's reach: **branch protection on `armistace/blog`** (403 - the bot is
a collaborator, not the owner). The test suite reports it on every run.
hermes
merged commit c65900638e into master2026-09-29 16:28:57 +10:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
"Deleting Terminal just cycles in the UI" turned out to be a platform bug that broke deletion
everywhere — not a plugin bug.
Root cause
traits/helpers.php::rDelete()called:GLOB_BRACEis a glibc extension. Alpine is musl, where the constant is undefined — andreferencing an undefined constant is a fatal in PHP 8:
The fatal kills the request before
Common::send(200), so the front end never receives a settleresponse — hence a hang, which is exactly what "cycles in the UI" looks like.
Blast radius — measured, not guessed
components/market/class.market.php:178components/filetree/class.filetree.php:59The second is core editor functionality. Deleting files in the editor was broken this whole time.
Installing the Terminal plugin is merely what made it visible.
Fix
patches/helpers-rDelete-GLOB_BRACE.diff— expand the brace pattern by hand whenGLOB_BRACEisabsent, keeping upstream's behaviour where it is defined:
Upstream has NOT fixed this — the line is still in
main(checked directly), so updating Atheosdoes not resolve it. Hence a regression test in this PR that fails loudly if the patch is lost.
Terminal plugin removed
Once deletion worked, removal through the market succeeded:
This closes the review's strongest recommendation (a browser shell was the largest attack surface, and
macros already provide command execution). Done via the market's own
remove()path so the pluginlist,
addonscache and generated bundle all stayed consistent.Test changes
"re-apply the patch" message if the constant is referenced again.
impersonate the operator. The operator has since explicitly asked for it to be set, so the old test
was asserting the opposite of the current design and failing forever. It now requires the
identity to be present and to not look agent-generated.
Result: 42 passed, 1 failed — the single failure being branch protection on
armistace/blog,which is a genuine, still-open operator action and is self-reporting.
Honest note on the platform choice
This is the real cost of Alpine. A glibc base image (Debian/Ubuntu) would not have hit it. It is
patchable in one place, so Alpine stays — but
GLOB_BRACEis the kind of thing musl breaks, and itis now documented as such rather than presented as a plugin quirk.
Test plan
php83 -lclean on the patched fileMarket->remove()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.Independent review (round 2 - incremental)
Both prior rounds' blocking items are now closed:
This PR additionally fixes a defect the reviewers did not see because it only manifests on musl:
GLOB_BRACEis undefined there, sorDelete()fataled - which disabled all deletion in theeditor and made plugin removal hang forever. Fixed, with a regression test so it cannot silently
return after an update.
Still open and outside the agent's reach: branch protection on
armistace/blog(403 - the bot isa collaborator, not the owner). The test suite reports it on every run.