Skip to content

hal0 v1.6.5

Choose a tag to compare

@github-actions github-actions released this 24 Oct 17:45
· 168 commits to main since this release
d6bd52e

Highlights

  • FLM on the NPU runs on FastFlowLM 1.0.10, inside its runner image.
    The installer's host .deb moves to 1.0.10, and flm pull and flm list
    now run in the pinned flm runner image through a new hal0-flmctl
    seam, so a box without the host .deb can pull and list NPU models. An
    NPU slot loaded with a model override no longer downloads a different
    model, and the NPU pickers list every installed FLM chat model once,
    with its flm chip.
  • Tighter privilege boundaries around the hal0 account. Root
    wrappers no longer import Python from the caller's working directory,
    root writes under /var/lib/hal0 and /etc/hal0 no longer follow a
    swapped-in symlink, the Hermes repair child gets an allow-listed
    environment, and Settings → Secrets refuses loader, TLS, OpenSSL, proxy,
    XDG and Hugging Face endpoint variables. hal0 doctor all flags any such
    entry already in api.env.
  • The upstream Hermes gateway unit move holds up in partial and masked
    states.
    A move that fails or is interrupted keeps exactly one gateway,
    a runtime or persistent mask is honoured, and the remedy it prints works.
  • Four CLI spellings are gone (see Breaking). hal0 agent upgrade hermes --to, hal0 update owui, hal0 agent bootstrap hermes, and
    hal0 uninstall --keep-data/--clean-slate now have one name each, and a
    Hermes --target persists until --clear-override.
  • Dashboard navigation. The sidebar lists every tab of every page, and
    each tab has its own address you can link to.

Breaking

  • hal0 agent upgrade hermes --to <sha> is gone: use hal0 update component hermes --target <sha>. There is no alias. hal0 agent upgrade hermes now exits with a message naming the new command, and hal0 agent upgrade takes only pi. The --target check is the one --to had: a full 40-hex commit listed in VETTED_HERMES_REFS, and a commit older than the live release is refused as a downgrade. The hidden --ref flag of hal0 agent bootstrap hermes, which only the old command used to hand a ref to the privilege-dropped child, is removed with it. (#2865)

  • hal0 update owui is gone: use hal0 update component openwebui. There is no alias. --target <digest>, --clear-override, --check and --yes work on hal0 update component <id> exactly as they did on hal0 update owui; the OpenWebUI override marker and the /api/updates/components/openwebui/converge body are unchanged. Every other command's remedy text that named hal0 update owui now names the new spelling. hal0 update owui exits with the CLI's unknown-command error. (#2868)

  • hal0 agent bootstrap hermes is gone: use hal0 agent reprovision hermes. The two were the same command, and there is no alias. reprovision takes --repair, --verbose and now --offline (a build that is needed fails and names the step that needs the network; a running host keeps its live release). The --dry-run and --skip-phase flags that only printed "not supported" are removed with the command: reprovision rejects them as unknown options. hal0 agent bootstrap exits with the CLI's unknown-command error. The dashboard's Hermes repair (POST /api/agents/hermes/repair) now runs hal0 agent reprovision hermes --repair --verbose, and every doctor row, remedy string, installer wrapper message and agent-facing template that named the old verb names the new one. (#2863)

  • hal0 uninstall --keep-data and --clean-slate are gone. --keep-data did nothing: the conservative default it named was already the default. --clean-slate was a second spelling of --purge. There are no aliases, so hal0 uninstall now has the conservative default, --purge, --force and --dev, and the removed flags fail with Typer's "No such option" (exit 2). installer/uninstall.sh follows: it no longer accepts either flag, and any flag it does not know now exits 2 instead of printing a warning and carrying on, so an old runbook that passed --clean-slate cannot quietly run the conservative teardown. Scripts that passed --keep-data should drop it; scripts that passed --clean-slate should pass --purge. (#2866)

Changed

  • The NPU slot drawer's chat-model picker matches the one on the slot card. It was a plain dropdown that appended "⬇ download" to each name. It now uses the same two-line picker as the card. Each row shows the tag and, for chat models that also transcribe, the asr tag on its first line, then the purple flm chip on the second. A tag that is not on disk yet adds a ⬇ download chip and is listed after the installed ones. Picking one still downloads it first.

  • The NPU occupancy card's slot cards have the same model dropdown as the inference cards. The model name was a static label. Each FLM slot card now carries the dashboard's rich model picker (quant, runtime and modality chips, with the runtime chip in the NPU purple). It lists only FLM models of the card's role: chat for the chat anchor, speech-to-text for STT, embedding for Embed. Picking a chat model writes the same config the slot editor writes ([npu] toggles plus [model].default on the anchor) and cold-restarts it. The STT and Embed pickers are read-only, because FastFlowLM loads one bundled model per role and has nothing to switch.

  • The dashboard sidebar has a sub-link for every tab on a page. Slots adds Inference and Image Gen beside Endpoints, Runner Images and Stacks. Models lists Inference Models, Image / ComfyUI, Upstream Models and Profiles. Benchmarks lists Roster, Runs, Evals and Run Queue. Agents adds Overview, and Settings lists its sections from the same list as the in-page settings nav. Every one of these tabs now has its own address (#slots/image, #models/upstream, #benchmarks/evals and so on), so the sidebar highlight follows the tab you pick on the page, and a tab can be bookmarked or linked. Bare #slots, #models, #benchmarks, #agent and #settings still open the first tab.

  • The installer installs FastFlowLM 1.0.10 on NPU hosts, up from 0.9.44. The pinned host .deb now comes from ROCm/FastFlowLM, where the project moved, with new SHA-256 pins for each distro build. FastFlowLM 1.0 drops ffmpeg and fftw and needs libxrt2 2.25 or newer. The matching flm runner image ships separately as a runner pin.

  • A Hermes --target now persists until --clear-override, the way the OpenWebUI digest override does. --to built a vetted commit that the next hal0 update replaced with the pin. hal0 update component hermes --target <sha> records the commit in /var/lib/hal0/state/hermes.pin-override once its release is live, and every later hal0 update keeps that release. hal0 update status shows the row as override with the override commit beside the pin, on every run, so a forgotten override stays visible; the converge counts a live release that matches the override as converged. When a later hal0 release no longer lists the override commit in VETTED_HERMES_REFS, hal0 update warns, refuses to keep or rebuild it, and names hal0 update component hermes --clear-override; it never builds an unvetted commit. hal0 update component <id> also takes --yes. --target with the release pin's own commit counts as --clear-override; --clear-override is refused when the pin is older than the live release and removes the marker only after the converge back to the pin succeeds; hal0 agent reprovision hermes --repair, the dashboard Repair and the MCP-client auto-repair in hal0 update rebuild the override commit instead of the pin (and refuse an override this release no longer vets); and the Agents card does not show an update for a box held at an override. hal0 agent uninstall hermes removes the override marker (a full hal0 uninstall --purge removes it with /var/lib/hal0; a conservative uninstall keeps it with the rest of the state), so a fresh install never rebuilds an old override. (#2865)

Fixed

  • The NPU model picker lists every installed FLM chat model, once, with its flm chip. FLM reports gemma4-it as chat plus speech-to-text, and /api/models typed its registry row transcription, so the NPU chat picker (which lists only chat models) dropped gemma4-it:e2b and showed the current gemma4-it:e4b as a bare row with no chips. FLM rows that can chat are now typed chat, the rule the rows built from FLM's own model list already followed. Installed FLM models with no registry row used the old <tag>-FLM id and carried no engine field, so they showed in a second naming style without the flm chip and read as duplicates. They now use FLM's native tag (qwen3:4b) like pulled models do, keep the old id as an alias for slots still bound to it, and carry provider_effective: flm. Assigning a native tag to a slot passes the installed-model check, which accepted only the -FLM form before.

  • Loading an NPU slot with a model override no longer downloads a different model and sticks in warming. The one-shot inference check that promotes an FLM slot to ready read the model from [model].default, which an override load (hal0 slot load flm -m <model>, or POST /api/slots/{name}/load with a model_id, as the dashboard pickers send) only writes after the check. With an empty or different default, the check fell back to the first model in FastFlowLM's /v1/models, and FastFlowLM 1.0.10, which lists models it has not installed, pulled that model (5.4 GB deepseek-r1:8b in the report) and timed out. The check now probes the model the load started, and fails with expected_model_not_served instead of naming any other model. The embeddings check on chat-off NPU slots names the bundled embed-gemma:300m instead of the first chat tag. hal0 slot load on a slot with no model now says "Not loaded" with the reason and exits 1, instead of "Loaded ... state=offline". (#3023)

  • The move off upstream's suffixed hermes-gateway-<hash>.service now holds up in mixed and interrupted states. After a move killed half way (a disabled, stopped hermes-gateway.service beside the enabled suffixed unit), hal0 agent install hermes and the installer read the canonical unit's "disabled" and stopped the gateway again; they now read the operator's on/off state by the same rule the move uses. A masked hermes-gateway.service no longer leaves a running suffixed gateway connected: both the command and the installer move it off, keeping the mask. A failed start during a resumed move now disables the canonical unit it had enabled, so both units are never left enabled. A timed-out or failed systemctl is-active probe aborts the move before any change instead of reading as "stopped". A suffixed unit with a second User= or HERMES_HOME= assignment is no longer treated as hal0's, a canonical unit linked in with systemctl link is no longer treated as a mask, and a runtime mask is honoured. When only the removal of the old suffixed file fails, the command says the gateway moved instead of saying it is not enabled. (#2950)

  • A runtime mask on hermes-gateway.service is honoured on a box that has only the suffixed unit. systemd reads /etc/systemd/system before /run/systemd/system, so systemctl mask --runtime hermes-gateway.service only takes effect while there is no /etc/systemd/system/hermes-gateway.service, which is exactly the state of a box a 1.6.0 nightly installed. There, hal0 agent install hermes and the installer used to write the /etc unit, which hid the mask, then enabled and started the gateway. They now read the mask, stop and remove the suffixed unit, and write no canonical unit, so the gateway stays off. The runtime mask is gone after a reboot, and with it the off switch; with no unit file left, the gateway stays off until sudo hal0 agent install hermes lays the unit down and turns it on. (#3024)

  • The left-off note for a masked gateway names a command that works. After the move past a mask, hal0 agent install hermes and the installer said to run systemctl unmask hermes-gateway.service && systemctl enable --now hermes-gateway.service. The enable failed with "Unit file hermes-gateway.service does not exist": a mask leaves no gateway unit file. The note now says systemctl unmask hermes-gateway.service && sudo hal0 agent install hermes (unmask --runtime for a runtime mask), which lays the unit down and turns it on. (#3025)

  • The removed suffixed gateway unit no longer shows up in systemctl --failed. After a successful move, hermes-gateway-<hash>.service stayed listed as not-found failed until systemctl reset-failed. The move now runs systemctl reset-failed on each suffixed unit it removed; a failure there is ignored.

  • The gateway move reports its leftovers and rollback failures correctly. When only the removal of the old suffixed unit file fails, the installer now says the gateway moved and that a re-run removes the file, instead of "hermes-gateway unit not installed"; this holds even when the daemon-reload after it fails too, and hal0 agent install hermes still turns linger on for hal0. A rollback that cannot disable or stop the canonical unit it had turned on now says so instead of quietly leaving both units enabled, and a rollback after a runtime-only enable now uses disable --runtime. (#2950)

  • A queued benchmark run that leaves a slot down no longer reads as a clean success. The queue worker dropped the slot-restore failures that hal0 bench run already reported (#2966), so the item left the queue as if it had passed while a slot such as brain stayed failed. The item now moves to the failed list with a note naming the slot that did not restart, which the Benchmarks queue view shows. (#2979)

  • The dashboard health strip and Utilization card no longer show a flat 100% GPU on hosts whose GPU performance level is forced high. #2967 captioned the Settings → Overview row as pinned, but the dashboard iGPU cell and the Utilization card still presented the pinned counter as real load. All three surfaces now format the reading through one shared helper and carry the same "pinned" caption as the telemetry header. (#2974)

  • FLM (NPU) models no longer show up in the model dropdown of GPU and CPU slots, and the API refuses the pairing. The inference card picker, the slot editor and the swap popover filtered only by slot type, so a llama.cpp slot offered FLM models it cannot load, and the Stacks editor listed every model whatever device a stack slot targeted. One shared rule now decides what a slot can bind: FLM models only on NPU slots, GGUF models only elsewhere. The server enforces it too: creating a slot, PUT /api/slots/{name}/config, load and swap return 400 slot.model_engine_mismatch for an FLM model on a non-NPU slot, or a GGUF model on an NPU slot, instead of accepting it and crash-looping the slot at load.

  • Streamed FastFlowLM chat keeps exact token and speed metrics on FastFlowLM 1.0.10. From 1.0.10 a streamed /v1/chat/completions response carries usage only when the request sets stream_options.include_usage, and hal0 never set it, so every NPU stream fell back to estimated token counts and speed. hal0 now sets it on streamed chat requests to NPU slots, merging into any stream_options the client sent. A client that did not ask for usage never sees the extra usage-only chunk, but the metrics row still records it, and the request's stop reason survives it. A client that set include_usage itself is passed through unchanged, and other upstreams are untouched. (#2994)

  • FLM model pulls and the NPU model catalogue now use the flm runner image's FastFlowLM, not the host .deb. flm pull and flm list ran the host /usr/bin/flm, and only the installer installs that .deb, so a box that only ran hal0 update kept an old FastFlowLM that pulled old weights, missed newer tags and disagreed with the container on renamed tags, all into the store the newer runner serves. A pull now runs in a one-shot container of the slot's runner image with the model store mounted where the slot mounts it, and the catalogue asks the running slot first, then a one-shot container, and only then the host binary. A new privileged seam, hal0-flmctl, runs these containers with a pinned entrypoint, as the hal0 account's own uid and groups, with no capabilities and no devices; hal0 update installs it. Cancelling a pull stops and removes its container. An FLM pull now needs the flm runner image downloaded and says so if it is missing, and creates a store that does not exist yet. The host .deb is still used for flm validate. See ADR-0036. (#2993)

  • The installer no longer reports "Verify FAILED" when the structured-output probe merely times out. On a CPU-only fresh install the model can take longer than the probe's 120 s to answer, which printed a false failure in the summary box. A curl timeout now prints a distinct warning (model slow or busy) and does not count as a failed probe; an HTTP error or a reply that is not JSON still fails. (#2952)

  • A second text-to-speech slot is now reachable over /v1/audio/speech. The route only ever resolved the slot named tts, so a slot created under another name (hal0 slot create voice2 --type tts -m kokoro-v1) loaded and advertised its model but answered every speech request with 404 dispatch.no_route. Speech is now routed by slot type: a serving tts slot bound to the requested model serves it, matching the registry id you bound (kokoro-v1) as well as the id the container advertises (kokoro). The slot named tts wins when several match, and a request with no match behaves as before. (#2784)

  • A persona edit or switch now reaches the running Hermes. Saving the active persona's name or prompt, and hal0 agent personas activate or its API route, now render the prompt into Hermes's config.yaml as agent.system_prompt, through the same config merge the provisioner uses. Hermes re-reads that file without a restart, so new sessions and the next gateway turn use the prompt; an open dashboard chat keeps the old one. Before, the change sat in the persona file until a reprovision. The retired reload nudge to 127.0.0.1:8765 (which nothing listens on) and the "next Hermes restart will pick up the new persona" message are gone: the API returns a refresh object and the CLI prints what was applied, or that the prompt is not live and the repair command to run. tools_allowed and preferred_model are documented as steward-only, and the dashboard persona editor and personas show hide them for personas other than hal0-brain. (#2921, #2850)

  • A Hermes gateway migration that fails partway can no longer leave two gateways running or lose track of one. Three gaps remained in the move off a 1.6.0-nightly suffixed hermes-gateway-<hash>.service (#2946). If an update was killed after taking over the gateway, its crash-recovery journal did not list the gateway, so recovery restarted every unit except it. The restart that loads a rewritten, stale canonical unit ran after the rollback window, so a failed restart left the operator's gateway stopped. And after a failed move, hal0 agent install hermes went on to enable the canonical unit even when the rollback had restored the suffixed gateway, starting a second poller on the same Telegram/Discord tokens. The journal is now rewritten as soon as the gateway is taken over. The restart runs inside the rollback, which brings the original gateway back if it fails. The install command stops after a failed move. (#2950)

  • The omni loop now runs for FLM slots. A chat request for a model served from an FLM slot never matched its slot, because the request carries the native colon tag while the slot config holds the -FLM catalog id, so the request silently ran as plain chat. The slot lookup now reads the model id from every model-config shape and matches a -FLM id against its native tag. (#2779)

  • Settings → Overview no longer shows a misleading flat 100% GPU utilization on a host whose GPU performance level is forced high. On those hosts the AMD busy counter always reads 100. The Live hardware row now adds "pinned (forced performance level)", as the telemetry header already did. (#2933)

  • A slot whose runner image cannot be resolved now shows "unknown" with a logged reason, not "not configured". When hal0 failed to work out a container slot's runner image (an unreadable profiles.toml, for example), the slot reported not-configured, which the docs describe as "not a fault", and nothing was logged. The slot now reports unknown and the API log carries slot_view.image_probe_failed ... reason=resolve-error, so a broken config no longer looks healthy. (#2800)

  • A bench session no longer leaves pinned slots stopped, and says so if it does. Every Tier-A sweep stopped and restarted each active slot, so a roster session of more than ten sweeps tripped the slot units' start limit and left slots such as brain and flm failed, with the bench reporting success. A run of sweeps now shares one exclusive window, slots are restored with a start-limit reset first, and a slot that still cannot be restarted is listed in the session result, logged as an error and makes hal0 bench run exit 1. (#2770)

  • The Hermes agent card keeps memory available on boxes with a large memory bank. The overview counted recent writes under a 2-second budget, and on a bank of several thousand facts the count timed out, so the card said "memory is not available" while memory worked. Availability now comes from a quick check; if the count is too slow the card omits the writes figure and memory stays available. (#2957)

  • The hermes_md_contains_primary check now verifies the primary model, and is skipped when none is set. HERMES.md never named the primary model, and the check passed on any file that mentioned hal0/agent, so a HERMES.md with the wrong model, or none, still read green. The file now carries a Primary model line that the check compares with the configured model, and no configured model reports skipped rather than passed. A box shows the new line after its next HERMES.md re-render; until then the check warns. (#2947)

  • hal0 model update --check now notices a replaced part of a split GGUF. The check compared only the first part, so a model whose later part was re-uploaded upstream was reported as up to date. It now compares every part, and also reports an update when the number of parts changed. (#2761)

  • The Pi agent card shows "unverified" for checks it cannot read, not "missing". When Pi is installed for a home the daemon cannot read (/root, for example), the provider extension, memory wiring and theme checks have no answer, yet Slot Sync and Dual Memory showed as locked with "the hal0 provider extension is not present" and the theme as "not deployed". They now read "unverified" with the reason, and "not present" is kept for checks that really failed. (#2958)

  • hal0 update now clears a stale failed Hermes install record. Only the provisioning commands wrote the Hermes provision report, so a box whose last bootstrap failed kept showing install: fail in hal0 agent status and on the agent card after an update had rebuilt a healthy release. A converge that leaves a release live on the pin now marks the install phase ok for that release. Other phases are left alone, and phases older than the live release no longer raise a provisioning warning on the card. (#2914)

  • hal0 bench cells no longer all fail on current runner images. The bench passed -mmp 0 to llama-bench, which the llama.cpp build in the standard image (b11716) dropped, so every Tier-A cell failed in under a second and the weekly session recorded 24 failed cells and no rows. It now passes -lm none, which also disables mmap and mlock, and the bench seam accepts -lm in place of -mmp. The agent-skill docs now use -lm. (#2769)

Security

  • hal0-agentenv and hal0-benchctl no longer import Python modules from the caller's working directory. Both run as root through the hal0 account's sudo grant and started Python with -c but without -I. sudo keeps the caller's working directory, and python -c puts that directory first on sys.path, so a tempfile.py or hal0/ package planted there ran as root: a local privilege escalation from hal0 to root. Both wrappers now cd / and start every Python with -I, as hal0-update already did. A test now fails if any wrapper under installer/wrappers/ starts Python without -I. (#3007)

  • A dashboard Repair no longer hands the Hermes provisioner every secret hal0-api holds. The repair child inherited hal0-api's whole environment minus HERMES_HOME, including the box keys, upstream API keys and tokens. Its environment is now built from an allow-list: PATH, locale (LANG, LC_*, TZ), the proxy variables, HOME, and the non-secret HAL0_* settings (including HAL0_HERMES_LIVE_RESOLVE and HAL0_INFERENCE_BASE) the provisioner needs to reach hal0-api. (#2938)

  • A root Hermes repair can no longer be tricked into overwriting a root-owned file. The Hermes provisioner wrote its files through a predictable <target>.tmp temp file, some of them in directories the hal0 service account can write, including provision.json in the Hermes state directory. That account could plant a symlink at that name, and the next sudo hal0 agent reprovision hermes --repair (or the installer's post-activation pass) would truncate, rewrite and chown whatever file it pointed to: a local escalation from hal0 to root. Temp files now get a random name opened exclusively without following symlinks, their mode and owner are set on the open file, and a failed write removes them. (#2903)

  • Root writes under /var/lib/hal0 and /etc/hal0 no longer follow a directory the hal0 account swapped for a symlink. Both trees are owned by the hal0 service account and are not sticky, so that account could rename .hermes, secrets/, agents/ or slots/ and put a symlink, or a directory it owns, in its place. A root --repair, the installer, hal0 update, hal0 doctor perms --fix or the hal0-agentenv sudo seam then created, rewrote, chowned or chmodded files inside whatever the link named: a local escalation from hal0 to root. Those writers now walk each directory from / by descriptor, refuse a symlink below any directory someone other than root can write, and require the secrets vault and /etc/hal0/agents to really be root's before they write into them. The Hermes venv repair and the update cache chown no longer follow the links inside the tree either. An operator-made symlink above the state root, such as /var/lib/hal0 pointing at another disk, still works; a symlinked .hermes or secrets/ is now refused. (#2975)

  • POST /api/models/{id}/pull rejects a malformed or path-traversing Hugging Face repo or filename before it records anything. The repo and filename fields in the request body (hf_repo, hf_filename, mmproj_filename, draft_repo, draft_filename) were passed on after only trimming whitespace, so a value such as ../../etc/x.gguf produced a registry row whose path pointed outside the models directory — a path that model scans, hal0 model rm --keep-files and the slot view then trust. Repos must now be org/name and filenames must be relative paths inside the repo (subfolders still work); anything else gets a 422 model.invalid_source naming the field, and no row is seeded and no download starts. Rows already holding a bad path are not rewritten. (#2775)

  • Settings → Secrets and provider credentials accept a TLS trust variable only as a path hal0 cannot write. SSL_CERT_FILE, SSL_CERT_DIR, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE and NODE_EXTRA_CA_CERTS choose the CA bundle every outbound TLS client under hal0-api trusts, so pointing one at a bundle the hal0 user can edit would let a man-in-the-middle read every token hal0 sends upstream (Hugging Face, provider keys). They are now stored (secrets routes, POST /api/providers/{name}/credentials, and the shared api.env writer) only when the value is an absolute path to an existing root-owned file (a directory for SSL_CERT_DIR) that is not group- or world-writable, under parents that are root-owned and not group- or world-writable, every symlink hop checked (the link, each intermediate hop and the final target), and every entry of an SSL_CERT_DIR directory too; anything else is refused with secret.protected (403, reason: "tls_trust") or a 400 on the credentials route. A custom corporate CA still works once an admin installs it as root. Delete still works on an entry already there, so check Settings → Secrets after updating (#2941).

  • Settings → Secrets and provider credentials refuse SSLKEYLOGFILE. Python's ssl, httpx, curl and Node write every TLS session key to the file it names, so an entry pointing at a file hal0 can read let a passive capture decrypt the Hugging Face and provider tokens hal0 sends upstream. It is now refused like the loader variables (secret.protected, reason: "process_control"); delete still works on an entry already there (#2997).

  • Settings → Secrets and provider credentials refuse OPENSSL_CONF, OPENSSL_MODULES, OPENSSL_ENGINES and every other OPENSSL_* name. An OpenSSL config can load an engine or provider shared object, so an entry ran code inside every OpenSSL user under hal0-api, the same class as LD_PRELOAD. Refused with secret.protected (reason: "process_control"); delete still works on an entry already there (#2998).

  • Settings → Secrets and provider credentials refuse NODE_TLS_REJECT_UNAUTHORIZED. Set to 0, it turned off certificate verification for every Node child of hal0-api (the Hermes web build, npm), a simpler route around the CA-bundle guard than #2941 closed. Refused with secret.protected (reason: "process_control"); delete still works on an entry already there (#2999).

  • Settings → Secrets and provider credentials refuse XDG_CONFIG_HOME and every other XDG_* name. git, pip, uv and npm under hal0-api read their configuration, data and caches from the XDG base directories, so an entry sidestepped the existing HOME, GIT_*, PIP_* and UV_* refusals: git config (core.sshCommand, core.fsmonitor, http.sslVerify=false) runs code or turns off TLS checks, XDG_DATA_HOME picks the uv-managed Python the Hermes build runs, and XDG_CACHE_HOME the wheels uv and pip reuse. Refused with secret.protected (reason: "process_control"); delete still works on an entry already there (#3000).

  • Settings → Secrets and provider credentials refuse HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, NO_PROXY and any other name ending in _PROXY, in either letter case. httpx, requests, git, curl, uv and pip honour them, so an entry sent hal0-api's outbound traffic through a proxy chosen by whoever wrote it, and on a plain-http:// remote upstream the bearer token went with it. Refused with secret.protected (reason: "process_control"); delete still works. A box behind a corporate proxy keeps setting these the documented way, by editing /etc/hal0/api.env as root; an entry already in the file keeps working (#3006).

  • Settings → Secrets and provider credentials refuse HF_ENDPOINT and the other Hugging Face endpoint and cache overrides. The ComfyUI model fetch runs hf download with hal0-api's environment plus HF_TOKEN, so an HF_ENDPOINT entry sent the operator's Hugging Face token to another host and let that host choose the weights hal0 later loads; a cache directory override served planted files the same way. HF_ENDPOINT, HF_INFERENCE_ENDPOINT, HUGGINGFACE_CO_STAGING, HF_TOKEN_PATH, HF_ASSETS_CACHE, HUGGINGFACE_ASSETS_CACHE and every HF_HUB_*, HUGGINGFACE_HUB_* and HF_XET_* name are now refused with secret.protected (reason: "process_control"), and the ComfyUI fetch drops them from its subprocess environment, so an entry written before this update no longer reaches hf download either. HF_TOKEN and HF_HOME are unchanged; delete still works (#3017).

  • hal0 doctor all flags api.env entries written before these guards. A new api-env-entries row warns on any loader, interpreter, TLS, configuration or proxy variable already in /etc/hal0/api.env (the #2936 names plus #2997–#3000 and #3006), and on a TLS trust variable whose value is not a root-owned path hal0 cannot write (#2941). Updating does not remove an entry, and hal0-api and every child keep inheriting it, so the row is how a box learns that one is there. It names the keys, never the values, and words proxy variables apart, since a box behind a corporate proxy sets them as root on purpose (#2942).