Skip to content

Ship a static (musl) headless Linux binary for RHEL 8 and WSL hosts - #200

Open
jmcshane wants to merge 3 commits into
agentdesktop-dev:mainfrom
jmcshane:headless-linux-glibc-2-28
Open

jmcshane wants to merge 3 commits into
agentdesktop-dev:mainfrom
jmcshane:headless-linux-glibc-2-28

Conversation

@jmcshane

@jmcshane jmcshane commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The agentdesktop-linux-* release binary does not start on RHEL 8 (and RHEL 8 under WSL2, as used on many virtual desktops), even for daemon and CLI commands:

./agentdesktop: error while loading shared libraries: libgdk-3.so.0: cannot open shared object file
./agentdesktop: error while loading shared libraries: libwebkit2gtk-4.1.so.0: cannot open shared object file

The binary contains the Tauri UI, so the loader needs GTK and WebKitGTK 4.1 before main() runs. It is also built on Ubuntu 22.04, so it needs GLIBC_2.34. RHEL 8 has glibc 2.28 and no WebKitGTK 4.1 (that arrived in RHEL 9), so installing packages cannot fix it.

Change

The daemon and CLI already exist without Tauri as the agentdesktop-headless bin in agentdesktop-agent, but it was never released. This PR ships it:

  • Release: new build-linux-headless-binaries job builds agentdesktop-headless for x86_64-unknown-linux-musl and aarch64-unknown-linux-musl on the standard Ubuntu runners (musl-tools, no container). The result is a fully static binary with no glibc or other shared-library dependency, so it runs on any Linux. The binaries are attested with the existing Linux binaries and attached to the release as agentdesktop-headless-linux-{amd64,arm64}.
  • Static-link check: .github/scripts/verify-static-binary.sh fails when the binary requests a dynamic loader (PT_INTERP) or has any NEEDED library. It runs in the release job and in a new CI job, so a dependency that breaks static linking fails on the PR instead of at a user.
  • Socket lookup: the desktop binary found a --user daemon through AGENTDESKTOP_SOCKET, then the system socket, then $XDG_RUNTIME_DIR / ~/.local/state. The headless binary only tried the system socket, so its CLI could not reach a user-mode daemon. The lookup moves to agentdesktop_core::client_socket_path() and both binaries use it.
  • Docs: README section for Linux without a desktop (RHEL 8, WSL, VDI): download, systemd user unit (with the WSL systemd=true note), and how sign-in works without a local browser.

No new dependencies and no code changes for musl: the native crates in the headless tree (aws-lc-sys, bundled SQLite, inotify) all build against musl. The headless binary has no GTK, WebKit, D-Bus, or OpenSSL dependency (TLS is rustls with aws-lc-rs).

An earlier revision of this PR built against glibc 2.28 in manylinux_2_28. Review feedback suggested musl instead: it removes the glibc floor entirely and drops the container and image digests from CI.

Testing

Build and portability

  • musl (this revision): built aarch64-unknown-linux-musl with the CI recipe (Ubuntu 24.04, musl-tools, Rust 1.98) and in Alpine. Both produce a static binary with no PT_INTERP and no NEEDED entries; verify-static-binary.sh passes on it and fails on a dynamic binary (/bin/ls). In an AlmaLinux 8 container (glibc 2.28) the musl binary runs --version, and daemon --user with the standalone example config reconciles the managed files, listens on the user socket, and, with the issuer pointed at Microsoft Entra ID, resolves DNS, fetches the OIDC discovery document over HTTPS (system CA store) and prints the authorization URL.
  • glibc 2.28 (earlier revision): built in manylinux_2_28 for aarch64 and x86_64; the end-to-end test below used this build.
  • cargo fmt --check, cargo clippy -D warnings (core and agent) and the agentdesktop-core tests pass. The agentdesktop desktop crate change is a rename to the shared function; CI covers its build because it needs WebKitGTK.

End to end on a RHEL 8-class WSL desktop behind an LLM gateway (glibc 2.28 build)

To reproduce the reported environment, we built a short-lived Azure lab and deleted it after the test:

  • Desktop VM: Windows Server 2025 (D4ds_v5, nested virtualization) with WSL 3.0.3 and the official AlmaLinux 8.10 WSL image (RHEL 8 ABI, glibc 2.28, no WebKitGTK 4.1). systemd was left off, which is the WSL default, as on the reporting user's machine.
  • Gateway VM: Ubuntu 24.04 with agentgateway v1.6.0 on a private address. A /v1 route to OpenAI with jwtAuth in strict mode accepts only Microsoft Entra ID access tokens for the Agentdesktop app registration (v2.0 issuer, app audience, remote JWKS), and the gateway injects the OpenAI key upstream.
  • Agentdesktop config (in WSL): llmGateway.url points to the gateway, with authentication.type: oidc against Entra and programs.codex.useLlmGateway: true. Agentdesktop wrote Codex's provider (base_url = <gateway>/v1) with itself as the credential helper. No provider block was written by hand.

