hal0 v1.0.0-rc.4
Pre-releaseHighlights
- Privileged-seam hardening. The
hal0-systemctlroot wrapper now
allow-lists the content of everything it writes — systemd drop-ins
(#1718), quadlet bodies with the rawwrite-unitverb 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=valueform (#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 updatenow converges an outstanding v1.0 profile-catalog reset instead
of going silent (#1585/#1757); staged trees get their perms normalized after
extraction (#1725); everyhal0.tomlwrite 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-execbenchctlshim (#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|npulinks redirect. First UI for the embed/rerank capability selections.
Security
- The
hal0-systemctl write-quadletallow-list now pinsPodmanArgs=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 copiesPodmanArgs=verbatim into the generated
root unit'spodman runargv, 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 unprivilegedhal0account, 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-formextra_argsescape hatch
(already loggingcontainer.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 ishal0:hal00o2775with no sticky bit, and the
post-stage ownership restore hands it back to the service account, so a
process compromised ashal0could substitute its own tarball in the window
betweencosign verify-blobexiting and root callingtarfile.open— and
root would extract andpip installit. Staging now happens in a root-only
0700directory created under the install root and destroyed when the stage
ends, and the authenticateddigest_sha256is 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 thatcommit()re-reads. - An interrupted extraction no longer wedges a version permanently. A killed
extractallused 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-stagingsentinel 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>-XXXXXXstaging directory (#1738) is now also
reaped on the next apply. Its teardown runs in afinally, 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-systemctlwrapper 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 unprivilegedhal0service account run the
wrapper as root — so a process compromised ashal0could previously supply
a[Service]fragment withUser=rootand a replacedExecStart=, then use
the wrapper's owndaemon-reload+svc-restartverbs 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-quadletverb now allow-lists its body too, and the
raw-stdinwrite-unitverb is removed. A.containerfile 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 unprivilegedhal0
account could write[Service] ExecStartPre=/bin/sh -c '…', then use the
wrapper's owndaemon-reload+startverbs 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-unithad 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-freecheck-quadlet [<slot-id>]verb dry-runs the allow-list.
Scope, stated honestly: this closes the direct host-side exec primitive;
it does not make anhal0-account compromise non-root-equivalent, because
slots run under rootful podman and a slot's[Container]section
legitimately carries config-derivedImage=/Volume=/AddDevice=/
PodmanArgs=/Exec=values. Containing that means running slots rootless
or pinning mount roots.
Fixed
- Enabling the rerank capability now creates/loads the
rerankslot the dispatcher actually routes/v1/rerankingsto (wasembed-rerank, which nothing routed to);embed-rerankresolves as an alias. hal0 updateon 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 ridescommit(), 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/checknow carries a read-onlyprofile_resetsnapshot, and a new localPOST /api/updates/converge-profilesruns the reset with no download or swap. The up-to-date CLI path consults the snapshot: it converges (prompting or honoring--yesfor 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.3→1.0.0-rc.4viahal0 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.1→1.0.0-rc.4directly, same mechanism.0.9.8→1.0.0-rc.4in place — re-run the installer orhal0 updateon
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-updatehal0 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_URLis 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 inapi.envare unchanged, so boxes using the GitHub asset URL
need no action; onlyfile://-staging boxes move the line. - Deprecated
extra_argsat the seam (#1759): a slot whose deprecated
free-formextra_argscarries apodman runflag outside
--group-add/--security-opt/--ipc/--ulimitis 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.