Skip to content

Reconcile install-mode taxonomy: editable /opt/hal0 vs tarball /usr/lib/hal0/current #406

@thinmintdev

Description

@thinmintdev

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):

  1. /opt/hal0/.git/ exists AND the running interpreter's *.dist-info reports Editable project locationeditable.
  2. /usr/lib/hal0/current symlink exists and resolves under /usr/lib/hal0/tarball.
  3. 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

  • ADR docs/internal/adr/0015-install-modes.md declares the two modes, the auto-detection rule, and the rationale
  • src/hal0/updater/install_mode.py (or equivalent) implements the detector with unit tests covering each branch and the falsy case
  • Updater.apply() consults the detector; editable mode raises system.update_unsupported_in_editable_mode with the upgrade hint in details
  • GET /api/updates/state returns install_mode
  • Dashboard Settings → Updates renders the editable-mode banner with a copy-command affordance when install_mode = "editable"
  • LXC 105 verified: /api/updates/state.install_mode == "editable", clicking Install update surfaces the banner instead of running the tarball flow
  • Clean fresh-CT verified (depends on #B below if landed first): install_mode == "tarball", full apply flow works
  • install.sh --dev flag produces an editable install; default behavior unchanged
  • PLAN.md §9 amended to reference the ADR + the install_mode contract
  • CHANGELOG entry under Unreleased
  • Memory hal0_lxc_install_layout_mismatch.md marked resolved on the user side

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestv0.4v0.4 scope — UI polish + install-mode reconciliation + Bootstrapped

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions