Box config: make /etc/lager writable by the box user, and fail loudly when a render fails#119
Conversation
There was a problem hiding this comment.
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.
| <<<<<<< 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. | ||
| ======= |
| 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 |
| <<<<<<< HEAD | ||
| __version__ = '0.31.11' | ||
| ======= | ||
| __version__ = '0.31.10' | ||
| >>>>>>> origin/main |
… when a render fails
d7fdb41 to
1623a7e
Compare
Problem
lager box config applyreported success while applying nothing. Adding a pip package and applying it printedApplied box config on <ip>, exited 0, stamped the applied-hash — and the package was not installed. Re-running then printedConfig 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:/etc/lagerlager install33:33, mode755(owner-only)lager updatechmod 777(world-writable)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
applyrunsstart_box.shon 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/lagerfails every render withEACCES.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:
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/lageris now33:<box-user-group>, mode2775(setgid), from bothlager installandlager update. Owner stayswww-data(uid 33) so the container is unaffected; the group is the box's login user, sostart_box.shcan write. This also removes thechmod 777: world-write was never needed, and that directory holdsbox_config.json,saved_nets.jsonand the org secrets.lager updaterepairs existing boxes in place — no reinstall.The narrow two-argument
chown/chmodforms are deliberate: the sudoers spec ischown * /etc/lager, which matches exactly one argument before the path, sochown -R ...would not match it. Only the directory's own group and mode matter — the renderers andstore_build_hashreplace 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 intobox_config/config.pyaswrite_atomic(), which reports which file could not be written, why, and how to repair it.start_box.shthen exits 3 — a new code meaning the container is up, but the config on disk was not applied to it.applytreats 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-containerpip/cargo/npminstall failures now exit 3 for the same reason: they run afterdocker runand already reported "container is up but ... may be incomplete", while exiting 1 and triggering a rollback of a healthy container.Unrelated but adjacent:
lager_excepthookreadLAGER_HOST_MODULE_FOLDERwithos.environ[...]. That variable is set bylager python; anything else that execs a script into the container leaves it unset, so the exception handler raisedKeyError. Python then printedError in sys.excepthook:plus a traceback from inside lager, and only then the user's real exception — so a one-lineModuleNotFoundErrorarrived 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 onmainand touch none of these files.)Hardware, full A/B on PRD-1:
start_box.shapplyAlso verified after the change that
lager updateleaves/etc/lagerasdrwxrwsr-x www-data:<box user>(not777), and that renders, the in-container installs, andstore_build_hashall 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.