Skip to content

Box config: make /etc/lager writable by the box user, and fail loudly when a render fails#119

Merged
danielrmerskine merged 1 commit into
mainfrom
de/boxcfg-render-perms
Jul 14, 2026
Merged

Box config: make /etc/lager writable by the box user, and fail loudly when a render fails#119
danielrmerskine merged 1 commit into
mainfrom
de/boxcfg-render-perms

Conversation

@danielrmerskine

Copy link
Copy Markdown
Collaborator

Problem

lager box config apply reported success while applying nothing. Adding a pip package and applying it printed Applied box config on <ip>, exited 0, stamped the applied-hash — and the package was not installed. Re-running then printed Config unchanged since last apply; skipping restart, so the config could never be applied at all.

This affected every box. It was invisible because the two provisioning paths disagreed about who owns /etc/lager:

last provisioning step /etc/lager box config renders
lager install 33:33, mode 755 (owner-only) fail silently — nothing is applied
lager update chmod 777 (world-writable) work, but the directory is writable by every local account

A box whose last step was an update looked healthy. A freshly installed box silently applied nothing, and any box that had rendered files from an earlier era kept running that stale output — so the config on disk and the config in the container could drift indefinitely with no error anywhere.

Root cause

apply runs start_box.sh on the box as the login user. Its four box-config renderers create files in /etc/lager (box_config.docker.sh, user_requirements.txt, cargo_packages.txt, npm_packages.txt). Creating a file requires write permission on the directory, so an owner-only /etc/lager fails every render with EACCES.

Renders are soft-failed by design — the container must always come up — which is what turned a hard permission error into a silent no-op:

render fails (EACCES)  ->  rendered file never written
                       ->  `[ -s "$PIP_REQS_FILE" ]` is false
                       ->  the in-container install step is skipped
                       ->  start_box.sh exits 0
                       ->  apply reports success and stamps the applied-hash

Stamping the applied-hash is what sealed it shut: the retry after any fix short-circuits on "Config unchanged since last apply".

Fix

Ownership. /etc/lager is now 33:<box-user-group>, mode 2775 (setgid), from both lager install and lager update. Owner stays www-data (uid 33) so the container is unaffected; the group is the box's login user, so start_box.sh can write. This also removes the chmod 777: world-write was never needed, and that directory holds box_config.json, saved_nets.json and the org secrets. lager update repairs existing boxes in place — no reinstall.

The narrow two-argument chown/chmod forms are deliberate: the sudoers spec is chown * /etc/lager, which matches exactly one argument before the path, so chown -R ... would not match it. Only the directory's own group and mode matter — the renderers and store_build_hash replace files via tmp+rename, which needs permission on the directory, not on the file being replaced.

Fail loudly. A render failure used to print a one-line warning and a raw Python traceback. The four duplicated _write() helpers are hoisted into box_config/config.py as write_atomic(), which reports which file could not be written, why, and how to repair it. start_box.sh then exits 3 — a new code meaning the container is up, but the config on disk was not applied to it.

apply treats rc 3 as: print the box-side error, do not stamp the applied-hash, do not roll back. Rolling back is wrong here because the container is healthy and the fault is environmental — restoring the previous config and re-bouncing would fail identically. A genuine bounce failure (container possibly down) still rolls back exactly as before. The in-container pip/cargo/npm install failures now exit 3 for the same reason: they run after docker run and already reported "container is up but ... may be incomplete", while exiting 1 and triggering a rollback of a healthy container.

Unrelated but adjacent: lager_excepthook read LAGER_HOST_MODULE_FOLDER with os.environ[...]. That variable is set by lager python; anything else that execs a script into the container leaves it unset, so the exception handler raised KeyError. Python then printed Error in sys.excepthook: plus a traceback from inside lager, and only then the user's real exception — so a one-line ModuleNotFoundError arrived buried under a stack trace pointing at lager internals. With no host paths to rewrite, the traceback is now printed unchanged.

Validation

Unit: 1638 pass. (Three pre-existing failures on this checkout — test_box_authorize, test_lock_state, test_monitor_state — fail identically on main and touch none of these files.)

Hardware, full A/B on PRD-1:

before after, perms still broken after, perms repaired
start_box.sh exit 0, silent exit 3, names each file + the repair exit 0
apply "Applied box config", exit 0 exit 1, "NOT applied" "Applied box config"
applied-hash stamped (seals the bug) not stamped stamped
rollback none (container is healthy)
requested package not installed not installed, correctly reported installed

Also verified after the change that lager update leaves /etc/lager as drwxrwsr-x www-data:<box user> (not 777), and that renders, the in-container installs, and store_build_hash all still work.

Risk

