Shebang'd files ship executable; update.sh heals existing installs (task 21 hotfix)

First field bug of v0.1-alpha: install.py was committed 100644, so
every install shipped it permission-denied for direct ./install.py use
(start.sh's python3 invocation masked it). Exec bit set on all four
shebang'd files (install.py, board.py, hook_settings.py,
permission_config.py) and install.py added to update.sh's post-update
chmod line so already-broken installs heal on their next update. The
artifact-side invariant test — every shipped #! file executable — is
card 21.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
istos
2026-07-30 08:50:08 +02:00
co-authored by Claude Fable 5
parent e12d5b1a85
commit ead190ac03
6 changed files with 100 additions and 1 deletions
Regular → Executable
View File
View File
View File
Regular → Executable
View File
@@ -0,0 +1,99 @@
# 21 — A shebang means executable: fix the shipped modes and test the invariant
**Status:** Backlog
**Priority:** High — first external-install bug report: ./install.py is permission-denied on every install of v0.1-alpha
**Type:** Bug
`install.py` is committed mode 100644 — the one shebang'd top-level
file that isn't executable. The artifact inherits repo modes, so every
install ships a non-runnable `./install.py`; a downstream user hit
permission-denied, chmod'd locally, and correctly reported that the
next `update.sh` will clobber their fix back. A sweep found three more
shebang'd-but-non-executable files (`board.py`,
`claude/hook_settings.py`, `opencode/permission_config.py`) — harmless
today because they are invoked via `python3 …`, but the same lie. The
invariant to enforce: any shipped file whose first two bytes are `#!`
carries the exec bit.
## Context
- Field report (2026-07-30, first external install): `./install.py`
permission denied; root-caused downstream to the repo mode, fixed
locally, flagged that update.sh would revert it — so the fix
belongs at the source.
- `update.sh:165` already repairs modes after every update
(`chmod +x start.sh stop.sh update.sh`) — `install.py` is missing
from the list, so existing installs (which cp from artifacts that
carried the bad mode) never heal.
- `release.sh` tars straight from the repo; `tests/test_release_artifact.py`
asserts contents against the manifest but says nothing about modes —
the missing check that would have caught this before v0.1-alpha.
- Precedent for the mode-loss failure class: release.sh itself lost
its exec bit once already (restored during PR #11's resolution) and
the opencode run/wire needed `git add --chmod=+x` because the
building agent couldn't chmod. Modes are bench's recurring blind
spot; the test is the cure, not vigilance.
**Affected areas:** repo file modes, `update.sh` (repair line),
`tests/test_release_artifact.py`.
## What to build
- Repo modes: exec bit on all four shebang'd files (the two hotfix
lines — repo chmod + update.sh repair list gaining `install.py`
may already be landed by the time this card is worked; verify
rather than redo).
- The invariant test, in the artifact suite: every member of the
built tarball whose content starts `#!` has the executable bit in
its tar header; fail naming the file. This covers all future
scripts automatically — adapters' run/wire included.
- A second assertion the field report implies: the update round-trip
test verifies `install.py` is executable after an update applied to
an install where it wasn't — proving the repair line heals existing
victims, not only fresh installs.
- Decide the patch-release question: v0.1-alpha ships the bad mode;
either cut v0.1-alpha.1 from the fixed tree or note in the release
that `./start.sh` (which invokes install.py via python3) is
unaffected and the next release heals it. Cutting the patch is
cheap and the honest move.
**Out of scope** — tempting neighbours left alone:
- A general lint pass over the repo; the invariant is scoped to what
ships in the artifact.
- Windows-style mode handling — tar + POSIX bits are the contract.
## Acceptance
- [ ] Given the v0.1-alpha artifact rebuilt from the fixed tree, when
the README one-liner runs, then `./install.py` executes directly.
- [ ] Given an existing install with non-executable install.py, when
`update.sh` applies any release from the fixed tree, then
install.py is executable afterwards.
- [ ] The artifact test fails if any shipped `#!`-file lacks the exec
bit — demonstrated by a deliberate local mode-strip before
committing the test.
- [ ] Edge case — a shebang'd file legitimately meant only for
`python3 x.py` invocation: there are none; the invariant is
absolute so the test needs no exception list, and gaining one
in future requires editing the test with a reason.
## Open questions
- None.
## Notes
Reported from the field within hours of v0.1-alpha — the release's
first bug is a fitting one: the artifact pipeline's whole promise is
"correct by construction", and modes were the one property nothing
constructed. The reporter's own diagnosis (repo bit, repair line,
shebang test) survives here nearly verbatim; good bug reports deserve
that.
**Risks**
- Git preserves only the exec bit, not full modes — the test must
check the tar header's mode, not assume repo mode equals artifact
mode forever (release.sh could gain umask surprises on other
machines).
+1 -1
View File
@@ -162,7 +162,7 @@ while read -r kind path _; do
mkdir -p "$TM/$(dirname "$path")"
cp "$dist/$path" "$TM/$path"
done < "$manifest"
chmod +x "$TM"/start.sh "$TM"/stop.sh "$TM"/update.sh 2>/dev/null || true
chmod +x "$TM"/start.sh "$TM"/stop.sh "$TM"/update.sh "$TM"/install.py 2>/dev/null || true
find "$TM/manager/core/adapters" -name run -o -name wire | xargs chmod +x 2>/dev/null || true
after="$(cat "$TM/manager/core/VERSION" 2>/dev/null || echo '?')"