fix(image): add /usr/bin/env so shebang scripts exec in the container #89

Merged
weiwen merged 1 commit from fm/evie-usr-bin-env-image into main 2026-08-05 03:44:52 +08:00
Owner

Root cause

nix dockerTools images only contain the store paths listed in contents plus whatever
extraCommands creates, so /usr/bin never existed in evie:latest. The kernel resolves a
shebang interpreter by absolute path, so any script starting with #!/usr/bin/env ... fails to
exec with ENOENT before the interpreter is ever consulted.

The scheduled briefings run exactly such scripts on the daemon/sudo path
(argv=["run", ".../prompts/morning-briefing.py"], first line #!/usr/bin/env -S uv run --script),
so on every fire the daemon logged:

Firing schedule name=evening-review ...
Schedule run spawn failed ... error=No such file or directory (os error 2)

The scripts, uv, python3 and chromium were all present in the image. Only /usr/bin/env was
missing. The NixOS host provided it, which is why this only broke after the Docker cutover.

Fix

eviedaemonImage's extraCommands now creates /usr/bin/env as a symlink to coreutils' env
(a multi-call binary that dispatches on argv0, so an env-named link behaves as env).

Verification

nix build .#eviedaemonImage succeeds; the symlink lands in the customisation layer:

$ ls -l $layer/usr/bin/
env -> /nix/store/...-coreutils-9.11/bin/env

Loaded the built image and ran a real uv run --script shebang script inside it:

$ ./result | docker load
$ docker run --rm --entrypoint /bin/sh -v ./shebang-check.py:/tmp/shebang-check.py:ro evie:latest \
    -c '... /shebang-check.py'
lrwxrwxrwx 1 root root 66 Jan  1  1980 /usr/bin/env -> /nix/store/...-coreutils-9.11/bin/env
env (GNU coreutils) 9.11
shebang exec OK: /usr/bin/env -S uv run --script

where shebang-check.py begins #!/usr/bin/env -S uv run --script. Before this change the same
invocation is the ENOENT seen in production.

just check is green (clippy -D warnings + 490 tests).

Deploy note

A temporary /usr/bin/env bind-mount hotfix is currently live in ~/evie-docker-deploy keeping
the schedules working. It can be retired once this image lands and is redeployed.

Follow-up (out of scope here)

The bwrap sandbox binds only /nix, so sandboxed children still have no /usr/bin/env. That is a
separate surface (sandbox_setenv_args / bind args in src/pi.rs) and was deliberately not
touched. Worth a follow-up if any sandboxed script relies on that shebang.

## Root cause nix `dockerTools` images only contain the store paths listed in `contents` plus whatever `extraCommands` creates, so `/usr/bin` never existed in `evie:latest`. The kernel resolves a shebang interpreter by absolute path, so any script starting with `#!/usr/bin/env ...` fails to exec with ENOENT before the interpreter is ever consulted. The scheduled briefings run exactly such scripts on the daemon/sudo path (`argv=["run", ".../prompts/morning-briefing.py"]`, first line `#!/usr/bin/env -S uv run --script`), so on every fire the daemon logged: ``` Firing schedule name=evening-review ... Schedule run spawn failed ... error=No such file or directory (os error 2) ``` The scripts, `uv`, `python3` and `chromium` were all present in the image. Only `/usr/bin/env` was missing. The NixOS host provided it, which is why this only broke after the Docker cutover. ## Fix `eviedaemonImage`'s `extraCommands` now creates `/usr/bin/env` as a symlink to coreutils' `env` (a multi-call binary that dispatches on argv0, so an `env`-named link behaves as `env`). ## Verification `nix build .#eviedaemonImage` succeeds; the symlink lands in the customisation layer: ``` $ ls -l $layer/usr/bin/ env -> /nix/store/...-coreutils-9.11/bin/env ``` Loaded the built image and ran a real `uv run --script` shebang script inside it: ``` $ ./result | docker load $ docker run --rm --entrypoint /bin/sh -v ./shebang-check.py:/tmp/shebang-check.py:ro evie:latest \ -c '... /shebang-check.py' lrwxrwxrwx 1 root root 66 Jan 1 1980 /usr/bin/env -> /nix/store/...-coreutils-9.11/bin/env env (GNU coreutils) 9.11 shebang exec OK: /usr/bin/env -S uv run --script ``` where `shebang-check.py` begins `#!/usr/bin/env -S uv run --script`. Before this change the same invocation is the ENOENT seen in production. `just check` is green (clippy `-D warnings` + 490 tests). ## Deploy note A temporary `/usr/bin/env` bind-mount hotfix is currently live in `~/evie-docker-deploy` keeping the schedules working. It can be retired once this image lands and is redeployed. ## Follow-up (out of scope here) The bwrap sandbox binds only `/nix`, so sandboxed children still have no `/usr/bin/env`. That is a separate surface (`sandbox_setenv_args` / bind args in `src/pi.rs`) and was deliberately not touched. Worth a follow-up if any sandboxed script relies on that shebang.
fix(image): add /usr/bin/env so shebang scripts exec in the container
All checks were successful
CI / check (pull_request) Successful in 1m43s
PR Triage — label changes-requested reviews / triage-review (pull_request) Successful in 1s
fe9f433c21
nix dockerTools does not populate /usr/bin, so any script whose shebang is
`#!/usr/bin/env ...` failed to exec with ENOENT once the daemon moved into the
Docker image. The scheduled briefing prompts (`#!/usr/bin/env -S uv run
--script`) are on that path, and every fire logged "Schedule run spawn failed
... No such file or directory (os error 2)".
weiwen merged commit 35d986a630 into main 2026-08-05 03:44:52 +08:00
weiwen deleted branch fm/evie-usr-bin-env-image 2026-08-05 03:44:52 +08:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
weiwen/evie!89
No description provided.