The permission change is the only thing that touches existing boxes, and it runs through the sudoers grants that already exist. A box that has not yet been updated keeps working exactly as it does today, except that a failed apply is now loud instead of silent.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request addresses permission issues with /etc/lager by changing its ownership and mode to allow both the container and host users to write to it safely, preventing silent configuration render failures. It also improves error handling during configuration rendering (exiting with code 3 instead of rolling back healthy containers) and fixes a bug in the exception hook that buried actual errors under a KeyError. However, there are unresolved merge conflicts in CHANGELOG.md and cli/__init__.py that must be resolved before merging.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread CHANGELOG.md Outdated
Comment on lines +5 to +62
<<<<<<< HEAD
## [0.31.11] - 2026-07-13

### Fixed

- **`lager box config apply` no longer reports success while applying nothing.**
`apply` runs `start_box.sh` on the box as the login user, and its four
box-config renderers *create* files in `/etc/lager` (`box_config.docker.sh`,
`user_requirements.txt`, `cargo_packages.txt`, `npm_packages.txt`). Creating a
file needs write permission on the **directory**, and `lager install` left
`/etc/lager` owned by the container user only (`33:33`, mode `755`) — so every
render failed with `EACCES`. Renders are soft-failed by design (the container
must always come up), which turned this into a silent no-op: the install steps
read files that were never written, so `apply` skipped them, stamped the
applied-hash, and printed "Applied box config". Every `pip`/`cargo`/`npm`
package, mount, volume, and env var added through `lager box config` was
quietly dropped on any box whose last provisioning step was an install.
`/etc/lager` is now `33:<box-user-group>` mode `2775` (setgid), so the
container (owner) and `start_box.sh` (group) can both write it.

- **`/etc/lager` is no longer world-writable.** `lager update` granted the box
user write access by running `chmod 777` on the directory, which also gave it
to every other local account — enough to replace `box_config.json`,
`saved_nets.json`, or the org secrets. It now gets the same owner/group/setgid
treatment as above, which is what the two writers actually need and nothing
more. This is also why the breakage above was invisible for so long: an
updated box ended up world-writable and rendered fine, while a freshly
*installed* box was owner-only and silently applied nothing — the two paths
disagreed.

- **A box-config render failure is now loud, and no longer poisons the retry.**
A failed render printed a one-line warning and a raw Python traceback. It now
reports which file could not be written, why, and how to repair it.
`start_box.sh` exits `3` — a new code meaning "the container is up, but the
config on disk was not applied to it" — and `apply` neither stamps the
applied-hash nor rolls back on it. Not stamping the hash is the load-bearing
half: it previously did, so the retry after a fix short-circuited on "Config
unchanged since last apply; skipping restart" and the config could never be
applied. Rolling back is wrong here because the container is healthy and the
fault is environmental — restoring the previous config and re-bouncing would
fail identically. A genuine bounce failure (container possibly down) still
rolls back exactly as before. A failed in-container `pip`/`cargo`/`npm`
install now exits `3` for the same reason — those steps run *after*
`docker run`, so they already reported "container is up but ... may be
incomplete" while exiting `1` and triggering a rollback of a healthy
container.

- **An uncaught exception in a script run inside the box container no longer
buries the real error.** `lager_excepthook` read `LAGER_HOST_MODULE_FOLDER`
with `os.environ[...]` in order to rewrite host paths out of tracebacks. That
variable is set by `lager python`; anything else that execs a script into the
container leaves it unset, so the exception *handler* raised `KeyError` —
Python then printed "Error in sys.excepthook:" followed by a traceback from
inside lager, and only after that the user's actual exception. A one-line
`ModuleNotFoundError` arrived buried under a stack trace pointing at lager
internals. With no host paths to rewrite, the traceback is now printed
unchanged.
=======

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Merge conflict markers found in CHANGELOG.md. Please resolve the conflict and remove the markers.

Comment thread CHANGELOG.md Outdated
The box endpoint returned driver errors as 5xx, tripping the CLI's legacy
direct-USB fallback; they now come back as ordinary command failures so the
real message is shown.
>>>>>>> origin/main

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Merge conflict markers found in CHANGELOG.md. Please resolve the conflict and remove the markers.

Comment thread cli/__init__.py Outdated
Comment on lines +10 to +14
<<<<<<< HEAD
__version__ = '0.31.11'
=======
__version__ = '0.31.10'
>>>>>>> origin/main

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Merge conflict markers found in cli/init.py. Please resolve the conflict and remove the markers.

@danielrmerskine danielrmerskine force-pushed the de/boxcfg-render-perms branch from d7fdb41 to 1623a7e Compare July 14, 2026 03:15
@danielrmerskine danielrmerskine merged commit d0ed797 into main Jul 14, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant