Skip to content

[DO NOT MERGE] feat(metal-agent): run on Linux hosts (integration branch for #2481) - #2482

Draft
cnuss wants to merge 6 commits into
defilantech:mainfrom
cnuss:feat/linux-host-agent
Draft

cnuss wants to merge 6 commits into
defilantech:mainfrom
cnuss:feat/linux-host-agent

Conversation

@cnuss

@cnuss cnuss commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Caution

DO NOT MERGE. This PR is the integration branch for Linux host support (#2481). The work lands upstream through the single-commit PRs below; this one only tracks them.

What this branch is

feat/linux-host-agent is upstream main plus every change in the Linux host-agent effort, one commit per sub-issue of #2481. Its diff against main is the whole remaining feature: the Linux metal-agent, hailo:// sources, the Hailo-10H simulator, the host-agent e2e, linux release builds and metal join on Linux.

How it is maintained:

  • It is rebased onto main and force-pushed (with lease) whenever main moves or a split PR changes. A split PR's commit here is that PR's commit, byte for byte. Where the two conflict, the PR's version wins.
  • When a split PR merges, its commit drops out on the next rebase, so this diff shrinks as the pieces land. Work not split out yet sits in one commit, which gets smaller as sub-issues get their own PRs.
  • Once everything is in main, the diff is empty and this PR is closed without merging.

Sub-issues and PRs

Sub-issue PR Commit on this branch
#2516 Start the metal-agent without llama-server when --runtime selects another engine #2529 merged in main
#2517 Read host memory from /proc and cgroup v2 on Linux #2531 merged in main
#2519 Default the model store and state directory under XDG_DATA_HOME off macOS #2528 merged in main
#2521 Hailo-10H simulator built from hailo-ollama 5.1.1 #2530 in review 3dd771ef
#2518 Report Linux host capacity on the MetalNode #2541 in review 4559bc6d
#2520 Run the metal-agent on Linux hosts (experimental) #2542 in review 3f6f6704
#2522 Serve and size hailo:// Model sources through hailo-ollama not yet in d47fdf5d (remaining work)
#2523 Run Linux host agents against the Hailo-10H simulator in e2e CI not yet in d47fdf5d
#2524 Ship the metal-agent for linux/arm64 and linux/amd64 not yet 439c7004
#2525 Run metal join and leave on Linux through a systemd user unit not yet 61d57dc0

The order follows the dependencies in #2481. #2522 and #2524 come next, once #2520 merges. #2523 needs #2521 and #2522, and #2525 needs #2520.

Separately: #2514 / #2515 adds the FederatedCluster CRD that make deploy leaves out. It isn't on this branch, but without it the controller-manager restarts every couple of minutes under kustomize.

Tested

At 61d57dc0, make gate BASE_REF=origin/main and make test-e2e-host-agent pass. The Raspberry Pi results below were taken at 7df0d2b9, which had the same code before #2530's, #2518's and #2520's review changes.

  • make gate BASE_REF=origin/main passes, and so does make test-e2e-host-agent. That runs the operator in kind and two Linux agents next to the Hailo-10H simulator. It checks dial-back, chat through the relay, memory admission before any pull, per-namespace serving, and that hailo:// is refused on other accelerators.
  • On a Raspberry Pi 5 with an AI HAT+ 2 (Debian 13, 16K-page kernel, hailo-ollama 5.1.1), joined to a kind cluster with this branch's llmkube metal join as a systemd user unit:
  • Found on the Pi: the CGO_ENABLED=0 Linux builds can't resolve an mDNS (*.local) API server name, because Go's built-in resolver skips nsswitch. A cgo build works. This is tracked for [Raspberry Pi] Ship the metal-agent for linux/arm64 and linux/amd64 #2524.

Assisted-by: Claude Code (Claude Opus 5.5) split the work into sub-issues and PRs, rebuilt this branch, ran the checks and the Pi test, and wrote this description in my session.

@codecov

codecov Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

@cnuss

cnuss commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator Author

Holding this in draft until I can run hack/hailo-probe.sh on my Raspberry Pi 5 with the AI HAT+ 2. Everything here so far is verified against the Hailo-10H simulator in test/hailosim, not real hardware. I'll post the probe output on #2481 and update this PR with whatever differs before marking it ready.

@cnuss
cnuss force-pushed the feat/linux-host-agent branch from d510fb3 to 7df0d2b Compare November 13, 2026 03:50
cnuss added a commit to cnuss/LLMKube that referenced this pull request Nov 13, 2026
…ning defilantech#2482 work)

This is the part of defilantech#2481 not yet split into its own PR. It sits on the
four split commits for defilantech#2516, defilantech#2517, defilantech#2519 and defilantech#2521 and holds the rest
of defilantech#2482.

API and controller:
- A hailo:// Model source names a HEF in Hailo's GenAI model zoo
  (api/v1alpha1/hailo_source.go); the source pattern and the generated
  CRDs accept it.
- The Model controller marks a valid hailo:// Model Ready without
  downloading it (MetalAgentManaged), and fails one whose tag is
  malformed or whose accelerator is not metal with InvalidSource.
- Runtime placement refuses a hailo:// Model on a runtime other than
  ollama.

metal-agent:
- The agent starts on Linux (experimental) as well as on a host with
  Metal; on Linux it logs whether a Hailo device node is present
  (cmd/metal-agent/hailo.go).
- The Ollama executor pulls a hailo:// source's tag, refuses it unless
  the daemon is hailo-ollama (GET /hailo/v1/list), and sends load and
  unload requests without a prompt field, which hailo-ollama needs.
- The agent refuses a hailo:// Model on a runtime other than ollama.
- Memory admission sizes a hailo:// source with a HEAD of its HEF,
  found through hailo-ollama's manifests, before anything is pulled.
  --hailo-manifests-dir and --hailo-library-url configure it, and
  metal join passes both through.
- Host capacity on Linux reads the board or CPU name from /proc and
  memory through LinuxMemoryProvider; the sysctl reader moves to the
  darwin file with its test.

e2e and CI:
- hack/e2e-host-agent.sh runs two Linux host agents, host-a and
  host-b, in kind against the Hailo-10H simulator: dial-back, relay
  chat, per-namespace isolation, admission before pull, and
  InvalidSource for hailo:// on another accelerator.
- make test-e2e-host-agent runs it; test-e2e.yml runs it as a
  non-blocking job, and the gate exempts the target.

Docs:
- docs/site/guides/linux-host-agent.md, linked from the nav and the
  macOS guide: any Linux host first (building the agent, joining by
  hand, keeping hosts apart, engines, memory and cgroups), then
  Hailo-10H hosts (hailo-ollama, its systemd unit, hailo:// sources,
  known limits), with the Raspberry Pi 5 and AI HAT+ 2 install in a
  subsection of its own.
- The generated CRD reference lists the hailo:// scheme.

Signed-off-by: Christian Nuss <cnuss@users.noreply.github.com>
@cnuss cnuss changed the title feat(metal-agent): run on Linux hosts (experimental) [DO NOT MERGE] feat(metal-agent): run on Linux hosts (integration branch for #2481) Nov 13, 2026
test/hailosim builds Hailo's hailo-ollama 5.1.1 server against HailoRT
5.1.1's public headers and links it to fake_hailort.cpp, a stand-in
libhailort that emulates a Hailo-10H. The HTTP layer is hailo-ollama's
own code, so its quirks (404 on GET /v1/models, created in nanoseconds,
500 without a JSON Content-Type) carry over. Fake HEFs of exactly
HAILO_SIM_HEF_SIZE bytes are seeded under the digests written into the
manifests, so /api/pull is an offline cache hit; a small server answers
HEAD /blob/sha256_<digest> like dev-public.hailo.ai.

The entrypoint validates HAILO_SIM_LISTEN and HAILO_SIM_LIBRARY_LISTEN,
exits if the library server cannot bind, and writes an edited config
under its own XDG_CONFIG_HOME, so the image runs as any user.

make docker-build-hailo-sim builds the image (HAILO_SIM_PLATFORM,
default the host's arch). It is not published.

hack/hailo-probe.sh checks every route the agent's Ollama executor and
ingress allow for the ollama runtime against a hailo-ollama server, real
or simulated, and prints each status. hailo-ollama 5.1.1 answers 404 on
the embedding, /health, /metrics, /v1/models, /v1/completions and
/v1/responses routes; the simulator matches it.

Fixes defilantech#2521

Signed-off-by: Christian Nuss <cnuss@users.noreply.github.com>
cnuss added a commit to cnuss/LLMKube that referenced this pull request Nov 13, 2026
…ning defilantech#2482 work)

This is the part of defilantech#2481 not yet split into its own PR. It sits on the
four split commits for defilantech#2516, defilantech#2517, defilantech#2519 and defilantech#2521 and holds the rest
of defilantech#2482.

API and controller:
- A hailo:// Model source names a HEF in Hailo's GenAI model zoo
  (api/v1alpha1/hailo_source.go); the source pattern and the generated
  CRDs accept it.
- The Model controller marks a valid hailo:// Model Ready without
  downloading it (MetalAgentManaged), and fails one whose tag is
  malformed or whose accelerator is not metal with InvalidSource.
- Runtime placement refuses a hailo:// Model on a runtime other than
  ollama.

metal-agent:
- The agent starts on Linux (experimental) as well as on a host with
  Metal; on Linux it logs whether a Hailo device node is present
  (cmd/metal-agent/hailo.go).
- The Ollama executor pulls a hailo:// source's tag, refuses it unless
  the daemon is hailo-ollama (GET /hailo/v1/list), and sends load and
  unload requests without a prompt field, which hailo-ollama needs.
- The agent refuses a hailo:// Model on a runtime other than ollama.
- Memory admission sizes a hailo:// source with a HEAD of its HEF,
  found through hailo-ollama's manifests, before anything is pulled.
  --hailo-manifests-dir and --hailo-library-url configure it, and
  metal join passes both through.
- Host capacity on Linux reads the board or CPU name from /proc and
  memory through LinuxMemoryProvider; the sysctl reader moves to the
  darwin file with its test.

e2e and CI:
- hack/e2e-host-agent.sh runs two Linux host agents, host-a and
  host-b, in kind against the Hailo-10H simulator: dial-back, relay
  chat, per-namespace isolation, admission before pull, and
  InvalidSource for hailo:// on another accelerator.
- make test-e2e-host-agent runs it; test-e2e.yml runs it as a
  non-blocking job, and the gate exempts the target.

Docs:
- docs/site/guides/linux-host-agent.md, linked from the nav and the
  macOS guide: any Linux host first (building the agent, joining by
  hand, keeping hosts apart, engines, memory and cgroups), then
  Hailo-10H hosts (hailo-ollama, its systemd unit, hailo:// sources,
  known limits), with the Raspberry Pi 5 and AI HAT+ 2 install in a
  subsection of its own.
- The generated CRD reference lists the hailo:// scheme.

Signed-off-by: Christian Nuss <cnuss@users.noreply.github.com>
@cnuss
cnuss force-pushed the feat/linux-host-agent branch from 7df0d2b to 9d6af22 Compare November 13, 2026 23:22
DetectHostCapacity on Linux reads the chip from /proc/device-tree/model
with its trailing NUL stripped, else the first "model name" or "Model"
line in /proc/cpuinfo, else leaves it empty. It goes through the same
printable-prefix and 128-byte limit as on darwin. Memory is
NewHostMemoryProvider().TotalMemory(), so a cgroup limit below MemTotal
is what the capacity reports.

hostCapacityFromSysctl and its test move into the darwin files, and the
stub for other OSes returns an empty HostCapacity, so the sysctl parser
is not dead code on Linux.

The MetalNodeCapacity field comments are now host-neutral: memory is
unified memory on a Mac and RAM under the agent's cgroup limit on
Linux, the chip is the CPU brand string on macOS and the device-tree or
cpuinfo model name on Linux, and GPU cores and the Metal version are
reported on macOS only. The generated CRDs, the chart's CRD and the
CRD reference page carry the new descriptions.

Fixes defilantech#2518

Signed-off-by: Christian Nuss <cnuss@users.noreply.github.com>
The agent no longer exits on a host without Metal when the host is
Linux: checkHostPlatform logs "Metal not available; Linux host support
is experimental" and whether a Hailo device node (/dev/hailo*) is
present, and goes on. Any other host without Metal still exits.
llamaServerInstallHint reads the same hostGOOS probe, and the probes are
replaced in platform_test.go.

docs/site/guides/linux-host-agent.md is the first version of the Linux
host guide: what has been tested, building the agent, joining by hand
with a minted ServiceAccount token (metal join runs only on macOS),
keeping hosts apart, engines and memory. The docs nav and the macOS
guide link to it.

Review follow-ups:
- Memory eviction skips a runtime that runs outside the agent's cgroup
  (runsOutsideAgentCgroup), whatever its ModelID.
- An agent whose memory controller is on cgroup v1, as on a hybrid
  v1/v2 host, logs once at startup that its memory figures are the
  host's.
- llmkube_metal_agent_memory_budget_bytes says it is the host-wide
  budget and that in-cgroup engines are admitted against the cgroup's;
  docs/site/reference/metrics.md is regenerated.
- The /tmp model-store refusal gives Linux steps (change --model-store
  where the supervisor sets it) off macOS, and the launchd steps on
  macOS.
- A test covers the generic RuntimeNotAvailable message, and the macOS
  guide has a troubleshooting entry for RuntimeNotAvailable.
- The "starting metal agent" log line drops the binary paths, which
  were empty strings when a binary was not found; the "binary found"
  lines report them.
- Shared code and messages say "host" instead of "Mac": --metal-node
  and --inference-service-allowlist help, the MetalNode address reasons
  and comments, the allowlist comments and log line, and the expired
  token message ("join the host again").
- The StateDir, InstallRoot and stop-engines --state-dir docs give the
  XDG_DATA_HOME default with its ~/.local/share fallback.
- memory_procfs.go says why it has no build tag.

Fixes defilantech#2520

Signed-off-by: Christian Nuss <cnuss@users.noreply.github.com>
…(remaining defilantech#2482 work)

This is the part of defilantech#2481 not yet split into its own PR: hailo://
sources (defilantech#2522) and the host-agent e2e (defilantech#2523). It sits on the split
commits for defilantech#2521, defilantech#2518 and defilantech#2520.

API and controller:
- A hailo:// Model source names a HEF in Hailo's GenAI model zoo
  (api/v1alpha1/hailo_source.go); the source pattern, the generated
  CRDs and the CRD reference accept it.
- The Model controller marks a valid hailo:// Model Ready without
  downloading it (MetalAgentManaged), and fails one whose tag is
  malformed or whose accelerator is not metal with InvalidSource.
- Runtime placement refuses a hailo:// Model on a runtime other than
  ollama.

metal-agent:
- The Ollama executor pulls a hailo:// source's tag, refuses it unless
  the daemon is hailo-ollama (GET /hailo/v1/list), and sends load and
  unload requests without a prompt field, which hailo-ollama needs.
- The agent refuses a hailo:// Model on a runtime other than ollama.
- Memory admission sizes a hailo:// source with a HEAD of its HEF,
  found through hailo-ollama's manifests, before anything is pulled.
  --hailo-manifests-dir and --hailo-library-url configure it, and
  metal join passes both through.

e2e and CI:
- hack/e2e-host-agent.sh runs two Linux host agents, host-a and
  host-b, in kind against the Hailo-10H simulator: dial-back, relay
  chat, per-namespace isolation, admission before pull, and
  InvalidSource for hailo:// on another accelerator.
- make test-e2e-host-agent runs it; test-e2e.yml runs it as a
  non-blocking job, and the gate exempts the target.

Docs: the Linux host agent guide gains a Hailo-10H hosts section
(installing hailo-ollama, its systemd unit, hailo:// sources, known
limits) with a Raspberry Pi 5 and AI HAT+ 2 subsection, and a section
on testing with the simulator. Its status table now reports MetalNode
hardware for Linux hosts (defilantech#2518) and the hailo-ollama results.

Signed-off-by: Christian Nuss <cnuss@users.noreply.github.com>
Releases carry static (CGO_ENABLED=0) linux/arm64 and linux/amd64
metal-agent archives, LLMKube-metal-agent_<version>_linux_<arch>.tar.gz,
next to the darwin ones. They come from their own build and archive id,
llmkube-metal-agent-linux, so the Homebrew cask, the Info.plist
embedding, codesigning and notarization keep seeing only the darwin
binaries.

A path-filtered workflow, goreleaser-check.yml, runs the release job's
pinned GoReleaser (v2.19.0) on PRs and pushes to main that touch the
config, the agent or go.mod: goreleaser check, then a snapshot build of
the linux metal-agent targets only. It publishes nothing and is not a
required check.

The Linux host agent guide lists the release archives and how to get
the agent from one.

Part of defilantech#2481. Fixes defilantech#2524.

Signed-off-by: Christian Nuss <cnuss@users.noreply.github.com>
metal join refused any OS but darwin, so a Linux host agent could only
be joined by hand, although redeeming the join string does not depend
on the OS. On Linux, join now installs the agent as the
llmkube-metal-agent.service systemd user unit, with the state directory
under $XDG_DATA_HOME/llmkube/metal-agent (the agent's --state-dir
default, datadir.Dir) and the unit under $XDG_CONFIG_HOME/systemd/user.
It runs the same checks, credential setup, argument handling and
Reachable wait as on a Mac; the plist and unit share one argument
planner. Instead of a GUI session the preflight checks that the user's
systemd instance answers, and join only suggests loginctl
enable-linger. leave stops, disables and removes the unit join wrote,
reloads systemd --user and stops the agent's engines; a unit join did
not write is left running.

A missing llama-server no longer stops join, on either OS, when the
agent's runtime is one the agent starts without it (the rule in
cmd/metal-agent resolveDefaultLlamaServerBin); join says the agent will
refuse services that select llama.cpp and writes no --llama-server. With
the llama.cpp runtime, or a --llama-server that is not executable, join
still refuses.

The join and leave help, flag help and messages shared by both OSes say
"host" instead of "Mac", and name a MetalNode <name> rather than <mac>.

Tests run join and leave against a fake systemctl. A live test, opted
into with LLMKUBE_TEST_SYSTEMD_USER=1, installs, starts and removes the
unit against the user's systemd instance, and a path-filtered workflow,
test-systemd-user.yml, runs it on ubuntu-latest with lingering on.

The Linux host agent guide and the generated CLI reference describe
join and leave on Linux; the guide records the end-to-end join of a
Raspberry Pi 5 as a systemd user unit with lingering on.

Part of defilantech#2481. Fixes defilantech#2525.

Signed-off-by: Christian Nuss <cnuss@users.noreply.github.com>
@cnuss
cnuss force-pushed the feat/linux-host-agent branch from 9d6af22 to 61d57dc Compare November 14, 2026 00:21

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant