What to build
Declare hal0 supports two distinct install modes — editable (dev) and tarball (production) — codify auto-detection, and make Updater.apply() + the dashboard's Settings → Updates surface behave correctly in each.
Why this exists
PR #386 fixed the Settings → Updates apply button (it now polls status and the extract step quarantines stale dirs). On the way to that fix we discovered LXC 105 carries BOTH layouts side-by-side:
| Path |
Role |
Used by |
/opt/hal0/ |
git checkout + pip install -e . venv |
systemd hal0-api.service |
/usr/lib/hal0/current → hal0-<v>/ |
Updater.apply() extract target |
nothing on this host |
Self-update on this LXC extracts the new release, swaps the current symlink, logs swap_ok — and the running editable venv stays on the old version because systemd points at /opt/hal0. The button "works" but the version stays stuck. End users on a clean curl … | bash install don't hit this because they only have the tarball layout. We need explicit, audited support for both modes.
Two modes — definitions
| Mode |
Source of truth |
Update mechanism |
Signing |
Who has it |
| editable |
/opt/hal0/ git checkout + pip install -e . venv |
git pull && pip install -e . && systemctl restart hal0-api hal0-lemonade |
trust your git remote |
contributors, LXC 105 |
| tarball |
/usr/lib/hal0/current → hal0-<v>/ symlink |
Updater.apply() — atomic symlink swap with sigstore-verified tarball |
full cosign chain |
end users from install.sh |
Auto-detection
Updater should detect mode at startup and cache the result. Heuristic (in priority order):
/opt/hal0/.git/ exists AND the running interpreter's *.dist-info reports Editable project location → editable.
/usr/lib/hal0/current symlink exists and resolves under /usr/lib/hal0/ → tarball.
- Neither → raise
system.install_mode_undetected; surface as a banner.
Don't gate on an env var or marker file unless the heuristic produces a false positive in the wild (file a follow-up if so).
What changes per mode
editable mode:
Updater.apply() raises a typed system.update_unsupported_in_editable_mode envelope with details.upgrade_hint = "git pull && pip install -e . && systemctl restart hal0-api hal0-lemonade".
- Dashboard Settings → Updates renders a banner explaining this is a dev install and shows the hint. The Install button is replaced with a "Copy upgrade command" affordance.
/api/updates/state includes install_mode: "editable" so the dashboard can branch without re-probing.
/api/updates/check still runs (so the user can see what version they would be on if they were on tarball mode).
tarball mode:
- Today's behavior is correct; no code changes.
/api/updates/state includes install_mode: "tarball".
install.sh impact
- Default: produce tarball layout (no change).
- New
--dev / --editable flag: clone the git repo to /opt/hal0, run pip install -e ., drop a .hal0-install-mode = editable marker file, point systemd at /opt/hal0/.venv/bin/hal0.
- Refuse silently to mix the two — if
/opt/hal0/.git and /usr/lib/hal0/current both exist, fail with guidance.
Acceptance criteria
Blocked by
None — can start immediately. The ADR draft is the first deliverable (HITL); the rest is AFK once the ADR is approved.
Notes
- This issue is HITL through the ADR step, then AFK for the implementation.
- The fresh-CT test infrastructure for verifying tarball mode end-to-end is intentionally split out into a separate ticket so each can land independently.
🤖 Generated with Claude Code
What to build
Declare hal0 supports two distinct install modes — editable (dev) and tarball (production) — codify auto-detection, and make
Updater.apply()+ the dashboard's Settings → Updates surface behave correctly in each.Why this exists
PR #386 fixed the Settings → Updates apply button (it now polls status and the extract step quarantines stale dirs). On the way to that fix we discovered LXC 105 carries BOTH layouts side-by-side:
/opt/hal0/pip install -e .venvsystemd hal0-api.service/usr/lib/hal0/current → hal0-<v>/Updater.apply()extract targetSelf-update on this LXC extracts the new release, swaps the
currentsymlink, logsswap_ok— and the running editable venv stays on the old version because systemd points at/opt/hal0. The button "works" but the version stays stuck. End users on a cleancurl … | bashinstall don't hit this because they only have the tarball layout. We need explicit, audited support for both modes.Two modes — definitions
/opt/hal0/git checkout +pip install -e .venvgit pull && pip install -e . && systemctl restart hal0-api hal0-lemonade/usr/lib/hal0/current → hal0-<v>/symlinkUpdater.apply()— atomic symlink swap with sigstore-verified tarballinstall.shAuto-detection
Updatershould detect mode at startup and cache the result. Heuristic (in priority order):/opt/hal0/.git/exists AND the running interpreter's*.dist-inforeportsEditable project location→ editable./usr/lib/hal0/currentsymlink exists and resolves under/usr/lib/hal0/→ tarball.system.install_mode_undetected; surface as a banner.Don't gate on an env var or marker file unless the heuristic produces a false positive in the wild (file a follow-up if so).
What changes per mode
editable mode:
Updater.apply()raises a typedsystem.update_unsupported_in_editable_modeenvelope withdetails.upgrade_hint = "git pull && pip install -e . && systemctl restart hal0-api hal0-lemonade"./api/updates/stateincludesinstall_mode: "editable"so the dashboard can branch without re-probing./api/updates/checkstill runs (so the user can see what version they would be on if they were on tarball mode).tarball mode:
/api/updates/stateincludesinstall_mode: "tarball".install.shimpact--dev/--editableflag: clone the git repo to/opt/hal0, runpip install -e ., drop a.hal0-install-mode = editablemarker file, point systemd at/opt/hal0/.venv/bin/hal0./opt/hal0/.gitand/usr/lib/hal0/currentboth exist, fail with guidance.Acceptance criteria
docs/internal/adr/0015-install-modes.mddeclares the two modes, the auto-detection rule, and the rationalesrc/hal0/updater/install_mode.py(or equivalent) implements the detector with unit tests covering each branch and the falsy caseUpdater.apply()consults the detector; editable mode raisessystem.update_unsupported_in_editable_modewith the upgrade hint indetailsGET /api/updates/statereturnsinstall_modeinstall_mode = "editable"/api/updates/state.install_mode == "editable", clicking Install update surfaces the banner instead of running the tarball flowinstall_mode == "tarball", full apply flow worksinstall.sh --devflag produces an editable install; default behavior unchangedhal0_lxc_install_layout_mismatch.mdmarked resolved on the user sideBlocked by
None — can start immediately. The ADR draft is the first deliverable (HITL); the rest is AFK once the ADR is approved.
Notes
🤖 Generated with Claude Code