Skip to content

hal0 v1.0.0-rc.4

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 09 Aug 20:37
· 46 commits to main since this release
59addb0

Highlights

  • Privileged-seam hardening. The hal0-systemctl root wrapper now
    allow-lists the content of everything it writes — systemd drop-ins
    (#1718), quadlet bodies with the raw write-unit verb removed (#1748), and
    PodmanArgs= pinned to the flags providers emit, closing a --runtime
    host-exec path (#1759). The update staging path closes a verify→extract
    TOCTOU (root-only stage dir + digest pinned to the extraction handle, #1745),
    reaps orphaned staging/quarantine trees (#1755), never dereferences symlinks
    when applying ownership (#1743), and only trusts a root-owned config for the
    releases-URL override (#1750). Slot flag denylisting canonicalizes the
    --flag=value form (#1746) and rejects malformed shell quoting at save time
    (#1737/#1749).
  • Fresh-install & update robustness. The installer waits on the dpkg/apt
    lock instead of dying (#1733), runs AppArmor remediation before the podman
    preflight gate (#1728), preflights git for Hermes (#1727), and applies the
    model-layout migration as its final model step (#1732). An up-to-date
    hal0 update now converges an outstanding v1.0 profile-catalog reset instead
    of going silent (#1585/#1757); staged trees get their perms normalized after
    extraction (#1725); every hal0.toml write goes through one serialized
    read-modify-write path (#1724).
  • Capabilities & settings UI. A unified AI Capabilities page (TTS, STT,
    embeddings, reranking, image generation, NPU) with a canonical rerank slot
    (#1747), a memory reranker model picker (#1781), and per-panel probe-failure
    surfacing instead of a silent grey Save (#1774).
  • Dashboard redesign. The main and Benchmarks dashboards now share the
    hal0.dev design-system chrome (#1764/#1768), with benchmark trigger/config/
    outcomes/regressions surfaced (#1744) and assorted tile/row polish
    (#1729/#1779/#1780/#1782).
  • Benchmark system overhaul. Adopts GuideLLM, llama-benchy and
    tool-eval-bench (#1765), adds record telemetry + regression journaling
    (#1766) and shareable result bundles (#1758), folds the shell harness into
    Python behind a validate-and-exec benchctl shim (#1761), and makes the
    measurements truthful and the pipeline reliable (#1736 and follow-ups).
  • Slots. Per-slot profile-flag divergence overlay with a cross-device
    picker (#1639), and a live slot now restarts when apply changes config but
    not the model (#1770).
  • Docs. Quick-start and migration guide rewritten for v1.0 (#1731).

Added

  • Settings: new unified AI Capabilities page (TTS, STT, embeddings, reranking, image generation, NPU anchor) replaces the Voice / Image Generation / NPU pages; old #settings/voice|imagegen|npu links redirect. First UI for the embed/rerank capability selections.

Security

  • The hal0-systemctl write-quadlet allow-list now pins PodmanArgs= to the
    exact flags hal0's providers emit — --group-add <gid>, --security-opt <token> (GPU/llama-server), --ipc <mode> (comfyui), --ulimit <name=v[:v]> (flm/NPU) — instead of accepting any non-empty value (#1759).
    Podman's quadlet generator copies PodmanArgs= verbatim into the generated
    root unit's podman run argv, so a persistent flag like --runtime <path>
    or --hooks-dir <dir> made podman exec an attacker-named binary as root
    with no container involved
    — a direct host-exec primitive reachable from
    the unprivileged hal0 account, exactly what #1740 set out to close. All
    shipped providers keep loading; only out-of-list flags are refused. Minor
    breaking (deprecated feature):
    the free-form extra_args escape hatch
    (already logging container.extra_args_deprecated) is honoured on a
    hal0-service install only for flags in that list — an out-of-list flag now
    makes the slot refuse at the root seam. Move those to typed Quadlet keys in a
    hal0-slot@<token>.container.d/ drop-in.
  • The privileged update stage no longer downloads, verifies and extracts the
    release tarball out of the service-writable /var/lib/hal0/cache/<version>/
    (#1738). That directory is hal0:hal0 0o2775 with no sticky bit, and the
    post-stage ownership restore hands it back to the service account, so a
    process compromised as hal0 could substitute its own tarball in the window
    between cosign verify-blob exiting and root calling tarfile.open — and
    root would extract and pip install it. Staging now happens in a root-only
    0700 directory created under the install root and destroyed when the stage
    ends, and the authenticated digest_sha256 is re-derived from the very file
    object the archive is read out of, so the bytes cosign accepted are provably
    the bytes that land on disk. The cache directory now holds only the verified
    manifest that commit() re-reads.
  • An interrupted extraction no longer wedges a version permanently. A killed
    extractall used to leave the destination holding just the un-flattened
    hal0-<version>/ prefix, which the "is this a prior hal0 install" check did
    not recognise, so every retry refused to extract over a non-empty directory —
    unrecoverable for the unprivileged daemon, since the wedged tree is
    root-owned. Extraction now drops a .hal0-staging sentinel for its duration
    and the check also recognises an un-flattened prefix directory, so an
    incomplete tree is quarantined and retried. The .stale-<ts> quarantine
    directories, which accumulated one whole install tree per retry and were
    never cleaned up, are now reaped: the newest three are kept for recovery and
    anything older than 30 days goes.
  • The root-only .stage-<version>-XXXXXX staging directory (#1738) is now also
    reaped on the next apply. Its teardown runs in a finally, which a SIGKILL /
    OOM / power-cut skips, leaking a full release tarball under the root
    filesystem per killed stage. Orphans older than an hour — comfortably above
    the 30-minute privileged stage timeout, so a live concurrent stage is never
    touched — are swept. (#1754)
  • The privileged hal0-systemctl wrapper now allow-lists the content of the
    two systemd drop-ins it writes as root (write-gateway-dropin,
    write-hindsight-dropin). Both verbs take their whole payload on stdin, and
    the sudoers grant lets the unprivileged hal0 service account run the
    wrapper as root — so a process compromised as hal0 could previously supply
    a [Service] fragment with User=root and a replaced ExecStart=, then use
    the wrapper's own daemon-reload + svc-restart verbs to run it as root.
    Validation is parsed on the root side of that boundary: only # comments,
    blank lines, a single [Service] header and a closed set of directives per
    verb (Environment=HINDSIGHT_API_LLM_MODEL / …_TIMEOUT; an
    EnvironmentFile= confined to the hal0 secrets vault) are accepted, each
    with a pinned value charset. Anything else — any other section or directive,
    a line continuation, a control byte, leading whitespace — is rejected with a
    loud error and nothing is written. The wrapper persists the validated
    reconstruction rather than raw stdin, and a new side-effect-free
    check-dropin <gateway|hindsight> verb dry-runs the allow-list.
  • The same wrapper's write-quadlet verb now allow-lists its body too, and the
    raw-stdin write-unit verb is removed. A .container file is not just a
    container spec — podman's quadlet generator copies its [Unit], [Service]
    and [Install] sections verbatim into the generated system unit — so an
    unvalidated body meant a process compromised as the unprivileged hal0
    account could write [Service] ExecStartPre=/bin/sh -c '…', then use the
    wrapper's own daemon-reload + start verbs for an unconditional root exec.
    The root side now accepts only the sections, directive keys and value shapes
    the one renderer (_render_quadlet_from_plan) emits, in that order, each at
    most once, and writes its own validated reconstruction. write-unit had no
    producer left after the Quadlet migration and no render contract to
    allow-list against, so it was deleted rather than guessed at. A new
    side-effect-free check-quadlet [<slot-id>] verb dry-runs the allow-list.
    Scope, stated honestly: this closes the direct host-side exec primitive;
    it does not make an hal0-account compromise non-root-equivalent, because
    slots run under rootful podman and a slot's [Container] section
    legitimately carries config-derived Image=/Volume=/AddDevice=/
    PodmanArgs=/Exec= values. Containing that means running slots rootless
    or pinning mount roots.

Fixed

  • Enabling the rerank capability now creates/loads the rerank slot the dispatcher actually routes /v1/rerankings to (was embed-rerank, which nothing routed to); embed-rerank resolves as an alias.
  • hal0 update on an already-current box now converges an outstanding one-shot v1.0 profile-catalog reset instead of printing "nothing to apply" and hiding it (#1585). The reset rides commit(), but a box updated 0.9.8→1.0 ran commit under the old daemon, which had no reset — so it landed converged-except-for-this and then went silent. /api/updates/check now carries a read-only profile_reset snapshot, and a new local POST /api/updates/converge-profiles runs the reset with no download or swap. The up-to-date CLI path consults the snapshot: it converges (prompting or honoring --yes for the consent-needing case), converges silently when there's nothing to lose, and otherwise reports the reset as outstanding and exits 2 (up to date, convergence outstanding) rather than exiting 0 in silence.

Audience

Preview-channel operators validating the 1.0 line ahead of GA, and fresh
installs that want the current build. This is the release-candidate carrying
the privileged-seam hardening cluster (#1738/#1740/#1750/#1759); it is the
recommended pre-GA validation target. Boxes on the stable channel are not
offered this tag.

Supported upgrades

  • 1.0.0-rc.31.0.0-rc.4 via hal0 update (preview channel). The GitHub
    release asset URL (https://github.com/Hal0ai/hal0/releases/download/v1.0.0-rc.4/preview.json)
    works end-to-end for install and update.
  • 1.0.0-rc.2 / 1.0.0-rc.11.0.0-rc.4 directly, same mechanism.
  • 0.9.81.0.0-rc.4 in place — re-run the installer or hal0 update on
    the preview channel; the R5 migration set applies (see the
    1.0.0 changelog section).
    The profile-catalog reset that used to defer silently on this transition now
    converges on the first post-update hal0 update (#1585).
  • Older than 0.9.8: step through 0.9.8 first.

Known issues

The 0.9.8 CLI's spurious end-of-update ConnectError (the pre-1.0 client dies
when the apply restarts hal0-api under its status poll, even though the update
succeeds — verify with hal0 --version) and the first-boot
unattended-upgrades dpkg race (#1584) carry forward. A box still on rc.1/rc.2
whose venv predates #1663 is not offered a newer tag by the passive check —
hal0 update --target <version> is the recovery path (#1715). The rc.1
profile-catalog-defer item is resolved this release (#1585).

Operator migrations

  • Releases-URL override (#1750): a file:// HAL0_RELEASES_URL is no
    longer accepted from the service-owned /etc/hal0/api.env — put it in the
    root-owned /etc/hal0/update.conf (root:root 0644) instead. https://
    overrides in api.env are unchanged, so boxes using the GitHub asset URL
    need no action; only file://-staging boxes move the line.
  • Deprecated extra_args at the seam (#1759): a slot whose deprecated
    free-form extra_args carries a podman run flag outside
    --group-add/--security-opt/--ipc/--ulimit is now refused at the root
    seam on a hal0-service install. Move those flags to typed Quadlet keys in a
    hal0-slot@<token>.container.d/ drop-in. Shipped providers are unaffected.
  • The R5 operator-run migrators (slot-flag fold, id-keying) are unchanged from
    rc.1.

Rollback

Release tarballs are immutable and cosign-signed; roll back by re-installing
the previous tag (v1.0.0-rc.3) from its GitHub release. No rc.4 change writes
a state shape rc.3 cannot read — the profile-catalog reset stamps the same
schema_version = 2 rc.3's commit path already used.