From ba04a5a360957a4ca58ff03910fb4119f79e4927 Mon Sep 17 00:00:00 2001 From: damocles Date: Wed, 20 May 2026 11:20:37 +0200 Subject: [PATCH 1/2] =?UTF-8?q?nix:=20hyperhive.westonRdp.enable=20?= =?UTF-8?q?=E2=80=94=20opt-in=20weston=20+=20RDP=20backend=20per=20agent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 18 +++++++ nix/templates/harness-base.nix | 5 ++ nix/templates/weston-rdp.nix | 97 ++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 nix/templates/weston-rdp.nix diff --git a/CLAUDE.md b/CLAUDE.md index c60a7263..b306ca15 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -130,6 +130,8 @@ nix/ templates/harness-base.nix shared scaffolding for sub-agents + manager templates/agent-base.nix sub-agent nixosConfiguration templates/manager.nix manager nixosConfiguration + templates/weston-rdp.nix optional `hyperhive.westonRdp.enable` + — weston + RDP backend systemd unit docs/ conventions.md naming, identity=socket, async forms, commit style @@ -183,6 +185,22 @@ read them à la carte. In-flight or recent context that hasn't earned a section yet. Prune freely. +- **Just landed:** `hyperhive.westonRdp.enable` option. New + `nix/templates/weston-rdp.nix` declares a per-agent bool; + enabling it runs weston with the RDP backend as a systemd + service (software/pixman render, self-signed TLS cert + generated first-boot under `/var/lib/weston`). Imported by + `harness-base.nix` so every agent has the option; an agent + opts in from its own `agent.nix`. Design note: it's a FLAT + per-agent option, not `hyperhive.agents..*` — each + agent is its own nixosConfiguration with no cross-agent + view, so the `` indirection is meaningless. The unit + is `Type = "simple"` with an always-exit-0 `ExecStartPre` so + it can never abort `nixos-container update` (a `Type=notify` + weston that never signals READY would fail activation every + reload — the trap `tea-login` documents). A misconfigured + weston degrades to a restart loop in `journalctl`, not a + blocked rebuild. - **Just landed:** `get_logs` now resolves the machine name. `journalctl -M` wants the *machine* name (`h-gui`), not the logical agent name (`gui`) — `get_logs` was the one manager diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index 26d059e3..d1601927 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -10,6 +10,11 @@ # this. The systemd service that actually runs the harness binary # differs per role and lives in the child module. + # Optional feature modules. Each declares its own `hyperhive.*` + # option(s), default-off, so every agent has them available but + # only opts in from its own `agent.nix`. + imports = [ ./weston-rdp.nix ]; + options.hyperhive.allowedRecipients = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; diff --git a/nix/templates/weston-rdp.nix b/nix/templates/weston-rdp.nix new file mode 100644 index 00000000..c6462186 --- /dev/null +++ b/nix/templates/weston-rdp.nix @@ -0,0 +1,97 @@ +{ + pkgs, + lib, + config, + ... +}: +{ + # Optional Weston (the reference Wayland compositor) with the RDP + # backend, surfaced as a per-agent hyperhive option. An agent turns + # it on from its own `agent.nix`: + # + # hyperhive.westonRdp.enable = true; + # + # Imported by `harness-base.nix`, so every sub-agent + the manager + # has the option available; only those that flip it on get the + # service. This is a flat per-agent option (evaluated inside that + # agent's own container build) — NOT a `hyperhive.agents..*` + # registry, which can't work: each agent is its own + # nixosConfiguration and has no cross-agent view. + + options.hyperhive.westonRdp.enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Run Weston with the RDP backend as a systemd service, for + remote-desktop access to the agent's container. A self-signed + TLS cert/key pair is generated on first start under + `/var/lib/weston`. Renders in software (pixman) — no GPU, DRM, + or VT access, so no extra container capabilities are needed. + + The unit is deliberately built so enabling it can NEVER abort + the agent's `nixos-container update`: `Type = "simple"` (so + `switch-to-configuration` doesn't block on weston readiness) + and an `ExecStartPre` that always exits 0. A misconfigured + weston degrades to a restart loop visible in `journalctl` — it + does not block the rebuild. (Same reasoning as the `tea-login` + unit in `harness-base.nix`.) + + Host networking means the RDP port (3389) is reachable on the + host. With more than one RDP-enabled agent the port would + collide — add a port knob to this option before that happens. + ''; + }; + + config = lib.mkIf config.hyperhive.westonRdp.enable { + systemd.services.weston = { + description = "Weston Wayland compositor (RDP backend)"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + # `simple`, not `notify`: switch-to-configuration must not + # wait on weston signalling readiness. A `notify` unit that + # never sends READY=1 fails after TimeoutStartSec, which + # aborts `nixos-container update` on every reload — exactly + # the trap `tea-login` documents. + Type = "simple"; + # Creates /var/lib/weston (0700 root) before ExecStartPre. + StateDirectory = "weston"; + Environment = "XDG_RUNTIME_DIR=/run/user/0"; + # Runtime dir + first-boot cert generation. No `set -e`, and + # it always exits 0: a cert hiccup must not fail the unit + # start (that would abort the rebuild). If the cert/key end + # up missing, weston's own ExecStart fails into the Restart + # loop instead — graceful degradation, not a blocked deploy. + ExecStartPre = pkgs.writeShellScript "weston-rdp-setup" '' + mkdir -p /run/user/0 && chmod 700 /run/user/0 || true + CERT=/var/lib/weston/weston-rdp.crt + KEY=/var/lib/weston/weston-rdp.key + if [ ! -f "$CERT" ] || [ ! -f "$KEY" ]; then + if ${pkgs.openssl}/bin/openssl req -x509 -newkey rsa:2048 \ + -keyout "$KEY" -out "$CERT" -days 3650 -nodes \ + -subj "/CN=localhost" 2>/dev/null; then + chmod 600 "$KEY" 2>/dev/null || true + chmod 644 "$CERT" 2>/dev/null || true + else + echo "weston-rdp-setup: cert generation failed; weston will retry" + fi + fi + exit 0 + ''; + ExecStart = lib.concatStringsSep " " [ + "${pkgs.weston}/bin/weston" + "--backend=rdp-backend.so" + "--renderer=pixman" + "--rdp-tls-cert=/var/lib/weston/weston-rdp.crt" + "--rdp-tls-key=/var/lib/weston/weston-rdp.key" + ]; + Restart = "on-failure"; + RestartSec = "5s"; + }; + }; + + # weston on the agent's interactive PATH too, so claude can run + # Wayland clients / `weston-info` against the compositor. + environment.systemPackages = [ pkgs.weston ]; + }; +} From 94781ccd0818d5eb243791255669b3a8f9b262f0 Mon Sep 17 00:00:00 2001 From: damocles Date: Wed, 20 May 2026 11:25:08 +0200 Subject: [PATCH 2/2] lifecycle: append container journal tail to failed nixos-container update --- CLAUDE.md | 11 +++++++++++ hive-c0re/src/lifecycle.rs | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index b306ca15..bf5c0d15 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -185,6 +185,17 @@ read them à la carte. In-flight or recent context that hasn't earned a section yet. Prune freely. +- **Just landed:** failed `nixos-container update` self-documents. + `lifecycle::run` now appends the tail (40 lines) of the target + container's own journal to the bail message when an `update` + fails. `nixos-container`'s own stderr on a reload-phase failure + is terse ("failed to reload container"); the real cause — + which unit failed `switch-to-configuration` — lives in the + *container* journal. Scoped to `update` (container's still up + on the old generation, so `journalctl -M` works); best-effort, + appends nothing if the journal can't be read. The manager's + `update` tool / rebuild errors now carry the failing-unit + detail without a second `get_logs` call. - **Just landed:** `hyperhive.westonRdp.enable` option. New `nix/templates/weston-rdp.nix` declares a per-agent bool; enabling it runs weston with the RDP backend as a systemd diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index c48300ce..ad96f6f7 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -959,7 +959,40 @@ async fn run(args: &[&str]) -> Result<()> { .cloned() .collect::>() .join("\n"); - bail!("nixos-container {cmdline} failed ({status}): {tail}"); + let journal = container_journal_tail(args).await; + bail!("nixos-container {cmdline} failed ({status}): {tail}{journal}"); } Ok(()) } + +/// On a failed `nixos-container update`, the stderr nixos-container +/// itself prints is often terse ("failed to reload container") — the +/// real reason (which unit failed `switch-to-configuration` during +/// the reload phase) lands in the *container's* own journal, not on +/// the host. Fetch the tail of it so a failed rebuild self-documents +/// the failing unit in the error string, no second round-trip. +/// +/// Scoped to `update`: that's the reload-phase case, and the +/// container is still up (running the old generation) so +/// `journalctl -M` works. Best-effort — returns "" for other verbs +/// or when the journal can't be read (machine gone, journalctl +/// missing); it never produces an error of its own. +async fn container_journal_tail(args: &[&str]) -> String { + if args.first().copied() != Some("update") { + return String::new(); + } + let Some(container) = args.get(1) else { + return String::new(); + }; + let out = Command::new("journalctl") + .args(["-M", container, "-n", "40", "--no-pager", "--output=short"]) + .output() + .await; + match out { + Ok(o) if !o.stdout.is_empty() => format!( + "\n--- last 40 journal lines from container '{container}' ---\n{}", + String::from_utf8_lossy(&o.stdout).trim_end() + ), + _ => String::new(), + } +}