Steps and results:

  1. The released v0.3.0 agentdesktop-linux-amd64 fails in the WSL distro with the missing-library errors above. This reproduces the report.

  2. The headless binary from this branch, installed as ~/.local/bin/agentdesktop, starts on the same distro with no missing libraries.

  3. agentdesktop daemon --user ran in the foreground (no systemd). WSL has no browser, so the daemon printed the Entra sign-in URL. Opening it in Edge on the Windows host completed the OIDC flow through WSL's localhost forwarding to the daemon's 127.0.0.1 callback.

  4. Codex 0.164.0 (static musl build) in WSL sent requests through the gateway using the Agentdesktop-issued Entra token. Gateway access log, abbreviated:

    Source Request Status User (from JWT) Model
    desktop VM GET /v1/models?client_version=0.164.0 (×6) 200 the signed-in Entra user
    desktop VM POST /v1/responses (×2) 200 the signed-in Entra user gpt-5.6-terra
    gateway host, no token POST /v1/responses 401

    Requests with no token or a malformed token got 401.

Not covered here

  • The release workflow changes only run on a release. A workflow_dispatch run of Release with skip_github_release: true would exercise them before the next tag.
  • The x86_64 musl build was not run locally (the host is arm64); CI's headless-linux job builds and checks it on this PR.
  • The end-to-end WSL and gateway test ran with the glibc 2.28 build. The musl build uses the same code, and the AlmaLinux 8 run covers startup, reconcile, the local socket and OIDC discovery over HTTPS, but completing sign-in and Codex traffic through the gateway were not repeated with the musl binary.
  • musl's allocator is slower than glibc's under heavy multithreaded load. For this daemon the load is low; if it matters, mimalloc can be enabled for musl targets only.
  • musl has no NSS, so name resolution uses /etc/hosts and DNS only (no SSSD/LDAP host lookups).
  • Corporate TLS-inspecting proxies: the LLM proxy's upstream client and the controller gRPC channel do not use HTTPS_PROXY, and the controller channel trusts only controller.caCertificatePath. That is a separate change.

🤖 Generated with Claude Code

The Linux release binary links GTK and WebKitGTK 4.1 through Tauri and is
built against glibc 2.35, so it fails to load on RHEL 8 (glibc 2.28, no
WebKitGTK 4.1) even for daemon and CLI commands.

- Build agentdesktop-headless (daemon + CLI, no Tauri) in a manylinux_2_28
  container (pinned by digest) for amd64 and arm64, attest it, and attach it to releases.
- Add verify-linux-portability.sh to fail when the binary needs a glibc newer
  than 2.28 or links a library outside the base system set; run it in CI and
  release.
- Move client socket resolution (AGENTDESKTOP_SOCKET, system socket, then the
  per-user socket) into agentdesktop-core so the headless CLI finds a user-mode
  daemon the same way the desktop binary does.
- Document the headless binary and a systemd user unit for RHEL 8, WSL and VDI.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The documented fresh-install service cannot start without a required configuration file that the setup instructions never prepare.

2 open issues
What changed in this PR

Adds a headless Linux release option for RHEL 8, WSL, and other hosts that cannot run the Tauri desktop binary.

Changes:

  • Builds and attests amd64 and arm64 headless binaries targeting glibc 2.28, with portability checks in CI and releases.
  • Shares daemon socket discovery between desktop and headless clients.
  • Documents headless installation, user-mode service setup, and authentication.
File Description
README.md Adds headless Linux installation and service instructions.
crates/​core/​src/​lib.rs Introduces shared client socket lookup.
crates/​agentdesktop/​src/​main.rs Uses the shared socket lookup.
crates/​agent/​src/​bin/​agentdesktop-headless.rs Adds automatic socket discovery for CLI commands.
.github/​workflows/​release.yml Describes headless assets in release notes.
.github/​workflows/​release-linux.yml Builds, packages, and attests headless Linux binaries.
.github/​workflows/​ci.yml Adds a headless Linux portability job.
.github/​scripts/​verify-linux-portability.sh Checks glibc requirements and shared-library dependencies.

Review effort: Balanced

Comment thread README.md Outdated

```sh
curl -fLO https://github.com/agentdesktop-dev/agentdesktop/releases/latest/download/agentdesktop-headless-linux-amd64
install -m 0755 agentdesktop-headless-linux-amd64 ~/.local/bin/agentdesktop

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1aedf11: the install step now uses install -D, which creates ~/.local/bin when it is missing.

Comment thread README.md
Description=Agentdesktop daemon

[Service]
ExecStart=%h/.local/bin/agentdesktop daemon --user

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1aedf11: the README now says that daemon --user needs ~/.config/agentdesktop/config.yaml (or the $XDG_CONFIG_HOME equivalent). It adds a step that copies examples/standalone/config.yaml there before you enable the unit.

@peterj

peterj commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

thanks for the PR @jmcshane! can you check the comments?

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@howardjohn

howardjohn commented Oct 26, 2026 via email

Copy link
Copy Markdown
Contributor

Replace the manylinux_2_28 (glibc 2.28) build with x86_64/aarch64
unknown-linux-musl targets on the standard Ubuntu runners. The binary
no longer depends on the host's libc, and CI no longer needs a build
container or pinned image digests. The portability check now verifies
that the binary has no dynamic loader and no NEEDED libraries.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@jmcshane jmcshane changed the title Ship a glibc 2.28 headless Linux binary for RHEL 8 and WSL hosts Ship a static (musl) headless Linux binary for RHEL 8 and WSL hosts Oct 26, 2026
@jmcshane

Copy link
Copy Markdown
Contributor Author

@howardjohn refactored to musl

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.

4 participants