From 32b2a94bde6920a96499b81e1598e1c0c3aee0aa Mon Sep 17 00:00:00 2001 From: Lio Lunesu Date: Wed, 19 Aug 2026 15:36:30 +0000 Subject: [PATCH 01/18] refactor(module): systemd template units, %i = user (#154 Phase 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert the per-user agent-box/agent-web-terminal/agent-box-settings/ agent-box-webhook services (+ settings/webhook sockets) from one Nix-generated unit per user to systemd %i template instances, per the Phase 3 design posted on #154. - modules/src/units/*.service|*.socket: verbatim, backend-neutral unit text with %i specifiers and bare (ExecSearchPath-resolved) ExecStart binaries — installed via systemd.packages, the shape a future native (non-NixOS) backend would also consume byte-for-byte. - Host-level config (PATH, pinned tool binaries, ExecSearchPath, the sudo-allowlist-conditional hardening knobs) renders as a drop-in on the "@" template (default overrideStrategy = "asDropinIfExists" detects the packaged unit and drops in automatically). - Per-user values move to generated env files under /etc/agent-box/units/*.env, read via EnvironmentFile=-...%i.env; the one exception (u.environment/u.environmentFiles) gets a per-instance asDropin drop-in alongside enablement. - Renamed the per-user unit family: agent-box- -> agent-box@ (and the web-terminal/settings/webhook peers); updated the one in-repo reference (spot-monitor.sh) and every tests/*.nix assertion. - Extended the golden-snapshot etcFilter to also capture the new per-user env files, and regenerated tests/golden. Proceeded on the design's two explicitly-flagged decisions (ExecSearchPath for bare ExecStart resolution; generated env files over per-instance drop-ins) since neither drew an objection on the issue in the 15 days since it was posted — flagging both again here for this review. Verified locally (aarch64-linux native): module-generated-up-to-date, multi-user, module-single-file, download-route, webhook-route and golden-snapshot all green. The x86_64-linux VM tests (sessions, webhook, settings-page, download-files, self-serve-domain, memory-protection) were only `nix eval`'d (drvPath) — no KVM on this box — and their testScript passes the repo's ty/ruff gate; they still need a real run in CI. 🤖 Generated with Claude Code Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_013SrgKvQmv9B4mS9BEBtJ6q --- flake.nix | 13 +- modules/agent-box.nix | 725 ++++++++---------- modules/agent-box.nix.in | 723 ++++++++--------- modules/src/spot-monitor.sh | 2 +- modules/src/units/agent-box-settings@.service | 31 + modules/src/units/agent-box-settings@.socket | 8 + modules/src/units/agent-box-webhook@.service | 29 + modules/src/units/agent-box-webhook@.socket | 8 + modules/src/units/agent-box@.service | 30 + modules/src/units/agent-web-terminal@.service | 14 + tests/golden/vm/etc/agent-box/units/agent.env | 4 + .../golden/vm/payloads/agent-box-spot-monitor | 2 +- .../bin}/agent-box-supervisor | 0 tests/golden/vm/units/agent-box-agent.service | 51 -- tests/golden/vm/units/agent-box@.service | 18 + tests/golden/vm/units/agent-box@agent.service | 11 + .../units/agent-box-settings-agent.env | 3 + .../units/agent-box-settings-robot.env | 2 + .../units/agent-box-webhook-agent.env | 1 + .../units/agent-box-webhook-robot.env | 1 + .../units/agent-web-terminal-agent.env | 1 + .../units/agent-web-terminal-robot.env | 1 + .../golden/web/etc/agent-box/units/agent.env | 6 + .../golden/web/etc/agent-box/units/robot.env | 6 + .../bin}/agent-box-attach | 0 .../web/payloads/agent-box-spot-monitor | 2 +- .../bin}/agent-box-supervisor | 0 .../bin}/agent-box-webhook-policy-apply | 0 .../bin/agent-box-webhook-receiver | 3 + .../golden/web/units/agent-box-agent.service | 56 -- .../golden/web/units/agent-box-robot.service | 56 -- .../units/agent-box-settings-agent.service | 53 -- .../web/units/agent-box-settings-agent.socket | 16 - .../web/units/agent-box-settings-robot.socket | 16 - ...ot.service => agent-box-settings@.service} | 36 +- .../units/agent-box-settings@agent.service | 11 + .../web/units/agent-box-settings@agent.socket | 11 + .../units/agent-box-settings@robot.service | 11 + .../web/units/agent-box-settings@robot.socket | 11 + .../web/units/agent-box-webhook-agent.socket | 16 - .../web/units/agent-box-webhook-robot.service | 36 - .../web/units/agent-box-webhook-robot.socket | 16 - ...ent.service => agent-box-webhook@.service} | 30 +- .../web/units/agent-box-webhook@agent.service | 11 + .../web/units/agent-box-webhook@agent.socket | 11 + .../web/units/agent-box-webhook@robot.service | 11 + .../web/units/agent-box-webhook@robot.socket | 11 + tests/golden/web/units/agent-box@.service | 21 + .../golden/web/units/agent-box@agent.service | 11 + .../golden/web/units/agent-box@robot.service | 11 + .../units/agent-web-terminal-agent.service | 20 - .../units/agent-web-terminal-robot.service | 20 - .../web/units/agent-web-terminal@.service | 8 + .../units/agent-web-terminal@agent.service | 11 + .../units/agent-web-terminal@robot.service | 11 + tests/memory-protection.nix | 8 +- tests/self-serve-domain.nix | 8 +- tests/sessions.nix | 20 +- tests/settings-page.nix | 12 +- tests/webhook.nix | 64 +- 60 files changed, 1077 insertions(+), 1262 deletions(-) create mode 100644 modules/src/units/agent-box-settings@.service create mode 100644 modules/src/units/agent-box-settings@.socket create mode 100644 modules/src/units/agent-box-webhook@.service create mode 100644 modules/src/units/agent-box-webhook@.socket create mode 100644 modules/src/units/agent-box@.service create mode 100644 modules/src/units/agent-web-terminal@.service create mode 100644 tests/golden/vm/etc/agent-box/units/agent.env rename tests/golden/vm/payloads/{ => agent-box-supervisor/bin}/agent-box-supervisor (100%) delete mode 100644 tests/golden/vm/units/agent-box-agent.service create mode 100644 tests/golden/vm/units/agent-box@.service create mode 100644 tests/golden/vm/units/agent-box@agent.service create mode 100644 tests/golden/web/etc/agent-box/units/agent-box-settings-agent.env create mode 100644 tests/golden/web/etc/agent-box/units/agent-box-settings-robot.env create mode 100644 tests/golden/web/etc/agent-box/units/agent-box-webhook-agent.env create mode 100644 tests/golden/web/etc/agent-box/units/agent-box-webhook-robot.env create mode 100644 tests/golden/web/etc/agent-box/units/agent-web-terminal-agent.env create mode 100644 tests/golden/web/etc/agent-box/units/agent-web-terminal-robot.env create mode 100644 tests/golden/web/etc/agent-box/units/agent.env create mode 100644 tests/golden/web/etc/agent-box/units/robot.env rename tests/golden/web/payloads/{ => agent-box-attach/bin}/agent-box-attach (100%) rename tests/golden/web/payloads/{ => agent-box-supervisor/bin}/agent-box-supervisor (100%) rename tests/golden/web/payloads/{ => agent-box-webhook-policy-apply/bin}/agent-box-webhook-policy-apply (100%) create mode 100644 tests/golden/web/payloads/agent-box-webhook-receiver/bin/agent-box-webhook-receiver delete mode 100644 tests/golden/web/units/agent-box-agent.service delete mode 100644 tests/golden/web/units/agent-box-robot.service delete mode 100644 tests/golden/web/units/agent-box-settings-agent.service delete mode 100644 tests/golden/web/units/agent-box-settings-agent.socket delete mode 100644 tests/golden/web/units/agent-box-settings-robot.socket rename tests/golden/web/units/{agent-box-settings-robot.service => agent-box-settings@.service} (56%) create mode 100644 tests/golden/web/units/agent-box-settings@agent.service create mode 100644 tests/golden/web/units/agent-box-settings@agent.socket create mode 100644 tests/golden/web/units/agent-box-settings@robot.service create mode 100644 tests/golden/web/units/agent-box-settings@robot.socket delete mode 100644 tests/golden/web/units/agent-box-webhook-agent.socket delete mode 100644 tests/golden/web/units/agent-box-webhook-robot.service delete mode 100644 tests/golden/web/units/agent-box-webhook-robot.socket rename tests/golden/web/units/{agent-box-webhook-agent.service => agent-box-webhook@.service} (59%) create mode 100644 tests/golden/web/units/agent-box-webhook@agent.service create mode 100644 tests/golden/web/units/agent-box-webhook@agent.socket create mode 100644 tests/golden/web/units/agent-box-webhook@robot.service create mode 100644 tests/golden/web/units/agent-box-webhook@robot.socket create mode 100644 tests/golden/web/units/agent-box@.service create mode 100644 tests/golden/web/units/agent-box@agent.service create mode 100644 tests/golden/web/units/agent-box@robot.service delete mode 100644 tests/golden/web/units/agent-web-terminal-agent.service delete mode 100644 tests/golden/web/units/agent-web-terminal-robot.service create mode 100644 tests/golden/web/units/agent-web-terminal@.service create mode 100644 tests/golden/web/units/agent-web-terminal@agent.service create mode 100644 tests/golden/web/units/agent-web-terminal@robot.service diff --git a/flake.nix b/flake.nix index 2c3c011f..29bebe57 100644 --- a/flake.nix +++ b/flake.nix @@ -52,7 +52,12 @@ # settings land in the files below. etcFilter = n: builtins.match - "agent-box-guides/.*|caddy/caddy_config|codex/config\\.toml|fail2ban/(fail2ban|jail)\\.local|fail2ban/filter\\.d/agent-web-auth\\.conf|sudoers" + # agent-box/units/*.env (issue #154 Phase 3): the per-user + # generated env files the "%i" template units' EnvironmentFile= + # reads — the payload capture below only scans Nix-visible + # `environment` attrs, so these plain-text files are the review + # surface for what moved out of that attrset. + "agent-box-guides/.*|agent-box/units/.*|caddy/caddy_config|codex/config\\.toml|fail2ban/(fail2ban|jail)\\.local|fail2ban/filter\\.d/agent-web-auth\\.conf|sudoers" n != null; manifestOf = modules: let sys = nixpkgs.lib.nixosSystem { inherit system modules; }; in @@ -171,7 +176,11 @@ ]; }; services = multiUser.config.systemd.services; - wanted = [ "agent-box-alice" "agent-box-bob" "agent-box-coder" "agent-box-ci" ]; + # issue #154 Phase 3: "agent-box@" is the systemd %i template unit + # itself; each configured user additionally gets its own + # "agent-box@" drop-in (enablement, at minimum) rather than a + # flat "agent-box-" unit. + wanted = [ "agent-box@" "agent-box@alice" "agent-box@bob" "agent-box@coder" "agent-box@ci" ]; missing = builtins.filter (n: ! builtins.hasAttr n services) wanted; in { diff --git a/modules/agent-box.nix b/modules/agent-box.nix index f199db6a..2209bf56 100644 --- a/modules/agent-box.nix +++ b/modules/agent-box.nix @@ -238,6 +238,35 @@ let # `nixos-rebuild switch` (i.e. every box update); readable but not writable # under ProtectSystem=strict. canonicalAgentsPath = name: "/etc/agent-box-guides/AGENTS.${name}.md"; + # issue #154 Phase 3: the per-user services are systemd %i template + # units, whose text lives as verbatim, backend-neutral files under + # src/units/ — the single place a hypothetical native (non-NixOS) + # backend would also install these from, byte-for-byte. Shipped here via + # systemd.packages; a `systemd.services."@"` definition elsewhere + # in this module then renders as a host-level drop-in automatically + # (default overrideStrategy = "asDropinIfExists" detects that a unit + # file of the same name already exists in this package). + agentBoxUnitsPackage = pkgs.runCommand "agent-box-units" { } '' + mkdir -p $out/etc/systemd/system + install -m444 ${pkgs.writeText "agent-box@.service" '' + @@include:src/units/agent-box@.service@@ + ''} $out/etc/systemd/system/agent-box@.service + install -m444 ${pkgs.writeText "agent-web-terminal@.service" '' + @@include:src/units/agent-web-terminal@.service@@ + ''} $out/etc/systemd/system/agent-web-terminal@.service + install -m444 ${pkgs.writeText "agent-box-settings@.service" '' + @@include:src/units/agent-box-settings@.service@@ + ''} $out/etc/systemd/system/agent-box-settings@.service + install -m444 ${pkgs.writeText "agent-box-settings@.socket" '' + @@include:src/units/agent-box-settings@.socket@@ + ''} $out/etc/systemd/system/agent-box-settings@.socket + install -m444 ${pkgs.writeText "agent-box-webhook@.service" '' + @@include:src/units/agent-box-webhook@.service@@ + ''} $out/etc/systemd/system/agent-box-webhook@.service + install -m444 ${pkgs.writeText "agent-box-webhook@.socket" '' + @@include:src/units/agent-box-webhook@.socket@@ + ''} $out/etc/systemd/system/agent-box-webhook@.socket + ''; tmuxSocketName = "agent-box"; runtimeDirectory = name: "agent-box-${name}"; # ttyd port base; ports are assigned in sorted user-name order (see @@ -298,6 +327,13 @@ let # 0.8.0 ported the plugin from node to the python3 already in # agentBaseTools, dropping the box's only nodejs dependency (issue #101). webhookPython = "${pkgs.python3}/bin/python3"; + # Bare-execable wrapper (issue #154 Phase 3): the webhook receiver + # unit's ExecStart names this binary, resolved via ExecSearchPath, + # instead of baking ${webhookPython} ${localWebhookScript} into the + # unit text (which would tie the shared unit file to this Nix build). + webhookReceiverBin = pkgs.writeShellScriptBin "agent-box-webhook-receiver" '' + exec ${webhookPython} ${localWebhookScript} "$@" + ''; # Session-spawn env loader (issue 89). Sessions are (re)created by the # long-lived supervisor inside the agent unit, so a unit-level # EnvironmentFile= snapshot of the user's env file goes stale the moment @@ -1890,7 +1926,9 @@ $PROMPT" # down, so every branch here exits 0. webhookWatchPolicyFile = pkgs.writeText "agent-box-webhook-watch-policy.json" (builtins.toJSON cfg.webhook.watchPolicy); - webhookPolicyApply = pkgs.writeShellScript "agent-box-webhook-policy-apply" '' + # writeShellScriptBin (issue #154 Phase 3): the webhook receiver unit's + # ExecStartPre names this binary bare, resolved via ExecSearchPath. + webhookPolicyApply = pkgs.writeShellScriptBin "agent-box-webhook-policy-apply" '' set -eu JQ=${pkgs.jq}/bin/jq FILE="''${LOCAL_WEBHOOK_STATE_DIR:?}/filter.dispatch.json" @@ -2187,7 +2225,7 @@ $PROMPT" user-name order starting at 7681. The top-level Caddyfile is module-managed, so adding/removing terminal users is a nixos-rebuild away — check the assigned ports with - `systemctl cat agent-web-terminal-`. + `systemctl cat agent-web-terminal@`. ''; }; }; @@ -2248,7 +2286,11 @@ $PROMPT" # ONE user-independent script (issue #154, Phase 2): every per-user value # reaches it through the unit's environment — see the AGENT_BOX_* variables # on the agent unit below. - supervisorScript = pkgs.writeShellScript "agent-box-supervisor" '' + # writeShellScriptBin (not writeShellScript, issue #154 Phase 3): the + # per-user agent unit's ExecStart names this binary bare + # ("agent-box-supervisor") and relies on the host drop-in's + # ExecSearchPath to find it, so it needs a bin/ directory to search. + supervisorScript = pkgs.writeShellScriptBin "agent-box-supervisor" '' set -u # One user-independent script (issue #154, Phase 2): everything user- # or host-specific arrives through the unit's environment (HOME, USER @@ -3668,6 +3710,28 @@ in mode = "0444"; }) ) cfg.users)) + # Per-user env file for the "agent-box@" template unit (issue #154 + # Phase 3): root-owned 0644, no secrets — the unit's + # EnvironmentFile=-/etc/agent-box/units/%i.env picks this up. Carries + # exactly what used to be per-instance `environment = {...}` entries: + # store paths that differ by user, and values only set for SOME users + # (a pointer/URL an unset var simply omits — EnvironmentFile tolerates + # a key that's just absent). Host-level values (pinned tool binaries, + # PATH, HOST_LABEL, WEBHOOK_REPO/PINNED_SCRIPT — the same for every + # instance) live in the "agent-box@" drop-in instead, not here. + // lib.mapAttrs' (name: u: lib.nameValuePair "agent-box/units/${name}.env" { + text = + "AGENT_BOX_SESSIONS_SEED=${sessionsSeedFile name u}\n" + + "AGENT_BOX_AGENT_BINS=${agentBinsFor name}\n" + + "AGENT_BOX_MARK_STOPPED=${markStopped name}\n" + + lib.optionalString (agentsMdPointer name u != null) + "AGENT_BOX_AGENTS_POINTER=${agentsMdPointer name u}\n" + + lib.optionalString (cfg.web.enable && u.web.passwordHashFile != null) ( + "AGENT_BOX_URL=https://${cfg.web.domain}/${name}/\n" + + lib.optionalString webhookEnabled + "AGENT_BOX_WEBHOOK_URL=https://${cfg.web.domain}${webhookPathOf name}\n" + ); + }) cfg.users # Codex autonomy for the WHOLE box (issue 234, see codexFullAccess). # codex reads /etc/codex/config.toml as its system layer, below the # user's own ~/.codex/config.toml, so this sets a default a human can @@ -3683,189 +3747,138 @@ in ''; }; - systemd.services = lib.mapAttrs' (name: u: - lib.nameValuePair "agent-box-${name}" { - description = "Coding agent sessions (tmux) for ${name}"; - wantedBy = [ "multi-user.target" ]; - after = [ "network-online.target" ]; - wants = [ "network-online.target" ]; - # System services get a minimal PATH; give the agent an explicit toolset. - # The user's nix-profile bin goes first so `nix profile add` tools are - # visible without a rebuild. It must be in the unit's PATH (not just a - # BASH_ENV hook): agent CLIs commonly snapshot their startup PATH and re-export - # it in every tool shell, clobbering anything BASH_ENV prepended. - # NOTE: `path` entries go through makeBinPath, which appends /bin — - # so list the profile ROOT, not its bin dir ('.../bin' became the - # nonexistent '.../bin/bin' and silently dropped nix-profile tools). - # /run/wrappers is added when the agent has any sudo allowlist entries, - # so the setuid `sudo` wrapper (which lives at /run/wrappers/bin/sudo, - # NOT on the default systemd unit PATH) resolves in agent tool shells. - # config.nix.package puts the `nix` CLI itself on the agent PATH — - # /run/current-system/sw/bin is NOT on systemd unit PATHs, so without - # it `nix profile add` is unreachable from agent tool shells. - path = [ "/home/${name}/.nix-profile" config.nix.package ] + systemd.packages = [ agentBoxUnitsPackage ]; + + # "agent-box@" template unit (issue #154 Phase 3): the verbatim text + # (src/units/agent-box@.service, shipped via systemd.packages above) + # already declares User=%i, TMUX_TMPDIR, RuntimeDirectory, + # EnvironmentFile=-/etc/agent-box/units/%i.env and the fixed hardening + # knobs. Everything below is HOST-level — the same for every + # instance — so it renders as a drop-in on top of that text (default + # overrideStrategy = "asDropinIfExists"). Per-user values live in the + # generated env file (environment.etc above) instead. + systemd.services = { + "agent-box@" = { + # System services get a minimal PATH; give the agent an explicit + # toolset. The user's nix-profile bin goes first so `nix profile + # add` tools are visible without a rebuild — %i is a literal + # specifier here, expanded by systemd at unit-start, not a Nix + # antiquote. supervisorScript's own bin/ has to be searchable too: + # the unit text's ExecStart names it bare ("agent-box-supervisor"). + # /run/wrappers is added when the agent has any sudo allowlist + # entries, so the setuid `sudo` wrapper resolves in agent tool + # shells. config.nix.package puts the `nix` CLI itself on the agent + # PATH — /run/current-system/sw/bin is NOT on systemd unit PATHs. + path = [ "/home/%i/.nix-profile" config.nix.package supervisorScript ] + ++ agentRuntimePackages + ++ [ pkgs.bashInteractive pkgs.coreutils pkgs.git pkgs.gh pkgs.tmux ] + # local-webhook's .mcp.json runs a bare `python3` (issue #101), which + # agentBaseTools already puts on PATH — no extra runtime needed. + ++ lib.optional (effectiveSudoAllowlist != [ ]) "/run/wrappers"; + # grep/find/flock/hostname are deliberately not on the PATH above + # (see agentBaseTools), so the supervisor's transcript lookups, the + # sessions.json lock (issue #254) and the codex remote-control + # wrapper's UTS re-exec get pinned binaries instead (the + # AGENT_BOX_*_BIN convention) — same store path for every instance, + # hence host-level rather than per-user env-file content. + # NOTE: every optional env var below is merged INTO this one + # `environment` value via nested `//` — chaining separate outer + # `// lib.optionalAttrs cond { environment.X = ...; }` blocks would + # each shallow-replace the whole `environment` attrset in turn and + # silently drop everything set before it. + environment = { + AGENT_BOX_GREP_BIN = "${pkgs.gnugrep}/bin/grep"; + AGENT_BOX_FIND_BIN = "${pkgs.findutils}/bin/find"; + AGENT_BOX_FLOCK_BIN = "${pkgs.util-linux}/bin/flock"; + AGENT_BOX_HOSTNAME_BIN = "${pkgs.unixtools.hostname}/bin/hostname"; + AGENT_BOX_ENV_EXEC = "${envExecWrapper}"; + AGENT_BOX_CODEX_RC = "${codexRemoteControl}"; + # Hook settings for claude spawns — the /clear-rotation record + # (issue #223) the supervisor follows on respawn. + AGENT_BOX_CLAUDE_SETTINGS = "${claudeHookSettings}"; + } + // lib.optionalAttrs (hostLabel != "") { + # Host suffix for auto-derived Remote Control session names. + AGENT_BOX_HOST_LABEL = hostLabel; + } + // lib.optionalAttrs codexFullAccess { + # /etc/codex/config.toml grants codex full access box-wide (issue + # 234); this env var lets a session with skipPermissions = false + # pin the restricted values back. Same for every instance. + AGENT_BOX_CODEX_FULL_ACCESS = "1"; + } + // lib.optionalAttrs webhookEnabled { + # Doubles as the supervisor's "webhook receiver is live" flag + # (per-session LOCAL_WEBHOOK_* env + claude plugin seeding). One + # repo, box-wide — host-level, not per-user. + AGENT_BOX_WEBHOOK_REPO = cfg.webhook.repo; + } + // lib.optionalAttrs (webhookEnabled && cfg.webhook.syncSessionPlugin) { + # Presence = "keep the session's plugin cache off the floor" + # (webhook.syncSessionPlugin, issue #193): one fetched script, + # same store path for every instance. + AGENT_BOX_WEBHOOK_PINNED_SCRIPT = "${localWebhookScript}"; + }; + serviceConfig = { + # ExecSearchPath resolves the unit text's bare `ExecStart= + # agent-box-supervisor`/`ExecStop=tmux …` — systemd's OWN exec + # lookup, independent of the process's runtime $PATH above (which + # matters for the supervisor's own internal tmux/jq/etc calls). + ExecSearchPath = lib.makeBinPath ( + [ "/home/%i/.nix-profile" config.nix.package supervisorScript ] ++ agentRuntimePackages - ++ [ pkgs.bashInteractive pkgs.coreutils pkgs.git pkgs.gh ] - # local-webhook's .mcp.json runs a bare `python3` (issue #101), which - # agentBaseTools already puts on PATH — no extra runtime needed. - ++ lib.optional (effectiveSudoAllowlist != [ ]) "/run/wrappers"; - # TMUX_TMPDIR puts the control socket under the /run RuntimeDirectory - # below instead of /tmp. PrivateTmp (in serviceConfig) gives this unit a - # PRIVATE /tmp, so a socket there would be invisible to the separate - # process that attaches (the AWS ttyd service, or `sudo -u tmux`). - # /run/agent-box- is a normal host path both sides can reach. - # Attach with: env TMUX_TMPDIR=/run/agent-box- tmux -L agent-box attach -t main - # - # AGENT_BOX_URL: the user's browser-terminal URL, exported only when - # this user actually has a terminal (web.enable + web.passwordHashFile). - # An AGENTS.md (see users..agentsMd) can reference it so any - # agent — claude-code, codex, opencode — can answer "where am I - # reachable?" without hard-coding the URL, which is useful because - # the hostname is a spot-restart away from changing. - environment = - { HOME = "/home/${name}"; TMUX_TMPDIR = "/run/${runtimeDirectory name}"; - # Everything user- or host-specific the supervisor script needs - # (issue #154, Phase 2 — src/supervisor.sh is user-independent; - # these variables are its whole contract with the module): - AGENT_BOX_SESSIONS_SEED = "${sessionsSeedFile name u}"; - AGENT_BOX_AGENT_BINS = agentBinsFor name; - AGENT_BOX_ENV_EXEC = "${envExecWrapper}"; - AGENT_BOX_CODEX_RC = "${codexRemoteControl}"; - # Hook settings for claude spawns — the /clear-rotation record - # (issue #223) the supervisor follows on respawn. - AGENT_BOX_CLAUDE_SETTINGS = "${claudeHookSettings}"; - # Clean-exit bookkeeping for the spawn epilogue (issue #167). - # Still per-user (it edits THIS user's sessions.json), so the - # user-independent script takes its path from the environment. - AGENT_BOX_MARK_STOPPED = "${markStopped name}"; - # grep/find are deliberately not on this unit's PATH (see - # agentBaseTools), so the supervisor's transcript lookups get - # pinned binaries instead (the AGENT_BOX_*_BIN convention). - AGENT_BOX_GREP_BIN = "${pkgs.gnugrep}/bin/grep"; - AGENT_BOX_FIND_BIN = "${pkgs.findutils}/bin/find"; - # Same convention for the sessions.json lock (issue #254). flock - # ships in util-linux ONLY — agentBaseTools puts that on this - # unit's PATH, but the supervisor must not depend on a package - # list a host can override with extraPackages, and the other - # writers run from PATHs that do not have it at all. - AGENT_BOX_FLOCK_BIN = "${pkgs.util-linux}/bin/flock"; - # For the codex remote-control wrapper's UTS-namespace re-exec - # (src/codex-remote-control.sh): `hostname` is not part of - # agentBaseTools either. - AGENT_BOX_HOSTNAME_BIN = "${pkgs.unixtools.hostname}/bin/hostname"; - } - // (lib.optionalAttrs (hostLabel != "") { - # Host suffix for auto-derived Remote Control session names. - AGENT_BOX_HOST_LABEL = hostLabel; - }) - // (lib.optionalAttrs codexFullAccess { - # /etc/codex/config.toml grants codex full access box-wide - # (issue 234). Set so a codex TUI session with - # skipPermissions = false can pin the restricted values back — - # the system layer is a default, not a lock. Unset means no such - # file, and codex's own defaults are what a session should get. - AGENT_BOX_CODEX_FULL_ACCESS = "1"; - }) - // (lib.optionalAttrs (agentsMdPointer name u != null) { - # Seeded-AGENTS.md pointer file; unset = agentsMd null opt-out. - AGENT_BOX_AGENTS_POINTER = "${agentsMdPointer name u}"; - }) - // (lib.optionalAttrs webhookEnabled { - # Doubles as the supervisor's "webhook receiver is live" flag - # (per-session LOCAL_WEBHOOK_* env + claude plugin seeding). - AGENT_BOX_WEBHOOK_REPO = cfg.webhook.repo; - }) - // (lib.optionalAttrs (webhookEnabled && cfg.webhook.syncSessionPlugin) { - # Presence = "keep the session's plugin cache off the floor" - # (webhook.syncSessionPlugin, issue #193). The supervisor reads the - # pinned VERSION out of this file to compare against claude's cache - # — the path, not the version, so nothing has to read the fetched - # file at eval time. - AGENT_BOX_WEBHOOK_PINNED_SCRIPT = "${localWebhookScript}"; - }) - // (lib.optionalAttrs (cfg.web.enable && u.web.passwordHashFile != null) { - AGENT_BOX_URL = "https://${cfg.web.domain}/${name}/"; - # Same idea for the webhook ingress (issue #101): the public URL to - # register in GitHub/Stripe/… so `agent-box-webhook url|setup` can - # print it without the agent deriving the hostname (a Spot restart - # away from changing). Unset ⇒ no endpoint is served for this user. - } // lib.optionalAttrs webhookEnabled { - AGENT_BOX_WEBHOOK_URL = "https://${cfg.web.domain}${webhookPathOf name}"; - }) - // u.environment; - serviceConfig = { - User = name; - # ExecStart is the session supervisor (issue #59): it reconciles - # tmux sessions against the user-owned sessions.json forever. - # Individual session restarts happen inside the loop; - # Restart=always only backstops a crashed supervisor. - Type = "exec"; - Restart = "always"; - RestartSec = "2s"; - ExecStart = supervisorScript; - # Stopping the unit stops every session: kill the whole per-user - # tmux server (the supervisor loop dies with the cgroup). - ExecStop = "${pkgs.tmux}/bin/tmux -L ${tmuxSocketName} kill-server"; - # Holds the tmux control socket (see TMUX_TMPDIR above). 0700 so only - # the agent user can reach its own socket; ExecStop/attachers run as - # the same user. Persist across restarts so an in-flight attach isn't - # racing the dir's teardown when Restart=always cycles the agent. - RuntimeDirectory = runtimeDirectory name; - RuntimeDirectoryMode = "0700"; - RuntimeDirectoryPreserve = true; - # NOTE: the settings page's user-owned ~/.config/agent-box/env is - # deliberately NOT listed here. Unit env is a snapshot from unit - # start, and sessions are respawned by the long-lived supervisor — - # so browser-added secrets never reached restarted sessions, and - # deleted keys never left (issue 89). The supervisor's env-exec - # wrapper reads that file at every session spawn instead; tokens - # (GH_TOKEN, etc.) go there too, via the settings page or - # `agent-box-session env set`, applying at next session spawn with - # no unit bounce. - EnvironmentFile = cfg.environmentFiles ++ u.environmentFiles; - - # Systemd hardening. The OS boundary has to stay meaningful even - # though the agent runs with its in-tool approval prompts disabled. - # This is the containment the agent CLI deliberately opts out of. - PrivateTmp = true; - PrivateDevices = true; # keeps pty subsystem; blocks /dev/mem etc. - ProtectSystem = "strict"; # entire fs read-only except explicit RW paths - # ~/sites is a SYMLINK to /var/lib/agent-box-sites/, so a write - # through it resolves outside /home and ProtectSystem=strict denied it - # with EROFS — the self-serve vhost flow the guide documents was - # impossible from inside the agent's own namespace. ReadWritePaths - # matches on the resolved path, so the target has to be listed too. - # The dir is tmpfiles-created (sysinit, well before this unit), so it - # always exists by unit start and needs no `-` prefix — but ONLY when - # web.enable is on, since that is what gates the tmpfiles rule. Listing - # it unconditionally fails the whole namespace setup with 226/NAMESPACE - # on a default (web-less) box, so the unit never starts at all: no `-` - # here, an explicit guard instead, so a dir that is genuinely missing - # while web is on stays loud rather than silently reverting to EROFS. - ReadWritePaths = [ "/home/${name}" ] - ++ lib.optional cfg.web.enable "/var/lib/agent-box-sites/${name}"; - ProtectKernelTunables = true; - ProtectKernelModules = true; - ProtectControlGroups = true; - ProtectClock = true; - RestrictSUIDSGID = true; - RestrictRealtime = true; - LockPersonality = true; - # NoNewPrivileges would break the sudoAllowlist escape hatch (sudo - # is setuid root; NNP blocks the euid transition). Enable it only - # when the effective allowlist is empty — with a non-empty allowlist - # (from cfg.sudoAllowlist or the web-implied caddy reload) we've - # traded some containment for scoped elevation as a host choice. - NoNewPrivileges = effectiveSudoAllowlist == [ ]; - } // lib.optionalAttrs cfg.protectMemory { - # Sacrifice agent work first under memory pressure: the kernel OOM - # killer and earlyoom both weigh oom_score_adj, so a runaway agent - # process dies before sshd/caddy/SSM — and Restart=always brings - # the session back fresh instead of leaving a frozen box. - OOMScoreAdjust = lib.mkDefault 500; - }; + ++ [ pkgs.bashInteractive pkgs.coreutils pkgs.git pkgs.gh pkgs.tmux ] + ++ lib.optional (effectiveSudoAllowlist != [ ]) "/run/wrappers" + ); + # NOTE: the settings page's user-owned ~/.config/agent-box/env is + # deliberately NOT listed here (nor in the per-user env file). + # See the original issue 89 rationale: unit env is a start-time + # snapshot, so browser-added secrets go through the supervisor's + # env-exec wrapper at every session spawn instead. + EnvironmentFile = cfg.environmentFiles; + # ~/sites is a SYMLINK to /var/lib/agent-box-sites/, so a + # write through it resolves outside /home and ProtectSystem=strict + # denies it with EROFS unless the target is ALSO listed — but only + # when web.enable is on, since that is what gates the tmpfiles + # rule that creates the dir; listing it unconditionally fails the + # whole namespace setup with 226/NAMESPACE on a default (web-less) + # box. systemd path lists accumulate across drop-ins onto the base + # unit's own ReadWritePaths=/home/%i, so this only ADDS the extra + # path, never replaces it. + ReadWritePaths = lib.optional cfg.web.enable "/var/lib/agent-box-sites/%i"; + # NoNewPrivileges would break the sudoAllowlist escape hatch (sudo + # is setuid root; NNP blocks the euid transition). Enable it only + # when the effective allowlist is empty — with a non-empty + # allowlist (from cfg.sudoAllowlist or the web-implied caddy + # reload) we've traded some containment for scoped elevation as a + # host choice. Same for every instance, hence host-level. + NoNewPrivileges = effectiveSudoAllowlist == [ ]; + } // lib.optionalAttrs cfg.protectMemory { + # Sacrifice agent work first under memory pressure: the kernel OOM + # killer and earlyoom both weigh oom_score_adj, so a runaway agent + # process dies before sshd/caddy/SSM — and Restart=always brings + # the session back fresh instead of leaving a frozen box. + OOMScoreAdjust = lib.mkDefault 500; + }; + }; + } + # Enablement + the one per-user drop-in the generated env file can't + # express: arbitrary u.environment/u.environmentFiles (issue #154 + # Phase 3 design's stated exception — an env file can't add its own + # EnvironmentFile= entries or override an already-set var per + # instance). overrideStrategy = "asDropin" is required here (unlike + # "agent-box@" above): nothing on disk is literally named + # "agent-box@alice.service" for the default detection to find, since + # instances are virtual. + // (lib.mapAttrs' (name: u: lib.nameValuePair "agent-box@${name}" ( + { overrideStrategy = "asDropin"; + wantedBy = [ "multi-user.target" ]; + } + // lib.optionalAttrs (u.environment != { }) { environment = u.environment; } + // lib.optionalAttrs (u.environmentFiles != [ ]) { + serviceConfig.EnvironmentFile = u.environmentFiles; } - ) cfg.users; + )) cfg.users); security.sudo.extraRules = lib.mkIf (effectiveSudoAllowlist != [ ]) [{ users = lib.attrNames cfg.users; @@ -7995,7 +8008,11 @@ in # ttyd/xterm supports OSC 8, but xterm-256color cannot advertise that # through terminfo. Tell tmux explicitly so it forwards stored # hyperlinks instead of redrawing only their visible labels. - attachScript = pkgs.writeShellScript "agent-box-attach" '' + # writeShellScriptBin (issue #154 Phase 3): the web-terminal unit's + # ttyd ExecStart passes this bare ("agent-box-attach"), resolved + # via that unit's PATH (ttyd execs it directly, not through + # systemd, so it needs the runtime PATH, not just ExecSearchPath). + attachScript = pkgs.writeShellScriptBin "agent-box-attach" '' set -u # tmux, jq and head resolve from the web-terminal unit's PATH; the socket # name is the module-wide constant. The sessions registry arrives as unit @@ -8442,224 +8459,150 @@ in # under // so one vhost can serve them all. # TMUX_TMPDIR must match the agent unit's RuntimeDirectory (the agent # runs with PrivateTmp, so the socket lives in /run, not /tmp). + # Per-user env files for the three web-gated template units (issue + # #154 Phase 3) — same rationale as the "agent-box@" one above: + # store paths/values that differ by user go here; the same value + # for every instance goes in that unit's host drop-in instead. + environment.etc = + lib.mapAttrs' (name: _: lib.nameValuePair "agent-box/units/agent-web-terminal-${name}.env" { + text = "AGENT_BOX_TTYD_PORT=${toString portOf.${name}}\n"; + }) portOf + // lib.listToAttrs (map (name: lib.nameValuePair "agent-box/units/agent-box-settings-${name}.env" { + text = + "AGENT_BOX_PASSWORD_CMD=/run/wrappers/bin/sudo -n ${passwordHelperCmdOf name}\n" + + lib.optionalString (name == rootUser) + # This daemon also serves the vhost root: GET / is the + # session manager (Caddy proxies it here behind the + # user's auth) and the session CRUD routes move to + # /sessions/*. + "AGENT_BOX_HOME=1\n" + + lib.optionalString webhookEnabled + "AGENT_BOX_WEBHOOK_STATE_DIR=${webhookStateDirOf name}\n"; + }) terminalUsers) + // lib.optionalAttrs webhookEnabled (lib.listToAttrs (map (name: lib.nameValuePair "agent-box/units/agent-box-webhook-${name}.env" { + text = "LOCAL_WEBHOOK_STATE_DIR=${webhookStateDirOf name}\n"; + }) terminalUsers)); + + # ttyd per terminal user — attaches to that user's tmux session over an + # internal port; --base-path keeps each terminal (and its /ws endpoint) + # under // so one vhost can serve them all. The unit text (see + # src/units/agent-web-terminal@.service, shipped via + # systemd.packages at the top of this module) already declares + # User=%i, TMUX_TMPDIR/AGENT_BOX_SESSIONS_FILE and the ttyd + # ExecStart, expanding AGENT_BOX_TTYD_PORT/AGENT_BOX_WEB_DOMAIN from + # environment at exec time; what's below is HOST-level (the same + # PATH/domain for every instance) so it renders as a drop-in. systemd.services = { agent-web-auth-secrets = webAuthSecretsService; caddy.serviceConfig.EnvironmentFile = "/run/agent-box-web/env"; - } // lib.listToAttrs (map (name: lib.nameValuePair "agent-web-terminal-${name}" { - description = "Browser terminal (ttyd) attached to ${name}'s tmux"; - after = [ "agent-box-${name}.service" "network-online.target" ]; - wants = [ "network-online.target" ]; - wantedBy = [ "multi-user.target" ]; - # The attach script (src/attach.sh) resolves tmux and jq from this - # PATH instead of baked store paths (issue #154, Phase 2); head comes - # from the coreutils already on every unit's default PATH. - path = [ pkgs.tmux pkgs.jq ]; - environment = { - TMUX_TMPDIR = "/run/${runtimeDirectory name}"; - # Read-only, and by the session's OWN user: what the attach - # script needs it for is telling a name this box has never heard - # of from one that is merely down (issue #241). - AGENT_BOX_SESSIONS_FILE = userSessionsFile name; + + "agent-web-terminal@" = { + # The attach script (src/attach.sh) resolves tmux and jq from + # this PATH instead of baked store paths (issue #154, Phase 2); + # ttyd itself execs the bare "agent-box-attach" argument + # through this same runtime PATH, not ExecSearchPath (that only + # covers systemd's own resolution of the unit's ExecStart). + path = [ pkgs.tmux pkgs.jq attachScript pkgs.ttyd ]; + environment.AGENT_BOX_WEB_DOMAIN = cfg.web.domain; + serviceConfig.ExecSearchPath = lib.makeBinPath [ pkgs.ttyd ]; }; - serviceConfig = { - User = name; - Restart = "always"; - RestartSec = "5s"; - ExecStart = lib.concatStringsSep " " [ - "${pkgs.ttyd}/bin/ttyd" - "--writable" - # ?arg= in the URL becomes $1 of the attach wrapper — - # session-level deep links from the root sessions page (issue #59). - "--url-arg" - "-p" (toString portOf.${name}) - "-i" "127.0.0.1" - "-b" "/${name}" - "-t" "disableLeaveAlert=true" - "-t" "titleFixed=${name}@${cfg.web.domain}" - (toString attachScript) - ]; + + # Settings daemon (issue #36), one per terminal user. Runs AS the + # agent user (no root, no privilege boundary): it only writes + # that user's own ~/.config/agent-box/env and kills that user's + # own tmux session. The agent unit's Restart=always then reloads + # it with the fresh env. Listens via socket activation (issue + # #49) on the same-named .socket instance below. + "agent-box-settings@" = { + # See the "agent-box@" comment above: every optional block + # merges INTO this one `environment` value via nested `//`, not + # as separate outer `// lib.optionalAttrs {...}` blocks (which + # would each shallow-replace the whole attrset in turn). + environment = { + AGENT_BOX_TMUX_BIN = "${pkgs.tmux}/bin/tmux"; + AGENT_BOX_AGENTS = lib.concatStringsSep "," (sessionKinds cfg.installAgents); + AGENT_BOX_DEFAULT_AGENT = cfg.agent; + } + // lib.optionalAttrs webhookEnabled { + # Webhook subscriptions panel (issue #227). The daemon runs + # the pinned webhook.py's own CLI once per session, with + # LOCAL_WEBHOOK_SESSION set to that session's key, to list + # and delete subscriptions — so the panel cannot drift from + # the semantics the receiver and the in-session tools + # implement. Same script and interpreter the receiver unit + # runs, hence host-level (the per-user state dir is in the + # env file above). + AGENT_BOX_WEBHOOK_SCRIPT = localWebhookScript; + AGENT_BOX_WEBHOOK_PYTHON = webhookPython; + # Same script the receiver runs on a match, for its + # --preamble mode only: the standing-watch panel prints the + # prompt the next match would launch (#259). + AGENT_BOX_HOOK_SPAWN_CMD = "${webhookSpawn}/bin/agent-box-webhook-spawn"; + } + // lib.optionalAttrs cfg.selfUpdate.enable { + # --no-block so the daemon's HTTP response goes out before + # the rebuild (possibly) restarts the daemon itself. + AGENT_BOX_UPDATE_CMD = "/run/wrappers/bin/sudo -n ${updateStartNoBlockCmd}"; + # Running rev + repo, rendered on the Update card as a + # GitHub commit link and used for its non-blocking compare + # request. + AGENT_BOX_REPO = cfg.selfUpdate.repo; + AGENT_BOX_REV = cfg.selfUpdate.rev; + # Read-only handles for the {BASE}/status progress endpoint + # the page long-polls after "Update box": no sudo needed + # here, unlike AGENT_BOX_UPDATE_CMD, which triggers the run. + AGENT_BOX_UPDATE_UNIT = "agent-box-update.service"; + AGENT_BOX_SYSTEMCTL = "/run/current-system/sw/bin/systemctl"; + }; + serviceConfig.ExecSearchPath = lib.makeBinPath [ settingsDaemon ]; }; + } + // lib.listToAttrs (map (name: lib.nameValuePair "agent-web-terminal@${name}" { + overrideStrategy = "asDropin"; + wantedBy = [ "multi-user.target" ]; }) terminalUsers) - # Settings daemon (issue #36), one per terminal user. Runs AS the agent - # user (no root, no privilege boundary): it only writes that user's own - # ~/.config/agent-box/env and kills that user's own tmux session. The - # agent unit's Restart=always then reloads it with the fresh env. - # Listens via socket activation on the systemd-owned unix socket - # (issue #49) — the same-named .socket unit below; requires/after kept - # explicit per this repo's explicit-over-implied-config convention. - // lib.listToAttrs (map (name: lib.nameValuePair "agent-box-settings-${name}" { - description = "Per-user secrets settings page for ${name}"; - after = [ "network-online.target" "agent-box-settings-${name}.socket" ]; - requires = [ "agent-box-settings-${name}.socket" ]; - wants = [ "network-online.target" ]; + // lib.listToAttrs (map (name: lib.nameValuePair "agent-box-settings@${name}" { + overrideStrategy = "asDropin"; wantedBy = [ "multi-user.target" ]; - # TMUX_TMPDIR must match the agent unit's RuntimeDirectory so the - # daemon can reach the (PrivateTmp) tmux socket to restart the agent. - environment = { - TMUX_TMPDIR = "/run/${runtimeDirectory name}"; - AGENT_BOX_SETTINGS_USER = name; - AGENT_BOX_SETTINGS_ENV_FILE = userEnvFile name; - AGENT_BOX_SETTINGS_BASE = settingsBaseOf name; - AGENT_BOX_TMUX_SOCKET = tmuxSocketName; - AGENT_BOX_TMUX_TMPDIR = "/run/${runtimeDirectory name}"; - AGENT_BOX_TMUX_BIN = "${pkgs.tmux}/bin/tmux"; - AGENT_BOX_SESSIONS_FILE = userSessionsFile name; - AGENT_BOX_AGENTS = lib.concatStringsSep "," (sessionKinds cfg.installAgents); - AGENT_BOX_DEFAULT_AGENT = cfg.agent; - AGENT_BOX_PASSWORD_CMD = - "/run/wrappers/bin/sudo -n ${passwordHelperCmdOf name}"; - } // lib.optionalAttrs (name == rootUser) { - # This daemon also serves the vhost root: GET / is the session - # manager (Caddy proxies it here behind the user's auth) and the - # session CRUD routes move to /sessions/*. - AGENT_BOX_HOME = "1"; - } // lib.optionalAttrs webhookEnabled { - # Webhook subscriptions panel (issue #227). The daemon runs the - # pinned webhook.py's own CLI once per session, with - # LOCAL_WEBHOOK_SESSION set to that session's key, to list and - # delete subscriptions — so the panel cannot drift from the - # semantics the receiver and the in-session tools implement. - # Same script and interpreter the receiver unit runs. - AGENT_BOX_WEBHOOK_SCRIPT = localWebhookScript; - AGENT_BOX_WEBHOOK_PYTHON = webhookPython; - AGENT_BOX_WEBHOOK_STATE_DIR = webhookStateDirOf name; - # Same script the receiver runs on a match, for its --preamble mode - # only: the standing-watch panel prints the prompt the next match - # would launch (#259). One text, one source — a copy in the daemon - # would drift from what the box actually sends. - AGENT_BOX_HOOK_SPAWN_CMD = "${webhookSpawn}/bin/agent-box-webhook-spawn"; - } // lib.optionalAttrs cfg.selfUpdate.enable { - # --no-block so the daemon's HTTP response goes out before the - # rebuild (possibly) restarts the daemon itself. - AGENT_BOX_UPDATE_CMD = "/run/wrappers/bin/sudo -n ${updateStartNoBlockCmd}"; - # Running rev + repo, rendered on the Update card as a GitHub - # commit link and used for its non-blocking compare request. - AGENT_BOX_REPO = cfg.selfUpdate.repo; - AGENT_BOX_REV = cfg.selfUpdate.rev; - # Read-only handles for the {BASE}/status progress endpoint the page - # long-polls after "Update box": the update oneshot to watch and an - # unprivileged `systemctl show` binary to read its state with (no - # sudo — unlike AGENT_BOX_UPDATE_CMD, which triggers the run). Only - # wired when selfUpdate is on, so the status endpoint omits the update - # block otherwise. - AGENT_BOX_UPDATE_UNIT = "agent-box-update.service"; - AGENT_BOX_SYSTEMCTL = "/run/current-system/sw/bin/systemctl"; - }; - serviceConfig = { - User = name; - Restart = "always"; - RestartSec = "5s"; - ExecStart = "${settingsDaemon}/bin/agent-box-settings"; - # Hardening: the daemon itself remains unable to read the root-owned - # web secrets. /var/lib/agent-box-web is writable in the mount - # namespace only so the sudo'd password helper can atomically replace - # this user's hash and cookie; normal Unix permissions still deny the - # unprivileged daemon. - ProtectSystem = "strict"; - ReadWritePaths = [ - "/home/${name}" - "/run/${runtimeDirectory name}" - "/var/lib/agent-box-web" - ]; - ProtectHome = false; - PrivateDevices = true; - ProtectKernelTunables = true; - ProtectKernelModules = true; - ProtectControlGroups = true; - RestrictSUIDSGID = true; - RestrictRealtime = true; - LockPersonality = true; - # Setuid sudo needs privilege escalation, which NNP vetoes. The - # daemon always has its per-user, argument-free password helper; with - # selfUpdate enabled it also has the argument-free update trigger. - NoNewPrivileges = false; - }; }) terminalUsers) # Webhook receiver daemon (issue #101), one per terminal user, gated on - # webhook.enable. Runs webhook.py in RECEIVER_ONLY mode as the agent - # user: owns the socket-activated UNIX ingress (the same-named .socket - # below) and fans HMAC-verified deliveries out to that user's sessions - # over IPC, with no MCP session of its own. - // lib.optionalAttrs webhookEnabled (lib.listToAttrs (map (name: lib.nameValuePair "agent-box-webhook-${name}" { - description = "Webhook receiver daemon (local-webhook) for ${name}"; - after = [ "network-online.target" "agent-box-webhook-${name}.socket" ]; - requires = [ "agent-box-webhook-${name}.socket" ]; - wants = [ "network-online.target" ]; - wantedBy = [ "multi-user.target" ]; - # The spawn wrapper (src/webhook-spawn.sh) runs as this unit's child - # and resolves jq/coreutils/agent-box-session from this PATH instead - # of baked store paths (issue #154, Phase 2). - path = [ pkgs.jq pkgs.coreutils sessionCli ]; - environment = { - LOCAL_WEBHOOK_RECEIVER_ONLY = "1"; - LOCAL_WEBHOOK_STATE_DIR = webhookStateDirOf name; - LOCAL_WEBHOOK_PORT = "0"; + # webhook.enable. Runs webhook.py (via the agent-box-webhook-receiver + # wrapper, resolved through ExecSearchPath) in RECEIVER_ONLY mode as + # the agent user: owns the socket-activated UNIX ingress (the + # same-named .socket instance below) and fans HMAC-verified + # deliveries out to that user's sessions over IPC. + // lib.optionalAttrs webhookEnabled ({ + "agent-box-webhook@" = { + # The spawn wrapper (src/webhook-spawn.sh) runs as this unit's + # child and resolves jq/coreutils/agent-box-session from this + # PATH instead of baked store paths (issue #154, Phase 2). + path = [ pkgs.jq pkgs.coreutils sessionCli ]; # Standing watches (deliver_to:"subagent", local-channels#1): a - # matching delivery spawns a fresh hook-* session for this user. - # webhook.py coalesces bursts and caps concurrent spawns; the - # wrapper additionally caps how many hook-* sessions may exist. - LOCAL_WEBHOOK_SPAWN_CMD = "${webhookSpawn}/bin/agent-box-webhook-spawn"; - # webhook.hookSessionArgs is NOT here: the wrapper carries it, so - # the settings page and the CLI resolve the same default this unit - # would have handed only to its own child (#292). - }; - serviceConfig = { - User = name; - Restart = "always"; - RestartSec = "5s"; - # Enforce the declared watch policy (webhook.watchPolicy) onto this - # user's filter.dispatch.json before the receiver starts routing. - # Runs as User (no "+" prefix); exits 0 on every path, because a - # policy hiccup must not take the box's one ingress down with it. - ExecStartPre = "${webhookPolicyApply}"; - ExecStart = "${webhookPython} ${localWebhookScript}"; - # systemd passes the ingress socket on fd 3 (LISTEN_FDS) and wires - # stdin to /dev/null; RECEIVER_ONLY tolerates that (no MCP stdio). - StandardInput = "null"; - ProtectSystem = "strict"; - ReadWritePaths = [ "/home/${name}" ]; - ProtectHome = false; - PrivateDevices = true; - ProtectKernelTunables = true; - ProtectKernelModules = true; - ProtectControlGroups = true; - RestrictSUIDSGID = true; - RestrictRealtime = true; - LockPersonality = true; - NoNewPrivileges = true; + # matching delivery spawns a fresh hook-* session for this + # user. One wrapper, same store path for every instance. + environment.LOCAL_WEBHOOK_SPAWN_CMD = "${webhookSpawn}/bin/agent-box-webhook-spawn"; + serviceConfig.ExecSearchPath = lib.makeBinPath [ webhookPolicyApply webhookReceiverBin ]; }; + } // lib.listToAttrs (map (name: lib.nameValuePair "agent-box-webhook@${name}" { + overrideStrategy = "asDropin"; + wantedBy = [ "multi-user.target" ]; }) terminalUsers)); - # The settings daemon's listening sockets (issue #49). systemd (root) - # binds each unix socket with exact ownership BEFORE the daemon starts: - # 0660 :caddy means only that user and the caddy reverse-proxy - # can connect — unlike the previous 127.0.0.1: listener, which - # every local user could reach. The daemon adopts the socket through - # socket activation (LISTEN_FDS, fd 3). - systemd.sockets = lib.listToAttrs (map (name: lib.nameValuePair "agent-box-settings-${name}" { - description = "Settings page socket for ${name}"; - wantedBy = [ "sockets.target" ]; - socketConfig = { - ListenStream = settingsSocketOf name; - SocketUser = name; - SocketGroup = "caddy"; - SocketMode = "0660"; - }; - }) terminalUsers) - # Webhook ingress sockets (issue #101). systemd binds each 0660 - # :caddy — same isolation as the settings socket — so only that - # user and the caddy reverse-proxy can POST; the daemon adopts it via - # socket activation (fd 3). - // lib.optionalAttrs webhookEnabled (lib.listToAttrs (map (name: lib.nameValuePair "agent-box-webhook-${name}" { - description = "Webhook ingress socket for ${name}"; - wantedBy = [ "sockets.target" ]; - socketConfig = { - ListenStream = webhookSocketOf name; - SocketUser = name; - SocketGroup = "caddy"; - SocketMode = "0660"; - }; - }) terminalUsers)); + # The settings daemon's listening sockets (issue #49) and the webhook + # ingress sockets (issue #101) are fully %i-derived already (see + # src/units/*.socket, shipped via systemd.packages) — root binds + # each one 0660 :caddy BEFORE the daemon starts, exactly as + # before; only per-instance ENABLEMENT (sockets.target) is needed + # here, same asDropin reasoning as the .service instances above. + systemd.sockets = + lib.listToAttrs (map (name: lib.nameValuePair "agent-box-settings@${name}" { + overrideStrategy = "asDropin"; + wantedBy = [ "sockets.target" ]; + }) terminalUsers) + // lib.optionalAttrs webhookEnabled (lib.listToAttrs (map (name: lib.nameValuePair "agent-box-webhook@${name}" { + overrideStrategy = "asDropin"; + wantedBy = [ "sockets.target" ]; + }) terminalUsers)); } )) @@ -8782,7 +8725,7 @@ in sleep "$GRACE" echo "spot-monitor: stopping agent units (no respawn)" >&2 for u in $USERS; do - $SYSTEMCTL stop "agent-box-$u.service" || true + $SYSTEMCTL stop "agent-box@$u.service" || true done $SYNC echo "spot-monitor: prepared; leaving the stop to AWS (preserves persistent-Spot auto-restart)" >&2 diff --git a/modules/agent-box.nix.in b/modules/agent-box.nix.in index e7c75a37..459b1ffa 100644 --- a/modules/agent-box.nix.in +++ b/modules/agent-box.nix.in @@ -55,6 +55,35 @@ let # `nixos-rebuild switch` (i.e. every box update); readable but not writable # under ProtectSystem=strict. canonicalAgentsPath = name: "/etc/agent-box-guides/AGENTS.${name}.md"; + # issue #154 Phase 3: the per-user services are systemd %i template + # units, whose text lives as verbatim, backend-neutral files under + # src/units/ — the single place a hypothetical native (non-NixOS) + # backend would also install these from, byte-for-byte. Shipped here via + # systemd.packages; a `systemd.services."@"` definition elsewhere + # in this module then renders as a host-level drop-in automatically + # (default overrideStrategy = "asDropinIfExists" detects that a unit + # file of the same name already exists in this package). + agentBoxUnitsPackage = pkgs.runCommand "agent-box-units" { } '' + mkdir -p $out/etc/systemd/system + install -m444 ${pkgs.writeText "agent-box@.service" '' + @@include:src/units/agent-box@.service@@ + ''} $out/etc/systemd/system/agent-box@.service + install -m444 ${pkgs.writeText "agent-web-terminal@.service" '' + @@include:src/units/agent-web-terminal@.service@@ + ''} $out/etc/systemd/system/agent-web-terminal@.service + install -m444 ${pkgs.writeText "agent-box-settings@.service" '' + @@include:src/units/agent-box-settings@.service@@ + ''} $out/etc/systemd/system/agent-box-settings@.service + install -m444 ${pkgs.writeText "agent-box-settings@.socket" '' + @@include:src/units/agent-box-settings@.socket@@ + ''} $out/etc/systemd/system/agent-box-settings@.socket + install -m444 ${pkgs.writeText "agent-box-webhook@.service" '' + @@include:src/units/agent-box-webhook@.service@@ + ''} $out/etc/systemd/system/agent-box-webhook@.service + install -m444 ${pkgs.writeText "agent-box-webhook@.socket" '' + @@include:src/units/agent-box-webhook@.socket@@ + ''} $out/etc/systemd/system/agent-box-webhook@.socket + ''; tmuxSocketName = "agent-box"; runtimeDirectory = name: "agent-box-${name}"; # ttyd port base; ports are assigned in sorted user-name order (see @@ -115,6 +144,13 @@ let # 0.8.0 ported the plugin from node to the python3 already in # agentBaseTools, dropping the box's only nodejs dependency (issue #101). webhookPython = "${pkgs.python3}/bin/python3"; + # Bare-execable wrapper (issue #154 Phase 3): the webhook receiver + # unit's ExecStart names this binary, resolved via ExecSearchPath, + # instead of baking ${webhookPython} ${localWebhookScript} into the + # unit text (which would tie the shared unit file to this Nix build). + webhookReceiverBin = pkgs.writeShellScriptBin "agent-box-webhook-receiver" '' + exec ${webhookPython} ${localWebhookScript} "$@" + ''; # Session-spawn env loader (issue 89). Sessions are (re)created by the # long-lived supervisor inside the agent unit, so a unit-level # EnvironmentFile= snapshot of the user's env file goes stale the moment @@ -387,7 +423,9 @@ let # down, so every branch here exits 0. webhookWatchPolicyFile = pkgs.writeText "agent-box-webhook-watch-policy.json" (builtins.toJSON cfg.webhook.watchPolicy); - webhookPolicyApply = pkgs.writeShellScript "agent-box-webhook-policy-apply" '' + # writeShellScriptBin (issue #154 Phase 3): the webhook receiver unit's + # ExecStartPre names this binary bare, resolved via ExecSearchPath. + webhookPolicyApply = pkgs.writeShellScriptBin "agent-box-webhook-policy-apply" '' set -eu JQ=${pkgs.jq}/bin/jq FILE="''${LOCAL_WEBHOOK_STATE_DIR:?}/filter.dispatch.json" @@ -684,7 +722,7 @@ let user-name order starting at 7681. The top-level Caddyfile is module-managed, so adding/removing terminal users is a nixos-rebuild away — check the assigned ports with - `systemctl cat agent-web-terminal-`. + `systemctl cat agent-web-terminal@`. ''; }; }; @@ -745,7 +783,11 @@ let # ONE user-independent script (issue #154, Phase 2): every per-user value # reaches it through the unit's environment — see the AGENT_BOX_* variables # on the agent unit below. - supervisorScript = pkgs.writeShellScript "agent-box-supervisor" '' + # writeShellScriptBin (not writeShellScript, issue #154 Phase 3): the + # per-user agent unit's ExecStart names this binary bare + # ("agent-box-supervisor") and relies on the host drop-in's + # ExecSearchPath to find it, so it needs a bin/ directory to search. + supervisorScript = pkgs.writeShellScriptBin "agent-box-supervisor" '' @@include:src/supervisor.sh@@ ''; @@ -1420,6 +1462,28 @@ in mode = "0444"; }) ) cfg.users)) + # Per-user env file for the "agent-box@" template unit (issue #154 + # Phase 3): root-owned 0644, no secrets — the unit's + # EnvironmentFile=-/etc/agent-box/units/%i.env picks this up. Carries + # exactly what used to be per-instance `environment = {...}` entries: + # store paths that differ by user, and values only set for SOME users + # (a pointer/URL an unset var simply omits — EnvironmentFile tolerates + # a key that's just absent). Host-level values (pinned tool binaries, + # PATH, HOST_LABEL, WEBHOOK_REPO/PINNED_SCRIPT — the same for every + # instance) live in the "agent-box@" drop-in instead, not here. + // lib.mapAttrs' (name: u: lib.nameValuePair "agent-box/units/${name}.env" { + text = + "AGENT_BOX_SESSIONS_SEED=${sessionsSeedFile name u}\n" + + "AGENT_BOX_AGENT_BINS=${agentBinsFor name}\n" + + "AGENT_BOX_MARK_STOPPED=${markStopped name}\n" + + lib.optionalString (agentsMdPointer name u != null) + "AGENT_BOX_AGENTS_POINTER=${agentsMdPointer name u}\n" + + lib.optionalString (cfg.web.enable && u.web.passwordHashFile != null) ( + "AGENT_BOX_URL=https://${cfg.web.domain}/${name}/\n" + + lib.optionalString webhookEnabled + "AGENT_BOX_WEBHOOK_URL=https://${cfg.web.domain}${webhookPathOf name}\n" + ); + }) cfg.users # Codex autonomy for the WHOLE box (issue 234, see codexFullAccess). # codex reads /etc/codex/config.toml as its system layer, below the # user's own ~/.codex/config.toml, so this sets a default a human can @@ -1435,189 +1499,138 @@ in ''; }; - systemd.services = lib.mapAttrs' (name: u: - lib.nameValuePair "agent-box-${name}" { - description = "Coding agent sessions (tmux) for ${name}"; - wantedBy = [ "multi-user.target" ]; - after = [ "network-online.target" ]; - wants = [ "network-online.target" ]; - # System services get a minimal PATH; give the agent an explicit toolset. - # The user's nix-profile bin goes first so `nix profile add` tools are - # visible without a rebuild. It must be in the unit's PATH (not just a - # BASH_ENV hook): agent CLIs commonly snapshot their startup PATH and re-export - # it in every tool shell, clobbering anything BASH_ENV prepended. - # NOTE: `path` entries go through makeBinPath, which appends /bin — - # so list the profile ROOT, not its bin dir ('.../bin' became the - # nonexistent '.../bin/bin' and silently dropped nix-profile tools). - # /run/wrappers is added when the agent has any sudo allowlist entries, - # so the setuid `sudo` wrapper (which lives at /run/wrappers/bin/sudo, - # NOT on the default systemd unit PATH) resolves in agent tool shells. - # config.nix.package puts the `nix` CLI itself on the agent PATH — - # /run/current-system/sw/bin is NOT on systemd unit PATHs, so without - # it `nix profile add` is unreachable from agent tool shells. - path = [ "/home/${name}/.nix-profile" config.nix.package ] + systemd.packages = [ agentBoxUnitsPackage ]; + + # "agent-box@" template unit (issue #154 Phase 3): the verbatim text + # (src/units/agent-box@.service, shipped via systemd.packages above) + # already declares User=%i, TMUX_TMPDIR, RuntimeDirectory, + # EnvironmentFile=-/etc/agent-box/units/%i.env and the fixed hardening + # knobs. Everything below is HOST-level — the same for every + # instance — so it renders as a drop-in on top of that text (default + # overrideStrategy = "asDropinIfExists"). Per-user values live in the + # generated env file (environment.etc above) instead. + systemd.services = { + "agent-box@" = { + # System services get a minimal PATH; give the agent an explicit + # toolset. The user's nix-profile bin goes first so `nix profile + # add` tools are visible without a rebuild — %i is a literal + # specifier here, expanded by systemd at unit-start, not a Nix + # antiquote. supervisorScript's own bin/ has to be searchable too: + # the unit text's ExecStart names it bare ("agent-box-supervisor"). + # /run/wrappers is added when the agent has any sudo allowlist + # entries, so the setuid `sudo` wrapper resolves in agent tool + # shells. config.nix.package puts the `nix` CLI itself on the agent + # PATH — /run/current-system/sw/bin is NOT on systemd unit PATHs. + path = [ "/home/%i/.nix-profile" config.nix.package supervisorScript ] + ++ agentRuntimePackages + ++ [ pkgs.bashInteractive pkgs.coreutils pkgs.git pkgs.gh pkgs.tmux ] + # local-webhook's .mcp.json runs a bare `python3` (issue #101), which + # agentBaseTools already puts on PATH — no extra runtime needed. + ++ lib.optional (effectiveSudoAllowlist != [ ]) "/run/wrappers"; + # grep/find/flock/hostname are deliberately not on the PATH above + # (see agentBaseTools), so the supervisor's transcript lookups, the + # sessions.json lock (issue #254) and the codex remote-control + # wrapper's UTS re-exec get pinned binaries instead (the + # AGENT_BOX_*_BIN convention) — same store path for every instance, + # hence host-level rather than per-user env-file content. + # NOTE: every optional env var below is merged INTO this one + # `environment` value via nested `//` — chaining separate outer + # `// lib.optionalAttrs cond { environment.X = ...; }` blocks would + # each shallow-replace the whole `environment` attrset in turn and + # silently drop everything set before it. + environment = { + AGENT_BOX_GREP_BIN = "${pkgs.gnugrep}/bin/grep"; + AGENT_BOX_FIND_BIN = "${pkgs.findutils}/bin/find"; + AGENT_BOX_FLOCK_BIN = "${pkgs.util-linux}/bin/flock"; + AGENT_BOX_HOSTNAME_BIN = "${pkgs.unixtools.hostname}/bin/hostname"; + AGENT_BOX_ENV_EXEC = "${envExecWrapper}"; + AGENT_BOX_CODEX_RC = "${codexRemoteControl}"; + # Hook settings for claude spawns — the /clear-rotation record + # (issue #223) the supervisor follows on respawn. + AGENT_BOX_CLAUDE_SETTINGS = "${claudeHookSettings}"; + } + // lib.optionalAttrs (hostLabel != "") { + # Host suffix for auto-derived Remote Control session names. + AGENT_BOX_HOST_LABEL = hostLabel; + } + // lib.optionalAttrs codexFullAccess { + # /etc/codex/config.toml grants codex full access box-wide (issue + # 234); this env var lets a session with skipPermissions = false + # pin the restricted values back. Same for every instance. + AGENT_BOX_CODEX_FULL_ACCESS = "1"; + } + // lib.optionalAttrs webhookEnabled { + # Doubles as the supervisor's "webhook receiver is live" flag + # (per-session LOCAL_WEBHOOK_* env + claude plugin seeding). One + # repo, box-wide — host-level, not per-user. + AGENT_BOX_WEBHOOK_REPO = cfg.webhook.repo; + } + // lib.optionalAttrs (webhookEnabled && cfg.webhook.syncSessionPlugin) { + # Presence = "keep the session's plugin cache off the floor" + # (webhook.syncSessionPlugin, issue #193): one fetched script, + # same store path for every instance. + AGENT_BOX_WEBHOOK_PINNED_SCRIPT = "${localWebhookScript}"; + }; + serviceConfig = { + # ExecSearchPath resolves the unit text's bare `ExecStart= + # agent-box-supervisor`/`ExecStop=tmux …` — systemd's OWN exec + # lookup, independent of the process's runtime $PATH above (which + # matters for the supervisor's own internal tmux/jq/etc calls). + ExecSearchPath = lib.makeBinPath ( + [ "/home/%i/.nix-profile" config.nix.package supervisorScript ] ++ agentRuntimePackages - ++ [ pkgs.bashInteractive pkgs.coreutils pkgs.git pkgs.gh ] - # local-webhook's .mcp.json runs a bare `python3` (issue #101), which - # agentBaseTools already puts on PATH — no extra runtime needed. - ++ lib.optional (effectiveSudoAllowlist != [ ]) "/run/wrappers"; - # TMUX_TMPDIR puts the control socket under the /run RuntimeDirectory - # below instead of /tmp. PrivateTmp (in serviceConfig) gives this unit a - # PRIVATE /tmp, so a socket there would be invisible to the separate - # process that attaches (the AWS ttyd service, or `sudo -u tmux`). - # /run/agent-box- is a normal host path both sides can reach. - # Attach with: env TMUX_TMPDIR=/run/agent-box- tmux -L agent-box attach -t main - # - # AGENT_BOX_URL: the user's browser-terminal URL, exported only when - # this user actually has a terminal (web.enable + web.passwordHashFile). - # An AGENTS.md (see users..agentsMd) can reference it so any - # agent — claude-code, codex, opencode — can answer "where am I - # reachable?" without hard-coding the URL, which is useful because - # the hostname is a spot-restart away from changing. - environment = - { HOME = "/home/${name}"; TMUX_TMPDIR = "/run/${runtimeDirectory name}"; - # Everything user- or host-specific the supervisor script needs - # (issue #154, Phase 2 — src/supervisor.sh is user-independent; - # these variables are its whole contract with the module): - AGENT_BOX_SESSIONS_SEED = "${sessionsSeedFile name u}"; - AGENT_BOX_AGENT_BINS = agentBinsFor name; - AGENT_BOX_ENV_EXEC = "${envExecWrapper}"; - AGENT_BOX_CODEX_RC = "${codexRemoteControl}"; - # Hook settings for claude spawns — the /clear-rotation record - # (issue #223) the supervisor follows on respawn. - AGENT_BOX_CLAUDE_SETTINGS = "${claudeHookSettings}"; - # Clean-exit bookkeeping for the spawn epilogue (issue #167). - # Still per-user (it edits THIS user's sessions.json), so the - # user-independent script takes its path from the environment. - AGENT_BOX_MARK_STOPPED = "${markStopped name}"; - # grep/find are deliberately not on this unit's PATH (see - # agentBaseTools), so the supervisor's transcript lookups get - # pinned binaries instead (the AGENT_BOX_*_BIN convention). - AGENT_BOX_GREP_BIN = "${pkgs.gnugrep}/bin/grep"; - AGENT_BOX_FIND_BIN = "${pkgs.findutils}/bin/find"; - # Same convention for the sessions.json lock (issue #254). flock - # ships in util-linux ONLY — agentBaseTools puts that on this - # unit's PATH, but the supervisor must not depend on a package - # list a host can override with extraPackages, and the other - # writers run from PATHs that do not have it at all. - AGENT_BOX_FLOCK_BIN = "${pkgs.util-linux}/bin/flock"; - # For the codex remote-control wrapper's UTS-namespace re-exec - # (src/codex-remote-control.sh): `hostname` is not part of - # agentBaseTools either. - AGENT_BOX_HOSTNAME_BIN = "${pkgs.unixtools.hostname}/bin/hostname"; - } - // (lib.optionalAttrs (hostLabel != "") { - # Host suffix for auto-derived Remote Control session names. - AGENT_BOX_HOST_LABEL = hostLabel; - }) - // (lib.optionalAttrs codexFullAccess { - # /etc/codex/config.toml grants codex full access box-wide - # (issue 234). Set so a codex TUI session with - # skipPermissions = false can pin the restricted values back — - # the system layer is a default, not a lock. Unset means no such - # file, and codex's own defaults are what a session should get. - AGENT_BOX_CODEX_FULL_ACCESS = "1"; - }) - // (lib.optionalAttrs (agentsMdPointer name u != null) { - # Seeded-AGENTS.md pointer file; unset = agentsMd null opt-out. - AGENT_BOX_AGENTS_POINTER = "${agentsMdPointer name u}"; - }) - // (lib.optionalAttrs webhookEnabled { - # Doubles as the supervisor's "webhook receiver is live" flag - # (per-session LOCAL_WEBHOOK_* env + claude plugin seeding). - AGENT_BOX_WEBHOOK_REPO = cfg.webhook.repo; - }) - // (lib.optionalAttrs (webhookEnabled && cfg.webhook.syncSessionPlugin) { - # Presence = "keep the session's plugin cache off the floor" - # (webhook.syncSessionPlugin, issue #193). The supervisor reads the - # pinned VERSION out of this file to compare against claude's cache - # — the path, not the version, so nothing has to read the fetched - # file at eval time. - AGENT_BOX_WEBHOOK_PINNED_SCRIPT = "${localWebhookScript}"; - }) - // (lib.optionalAttrs (cfg.web.enable && u.web.passwordHashFile != null) { - AGENT_BOX_URL = "https://${cfg.web.domain}/${name}/"; - # Same idea for the webhook ingress (issue #101): the public URL to - # register in GitHub/Stripe/… so `agent-box-webhook url|setup` can - # print it without the agent deriving the hostname (a Spot restart - # away from changing). Unset ⇒ no endpoint is served for this user. - } // lib.optionalAttrs webhookEnabled { - AGENT_BOX_WEBHOOK_URL = "https://${cfg.web.domain}${webhookPathOf name}"; - }) - // u.environment; - serviceConfig = { - User = name; - # ExecStart is the session supervisor (issue #59): it reconciles - # tmux sessions against the user-owned sessions.json forever. - # Individual session restarts happen inside the loop; - # Restart=always only backstops a crashed supervisor. - Type = "exec"; - Restart = "always"; - RestartSec = "2s"; - ExecStart = supervisorScript; - # Stopping the unit stops every session: kill the whole per-user - # tmux server (the supervisor loop dies with the cgroup). - ExecStop = "${pkgs.tmux}/bin/tmux -L ${tmuxSocketName} kill-server"; - # Holds the tmux control socket (see TMUX_TMPDIR above). 0700 so only - # the agent user can reach its own socket; ExecStop/attachers run as - # the same user. Persist across restarts so an in-flight attach isn't - # racing the dir's teardown when Restart=always cycles the agent. - RuntimeDirectory = runtimeDirectory name; - RuntimeDirectoryMode = "0700"; - RuntimeDirectoryPreserve = true; - # NOTE: the settings page's user-owned ~/.config/agent-box/env is - # deliberately NOT listed here. Unit env is a snapshot from unit - # start, and sessions are respawned by the long-lived supervisor — - # so browser-added secrets never reached restarted sessions, and - # deleted keys never left (issue 89). The supervisor's env-exec - # wrapper reads that file at every session spawn instead; tokens - # (GH_TOKEN, etc.) go there too, via the settings page or - # `agent-box-session env set`, applying at next session spawn with - # no unit bounce. - EnvironmentFile = cfg.environmentFiles ++ u.environmentFiles; - - # Systemd hardening. The OS boundary has to stay meaningful even - # though the agent runs with its in-tool approval prompts disabled. - # This is the containment the agent CLI deliberately opts out of. - PrivateTmp = true; - PrivateDevices = true; # keeps pty subsystem; blocks /dev/mem etc. - ProtectSystem = "strict"; # entire fs read-only except explicit RW paths - # ~/sites is a SYMLINK to /var/lib/agent-box-sites/, so a write - # through it resolves outside /home and ProtectSystem=strict denied it - # with EROFS — the self-serve vhost flow the guide documents was - # impossible from inside the agent's own namespace. ReadWritePaths - # matches on the resolved path, so the target has to be listed too. - # The dir is tmpfiles-created (sysinit, well before this unit), so it - # always exists by unit start and needs no `-` prefix — but ONLY when - # web.enable is on, since that is what gates the tmpfiles rule. Listing - # it unconditionally fails the whole namespace setup with 226/NAMESPACE - # on a default (web-less) box, so the unit never starts at all: no `-` - # here, an explicit guard instead, so a dir that is genuinely missing - # while web is on stays loud rather than silently reverting to EROFS. - ReadWritePaths = [ "/home/${name}" ] - ++ lib.optional cfg.web.enable "/var/lib/agent-box-sites/${name}"; - ProtectKernelTunables = true; - ProtectKernelModules = true; - ProtectControlGroups = true; - ProtectClock = true; - RestrictSUIDSGID = true; - RestrictRealtime = true; - LockPersonality = true; - # NoNewPrivileges would break the sudoAllowlist escape hatch (sudo - # is setuid root; NNP blocks the euid transition). Enable it only - # when the effective allowlist is empty — with a non-empty allowlist - # (from cfg.sudoAllowlist or the web-implied caddy reload) we've - # traded some containment for scoped elevation as a host choice. - NoNewPrivileges = effectiveSudoAllowlist == [ ]; - } // lib.optionalAttrs cfg.protectMemory { - # Sacrifice agent work first under memory pressure: the kernel OOM - # killer and earlyoom both weigh oom_score_adj, so a runaway agent - # process dies before sshd/caddy/SSM — and Restart=always brings - # the session back fresh instead of leaving a frozen box. - OOMScoreAdjust = lib.mkDefault 500; - }; + ++ [ pkgs.bashInteractive pkgs.coreutils pkgs.git pkgs.gh pkgs.tmux ] + ++ lib.optional (effectiveSudoAllowlist != [ ]) "/run/wrappers" + ); + # NOTE: the settings page's user-owned ~/.config/agent-box/env is + # deliberately NOT listed here (nor in the per-user env file). + # See the original issue 89 rationale: unit env is a start-time + # snapshot, so browser-added secrets go through the supervisor's + # env-exec wrapper at every session spawn instead. + EnvironmentFile = cfg.environmentFiles; + # ~/sites is a SYMLINK to /var/lib/agent-box-sites/, so a + # write through it resolves outside /home and ProtectSystem=strict + # denies it with EROFS unless the target is ALSO listed — but only + # when web.enable is on, since that is what gates the tmpfiles + # rule that creates the dir; listing it unconditionally fails the + # whole namespace setup with 226/NAMESPACE on a default (web-less) + # box. systemd path lists accumulate across drop-ins onto the base + # unit's own ReadWritePaths=/home/%i, so this only ADDS the extra + # path, never replaces it. + ReadWritePaths = lib.optional cfg.web.enable "/var/lib/agent-box-sites/%i"; + # NoNewPrivileges would break the sudoAllowlist escape hatch (sudo + # is setuid root; NNP blocks the euid transition). Enable it only + # when the effective allowlist is empty — with a non-empty + # allowlist (from cfg.sudoAllowlist or the web-implied caddy + # reload) we've traded some containment for scoped elevation as a + # host choice. Same for every instance, hence host-level. + NoNewPrivileges = effectiveSudoAllowlist == [ ]; + } // lib.optionalAttrs cfg.protectMemory { + # Sacrifice agent work first under memory pressure: the kernel OOM + # killer and earlyoom both weigh oom_score_adj, so a runaway agent + # process dies before sshd/caddy/SSM — and Restart=always brings + # the session back fresh instead of leaving a frozen box. + OOMScoreAdjust = lib.mkDefault 500; + }; + }; + } + # Enablement + the one per-user drop-in the generated env file can't + # express: arbitrary u.environment/u.environmentFiles (issue #154 + # Phase 3 design's stated exception — an env file can't add its own + # EnvironmentFile= entries or override an already-set var per + # instance). overrideStrategy = "asDropin" is required here (unlike + # "agent-box@" above): nothing on disk is literally named + # "agent-box@alice.service" for the default detection to find, since + # instances are virtual. + // (lib.mapAttrs' (name: u: lib.nameValuePair "agent-box@${name}" ( + { overrideStrategy = "asDropin"; + wantedBy = [ "multi-user.target" ]; + } + // lib.optionalAttrs (u.environment != { }) { environment = u.environment; } + // lib.optionalAttrs (u.environmentFiles != [ ]) { + serviceConfig.EnvironmentFile = u.environmentFiles; } - ) cfg.users; + )) cfg.users); security.sudo.extraRules = lib.mkIf (effectiveSudoAllowlist != [ ]) [{ users = lib.attrNames cfg.users; @@ -1770,7 +1783,11 @@ in # ttyd/xterm supports OSC 8, but xterm-256color cannot advertise that # through terminfo. Tell tmux explicitly so it forwards stored # hyperlinks instead of redrawing only their visible labels. - attachScript = pkgs.writeShellScript "agent-box-attach" '' + # writeShellScriptBin (issue #154 Phase 3): the web-terminal unit's + # ttyd ExecStart passes this bare ("agent-box-attach"), resolved + # via that unit's PATH (ttyd execs it directly, not through + # systemd, so it needs the runtime PATH, not just ExecSearchPath). + attachScript = pkgs.writeShellScriptBin "agent-box-attach" '' @@include:src/attach.sh@@ ''; # Per-user env var suffix for the Caddyfile placeholders; linux user @@ -2016,224 +2033,150 @@ in # under // so one vhost can serve them all. # TMUX_TMPDIR must match the agent unit's RuntimeDirectory (the agent # runs with PrivateTmp, so the socket lives in /run, not /tmp). + # Per-user env files for the three web-gated template units (issue + # #154 Phase 3) — same rationale as the "agent-box@" one above: + # store paths/values that differ by user go here; the same value + # for every instance goes in that unit's host drop-in instead. + environment.etc = + lib.mapAttrs' (name: _: lib.nameValuePair "agent-box/units/agent-web-terminal-${name}.env" { + text = "AGENT_BOX_TTYD_PORT=${toString portOf.${name}}\n"; + }) portOf + // lib.listToAttrs (map (name: lib.nameValuePair "agent-box/units/agent-box-settings-${name}.env" { + text = + "AGENT_BOX_PASSWORD_CMD=/run/wrappers/bin/sudo -n ${passwordHelperCmdOf name}\n" + + lib.optionalString (name == rootUser) + # This daemon also serves the vhost root: GET / is the + # session manager (Caddy proxies it here behind the + # user's auth) and the session CRUD routes move to + # /sessions/*. + "AGENT_BOX_HOME=1\n" + + lib.optionalString webhookEnabled + "AGENT_BOX_WEBHOOK_STATE_DIR=${webhookStateDirOf name}\n"; + }) terminalUsers) + // lib.optionalAttrs webhookEnabled (lib.listToAttrs (map (name: lib.nameValuePair "agent-box/units/agent-box-webhook-${name}.env" { + text = "LOCAL_WEBHOOK_STATE_DIR=${webhookStateDirOf name}\n"; + }) terminalUsers)); + + # ttyd per terminal user — attaches to that user's tmux session over an + # internal port; --base-path keeps each terminal (and its /ws endpoint) + # under // so one vhost can serve them all. The unit text (see + # src/units/agent-web-terminal@.service, shipped via + # systemd.packages at the top of this module) already declares + # User=%i, TMUX_TMPDIR/AGENT_BOX_SESSIONS_FILE and the ttyd + # ExecStart, expanding AGENT_BOX_TTYD_PORT/AGENT_BOX_WEB_DOMAIN from + # environment at exec time; what's below is HOST-level (the same + # PATH/domain for every instance) so it renders as a drop-in. systemd.services = { agent-web-auth-secrets = webAuthSecretsService; caddy.serviceConfig.EnvironmentFile = "/run/agent-box-web/env"; - } // lib.listToAttrs (map (name: lib.nameValuePair "agent-web-terminal-${name}" { - description = "Browser terminal (ttyd) attached to ${name}'s tmux"; - after = [ "agent-box-${name}.service" "network-online.target" ]; - wants = [ "network-online.target" ]; - wantedBy = [ "multi-user.target" ]; - # The attach script (src/attach.sh) resolves tmux and jq from this - # PATH instead of baked store paths (issue #154, Phase 2); head comes - # from the coreutils already on every unit's default PATH. - path = [ pkgs.tmux pkgs.jq ]; - environment = { - TMUX_TMPDIR = "/run/${runtimeDirectory name}"; - # Read-only, and by the session's OWN user: what the attach - # script needs it for is telling a name this box has never heard - # of from one that is merely down (issue #241). - AGENT_BOX_SESSIONS_FILE = userSessionsFile name; + + "agent-web-terminal@" = { + # The attach script (src/attach.sh) resolves tmux and jq from + # this PATH instead of baked store paths (issue #154, Phase 2); + # ttyd itself execs the bare "agent-box-attach" argument + # through this same runtime PATH, not ExecSearchPath (that only + # covers systemd's own resolution of the unit's ExecStart). + path = [ pkgs.tmux pkgs.jq attachScript pkgs.ttyd ]; + environment.AGENT_BOX_WEB_DOMAIN = cfg.web.domain; + serviceConfig.ExecSearchPath = lib.makeBinPath [ pkgs.ttyd ]; }; - serviceConfig = { - User = name; - Restart = "always"; - RestartSec = "5s"; - ExecStart = lib.concatStringsSep " " [ - "${pkgs.ttyd}/bin/ttyd" - "--writable" - # ?arg= in the URL becomes $1 of the attach wrapper — - # session-level deep links from the root sessions page (issue #59). - "--url-arg" - "-p" (toString portOf.${name}) - "-i" "127.0.0.1" - "-b" "/${name}" - "-t" "disableLeaveAlert=true" - "-t" "titleFixed=${name}@${cfg.web.domain}" - (toString attachScript) - ]; + + # Settings daemon (issue #36), one per terminal user. Runs AS the + # agent user (no root, no privilege boundary): it only writes + # that user's own ~/.config/agent-box/env and kills that user's + # own tmux session. The agent unit's Restart=always then reloads + # it with the fresh env. Listens via socket activation (issue + # #49) on the same-named .socket instance below. + "agent-box-settings@" = { + # See the "agent-box@" comment above: every optional block + # merges INTO this one `environment` value via nested `//`, not + # as separate outer `// lib.optionalAttrs {...}` blocks (which + # would each shallow-replace the whole attrset in turn). + environment = { + AGENT_BOX_TMUX_BIN = "${pkgs.tmux}/bin/tmux"; + AGENT_BOX_AGENTS = lib.concatStringsSep "," (sessionKinds cfg.installAgents); + AGENT_BOX_DEFAULT_AGENT = cfg.agent; + } + // lib.optionalAttrs webhookEnabled { + # Webhook subscriptions panel (issue #227). The daemon runs + # the pinned webhook.py's own CLI once per session, with + # LOCAL_WEBHOOK_SESSION set to that session's key, to list + # and delete subscriptions — so the panel cannot drift from + # the semantics the receiver and the in-session tools + # implement. Same script and interpreter the receiver unit + # runs, hence host-level (the per-user state dir is in the + # env file above). + AGENT_BOX_WEBHOOK_SCRIPT = localWebhookScript; + AGENT_BOX_WEBHOOK_PYTHON = webhookPython; + # Same script the receiver runs on a match, for its + # --preamble mode only: the standing-watch panel prints the + # prompt the next match would launch (#259). + AGENT_BOX_HOOK_SPAWN_CMD = "${webhookSpawn}/bin/agent-box-webhook-spawn"; + } + // lib.optionalAttrs cfg.selfUpdate.enable { + # --no-block so the daemon's HTTP response goes out before + # the rebuild (possibly) restarts the daemon itself. + AGENT_BOX_UPDATE_CMD = "/run/wrappers/bin/sudo -n ${updateStartNoBlockCmd}"; + # Running rev + repo, rendered on the Update card as a + # GitHub commit link and used for its non-blocking compare + # request. + AGENT_BOX_REPO = cfg.selfUpdate.repo; + AGENT_BOX_REV = cfg.selfUpdate.rev; + # Read-only handles for the {BASE}/status progress endpoint + # the page long-polls after "Update box": no sudo needed + # here, unlike AGENT_BOX_UPDATE_CMD, which triggers the run. + AGENT_BOX_UPDATE_UNIT = "agent-box-update.service"; + AGENT_BOX_SYSTEMCTL = "/run/current-system/sw/bin/systemctl"; + }; + serviceConfig.ExecSearchPath = lib.makeBinPath [ settingsDaemon ]; }; + } + // lib.listToAttrs (map (name: lib.nameValuePair "agent-web-terminal@${name}" { + overrideStrategy = "asDropin"; + wantedBy = [ "multi-user.target" ]; }) terminalUsers) - # Settings daemon (issue #36), one per terminal user. Runs AS the agent - # user (no root, no privilege boundary): it only writes that user's own - # ~/.config/agent-box/env and kills that user's own tmux session. The - # agent unit's Restart=always then reloads it with the fresh env. - # Listens via socket activation on the systemd-owned unix socket - # (issue #49) — the same-named .socket unit below; requires/after kept - # explicit per this repo's explicit-over-implied-config convention. - // lib.listToAttrs (map (name: lib.nameValuePair "agent-box-settings-${name}" { - description = "Per-user secrets settings page for ${name}"; - after = [ "network-online.target" "agent-box-settings-${name}.socket" ]; - requires = [ "agent-box-settings-${name}.socket" ]; - wants = [ "network-online.target" ]; + // lib.listToAttrs (map (name: lib.nameValuePair "agent-box-settings@${name}" { + overrideStrategy = "asDropin"; wantedBy = [ "multi-user.target" ]; - # TMUX_TMPDIR must match the agent unit's RuntimeDirectory so the - # daemon can reach the (PrivateTmp) tmux socket to restart the agent. - environment = { - TMUX_TMPDIR = "/run/${runtimeDirectory name}"; - AGENT_BOX_SETTINGS_USER = name; - AGENT_BOX_SETTINGS_ENV_FILE = userEnvFile name; - AGENT_BOX_SETTINGS_BASE = settingsBaseOf name; - AGENT_BOX_TMUX_SOCKET = tmuxSocketName; - AGENT_BOX_TMUX_TMPDIR = "/run/${runtimeDirectory name}"; - AGENT_BOX_TMUX_BIN = "${pkgs.tmux}/bin/tmux"; - AGENT_BOX_SESSIONS_FILE = userSessionsFile name; - AGENT_BOX_AGENTS = lib.concatStringsSep "," (sessionKinds cfg.installAgents); - AGENT_BOX_DEFAULT_AGENT = cfg.agent; - AGENT_BOX_PASSWORD_CMD = - "/run/wrappers/bin/sudo -n ${passwordHelperCmdOf name}"; - } // lib.optionalAttrs (name == rootUser) { - # This daemon also serves the vhost root: GET / is the session - # manager (Caddy proxies it here behind the user's auth) and the - # session CRUD routes move to /sessions/*. - AGENT_BOX_HOME = "1"; - } // lib.optionalAttrs webhookEnabled { - # Webhook subscriptions panel (issue #227). The daemon runs the - # pinned webhook.py's own CLI once per session, with - # LOCAL_WEBHOOK_SESSION set to that session's key, to list and - # delete subscriptions — so the panel cannot drift from the - # semantics the receiver and the in-session tools implement. - # Same script and interpreter the receiver unit runs. - AGENT_BOX_WEBHOOK_SCRIPT = localWebhookScript; - AGENT_BOX_WEBHOOK_PYTHON = webhookPython; - AGENT_BOX_WEBHOOK_STATE_DIR = webhookStateDirOf name; - # Same script the receiver runs on a match, for its --preamble mode - # only: the standing-watch panel prints the prompt the next match - # would launch (#259). One text, one source — a copy in the daemon - # would drift from what the box actually sends. - AGENT_BOX_HOOK_SPAWN_CMD = "${webhookSpawn}/bin/agent-box-webhook-spawn"; - } // lib.optionalAttrs cfg.selfUpdate.enable { - # --no-block so the daemon's HTTP response goes out before the - # rebuild (possibly) restarts the daemon itself. - AGENT_BOX_UPDATE_CMD = "/run/wrappers/bin/sudo -n ${updateStartNoBlockCmd}"; - # Running rev + repo, rendered on the Update card as a GitHub - # commit link and used for its non-blocking compare request. - AGENT_BOX_REPO = cfg.selfUpdate.repo; - AGENT_BOX_REV = cfg.selfUpdate.rev; - # Read-only handles for the {BASE}/status progress endpoint the page - # long-polls after "Update box": the update oneshot to watch and an - # unprivileged `systemctl show` binary to read its state with (no - # sudo — unlike AGENT_BOX_UPDATE_CMD, which triggers the run). Only - # wired when selfUpdate is on, so the status endpoint omits the update - # block otherwise. - AGENT_BOX_UPDATE_UNIT = "agent-box-update.service"; - AGENT_BOX_SYSTEMCTL = "/run/current-system/sw/bin/systemctl"; - }; - serviceConfig = { - User = name; - Restart = "always"; - RestartSec = "5s"; - ExecStart = "${settingsDaemon}/bin/agent-box-settings"; - # Hardening: the daemon itself remains unable to read the root-owned - # web secrets. /var/lib/agent-box-web is writable in the mount - # namespace only so the sudo'd password helper can atomically replace - # this user's hash and cookie; normal Unix permissions still deny the - # unprivileged daemon. - ProtectSystem = "strict"; - ReadWritePaths = [ - "/home/${name}" - "/run/${runtimeDirectory name}" - "/var/lib/agent-box-web" - ]; - ProtectHome = false; - PrivateDevices = true; - ProtectKernelTunables = true; - ProtectKernelModules = true; - ProtectControlGroups = true; - RestrictSUIDSGID = true; - RestrictRealtime = true; - LockPersonality = true; - # Setuid sudo needs privilege escalation, which NNP vetoes. The - # daemon always has its per-user, argument-free password helper; with - # selfUpdate enabled it also has the argument-free update trigger. - NoNewPrivileges = false; - }; }) terminalUsers) # Webhook receiver daemon (issue #101), one per terminal user, gated on - # webhook.enable. Runs webhook.py in RECEIVER_ONLY mode as the agent - # user: owns the socket-activated UNIX ingress (the same-named .socket - # below) and fans HMAC-verified deliveries out to that user's sessions - # over IPC, with no MCP session of its own. - // lib.optionalAttrs webhookEnabled (lib.listToAttrs (map (name: lib.nameValuePair "agent-box-webhook-${name}" { - description = "Webhook receiver daemon (local-webhook) for ${name}"; - after = [ "network-online.target" "agent-box-webhook-${name}.socket" ]; - requires = [ "agent-box-webhook-${name}.socket" ]; - wants = [ "network-online.target" ]; - wantedBy = [ "multi-user.target" ]; - # The spawn wrapper (src/webhook-spawn.sh) runs as this unit's child - # and resolves jq/coreutils/agent-box-session from this PATH instead - # of baked store paths (issue #154, Phase 2). - path = [ pkgs.jq pkgs.coreutils sessionCli ]; - environment = { - LOCAL_WEBHOOK_RECEIVER_ONLY = "1"; - LOCAL_WEBHOOK_STATE_DIR = webhookStateDirOf name; - LOCAL_WEBHOOK_PORT = "0"; + # webhook.enable. Runs webhook.py (via the agent-box-webhook-receiver + # wrapper, resolved through ExecSearchPath) in RECEIVER_ONLY mode as + # the agent user: owns the socket-activated UNIX ingress (the + # same-named .socket instance below) and fans HMAC-verified + # deliveries out to that user's sessions over IPC. + // lib.optionalAttrs webhookEnabled ({ + "agent-box-webhook@" = { + # The spawn wrapper (src/webhook-spawn.sh) runs as this unit's + # child and resolves jq/coreutils/agent-box-session from this + # PATH instead of baked store paths (issue #154, Phase 2). + path = [ pkgs.jq pkgs.coreutils sessionCli ]; # Standing watches (deliver_to:"subagent", local-channels#1): a - # matching delivery spawns a fresh hook-* session for this user. - # webhook.py coalesces bursts and caps concurrent spawns; the - # wrapper additionally caps how many hook-* sessions may exist. - LOCAL_WEBHOOK_SPAWN_CMD = "${webhookSpawn}/bin/agent-box-webhook-spawn"; - # webhook.hookSessionArgs is NOT here: the wrapper carries it, so - # the settings page and the CLI resolve the same default this unit - # would have handed only to its own child (#292). - }; - serviceConfig = { - User = name; - Restart = "always"; - RestartSec = "5s"; - # Enforce the declared watch policy (webhook.watchPolicy) onto this - # user's filter.dispatch.json before the receiver starts routing. - # Runs as User (no "+" prefix); exits 0 on every path, because a - # policy hiccup must not take the box's one ingress down with it. - ExecStartPre = "${webhookPolicyApply}"; - ExecStart = "${webhookPython} ${localWebhookScript}"; - # systemd passes the ingress socket on fd 3 (LISTEN_FDS) and wires - # stdin to /dev/null; RECEIVER_ONLY tolerates that (no MCP stdio). - StandardInput = "null"; - ProtectSystem = "strict"; - ReadWritePaths = [ "/home/${name}" ]; - ProtectHome = false; - PrivateDevices = true; - ProtectKernelTunables = true; - ProtectKernelModules = true; - ProtectControlGroups = true; - RestrictSUIDSGID = true; - RestrictRealtime = true; - LockPersonality = true; - NoNewPrivileges = true; + # matching delivery spawns a fresh hook-* session for this + # user. One wrapper, same store path for every instance. + environment.LOCAL_WEBHOOK_SPAWN_CMD = "${webhookSpawn}/bin/agent-box-webhook-spawn"; + serviceConfig.ExecSearchPath = lib.makeBinPath [ webhookPolicyApply webhookReceiverBin ]; }; + } // lib.listToAttrs (map (name: lib.nameValuePair "agent-box-webhook@${name}" { + overrideStrategy = "asDropin"; + wantedBy = [ "multi-user.target" ]; }) terminalUsers)); - # The settings daemon's listening sockets (issue #49). systemd (root) - # binds each unix socket with exact ownership BEFORE the daemon starts: - # 0660 :caddy means only that user and the caddy reverse-proxy - # can connect — unlike the previous 127.0.0.1: listener, which - # every local user could reach. The daemon adopts the socket through - # socket activation (LISTEN_FDS, fd 3). - systemd.sockets = lib.listToAttrs (map (name: lib.nameValuePair "agent-box-settings-${name}" { - description = "Settings page socket for ${name}"; - wantedBy = [ "sockets.target" ]; - socketConfig = { - ListenStream = settingsSocketOf name; - SocketUser = name; - SocketGroup = "caddy"; - SocketMode = "0660"; - }; - }) terminalUsers) - # Webhook ingress sockets (issue #101). systemd binds each 0660 - # :caddy — same isolation as the settings socket — so only that - # user and the caddy reverse-proxy can POST; the daemon adopts it via - # socket activation (fd 3). - // lib.optionalAttrs webhookEnabled (lib.listToAttrs (map (name: lib.nameValuePair "agent-box-webhook-${name}" { - description = "Webhook ingress socket for ${name}"; - wantedBy = [ "sockets.target" ]; - socketConfig = { - ListenStream = webhookSocketOf name; - SocketUser = name; - SocketGroup = "caddy"; - SocketMode = "0660"; - }; - }) terminalUsers)); + # The settings daemon's listening sockets (issue #49) and the webhook + # ingress sockets (issue #101) are fully %i-derived already (see + # src/units/*.socket, shipped via systemd.packages) — root binds + # each one 0660 :caddy BEFORE the daemon starts, exactly as + # before; only per-instance ENABLEMENT (sockets.target) is needed + # here, same asDropin reasoning as the .service instances above. + systemd.sockets = + lib.listToAttrs (map (name: lib.nameValuePair "agent-box-settings@${name}" { + overrideStrategy = "asDropin"; + wantedBy = [ "sockets.target" ]; + }) terminalUsers) + // lib.optionalAttrs webhookEnabled (lib.listToAttrs (map (name: lib.nameValuePair "agent-box-webhook@${name}" { + overrideStrategy = "asDropin"; + wantedBy = [ "sockets.target" ]; + }) terminalUsers)); } )) diff --git a/modules/src/spot-monitor.sh b/modules/src/spot-monitor.sh index 87b900f7..99fc5787 100644 --- a/modules/src/spot-monitor.sh +++ b/modules/src/spot-monitor.sh @@ -97,7 +97,7 @@ while true; do sleep "$GRACE" echo "spot-monitor: stopping agent units (no respawn)" >&2 for u in $USERS; do - $SYSTEMCTL stop "agent-box-$u.service" || true + $SYSTEMCTL stop "agent-box@$u.service" || true done $SYNC echo "spot-monitor: prepared; leaving the stop to AWS (preserves persistent-Spot auto-restart)" >&2 diff --git a/modules/src/units/agent-box-settings@.service b/modules/src/units/agent-box-settings@.service new file mode 100644 index 00000000..be725b13 --- /dev/null +++ b/modules/src/units/agent-box-settings@.service @@ -0,0 +1,31 @@ +[Unit] +Description=Per-user secrets settings page for %i +After=network-online.target agent-box-settings@%i.socket +Requires=agent-box-settings@%i.socket +Wants=network-online.target + +[Service] +User=%i +Restart=always +RestartSec=5s +Environment=TMUX_TMPDIR=/run/agent-box-%i +Environment=AGENT_BOX_SETTINGS_USER=%i +Environment=AGENT_BOX_SETTINGS_ENV_FILE=/home/%i/.config/agent-box/env +Environment=AGENT_BOX_SETTINGS_BASE=/%i/settings +Environment=AGENT_BOX_TMUX_SOCKET=agent-box +Environment=AGENT_BOX_TMUX_TMPDIR=/run/agent-box-%i +Environment=AGENT_BOX_SESSIONS_FILE=/home/%i/.config/agent-box/sessions.json +EnvironmentFile=-/etc/agent-box/units/agent-box-settings-%i.env +EnvironmentFile=-/etc/agent-box/units/agent-box-settings-%i.local.env +ExecStart=agent-box-settings +ProtectSystem=strict +ReadWritePaths=/home/%i /run/agent-box-%i /var/lib/agent-box-web +ProtectHome=false +PrivateDevices=true +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +RestrictSUIDSGID=true +RestrictRealtime=true +LockPersonality=true +NoNewPrivileges=false diff --git a/modules/src/units/agent-box-settings@.socket b/modules/src/units/agent-box-settings@.socket new file mode 100644 index 00000000..10caacc1 --- /dev/null +++ b/modules/src/units/agent-box-settings@.socket @@ -0,0 +1,8 @@ +[Unit] +Description=Settings page socket for %i + +[Socket] +ListenStream=/run/agent-box-settings/%i.sock +SocketUser=%i +SocketGroup=caddy +SocketMode=0660 diff --git a/modules/src/units/agent-box-webhook@.service b/modules/src/units/agent-box-webhook@.service new file mode 100644 index 00000000..ee9ad800 --- /dev/null +++ b/modules/src/units/agent-box-webhook@.service @@ -0,0 +1,29 @@ +[Unit] +Description=Webhook receiver daemon (local-webhook) for %i +After=network-online.target agent-box-webhook@%i.socket +Requires=agent-box-webhook@%i.socket +Wants=network-online.target + +[Service] +User=%i +Restart=always +RestartSec=5s +Environment=LOCAL_WEBHOOK_RECEIVER_ONLY=1 +Environment=LOCAL_WEBHOOK_STATE_DIR=/home/%i/.local/state/local-webhook +Environment=LOCAL_WEBHOOK_PORT=0 +EnvironmentFile=-/etc/agent-box/units/agent-box-webhook-%i.env +EnvironmentFile=-/etc/agent-box/units/agent-box-webhook-%i.local.env +ExecStartPre=agent-box-webhook-policy-apply +ExecStart=agent-box-webhook-receiver +StandardInput=null +ProtectSystem=strict +ReadWritePaths=/home/%i +ProtectHome=false +PrivateDevices=true +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +RestrictSUIDSGID=true +RestrictRealtime=true +LockPersonality=true +NoNewPrivileges=true diff --git a/modules/src/units/agent-box-webhook@.socket b/modules/src/units/agent-box-webhook@.socket new file mode 100644 index 00000000..2699cf0d --- /dev/null +++ b/modules/src/units/agent-box-webhook@.socket @@ -0,0 +1,8 @@ +[Unit] +Description=Webhook ingress socket for %i + +[Socket] +ListenStream=/run/agent-box-webhook/%i.sock +SocketUser=%i +SocketGroup=caddy +SocketMode=0660 diff --git a/modules/src/units/agent-box@.service b/modules/src/units/agent-box@.service new file mode 100644 index 00000000..6b5990a0 --- /dev/null +++ b/modules/src/units/agent-box@.service @@ -0,0 +1,30 @@ +[Unit] +Description=Coding agent sessions (tmux) for %i +After=network-online.target +Wants=network-online.target + +[Service] +Type=exec +User=%i +Restart=always +RestartSec=2s +ExecStart=agent-box-supervisor +ExecStop=tmux -L agent-box kill-server +RuntimeDirectory=agent-box-%i +RuntimeDirectoryMode=0700 +RuntimeDirectoryPreserve=yes +Environment=HOME=/home/%i +Environment=TMUX_TMPDIR=/run/agent-box-%i +EnvironmentFile=-/etc/agent-box/units/%i.env +EnvironmentFile=-/etc/agent-box/units/%i.local.env +PrivateTmp=true +PrivateDevices=true +ProtectSystem=strict +ReadWritePaths=/home/%i +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectControlGroups=true +ProtectClock=true +RestrictSUIDSGID=true +RestrictRealtime=true +LockPersonality=true diff --git a/modules/src/units/agent-web-terminal@.service b/modules/src/units/agent-web-terminal@.service new file mode 100644 index 00000000..383ae67d --- /dev/null +++ b/modules/src/units/agent-web-terminal@.service @@ -0,0 +1,14 @@ +[Unit] +Description=Browser terminal (ttyd) attached to %i's tmux +After=agent-box@%i.service network-online.target +Wants=network-online.target + +[Service] +User=%i +Restart=always +RestartSec=5s +Environment=TMUX_TMPDIR=/run/agent-box-%i +Environment=AGENT_BOX_SESSIONS_FILE=/home/%i/.config/agent-box/sessions.json +EnvironmentFile=-/etc/agent-box/units/agent-web-terminal-%i.env +EnvironmentFile=-/etc/agent-box/units/agent-web-terminal-%i.local.env +ExecStart=ttyd --writable --url-arg -p ${AGENT_BOX_TTYD_PORT} -i 127.0.0.1 -b /%i -t disableLeaveAlert=true -t titleFixed=%i@${AGENT_BOX_WEB_DOMAIN} agent-box-attach diff --git a/tests/golden/vm/etc/agent-box/units/agent.env b/tests/golden/vm/etc/agent-box/units/agent.env new file mode 100644 index 00000000..a6ae3b5d --- /dev/null +++ b/tests/golden/vm/etc/agent-box/units/agent.env @@ -0,0 +1,4 @@ +AGENT_BOX_SESSIONS_SEED=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-agent-sessions.json +AGENT_BOX_AGENT_BINS=claude=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin/claude codex=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin/codex shell=/run/current-system/sw/bin/bash +AGENT_BOX_MARK_STOPPED=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-agent-mark-stopped +AGENT_BOX_AGENTS_POINTER=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-agent-agents-pointer.md diff --git a/tests/golden/vm/payloads/agent-box-spot-monitor b/tests/golden/vm/payloads/agent-box-spot-monitor index d596430c..7938927e 100644 --- a/tests/golden/vm/payloads/agent-box-spot-monitor +++ b/tests/golden/vm/payloads/agent-box-spot-monitor @@ -98,7 +98,7 @@ while true; do sleep "$GRACE" echo "spot-monitor: stopping agent units (no respawn)" >&2 for u in $USERS; do - $SYSTEMCTL stop "agent-box-$u.service" || true + $SYSTEMCTL stop "agent-box@$u.service" || true done $SYNC echo "spot-monitor: prepared; leaving the stop to AWS (preserves persistent-Spot auto-restart)" >&2 diff --git a/tests/golden/vm/payloads/agent-box-supervisor b/tests/golden/vm/payloads/agent-box-supervisor/bin/agent-box-supervisor similarity index 100% rename from tests/golden/vm/payloads/agent-box-supervisor rename to tests/golden/vm/payloads/agent-box-supervisor/bin/agent-box-supervisor diff --git a/tests/golden/vm/units/agent-box-agent.service b/tests/golden/vm/units/agent-box-agent.service deleted file mode 100644 index 7a5e23ed..00000000 --- a/tests/golden/vm/units/agent-box-agent.service +++ /dev/null @@ -1,51 +0,0 @@ -[Unit] -After=network-online.target -Description=Coding agent sessions (tmux) for agent -Wants=network-online.target - -[Service] -Environment="AGENT_BOX_AGENTS_POINTER=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-agent-agents-pointer.md" -Environment="AGENT_BOX_AGENT_BINS=claude=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin/claude codex=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin/codex shell=/run/current-system/sw/bin/bash" -Environment="AGENT_BOX_CLAUDE_SETTINGS=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-claude-hook-settings.json" -Environment="AGENT_BOX_CODEX_FULL_ACCESS=1" -Environment="AGENT_BOX_CODEX_RC=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-codex-remote-control" -Environment="AGENT_BOX_ENV_EXEC=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-env-exec" -Environment="AGENT_BOX_FIND_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin/find" -Environment="AGENT_BOX_FLOCK_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin/flock" -Environment="AGENT_BOX_GREP_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin/grep" -Environment="AGENT_BOX_HOSTNAME_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-hostname-hostname-debian-3.25/bin/hostname" -Environment="AGENT_BOX_HOST_LABEL=nixos" -Environment="AGENT_BOX_MARK_STOPPED=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-agent-mark-stopped" -Environment="AGENT_BOX_SESSIONS_SEED=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-agent-sessions.json" -Environment="HOME=/home/agent" -Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/home/agent/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/run/wrappers/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/home/agent/.nix-profile/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/sbin:/run/wrappers/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" -Environment="TMUX_TMPDIR=/run/agent-box-agent" -Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" -ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor -ExecStop=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin/tmux -L agent-box kill-server -LockPersonality=true -NoNewPrivileges=false -OOMScoreAdjust=500 -PrivateDevices=true -PrivateTmp=true -ProtectClock=true -ProtectControlGroups=true -ProtectKernelModules=true -ProtectKernelTunables=true -ProtectSystem=strict -ReadWritePaths=/home/agent -Restart=always -RestartSec=2s -RestrictRealtime=true -RestrictSUIDSGID=true -RuntimeDirectory=agent-box-agent -RuntimeDirectoryMode=0700 -RuntimeDirectoryPreserve=true -Type=exec -User=agent - -[Install] -WantedBy=multi-user.target - -# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/vm/units/agent-box@.service b/tests/golden/vm/units/agent-box@.service new file mode 100644 index 00000000..e6089d45 --- /dev/null +++ b/tests/golden/vm/units/agent-box@.service @@ -0,0 +1,18 @@ +[Unit] + +[Service] +Environment="AGENT_BOX_CLAUDE_SETTINGS=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-claude-hook-settings.json" +Environment="AGENT_BOX_CODEX_FULL_ACCESS=1" +Environment="AGENT_BOX_CODEX_RC=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-codex-remote-control" +Environment="AGENT_BOX_ENV_EXEC=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-env-exec" +Environment="AGENT_BOX_FIND_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin/find" +Environment="AGENT_BOX_FLOCK_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin/flock" +Environment="AGENT_BOX_GREP_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin/grep" +Environment="AGENT_BOX_HOSTNAME_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-hostname-hostname-debian-3.25/bin/hostname" +Environment="AGENT_BOX_HOST_LABEL=nixos" +Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" +Environment="PATH=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/home/%i/.nix-profile/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/sbin:/run/wrappers/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" +Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" +ExecSearchPath=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin +NoNewPrivileges=false +OOMScoreAdjust=500 diff --git a/tests/golden/vm/units/agent-box@agent.service b/tests/golden/vm/units/agent-box@agent.service new file mode 100644 index 00000000..ee98bbc6 --- /dev/null +++ b/tests/golden/vm/units/agent-box@agent.service @@ -0,0 +1,11 @@ +[Unit] + +[Service] +Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" +Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" +Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" + +[Install] +WantedBy=multi-user.target + +# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/etc/agent-box/units/agent-box-settings-agent.env b/tests/golden/web/etc/agent-box/units/agent-box-settings-agent.env new file mode 100644 index 00000000..c8a5e5c8 --- /dev/null +++ b/tests/golden/web/etc/agent-box/units/agent-box-settings-agent.env @@ -0,0 +1,3 @@ +AGENT_BOX_PASSWORD_CMD=/run/wrappers/bin/sudo -n /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-password-agent/bin/agent-box-password-agent +AGENT_BOX_HOME=1 +AGENT_BOX_WEBHOOK_STATE_DIR=/home/agent/.local/state/local-webhook diff --git a/tests/golden/web/etc/agent-box/units/agent-box-settings-robot.env b/tests/golden/web/etc/agent-box/units/agent-box-settings-robot.env new file mode 100644 index 00000000..1a6a3fc2 --- /dev/null +++ b/tests/golden/web/etc/agent-box/units/agent-box-settings-robot.env @@ -0,0 +1,2 @@ +AGENT_BOX_PASSWORD_CMD=/run/wrappers/bin/sudo -n /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-password-robot/bin/agent-box-password-robot +AGENT_BOX_WEBHOOK_STATE_DIR=/home/robot/.local/state/local-webhook diff --git a/tests/golden/web/etc/agent-box/units/agent-box-webhook-agent.env b/tests/golden/web/etc/agent-box/units/agent-box-webhook-agent.env new file mode 100644 index 00000000..ecfa4ab9 --- /dev/null +++ b/tests/golden/web/etc/agent-box/units/agent-box-webhook-agent.env @@ -0,0 +1 @@ +LOCAL_WEBHOOK_STATE_DIR=/home/agent/.local/state/local-webhook diff --git a/tests/golden/web/etc/agent-box/units/agent-box-webhook-robot.env b/tests/golden/web/etc/agent-box/units/agent-box-webhook-robot.env new file mode 100644 index 00000000..9c4abd6d --- /dev/null +++ b/tests/golden/web/etc/agent-box/units/agent-box-webhook-robot.env @@ -0,0 +1 @@ +LOCAL_WEBHOOK_STATE_DIR=/home/robot/.local/state/local-webhook diff --git a/tests/golden/web/etc/agent-box/units/agent-web-terminal-agent.env b/tests/golden/web/etc/agent-box/units/agent-web-terminal-agent.env new file mode 100644 index 00000000..851b7c43 --- /dev/null +++ b/tests/golden/web/etc/agent-box/units/agent-web-terminal-agent.env @@ -0,0 +1 @@ +AGENT_BOX_TTYD_PORT=7681 diff --git a/tests/golden/web/etc/agent-box/units/agent-web-terminal-robot.env b/tests/golden/web/etc/agent-box/units/agent-web-terminal-robot.env new file mode 100644 index 00000000..fc8c00e9 --- /dev/null +++ b/tests/golden/web/etc/agent-box/units/agent-web-terminal-robot.env @@ -0,0 +1 @@ +AGENT_BOX_TTYD_PORT=7682 diff --git a/tests/golden/web/etc/agent-box/units/agent.env b/tests/golden/web/etc/agent-box/units/agent.env new file mode 100644 index 00000000..b6d30488 --- /dev/null +++ b/tests/golden/web/etc/agent-box/units/agent.env @@ -0,0 +1,6 @@ +AGENT_BOX_SESSIONS_SEED=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-agent-sessions.json +AGENT_BOX_AGENT_BINS=claude=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin/claude codex=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin/codex shell=/run/current-system/sw/bin/bash +AGENT_BOX_MARK_STOPPED=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-agent-mark-stopped +AGENT_BOX_AGENTS_POINTER=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-agent-agents-pointer.md +AGENT_BOX_URL=https://golden.example.org/agent/ +AGENT_BOX_WEBHOOK_URL=https://golden.example.org/agent/webhook diff --git a/tests/golden/web/etc/agent-box/units/robot.env b/tests/golden/web/etc/agent-box/units/robot.env new file mode 100644 index 00000000..22222b00 --- /dev/null +++ b/tests/golden/web/etc/agent-box/units/robot.env @@ -0,0 +1,6 @@ +AGENT_BOX_SESSIONS_SEED=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-robot-sessions.json +AGENT_BOX_AGENT_BINS=claude=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin/claude codex=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin/codex shell=/run/current-system/sw/bin/bash +AGENT_BOX_MARK_STOPPED=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-robot-mark-stopped +AGENT_BOX_AGENTS_POINTER=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-robot-agents-pointer.md +AGENT_BOX_URL=https://golden.example.org/robot/ +AGENT_BOX_WEBHOOK_URL=https://golden.example.org/robot/webhook diff --git a/tests/golden/web/payloads/agent-box-attach b/tests/golden/web/payloads/agent-box-attach/bin/agent-box-attach similarity index 100% rename from tests/golden/web/payloads/agent-box-attach rename to tests/golden/web/payloads/agent-box-attach/bin/agent-box-attach diff --git a/tests/golden/web/payloads/agent-box-spot-monitor b/tests/golden/web/payloads/agent-box-spot-monitor index d596430c..7938927e 100644 --- a/tests/golden/web/payloads/agent-box-spot-monitor +++ b/tests/golden/web/payloads/agent-box-spot-monitor @@ -98,7 +98,7 @@ while true; do sleep "$GRACE" echo "spot-monitor: stopping agent units (no respawn)" >&2 for u in $USERS; do - $SYSTEMCTL stop "agent-box-$u.service" || true + $SYSTEMCTL stop "agent-box@$u.service" || true done $SYNC echo "spot-monitor: prepared; leaving the stop to AWS (preserves persistent-Spot auto-restart)" >&2 diff --git a/tests/golden/web/payloads/agent-box-supervisor b/tests/golden/web/payloads/agent-box-supervisor/bin/agent-box-supervisor similarity index 100% rename from tests/golden/web/payloads/agent-box-supervisor rename to tests/golden/web/payloads/agent-box-supervisor/bin/agent-box-supervisor diff --git a/tests/golden/web/payloads/agent-box-webhook-policy-apply b/tests/golden/web/payloads/agent-box-webhook-policy-apply/bin/agent-box-webhook-policy-apply similarity index 100% rename from tests/golden/web/payloads/agent-box-webhook-policy-apply rename to tests/golden/web/payloads/agent-box-webhook-policy-apply/bin/agent-box-webhook-policy-apply diff --git a/tests/golden/web/payloads/agent-box-webhook-receiver/bin/agent-box-webhook-receiver b/tests/golden/web/payloads/agent-box-webhook-receiver/bin/agent-box-webhook-receiver new file mode 100644 index 00000000..5f391b7d --- /dev/null +++ b/tests/golden/web/payloads/agent-box-webhook-receiver/bin/agent-box-webhook-receiver @@ -0,0 +1,3 @@ +#!/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-5.3p9/bin/bash +exec /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin/python3 /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-webhook.py "$@" + diff --git a/tests/golden/web/units/agent-box-agent.service b/tests/golden/web/units/agent-box-agent.service deleted file mode 100644 index 51fb8177..00000000 --- a/tests/golden/web/units/agent-box-agent.service +++ /dev/null @@ -1,56 +0,0 @@ -[Unit] -After=network-online.target -Description=Coding agent sessions (tmux) for agent -Wants=network-online.target - -[Service] -Environment="AGENT_BOX_AGENTS_POINTER=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-agent-agents-pointer.md" -Environment="AGENT_BOX_AGENT_BINS=claude=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin/claude codex=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin/codex shell=/run/current-system/sw/bin/bash" -Environment="AGENT_BOX_CLAUDE_SETTINGS=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-claude-hook-settings.json" -Environment="AGENT_BOX_CODEX_FULL_ACCESS=1" -Environment="AGENT_BOX_CODEX_RC=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-codex-remote-control" -Environment="AGENT_BOX_ENV_EXEC=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-env-exec" -Environment="AGENT_BOX_FIND_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin/find" -Environment="AGENT_BOX_FLOCK_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin/flock" -Environment="AGENT_BOX_GREP_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin/grep" -Environment="AGENT_BOX_HOSTNAME_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-hostname-hostname-debian-3.25/bin/hostname" -Environment="AGENT_BOX_HOST_LABEL=nixos" -Environment="AGENT_BOX_MARK_STOPPED=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-agent-mark-stopped" -Environment="AGENT_BOX_SESSIONS_SEED=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-agent-sessions.json" -Environment="AGENT_BOX_URL=https://golden.example.org/agent/" -Environment="AGENT_BOX_WEBHOOK_PINNED_SCRIPT=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-webhook.py" -Environment="AGENT_BOX_WEBHOOK_REPO=defangdevs/local-channels" -Environment="AGENT_BOX_WEBHOOK_URL=https://golden.example.org/agent/webhook" -Environment="HOME=/home/agent" -Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/home/agent/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/run/wrappers/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/home/agent/.nix-profile/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/sbin:/run/wrappers/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" -Environment="TMUX_TMPDIR=/run/agent-box-agent" -Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" -ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor -ExecStop=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin/tmux -L agent-box kill-server -LockPersonality=true -NoNewPrivileges=false -OOMScoreAdjust=500 -PrivateDevices=true -PrivateTmp=true -ProtectClock=true -ProtectControlGroups=true -ProtectKernelModules=true -ProtectKernelTunables=true -ProtectSystem=strict -ReadWritePaths=/home/agent -ReadWritePaths=/var/lib/agent-box-sites/agent -Restart=always -RestartSec=2s -RestrictRealtime=true -RestrictSUIDSGID=true -RuntimeDirectory=agent-box-agent -RuntimeDirectoryMode=0700 -RuntimeDirectoryPreserve=true -Type=exec -User=agent - -[Install] -WantedBy=multi-user.target - -# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-box-robot.service b/tests/golden/web/units/agent-box-robot.service deleted file mode 100644 index 0ba25da7..00000000 --- a/tests/golden/web/units/agent-box-robot.service +++ /dev/null @@ -1,56 +0,0 @@ -[Unit] -After=network-online.target -Description=Coding agent sessions (tmux) for robot -Wants=network-online.target - -[Service] -Environment="AGENT_BOX_AGENTS_POINTER=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-robot-agents-pointer.md" -Environment="AGENT_BOX_AGENT_BINS=claude=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin/claude codex=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin/codex shell=/run/current-system/sw/bin/bash" -Environment="AGENT_BOX_CLAUDE_SETTINGS=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-claude-hook-settings.json" -Environment="AGENT_BOX_CODEX_FULL_ACCESS=1" -Environment="AGENT_BOX_CODEX_RC=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-codex-remote-control" -Environment="AGENT_BOX_ENV_EXEC=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-env-exec" -Environment="AGENT_BOX_FIND_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin/find" -Environment="AGENT_BOX_FLOCK_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin/flock" -Environment="AGENT_BOX_GREP_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin/grep" -Environment="AGENT_BOX_HOSTNAME_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-hostname-hostname-debian-3.25/bin/hostname" -Environment="AGENT_BOX_HOST_LABEL=nixos" -Environment="AGENT_BOX_MARK_STOPPED=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-robot-mark-stopped" -Environment="AGENT_BOX_SESSIONS_SEED=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-robot-sessions.json" -Environment="AGENT_BOX_URL=https://golden.example.org/robot/" -Environment="AGENT_BOX_WEBHOOK_PINNED_SCRIPT=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-webhook.py" -Environment="AGENT_BOX_WEBHOOK_REPO=defangdevs/local-channels" -Environment="AGENT_BOX_WEBHOOK_URL=https://golden.example.org/robot/webhook" -Environment="HOME=/home/robot" -Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/home/robot/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/run/wrappers/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/home/robot/.nix-profile/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/sbin:/run/wrappers/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" -Environment="TMUX_TMPDIR=/run/agent-box-robot" -Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" -ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor -ExecStop=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin/tmux -L agent-box kill-server -LockPersonality=true -NoNewPrivileges=false -OOMScoreAdjust=500 -PrivateDevices=true -PrivateTmp=true -ProtectClock=true -ProtectControlGroups=true -ProtectKernelModules=true -ProtectKernelTunables=true -ProtectSystem=strict -ReadWritePaths=/home/robot -ReadWritePaths=/var/lib/agent-box-sites/robot -Restart=always -RestartSec=2s -RestrictRealtime=true -RestrictSUIDSGID=true -RuntimeDirectory=agent-box-robot -RuntimeDirectoryMode=0700 -RuntimeDirectoryPreserve=true -Type=exec -User=robot - -[Install] -WantedBy=multi-user.target - -# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-box-settings-agent.service b/tests/golden/web/units/agent-box-settings-agent.service deleted file mode 100644 index fda45279..00000000 --- a/tests/golden/web/units/agent-box-settings-agent.service +++ /dev/null @@ -1,53 +0,0 @@ -[Unit] -After=network-online.target agent-box-settings-agent.socket -Description=Per-user secrets settings page for agent -Requires=agent-box-settings-agent.socket -Wants=network-online.target - -[Service] -Environment="AGENT_BOX_AGENTS=claude,codex,shell" -Environment="AGENT_BOX_DEFAULT_AGENT=claude" -Environment="AGENT_BOX_HOME=1" -Environment="AGENT_BOX_HOOK_SPAWN_CMD=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook-spawn/bin/agent-box-webhook-spawn" -Environment="AGENT_BOX_PASSWORD_CMD=/run/wrappers/bin/sudo -n /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-password-agent/bin/agent-box-password-agent" -Environment="AGENT_BOX_REPO=defangdevs/agent-box" -Environment="AGENT_BOX_REV=0000000000000000000000000000000000000000" -Environment="AGENT_BOX_SESSIONS_FILE=/home/agent/.config/agent-box/sessions.json" -Environment="AGENT_BOX_SETTINGS_BASE=/agent/settings" -Environment="AGENT_BOX_SETTINGS_ENV_FILE=/home/agent/.config/agent-box/env" -Environment="AGENT_BOX_SETTINGS_USER=agent" -Environment="AGENT_BOX_SYSTEMCTL=/run/current-system/sw/bin/systemctl" -Environment="AGENT_BOX_TMUX_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin/tmux" -Environment="AGENT_BOX_TMUX_SOCKET=agent-box" -Environment="AGENT_BOX_TMUX_TMPDIR=/run/agent-box-agent" -Environment="AGENT_BOX_UPDATE_CMD=/run/wrappers/bin/sudo -n /run/current-system/sw/bin/systemctl start --no-block agent-box-update.service" -Environment="AGENT_BOX_UPDATE_UNIT=agent-box-update.service" -Environment="AGENT_BOX_WEBHOOK_PYTHON=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin/python3" -Environment="AGENT_BOX_WEBHOOK_SCRIPT=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-webhook.py" -Environment="AGENT_BOX_WEBHOOK_STATE_DIR=/home/agent/.local/state/local-webhook" -Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" -Environment="TMUX_TMPDIR=/run/agent-box-agent" -Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" -ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-settings/bin/agent-box-settings -LockPersonality=true -NoNewPrivileges=false -PrivateDevices=true -ProtectControlGroups=true -ProtectHome=false -ProtectKernelModules=true -ProtectKernelTunables=true -ProtectSystem=strict -ReadWritePaths=/home/agent -ReadWritePaths=/run/agent-box-agent -ReadWritePaths=/var/lib/agent-box-web -Restart=always -RestartSec=5s -RestrictRealtime=true -RestrictSUIDSGID=true -User=agent - -[Install] -WantedBy=multi-user.target - -# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-box-settings-agent.socket b/tests/golden/web/units/agent-box-settings-agent.socket deleted file mode 100644 index c7492d4c..00000000 --- a/tests/golden/web/units/agent-box-settings-agent.socket +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=Settings page socket for agent - -[Socket] -ListenStream=/run/agent-box-settings/agent.sock -SocketGroup=caddy -SocketMode=0660 -SocketUser=agent - - - - -[Install] -WantedBy=sockets.target - -# X-Golden-wantedBy: sockets.target diff --git a/tests/golden/web/units/agent-box-settings-robot.socket b/tests/golden/web/units/agent-box-settings-robot.socket deleted file mode 100644 index 62e2238d..00000000 --- a/tests/golden/web/units/agent-box-settings-robot.socket +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=Settings page socket for robot - -[Socket] -ListenStream=/run/agent-box-settings/robot.sock -SocketGroup=caddy -SocketMode=0660 -SocketUser=robot - - - - -[Install] -WantedBy=sockets.target - -# X-Golden-wantedBy: sockets.target diff --git a/tests/golden/web/units/agent-box-settings-robot.service b/tests/golden/web/units/agent-box-settings@.service similarity index 56% rename from tests/golden/web/units/agent-box-settings-robot.service rename to tests/golden/web/units/agent-box-settings@.service index ad238ca7..a97e4e6e 100644 --- a/tests/golden/web/units/agent-box-settings-robot.service +++ b/tests/golden/web/units/agent-box-settings@.service @@ -1,52 +1,18 @@ [Unit] -After=network-online.target agent-box-settings-robot.socket -Description=Per-user secrets settings page for robot -Requires=agent-box-settings-robot.socket -Wants=network-online.target [Service] Environment="AGENT_BOX_AGENTS=claude,codex,shell" Environment="AGENT_BOX_DEFAULT_AGENT=claude" Environment="AGENT_BOX_HOOK_SPAWN_CMD=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook-spawn/bin/agent-box-webhook-spawn" -Environment="AGENT_BOX_PASSWORD_CMD=/run/wrappers/bin/sudo -n /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-password-robot/bin/agent-box-password-robot" Environment="AGENT_BOX_REPO=defangdevs/agent-box" Environment="AGENT_BOX_REV=0000000000000000000000000000000000000000" -Environment="AGENT_BOX_SESSIONS_FILE=/home/robot/.config/agent-box/sessions.json" -Environment="AGENT_BOX_SETTINGS_BASE=/robot/settings" -Environment="AGENT_BOX_SETTINGS_ENV_FILE=/home/robot/.config/agent-box/env" -Environment="AGENT_BOX_SETTINGS_USER=robot" Environment="AGENT_BOX_SYSTEMCTL=/run/current-system/sw/bin/systemctl" Environment="AGENT_BOX_TMUX_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin/tmux" -Environment="AGENT_BOX_TMUX_SOCKET=agent-box" -Environment="AGENT_BOX_TMUX_TMPDIR=/run/agent-box-robot" Environment="AGENT_BOX_UPDATE_CMD=/run/wrappers/bin/sudo -n /run/current-system/sw/bin/systemctl start --no-block agent-box-update.service" Environment="AGENT_BOX_UPDATE_UNIT=agent-box-update.service" Environment="AGENT_BOX_WEBHOOK_PYTHON=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin/python3" Environment="AGENT_BOX_WEBHOOK_SCRIPT=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-webhook.py" -Environment="AGENT_BOX_WEBHOOK_STATE_DIR=/home/robot/.local/state/local-webhook" Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" -Environment="TMUX_TMPDIR=/run/agent-box-robot" Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" -ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-settings/bin/agent-box-settings -LockPersonality=true -NoNewPrivileges=false -PrivateDevices=true -ProtectControlGroups=true -ProtectHome=false -ProtectKernelModules=true -ProtectKernelTunables=true -ProtectSystem=strict -ReadWritePaths=/home/robot -ReadWritePaths=/run/agent-box-robot -ReadWritePaths=/var/lib/agent-box-web -Restart=always -RestartSec=5s -RestrictRealtime=true -RestrictSUIDSGID=true -User=robot - -[Install] -WantedBy=multi-user.target - -# X-Golden-wantedBy: multi-user.target +ExecSearchPath=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-settings/bin diff --git a/tests/golden/web/units/agent-box-settings@agent.service b/tests/golden/web/units/agent-box-settings@agent.service new file mode 100644 index 00000000..ee98bbc6 --- /dev/null +++ b/tests/golden/web/units/agent-box-settings@agent.service @@ -0,0 +1,11 @@ +[Unit] + +[Service] +Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" +Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" +Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" + +[Install] +WantedBy=multi-user.target + +# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-box-settings@agent.socket b/tests/golden/web/units/agent-box-settings@agent.socket new file mode 100644 index 00000000..3ccc6f5c --- /dev/null +++ b/tests/golden/web/units/agent-box-settings@agent.socket @@ -0,0 +1,11 @@ +[Unit] + +[Socket] + + + + +[Install] +WantedBy=sockets.target + +# X-Golden-wantedBy: sockets.target diff --git a/tests/golden/web/units/agent-box-settings@robot.service b/tests/golden/web/units/agent-box-settings@robot.service new file mode 100644 index 00000000..ee98bbc6 --- /dev/null +++ b/tests/golden/web/units/agent-box-settings@robot.service @@ -0,0 +1,11 @@ +[Unit] + +[Service] +Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" +Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" +Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" + +[Install] +WantedBy=multi-user.target + +# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-box-settings@robot.socket b/tests/golden/web/units/agent-box-settings@robot.socket new file mode 100644 index 00000000..3ccc6f5c --- /dev/null +++ b/tests/golden/web/units/agent-box-settings@robot.socket @@ -0,0 +1,11 @@ +[Unit] + +[Socket] + + + + +[Install] +WantedBy=sockets.target + +# X-Golden-wantedBy: sockets.target diff --git a/tests/golden/web/units/agent-box-webhook-agent.socket b/tests/golden/web/units/agent-box-webhook-agent.socket deleted file mode 100644 index 03caa891..00000000 --- a/tests/golden/web/units/agent-box-webhook-agent.socket +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=Webhook ingress socket for agent - -[Socket] -ListenStream=/run/agent-box-webhook/agent.sock -SocketGroup=caddy -SocketMode=0660 -SocketUser=agent - - - - -[Install] -WantedBy=sockets.target - -# X-Golden-wantedBy: sockets.target diff --git a/tests/golden/web/units/agent-box-webhook-robot.service b/tests/golden/web/units/agent-box-webhook-robot.service deleted file mode 100644 index 5e877d5a..00000000 --- a/tests/golden/web/units/agent-box-webhook-robot.service +++ /dev/null @@ -1,36 +0,0 @@ -[Unit] -After=network-online.target agent-box-webhook-robot.socket -Description=Webhook receiver daemon (local-webhook) for robot -Requires=agent-box-webhook-robot.socket -Wants=network-online.target - -[Service] -Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="LOCAL_WEBHOOK_PORT=0" -Environment="LOCAL_WEBHOOK_RECEIVER_ONLY=1" -Environment="LOCAL_WEBHOOK_SPAWN_CMD=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook-spawn/bin/agent-box-webhook-spawn" -Environment="LOCAL_WEBHOOK_STATE_DIR=/home/robot/.local/state/local-webhook" -Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" -Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" -ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin/python3 /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-webhook.py -ExecStartPre=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook-policy-apply -LockPersonality=true -NoNewPrivileges=true -PrivateDevices=true -ProtectControlGroups=true -ProtectHome=false -ProtectKernelModules=true -ProtectKernelTunables=true -ProtectSystem=strict -ReadWritePaths=/home/robot -Restart=always -RestartSec=5s -RestrictRealtime=true -RestrictSUIDSGID=true -StandardInput=null -User=robot - -[Install] -WantedBy=multi-user.target - -# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-box-webhook-robot.socket b/tests/golden/web/units/agent-box-webhook-robot.socket deleted file mode 100644 index 5e577b04..00000000 --- a/tests/golden/web/units/agent-box-webhook-robot.socket +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=Webhook ingress socket for robot - -[Socket] -ListenStream=/run/agent-box-webhook/robot.sock -SocketGroup=caddy -SocketMode=0660 -SocketUser=robot - - - - -[Install] -WantedBy=sockets.target - -# X-Golden-wantedBy: sockets.target diff --git a/tests/golden/web/units/agent-box-webhook-agent.service b/tests/golden/web/units/agent-box-webhook@.service similarity index 59% rename from tests/golden/web/units/agent-box-webhook-agent.service rename to tests/golden/web/units/agent-box-webhook@.service index db17d2ec..9b566228 100644 --- a/tests/golden/web/units/agent-box-webhook-agent.service +++ b/tests/golden/web/units/agent-box-webhook@.service @@ -1,36 +1,8 @@ [Unit] -After=network-online.target agent-box-webhook-agent.socket -Description=Webhook receiver daemon (local-webhook) for agent -Requires=agent-box-webhook-agent.socket -Wants=network-online.target [Service] Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="LOCAL_WEBHOOK_PORT=0" -Environment="LOCAL_WEBHOOK_RECEIVER_ONLY=1" Environment="LOCAL_WEBHOOK_SPAWN_CMD=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook-spawn/bin/agent-box-webhook-spawn" -Environment="LOCAL_WEBHOOK_STATE_DIR=/home/agent/.local/state/local-webhook" Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" -ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin/python3 /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-webhook.py -ExecStartPre=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook-policy-apply -LockPersonality=true -NoNewPrivileges=true -PrivateDevices=true -ProtectControlGroups=true -ProtectHome=false -ProtectKernelModules=true -ProtectKernelTunables=true -ProtectSystem=strict -ReadWritePaths=/home/agent -Restart=always -RestartSec=5s -RestrictRealtime=true -RestrictSUIDSGID=true -StandardInput=null -User=agent - -[Install] -WantedBy=multi-user.target - -# X-Golden-wantedBy: multi-user.target +ExecSearchPath=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook-policy-apply/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook-receiver/bin diff --git a/tests/golden/web/units/agent-box-webhook@agent.service b/tests/golden/web/units/agent-box-webhook@agent.service new file mode 100644 index 00000000..ee98bbc6 --- /dev/null +++ b/tests/golden/web/units/agent-box-webhook@agent.service @@ -0,0 +1,11 @@ +[Unit] + +[Service] +Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" +Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" +Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" + +[Install] +WantedBy=multi-user.target + +# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-box-webhook@agent.socket b/tests/golden/web/units/agent-box-webhook@agent.socket new file mode 100644 index 00000000..3ccc6f5c --- /dev/null +++ b/tests/golden/web/units/agent-box-webhook@agent.socket @@ -0,0 +1,11 @@ +[Unit] + +[Socket] + + + + +[Install] +WantedBy=sockets.target + +# X-Golden-wantedBy: sockets.target diff --git a/tests/golden/web/units/agent-box-webhook@robot.service b/tests/golden/web/units/agent-box-webhook@robot.service new file mode 100644 index 00000000..ee98bbc6 --- /dev/null +++ b/tests/golden/web/units/agent-box-webhook@robot.service @@ -0,0 +1,11 @@ +[Unit] + +[Service] +Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" +Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" +Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" + +[Install] +WantedBy=multi-user.target + +# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-box-webhook@robot.socket b/tests/golden/web/units/agent-box-webhook@robot.socket new file mode 100644 index 00000000..3ccc6f5c --- /dev/null +++ b/tests/golden/web/units/agent-box-webhook@robot.socket @@ -0,0 +1,11 @@ +[Unit] + +[Socket] + + + + +[Install] +WantedBy=sockets.target + +# X-Golden-wantedBy: sockets.target diff --git a/tests/golden/web/units/agent-box@.service b/tests/golden/web/units/agent-box@.service new file mode 100644 index 00000000..e26f184f --- /dev/null +++ b/tests/golden/web/units/agent-box@.service @@ -0,0 +1,21 @@ +[Unit] + +[Service] +Environment="AGENT_BOX_CLAUDE_SETTINGS=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-claude-hook-settings.json" +Environment="AGENT_BOX_CODEX_FULL_ACCESS=1" +Environment="AGENT_BOX_CODEX_RC=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-codex-remote-control" +Environment="AGENT_BOX_ENV_EXEC=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-env-exec" +Environment="AGENT_BOX_FIND_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin/find" +Environment="AGENT_BOX_FLOCK_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin/flock" +Environment="AGENT_BOX_GREP_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin/grep" +Environment="AGENT_BOX_HOSTNAME_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-hostname-hostname-debian-3.25/bin/hostname" +Environment="AGENT_BOX_HOST_LABEL=nixos" +Environment="AGENT_BOX_WEBHOOK_PINNED_SCRIPT=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-webhook.py" +Environment="AGENT_BOX_WEBHOOK_REPO=defangdevs/local-channels" +Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" +Environment="PATH=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/home/%i/.nix-profile/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/sbin:/run/wrappers/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" +Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" +ExecSearchPath=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin +NoNewPrivileges=false +OOMScoreAdjust=500 +ReadWritePaths=/var/lib/agent-box-sites/%i diff --git a/tests/golden/web/units/agent-box@agent.service b/tests/golden/web/units/agent-box@agent.service new file mode 100644 index 00000000..ee98bbc6 --- /dev/null +++ b/tests/golden/web/units/agent-box@agent.service @@ -0,0 +1,11 @@ +[Unit] + +[Service] +Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" +Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" +Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" + +[Install] +WantedBy=multi-user.target + +# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-box@robot.service b/tests/golden/web/units/agent-box@robot.service new file mode 100644 index 00000000..ee98bbc6 --- /dev/null +++ b/tests/golden/web/units/agent-box@robot.service @@ -0,0 +1,11 @@ +[Unit] + +[Service] +Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" +Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" +Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" + +[Install] +WantedBy=multi-user.target + +# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-web-terminal-agent.service b/tests/golden/web/units/agent-web-terminal-agent.service deleted file mode 100644 index eaa14e7a..00000000 --- a/tests/golden/web/units/agent-web-terminal-agent.service +++ /dev/null @@ -1,20 +0,0 @@ -[Unit] -After=agent-box-agent.service network-online.target -Description=Browser terminal (ttyd) attached to agent's tmux -Wants=network-online.target - -[Service] -Environment="AGENT_BOX_SESSIONS_FILE=/home/agent/.config/agent-box/sessions.json" -Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" -Environment="TMUX_TMPDIR=/run/agent-box-agent" -Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" -ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/bin/ttyd --writable --url-arg -p 7681 -i 127.0.0.1 -b /agent -t disableLeaveAlert=true -t titleFixed=agent@golden.example.org /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-attach -Restart=always -RestartSec=5s -User=agent - -[Install] -WantedBy=multi-user.target - -# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-web-terminal-robot.service b/tests/golden/web/units/agent-web-terminal-robot.service deleted file mode 100644 index 880a32dd..00000000 --- a/tests/golden/web/units/agent-web-terminal-robot.service +++ /dev/null @@ -1,20 +0,0 @@ -[Unit] -After=agent-box-robot.service network-online.target -Description=Browser terminal (ttyd) attached to robot's tmux -Wants=network-online.target - -[Service] -Environment="AGENT_BOX_SESSIONS_FILE=/home/robot/.config/agent-box/sessions.json" -Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" -Environment="TMUX_TMPDIR=/run/agent-box-robot" -Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" -ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/bin/ttyd --writable --url-arg -p 7682 -i 127.0.0.1 -b /robot -t disableLeaveAlert=true -t titleFixed=robot@golden.example.org /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-attach -Restart=always -RestartSec=5s -User=robot - -[Install] -WantedBy=multi-user.target - -# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-web-terminal@.service b/tests/golden/web/units/agent-web-terminal@.service new file mode 100644 index 00000000..8d64d4fa --- /dev/null +++ b/tests/golden/web/units/agent-web-terminal@.service @@ -0,0 +1,8 @@ +[Unit] + +[Service] +Environment="AGENT_BOX_WEB_DOMAIN=golden.example.org" +Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" +Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-attach/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-attach/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" +Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" +ExecSearchPath=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/bin diff --git a/tests/golden/web/units/agent-web-terminal@agent.service b/tests/golden/web/units/agent-web-terminal@agent.service new file mode 100644 index 00000000..ee98bbc6 --- /dev/null +++ b/tests/golden/web/units/agent-web-terminal@agent.service @@ -0,0 +1,11 @@ +[Unit] + +[Service] +Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" +Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" +Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" + +[Install] +WantedBy=multi-user.target + +# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-web-terminal@robot.service b/tests/golden/web/units/agent-web-terminal@robot.service new file mode 100644 index 00000000..ee98bbc6 --- /dev/null +++ b/tests/golden/web/units/agent-web-terminal@robot.service @@ -0,0 +1,11 @@ +[Unit] + +[Service] +Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" +Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" +Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" + +[Install] +WantedBy=multi-user.target + +# X-Golden-wantedBy: multi-user.target diff --git a/tests/memory-protection.nix b/tests/memory-protection.nix index c24988cb..3adefef0 100644 --- a/tests/memory-protection.nix +++ b/tests/memory-protection.nix @@ -23,7 +23,7 @@ testScript = '' machine.wait_for_unit("multi-user.target") - machine.wait_for_unit("agent-box-agent.service") + machine.wait_for_unit("agent-box@agent.service") machine.wait_for_unit("earlyoom.service") # This is the only test that runs with web.enable off (the default), so it @@ -31,7 +31,7 @@ # tmpfiles-created only when web is on, and listing it unconditionally made # namespace setup fail with 226/NAMESPACE — the unit above never started. machine.fail( - "systemctl show agent-box-agent --property=ReadWritePaths --value " + "systemctl show agent-box@agent --property=ReadWritePaths --value " "| grep agent-box-sites >/dev/null" ) @@ -45,7 +45,7 @@ # the agent unit's main process runs with the raised OOM score main_pid = machine.succeed( - "systemctl show -p MainPID --value agent-box-agent.service" + "systemctl show -p MainPID --value agent-box@agent.service" ).strip() assert main_pid != "0", "agent unit has no main PID" adj = machine.succeed(f"cat /proc/{main_pid}/oom_score_adj").strip() @@ -70,7 +70,7 @@ # ...and the box came through responsive, management plane intact. machine.succeed("systemctl is-active earlyoom.service") - machine.succeed("systemctl is-active agent-box-agent.service") + machine.succeed("systemctl is-active agent-box@agent.service") print(machine.succeed("journalctl -u earlyoom.service | tail -20")) ''; } diff --git a/tests/self-serve-domain.nix b/tests/self-serve-domain.nix index ad106e46..178ba170 100644 --- a/tests/self-serve-domain.nix +++ b/tests/self-serve-domain.nix @@ -61,7 +61,7 @@ testScript = '' start_all() machine.wait_for_unit("caddy.service") - machine.wait_for_unit("agent-box-agent.service") + machine.wait_for_unit("agent-box@agent.service") client.wait_for_unit("multi-user.target") machine_ip = machine.succeed("ip -4 -o addr show eth1 | head -1").split()[3].split("/")[0] @@ -82,7 +82,7 @@ # flow returned EROFS for every real agent while this test (which used to # write as plain `sudo -u agent` from the driver's root namespace) passed. machine.succeed( - "systemctl show agent-box-agent --property=ReadWritePaths --value " + "systemctl show agent-box@agent --property=ReadWritePaths --value " "| grep /var/lib/agent-box-sites/agent >/dev/null" ) @@ -92,7 +92,7 @@ # to the same read-only remount a tool shell inside the session gets; # runuser then drops to the agent uid for the ownership check below. agent_pid = machine.succeed( - "systemctl show -p MainPID --value agent-box-agent.service" + "systemctl show -p MainPID --value agent-box@agent.service" ).strip() assert agent_pid not in ("", "0"), "agent unit has no main PID" machine.succeed( @@ -114,7 +114,7 @@ # sudo wrapper, without which shells started by the agent CLI can't # invoke sudo even though the sudoers rule permits the command. machine.succeed( - "systemctl show agent-box-agent --property=Environment " + "systemctl show agent-box@agent --property=Environment " "| grep '/run/wrappers/bin' >/dev/null" ) diff --git a/tests/sessions.nix b/tests/sessions.nix index a43ecc77..16509cfd 100644 --- a/tests/sessions.nix +++ b/tests/sessions.nix @@ -106,8 +106,8 @@ import shlex start_all() - machine.wait_for_unit("agent-box-agent.service") - machine.wait_for_unit("agent-box-settings-agent.service") + machine.wait_for_unit("agent-box@agent.service") + machine.wait_for_unit("agent-box-settings@agent.service") machine.wait_for_unit("caddy.service") client.wait_for_unit("multi-user.target") @@ -146,11 +146,11 @@ # backdoor shell EXECUTES the supervisor script as root and never returns # (the CI hang on this PR's first three runs). start_script = machine.succeed( - "systemctl show agent-box-agent --property=ExecStart --value " + "systemctl show agent-box@agent --property=ExecStart --value " "| grep -o '/nix/store/[^ ;]*-agent-box-supervisor' | head -n1" ).strip() machine.succeed( - "systemctl show agent-box-agent -p Environment --value " + "systemctl show agent-box@agent -p Environment --value " "| grep -F 'AGENT_BOX_HOST_LABEL=box.test' >/dev/null" ) machine.succeed(f"grep -qF 'rcname=$USER-$sname' {start_script}") @@ -166,7 +166,7 @@ # which gets the full system path and would pass even when the unit PATH # is missing them (the bug this guards against). unit_path = machine.succeed( - "systemctl show agent-box-agent -p Environment --value" + "systemctl show agent-box@agent -p Environment --value" ).split("PATH=")[1].split()[0] for tool in ["curl", "wget", "awk", "tar", "gzip", "bzip2", "xz", "zip", "unzip", "diff", "patch", "less", "file", "ps", "killall", @@ -182,7 +182,7 @@ "su -s /bin/sh agent -c " "'git config --get credential.https://github.com.helper' | grep 'gh auth git-credential' >/dev/null" ) - machine.succeed("systemctl cat agent-box-agent | grep -- '-gh-' >/dev/null") + machine.succeed("systemctl cat agent-box@agent | grep -- '-gh-' >/dev/null") # Claude emits its long OAuth URL inside one complete OSC 8 sequence. # tmux stores that metadata, but redraws plain text unless the attaching @@ -427,7 +427,7 @@ # PANE, and a bare "=name" only resolves when that session is tmux's # idea of the current one — otherwise it silently expands to "" (rc 0). server_pid = machine.succeed(tmux('display -p -t "=helper:" "#{pid}"')).strip() - machine.succeed(f"grep -q agent-box-agent.service /proc/{server_pid}/cgroup") + machine.succeed(f"grep -q agent-box@agent.service /proc/{server_pid}/cgroup") # ls shows both sessions with their agents. listing = machine.succeed("su -s /bin/sh agent -c 'agent-box-session ls'") @@ -1032,7 +1032,7 @@ # Print the wrapper's path (see the grep -o note below) — running # the substitution as the command would run the WRAPPER instead. attach = machine.succeed( - "{ systemctl show agent-web-terminal-agent --property=ExecStart " + "{ systemctl show agent-web-terminal@agent --property=ExecStart " "--value | grep -o '/nix/store/[^ ]*-agent-box-attach' " "|| echo /missing; } | head -n1" ).strip() @@ -1066,14 +1066,14 @@ assert 'data-ph="live"' in live_ws, live_ws # ttyd serves per-session deep links: the unit runs with --url-arg. - machine.succeed("systemctl cat agent-web-terminal-agent | grep -- --url-arg >/dev/null") + machine.succeed("systemctl cat agent-web-terminal@agent | grep -- --url-arg >/dev/null") # The attach script is the shared agent-box-attach since issue #154 # Phase 2. `grep -o ... || echo missing`: an empty substitution would # leave `grep -q` reading stdin — the backdoor shell then hangs the whole # test until the CI timeout (exactly how the rename was first caught). machine.succeed( "grep -q -- '-T hyperlinks' " - "$({ systemctl show agent-web-terminal-agent --property=ExecStart --value " + "$({ systemctl show agent-web-terminal@agent --property=ExecStart --value " "| grep -o '/nix/store/[^ ]*-agent-box-attach' || echo /missing; } | head -n1)" ) diff --git a/tests/settings-page.nix b/tests/settings-page.nix index a9ffdd16..1824a7ee 100644 --- a/tests/settings-page.nix +++ b/tests/settings-page.nix @@ -116,8 +116,8 @@ testScript = '' start_all() machine.wait_for_unit("caddy.service") - machine.wait_for_unit("agent-box-agent.service") - machine.wait_for_unit("agent-box-settings-agent.service") + machine.wait_for_unit("agent-box@agent.service") + machine.wait_for_unit("agent-box-settings@agent.service") client.wait_for_unit("multi-user.target") def tmux(cmd): @@ -133,7 +133,7 @@ # Daemon runs AS the agent user (no root). machine.succeed( - "systemctl show agent-box-settings-agent --property=User | grep -x 'User=agent'" + "systemctl show agent-box-settings@agent --property=User | grep -x 'User=agent'" ) # Issue #49: the daemon listens ONLY on the systemd-owned unix socket — @@ -240,7 +240,7 @@ # sessions (and deleted keys never left). The spawn wrapper below is # the live source instead. machine.fail( - "systemctl show agent-box-agent --property=EnvironmentFiles " + "systemctl show agent-box@agent --property=EnvironmentFiles " "| grep '/home/agent/.config/agent-box/env' >/dev/null" ) @@ -299,14 +299,14 @@ "-d 'key=UI_SECRET' https://box.test/agent/settings/delete | grep -x 303" ) old_main = machine.succeed( - "systemctl show agent-box-agent --property=MainPID --value" + "systemctl show agent-box@agent --property=MainPID --value" ).strip() wait_new_pane( f"{curl} -u agent:testpassword -o /dev/null -w '%{{http_code}}' " "-X POST https://box.test/agent/settings/restart | grep -x 303" ) machine.wait_until_succeeds( - "p=$(systemctl show agent-box-agent --property=MainPID --value); " + "p=$(systemctl show agent-box@agent --property=MainPID --value); " f"[ -n \"$p\" ] && [ \"$p\" != 0 ] && [ \"$p\" != {old_main} ]", timeout=60, ) diff --git a/tests/webhook.nix b/tests/webhook.nix index b1e607b6..8925e2eb 100644 --- a/tests/webhook.nix +++ b/tests/webhook.nix @@ -124,20 +124,20 @@ start_all() machine.wait_for_unit("caddy.service") - machine.wait_for_unit("agent-box-agent.service") + machine.wait_for_unit("agent-box@agent.service") client.wait_for_unit("multi-user.target") machine_ip = machine.succeed("ip -4 -o addr show eth1 | head -1").split()[3].split("/")[0] # --- default-on: the units exist without anyone setting webhook.enable --- - machine.wait_for_unit("agent-box-webhook-agent.socket") - machine.wait_for_unit("agent-box-webhook-agent.service") + machine.wait_for_unit("agent-box-webhook@agent.socket") + machine.wait_for_unit("agent-box-webhook@agent.service") # Socket ownership is the isolation boundary: the user and caddy, nobody # else. systemd (root) binds it before the daemon starts, and the daemon # adopts that fd rather than binding a path or a port itself. machine.succeed( "stat -c '%U:%G %a' /run/agent-box-webhook/agent.sock | grep -x 'agent:caddy 660'" ) - machine.succeed("systemctl show -p User --value agent-box-webhook-agent.service | grep -x agent") + machine.succeed("systemctl show -p User --value agent-box-webhook@agent.service | grep -x agent") # --- discovery surface ------------------------------------------------- # The CLI is on the agent's PATH and the endpoint URL is in its @@ -149,10 +149,10 @@ machine.succeed("agent-box-webhook --help | grep -- '--deliver-to subagent' >/dev/null") machine.succeed("agent-box-webhook --help | grep -- '--ignore-sender' >/dev/null") machine.succeed( - "systemctl show -p Environment agent-box-agent.service | grep agent-box-webhook/bin >/dev/null" + "systemctl show -p Environment agent-box@agent.service | grep agent-box-webhook/bin >/dev/null" ) machine.succeed( - "systemctl show -p Environment agent-box-agent.service" + "systemctl show -p Environment agent-box@agent.service" " | grep 'AGENT_BOX_WEBHOOK_URL=https://box.test/agent/webhook' >/dev/null" ) # The supervisor gives each tmux session its own subscription scope, so a @@ -244,14 +244,14 @@ # stream. Both the interpreter and the script come from the daemon unit's # own ExecStart, so the test cannot drift from the pinned pair. exec_start = machine.succeed( - "systemctl show -p ExecStart --value agent-box-webhook-agent.service" + "systemctl show -p ExecStart --value agent-box-webhook@agent.service" ) python = machine.succeed( - "systemctl show -p ExecStart --value agent-box-webhook-agent.service" + "systemctl show -p ExecStart --value agent-box-webhook@agent.service" " | grep -o '/nix/store/[^ ;]*/bin/python3' | head -1" ).strip() script = machine.succeed( - "systemctl show -p ExecStart --value agent-box-webhook-agent.service" + "systemctl show -p ExecStart --value agent-box-webhook@agent.service" " | grep -o '/nix/store/[^ ;]*webhook.py' | head -1" ).strip() assert python and script, exec_start @@ -463,7 +463,7 @@ # The daemon advertises the spawn wiring, so subscribe could warn if the # unit ever lost LOCAL_WEBHOOK_SPAWN_CMD. machine.succeed( - "systemctl show -p Environment agent-box-webhook-agent.service" + "systemctl show -p Environment agent-box-webhook@agent.service" " | grep 'LOCAL_WEBHOOK_SPAWN_CMD=/nix/store/' >/dev/null" ) machine.succeed("jq -e '.spawn == true' /home/agent/.local/state/local-webhook/receiver.json") @@ -479,7 +479,7 @@ f"https://box.test/agent/webhook/github | grep -x 200" ) machine.wait_until_succeeds( - "journalctl -u agent-box-webhook-agent --no-pager" + "journalctl -u agent-box-webhook@agent --no-pager" " | grep 'not spawning for workflow_run on defangdevs/agent-box' >/dev/null", timeout=30, ) @@ -523,7 +523,7 @@ " https://box.test/agent/webhook/github | grep -x 200" ) machine.wait_until_succeeds( - "journalctl -u agent-box-webhook-agent --no-pager | grep 'no failing outcome' >/dev/null", + "journalctl -u agent-box-webhook@agent --no-pager | grep 'no failing outcome' >/dev/null", timeout=30, ) machine.fail( @@ -540,7 +540,7 @@ # the prompt via sessions.json is a race otherwise (lost on master run # 30740226645). With it stopped, the wrapper's write is the only actor; # restarting it afterwards proves the spawn + consumption half. - machine.succeed("systemctl stop agent-box-agent.service") + machine.succeed("systemctl stop agent-box@agent.service") client.succeed( f"{post} -H 'x-hub-signature-256: sha256={sig}' " f"https://box.test/agent/webhook/github | grep -x 200" @@ -597,7 +597,7 @@ assert "already subscribed to github:defangdevs/agent-box" in hook_prompt, hook_prompt # Supervisor back up: it starts the hook session and consumes the prompt. - machine.succeed("systemctl start agent-box-agent.service") + machine.succeed("systemctl start agent-box@agent.service") machine.wait_until_succeeds( "sudo -u agent env TMUX_TMPDIR=/run/agent-box-agent tmux -L agent-box" " list-sessions -F '#S' | grep '^hook-' >/dev/null", @@ -640,7 +640,7 @@ # duplicate would be a coalesced spawn 60s later (the dispatcher's window), # not a second session the assertion below could catch immediately. machine.wait_until_succeeds( - "journalctl -u agent-box-webhook-agent --no-pager" + "journalctl -u agent-box-webhook@agent --no-pager" f" | grep 'session agent-{hook_name} is subscribed to it' >/dev/null", timeout=30, ) @@ -659,7 +659,7 @@ # the wrapper directly also proves it needs nothing from the daemon but its # environment. Nothing below depends on the extra session it creates. spawn_cmd = machine.succeed( - "systemctl show -p Environment agent-box-webhook-agent.service" + "systemctl show -p Environment agent-box-webhook@agent.service" " | grep -o '/nix/store/[^ ]*agent-box-webhook-spawn' | head -1" ).strip() machine.succeed( @@ -887,7 +887,7 @@ " && chown agent:users" " /home/agent/.local/state/local-webhook/filter.agent-ghost.json" ) - machine.succeed("systemctl restart agent-box-agent.service") + machine.succeed("systemctl restart agent-box@agent.service") machine.wait_until_succeeds( "test ! -e /home/agent/.local/state/local-webhook/filter.agent-ghost.json", timeout=60, @@ -910,10 +910,10 @@ " agent-box-webhook subscribe defangdevs/local-channels --deliver-to subagent" " --note 'to be governed' --ignore-sender human" ) - machine.succeed("systemctl restart agent-box-webhook-agent.service") - machine.wait_for_unit("agent-box-webhook-agent.service") + machine.succeed("systemctl restart agent-box-webhook@agent.service") + machine.wait_for_unit("agent-box-webhook@agent.service") machine.wait_until_succeeds( - "journalctl -u agent-box-webhook-agent --no-pager" + "journalctl -u agent-box-webhook@agent --no-pager" " | grep 'enforced declared rules on github:defangdevs/local-channels' >/dev/null", timeout=30, ) @@ -954,7 +954,7 @@ " https://box.test/agent/webhook/github | grep -x 200" ) machine.wait_until_succeeds( - "journalctl -u agent-box-webhook-agent --no-pager" + "journalctl -u agent-box-webhook@agent --no-pager" " | grep 'not spawning for pull_request on defangdevs/local-channels' >/dev/null", timeout=30, ) @@ -994,7 +994,7 @@ # reaches it. The daemon shells out to the SAME pinned webhook.py, one # invocation per session key, so nothing here re-implements the filter # format. - machine.wait_for_unit("agent-box-settings-agent.socket") + machine.wait_for_unit("agent-box-settings@agent.socket") settings_curl = ( "curl -s --max-time 20 --unix-socket /run/agent-box-settings/agent.sock" ) @@ -1169,27 +1169,27 @@ # a session loads its interpreter once. Asserted last: it restarts the agent # unit, and nothing above should have to survive that. machine.succeed( - "systemctl show -p Environment agent-box-agent.service" + "systemctl show -p Environment agent-box@agent.service" " | grep 'AGENT_BOX_WEBHOOK_PINNED_SCRIPT=/nix/store/' >/dev/null" ) set_cache_version("0.0.1") machine.succeed("rm -f /home/agent/.claude/plugins/.agent-box-plugin-sync") - machine.succeed("systemctl restart agent-box-agent.service") - machine.wait_for_unit("agent-box-agent.service") + machine.succeed("systemctl restart agent-box@agent.service") + machine.wait_for_unit("agent-box@agent.service") # It notices, and names both versions. machine.wait_until_succeeds( - "journalctl -u agent-box-agent --no-pager" + "journalctl -u agent-box@agent --no-pager" f" | grep 'cache 0.0.1 is older than the pinned {pinned} — refreshing' >/dev/null", timeout=60, ) # This VM has no route to GitHub, so the refresh fails — and that must be a # logged line, not a session that never starts. machine.wait_until_succeeds( - "journalctl -u agent-box-agent --no-pager" + "journalctl -u agent-box@agent --no-pager" " | grep 'could not refresh the cache' >/dev/null", timeout=120, ) - machine.succeed("systemctl is-active agent-box-agent.service") + machine.succeed("systemctl is-active agent-box@agent.service") # The attempt is stamped, so the next session start inside the retry window # does not pay the timeout again. A box whose claude keeps exiting restarts # sessions in a loop; without this the loop would be a loop of timeouts. @@ -1197,15 +1197,15 @@ "grep -q '^%s ' /home/agent/.claude/plugins/.agent-box-plugin-sync" % pinned ) machine.succeed("journalctl --rotate --vacuum-time=1s") - machine.succeed("systemctl restart agent-box-agent.service") - machine.wait_for_unit("agent-box-agent.service") + machine.succeed("systemctl restart agent-box@agent.service") + machine.wait_for_unit("agent-box@agent.service") machine.wait_until_succeeds( - "journalctl -u agent-box-agent --no-pager | grep 'not retrying yet' >/dev/null", + "journalctl -u agent-box@agent --no-pager | grep 'not retrying yet' >/dev/null", timeout=60, ) # The daemon is the ingress owner and survives every delivery — the box's # endpoint must not depend on which sessions happen to be alive. - machine.succeed("systemctl is-active agent-box-webhook-agent.service") + machine.succeed("systemctl is-active agent-box-webhook@agent.service") ''; } From 99efbec6eed43c464a0593a2945d0bb8414a3921 Mon Sep 17 00:00:00 2001 From: Lio Lunesu Date: Wed, 19 Aug 2026 16:00:16 +0000 Subject: [PATCH 02/18] fix(module): enable per-user template instances via target Wants, not asDropin wantedBy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI on #295 caught every service-dependent VM test failing with "unit agent-box@agent.service is inactive and there are no pending jobs" — none of the templated instances actually started. A `wantedBy` on a systemd.services. entry with overrideStrategy = "asDropin" is a dead end: NixOS's .wants-symlink generation links "../", which needs a real top-level file named e.g. "agent-box@alice.service" to point at. An asDropin instance never has one — only a "agent-box@alice.service.d/overrides.conf" — so the symlink dangles and systemd never schedules a start job, silently. systemd.targets.multi-user.wants (and .sockets.wants for the two socket units) is the mechanism that actually works: it renders a literal Wants= line into the target's own unit (verified by building the same aarch64 multiUser config the `multi-user` check uses and inspecting multi-user.target.d/overrides.conf directly), and systemd resolves a wanted instance name from the %i template at start time — no per-instance file needed. Same pattern nixpkgs itself uses to enable one instance of a template unit. Dropped the now-empty per-instance overrides that carried nothing but overrideStrategy + wantedBy (agent-web-terminal@, agent-box-settings@ service+socket, agent-box-webhook@ service+socket); kept the agent-box@ per-instance drop-in, which still carries real content (u.environment / u.environmentFiles). --- modules/agent-box.nix | 89 +++++++++++-------- modules/agent-box.nix.in | 89 +++++++++++-------- tests/golden/vm/units/agent-box@agent.service | 5 -- .../units/agent-box-settings@agent.service | 11 --- .../web/units/agent-box-settings@agent.socket | 11 --- .../units/agent-box-settings@robot.service | 11 --- .../web/units/agent-box-settings@robot.socket | 11 --- .../web/units/agent-box-webhook@agent.service | 11 --- .../web/units/agent-box-webhook@agent.socket | 11 --- .../web/units/agent-box-webhook@robot.service | 11 --- .../web/units/agent-box-webhook@robot.socket | 11 --- .../golden/web/units/agent-box@agent.service | 5 -- .../golden/web/units/agent-box@robot.service | 5 -- .../units/agent-web-terminal@agent.service | 11 --- .../units/agent-web-terminal@robot.service | 11 --- 15 files changed, 108 insertions(+), 195 deletions(-) delete mode 100644 tests/golden/web/units/agent-box-settings@agent.service delete mode 100644 tests/golden/web/units/agent-box-settings@agent.socket delete mode 100644 tests/golden/web/units/agent-box-settings@robot.service delete mode 100644 tests/golden/web/units/agent-box-settings@robot.socket delete mode 100644 tests/golden/web/units/agent-box-webhook@agent.service delete mode 100644 tests/golden/web/units/agent-box-webhook@agent.socket delete mode 100644 tests/golden/web/units/agent-box-webhook@robot.service delete mode 100644 tests/golden/web/units/agent-box-webhook@robot.socket delete mode 100644 tests/golden/web/units/agent-web-terminal@agent.service delete mode 100644 tests/golden/web/units/agent-web-terminal@robot.service diff --git a/modules/agent-box.nix b/modules/agent-box.nix index 2209bf56..12074541 100644 --- a/modules/agent-box.nix +++ b/modules/agent-box.nix @@ -3862,24 +3862,43 @@ in }; }; } - # Enablement + the one per-user drop-in the generated env file can't - # express: arbitrary u.environment/u.environmentFiles (issue #154 - # Phase 3 design's stated exception — an env file can't add its own + # The one per-user drop-in the generated env file can't express: + # arbitrary u.environment/u.environmentFiles (issue #154 Phase 3 + # design's stated exception — an env file can't add its own # EnvironmentFile= entries or override an already-set var per # instance). overrideStrategy = "asDropin" is required here (unlike # "agent-box@" above): nothing on disk is literally named # "agent-box@alice.service" for the default detection to find, since - # instances are virtual. + # instances are virtual — so "asDropinIfExists" would render this as a + # (broken, ExecStart-less) STANDALONE unit instead of extending the + # template. NOTE: enablement is NOT here — see systemd.targets.multi-user + # below for why a per-instance `wantedBy` on an asDropin unit doesn't + # actually start anything. // (lib.mapAttrs' (name: u: lib.nameValuePair "agent-box@${name}" ( - { overrideStrategy = "asDropin"; - wantedBy = [ "multi-user.target" ]; - } + { overrideStrategy = "asDropin"; } // lib.optionalAttrs (u.environment != { }) { environment = u.environment; } // lib.optionalAttrs (u.environmentFiles != [ ]) { serviceConfig.EnvironmentFile = u.environmentFiles; } )) cfg.users); + # Enablement for every per-user template instance (issue #154 Phase 3). + # `wantedBy` on an "asDropin"-strategy systemd.services entry is a dead + # end: NixOS's .wants-symlink generation links "../" — i.e. it + # needs a real top-level file named e.g. "agent-box@alice.service" to + # point at, and an asDropin instance never has one (only a + # "agent-box@alice.service.d/overrides.conf"). The symlink ends up + # dangling, systemd never schedules a start job, and the unit sits + # "inactive" forever with no error anywhere (caught by every VM test + # that waits on it — see PR #295 CI). `systemd.targets..wants` is + # different: it renders as a literal `Wants=` line in the TARGET's own + # unit file (systemd-lib.nix's generic `wants` -> `Wants=` mapping), and + # systemd resolves a wanted instance name straight from the `%i` + # template at boot — no per-instance file needed at all. This is the + # same pattern nixpkgs itself uses to enable one instance of a template + # unit (e.g. containers.nix's `systemd-nspawn@`). + systemd.targets.multi-user.wants = map (name: "agent-box@${name}.service") (lib.attrNames cfg.users); + security.sudo.extraRules = lib.mkIf (effectiveSudoAllowlist != [ ]) [{ users = lib.attrNames cfg.users; # NOPASSWD only — no SETENV. SETENV lets the caller alter env vars @@ -8557,14 +8576,12 @@ in serviceConfig.ExecSearchPath = lib.makeBinPath [ settingsDaemon ]; }; } - // lib.listToAttrs (map (name: lib.nameValuePair "agent-web-terminal@${name}" { - overrideStrategy = "asDropin"; - wantedBy = [ "multi-user.target" ]; - }) terminalUsers) - // lib.listToAttrs (map (name: lib.nameValuePair "agent-box-settings@${name}" { - overrideStrategy = "asDropin"; - wantedBy = [ "multi-user.target" ]; - }) terminalUsers) + # No per-instance drop-in for agent-web-terminal@/agent-box-settings@: + # neither has any per-user content beyond the %i template itself, so + # there is nothing to override — enablement is + # systemd.targets.multi-user.wants below, not a `wantedBy` here (see + # the note by that option: a `wantedBy` on an "asDropin" instance + # produces a dangling .wants symlink and never actually starts). # Webhook receiver daemon (issue #101), one per terminal user, gated on # webhook.enable. Runs webhook.py (via the agent-box-webhook-receiver # wrapper, resolved through ExecSearchPath) in RECEIVER_ONLY mode as @@ -8583,26 +8600,28 @@ in environment.LOCAL_WEBHOOK_SPAWN_CMD = "${webhookSpawn}/bin/agent-box-webhook-spawn"; serviceConfig.ExecSearchPath = lib.makeBinPath [ webhookPolicyApply webhookReceiverBin ]; }; - } // lib.listToAttrs (map (name: lib.nameValuePair "agent-box-webhook@${name}" { - overrideStrategy = "asDropin"; - wantedBy = [ "multi-user.target" ]; - }) terminalUsers)); - - # The settings daemon's listening sockets (issue #49) and the webhook - # ingress sockets (issue #101) are fully %i-derived already (see - # src/units/*.socket, shipped via systemd.packages) — root binds - # each one 0660 :caddy BEFORE the daemon starts, exactly as - # before; only per-instance ENABLEMENT (sockets.target) is needed - # here, same asDropin reasoning as the .service instances above. - systemd.sockets = - lib.listToAttrs (map (name: lib.nameValuePair "agent-box-settings@${name}" { - overrideStrategy = "asDropin"; - wantedBy = [ "sockets.target" ]; - }) terminalUsers) - // lib.optionalAttrs webhookEnabled (lib.listToAttrs (map (name: lib.nameValuePair "agent-box-webhook@${name}" { - overrideStrategy = "asDropin"; - wantedBy = [ "sockets.target" ]; - }) terminalUsers)); + }); + # No systemd.sockets overrides: the settings daemon's listening + # sockets (issue #49) and the webhook ingress sockets (issue #101) + # are fully %i-derived already (see src/units/*.socket, shipped via + # systemd.packages) — root binds each one 0660 :caddy BEFORE + # the daemon starts, exactly as before. Only ENABLEMENT is + # per-instance, and that belongs in systemd.targets.sockets.wants + # below (same dangling-.wants-symlink reasoning as the .service + # instances — see the note by systemd.targets.multi-user.wants + # above). + + # Enablement for every per-terminal-user template instance (issue + # #154 Phase 3) — see the note by the box-wide + # systemd.targets.multi-user.wants above for why this can't be a + # per-instance `wantedBy` on an asDropin unit. + systemd.targets.multi-user.wants = + (map (name: "agent-web-terminal@${name}.service") terminalUsers) + ++ (map (name: "agent-box-settings@${name}.service") terminalUsers) + ++ lib.optionals webhookEnabled (map (name: "agent-box-webhook@${name}.service") terminalUsers); + systemd.targets.sockets.wants = + (map (name: "agent-box-settings@${name}.socket") terminalUsers) + ++ lib.optionals webhookEnabled (map (name: "agent-box-webhook@${name}.socket") terminalUsers); } )) diff --git a/modules/agent-box.nix.in b/modules/agent-box.nix.in index 459b1ffa..ab2c8fdb 100644 --- a/modules/agent-box.nix.in +++ b/modules/agent-box.nix.in @@ -1614,24 +1614,43 @@ in }; }; } - # Enablement + the one per-user drop-in the generated env file can't - # express: arbitrary u.environment/u.environmentFiles (issue #154 - # Phase 3 design's stated exception — an env file can't add its own + # The one per-user drop-in the generated env file can't express: + # arbitrary u.environment/u.environmentFiles (issue #154 Phase 3 + # design's stated exception — an env file can't add its own # EnvironmentFile= entries or override an already-set var per # instance). overrideStrategy = "asDropin" is required here (unlike # "agent-box@" above): nothing on disk is literally named # "agent-box@alice.service" for the default detection to find, since - # instances are virtual. + # instances are virtual — so "asDropinIfExists" would render this as a + # (broken, ExecStart-less) STANDALONE unit instead of extending the + # template. NOTE: enablement is NOT here — see systemd.targets.multi-user + # below for why a per-instance `wantedBy` on an asDropin unit doesn't + # actually start anything. // (lib.mapAttrs' (name: u: lib.nameValuePair "agent-box@${name}" ( - { overrideStrategy = "asDropin"; - wantedBy = [ "multi-user.target" ]; - } + { overrideStrategy = "asDropin"; } // lib.optionalAttrs (u.environment != { }) { environment = u.environment; } // lib.optionalAttrs (u.environmentFiles != [ ]) { serviceConfig.EnvironmentFile = u.environmentFiles; } )) cfg.users); + # Enablement for every per-user template instance (issue #154 Phase 3). + # `wantedBy` on an "asDropin"-strategy systemd.services entry is a dead + # end: NixOS's .wants-symlink generation links "../" — i.e. it + # needs a real top-level file named e.g. "agent-box@alice.service" to + # point at, and an asDropin instance never has one (only a + # "agent-box@alice.service.d/overrides.conf"). The symlink ends up + # dangling, systemd never schedules a start job, and the unit sits + # "inactive" forever with no error anywhere (caught by every VM test + # that waits on it — see PR #295 CI). `systemd.targets..wants` is + # different: it renders as a literal `Wants=` line in the TARGET's own + # unit file (systemd-lib.nix's generic `wants` -> `Wants=` mapping), and + # systemd resolves a wanted instance name straight from the `%i` + # template at boot — no per-instance file needed at all. This is the + # same pattern nixpkgs itself uses to enable one instance of a template + # unit (e.g. containers.nix's `systemd-nspawn@`). + systemd.targets.multi-user.wants = map (name: "agent-box@${name}.service") (lib.attrNames cfg.users); + security.sudo.extraRules = lib.mkIf (effectiveSudoAllowlist != [ ]) [{ users = lib.attrNames cfg.users; # NOPASSWD only — no SETENV. SETENV lets the caller alter env vars @@ -2131,14 +2150,12 @@ in serviceConfig.ExecSearchPath = lib.makeBinPath [ settingsDaemon ]; }; } - // lib.listToAttrs (map (name: lib.nameValuePair "agent-web-terminal@${name}" { - overrideStrategy = "asDropin"; - wantedBy = [ "multi-user.target" ]; - }) terminalUsers) - // lib.listToAttrs (map (name: lib.nameValuePair "agent-box-settings@${name}" { - overrideStrategy = "asDropin"; - wantedBy = [ "multi-user.target" ]; - }) terminalUsers) + # No per-instance drop-in for agent-web-terminal@/agent-box-settings@: + # neither has any per-user content beyond the %i template itself, so + # there is nothing to override — enablement is + # systemd.targets.multi-user.wants below, not a `wantedBy` here (see + # the note by that option: a `wantedBy` on an "asDropin" instance + # produces a dangling .wants symlink and never actually starts). # Webhook receiver daemon (issue #101), one per terminal user, gated on # webhook.enable. Runs webhook.py (via the agent-box-webhook-receiver # wrapper, resolved through ExecSearchPath) in RECEIVER_ONLY mode as @@ -2157,26 +2174,28 @@ in environment.LOCAL_WEBHOOK_SPAWN_CMD = "${webhookSpawn}/bin/agent-box-webhook-spawn"; serviceConfig.ExecSearchPath = lib.makeBinPath [ webhookPolicyApply webhookReceiverBin ]; }; - } // lib.listToAttrs (map (name: lib.nameValuePair "agent-box-webhook@${name}" { - overrideStrategy = "asDropin"; - wantedBy = [ "multi-user.target" ]; - }) terminalUsers)); - - # The settings daemon's listening sockets (issue #49) and the webhook - # ingress sockets (issue #101) are fully %i-derived already (see - # src/units/*.socket, shipped via systemd.packages) — root binds - # each one 0660 :caddy BEFORE the daemon starts, exactly as - # before; only per-instance ENABLEMENT (sockets.target) is needed - # here, same asDropin reasoning as the .service instances above. - systemd.sockets = - lib.listToAttrs (map (name: lib.nameValuePair "agent-box-settings@${name}" { - overrideStrategy = "asDropin"; - wantedBy = [ "sockets.target" ]; - }) terminalUsers) - // lib.optionalAttrs webhookEnabled (lib.listToAttrs (map (name: lib.nameValuePair "agent-box-webhook@${name}" { - overrideStrategy = "asDropin"; - wantedBy = [ "sockets.target" ]; - }) terminalUsers)); + }); + # No systemd.sockets overrides: the settings daemon's listening + # sockets (issue #49) and the webhook ingress sockets (issue #101) + # are fully %i-derived already (see src/units/*.socket, shipped via + # systemd.packages) — root binds each one 0660 :caddy BEFORE + # the daemon starts, exactly as before. Only ENABLEMENT is + # per-instance, and that belongs in systemd.targets.sockets.wants + # below (same dangling-.wants-symlink reasoning as the .service + # instances — see the note by systemd.targets.multi-user.wants + # above). + + # Enablement for every per-terminal-user template instance (issue + # #154 Phase 3) — see the note by the box-wide + # systemd.targets.multi-user.wants above for why this can't be a + # per-instance `wantedBy` on an asDropin unit. + systemd.targets.multi-user.wants = + (map (name: "agent-web-terminal@${name}.service") terminalUsers) + ++ (map (name: "agent-box-settings@${name}.service") terminalUsers) + ++ lib.optionals webhookEnabled (map (name: "agent-box-webhook@${name}.service") terminalUsers); + systemd.targets.sockets.wants = + (map (name: "agent-box-settings@${name}.socket") terminalUsers) + ++ lib.optionals webhookEnabled (map (name: "agent-box-webhook@${name}.socket") terminalUsers); } )) diff --git a/tests/golden/vm/units/agent-box@agent.service b/tests/golden/vm/units/agent-box@agent.service index ee98bbc6..3b093774 100644 --- a/tests/golden/vm/units/agent-box@agent.service +++ b/tests/golden/vm/units/agent-box@agent.service @@ -4,8 +4,3 @@ Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" - -[Install] -WantedBy=multi-user.target - -# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-box-settings@agent.service b/tests/golden/web/units/agent-box-settings@agent.service deleted file mode 100644 index ee98bbc6..00000000 --- a/tests/golden/web/units/agent-box-settings@agent.service +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] - -[Service] -Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" -Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" - -[Install] -WantedBy=multi-user.target - -# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-box-settings@agent.socket b/tests/golden/web/units/agent-box-settings@agent.socket deleted file mode 100644 index 3ccc6f5c..00000000 --- a/tests/golden/web/units/agent-box-settings@agent.socket +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] - -[Socket] - - - - -[Install] -WantedBy=sockets.target - -# X-Golden-wantedBy: sockets.target diff --git a/tests/golden/web/units/agent-box-settings@robot.service b/tests/golden/web/units/agent-box-settings@robot.service deleted file mode 100644 index ee98bbc6..00000000 --- a/tests/golden/web/units/agent-box-settings@robot.service +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] - -[Service] -Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" -Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" - -[Install] -WantedBy=multi-user.target - -# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-box-settings@robot.socket b/tests/golden/web/units/agent-box-settings@robot.socket deleted file mode 100644 index 3ccc6f5c..00000000 --- a/tests/golden/web/units/agent-box-settings@robot.socket +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] - -[Socket] - - - - -[Install] -WantedBy=sockets.target - -# X-Golden-wantedBy: sockets.target diff --git a/tests/golden/web/units/agent-box-webhook@agent.service b/tests/golden/web/units/agent-box-webhook@agent.service deleted file mode 100644 index ee98bbc6..00000000 --- a/tests/golden/web/units/agent-box-webhook@agent.service +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] - -[Service] -Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" -Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" - -[Install] -WantedBy=multi-user.target - -# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-box-webhook@agent.socket b/tests/golden/web/units/agent-box-webhook@agent.socket deleted file mode 100644 index 3ccc6f5c..00000000 --- a/tests/golden/web/units/agent-box-webhook@agent.socket +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] - -[Socket] - - - - -[Install] -WantedBy=sockets.target - -# X-Golden-wantedBy: sockets.target diff --git a/tests/golden/web/units/agent-box-webhook@robot.service b/tests/golden/web/units/agent-box-webhook@robot.service deleted file mode 100644 index ee98bbc6..00000000 --- a/tests/golden/web/units/agent-box-webhook@robot.service +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] - -[Service] -Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" -Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" - -[Install] -WantedBy=multi-user.target - -# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-box-webhook@robot.socket b/tests/golden/web/units/agent-box-webhook@robot.socket deleted file mode 100644 index 3ccc6f5c..00000000 --- a/tests/golden/web/units/agent-box-webhook@robot.socket +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] - -[Socket] - - - - -[Install] -WantedBy=sockets.target - -# X-Golden-wantedBy: sockets.target diff --git a/tests/golden/web/units/agent-box@agent.service b/tests/golden/web/units/agent-box@agent.service index ee98bbc6..3b093774 100644 --- a/tests/golden/web/units/agent-box@agent.service +++ b/tests/golden/web/units/agent-box@agent.service @@ -4,8 +4,3 @@ Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" - -[Install] -WantedBy=multi-user.target - -# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-box@robot.service b/tests/golden/web/units/agent-box@robot.service index ee98bbc6..3b093774 100644 --- a/tests/golden/web/units/agent-box@robot.service +++ b/tests/golden/web/units/agent-box@robot.service @@ -4,8 +4,3 @@ Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" - -[Install] -WantedBy=multi-user.target - -# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-web-terminal@agent.service b/tests/golden/web/units/agent-web-terminal@agent.service deleted file mode 100644 index ee98bbc6..00000000 --- a/tests/golden/web/units/agent-web-terminal@agent.service +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] - -[Service] -Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" -Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" - -[Install] -WantedBy=multi-user.target - -# X-Golden-wantedBy: multi-user.target diff --git a/tests/golden/web/units/agent-web-terminal@robot.service b/tests/golden/web/units/agent-web-terminal@robot.service deleted file mode 100644 index ee98bbc6..00000000 --- a/tests/golden/web/units/agent-web-terminal@robot.service +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] - -[Service] -Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" -Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" - -[Install] -WantedBy=multi-user.target - -# X-Golden-wantedBy: multi-user.target From 5bfe9f7c8a72702fac9e238f70bcd53e8b52b595 Mon Sep 17 00:00:00 2001 From: Lio Lunesu Date: Wed, 19 Aug 2026 16:03:09 +0000 Subject: [PATCH 03/18] test(golden): capture multi-user.target/sockets.target in the snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Wants= override that actually enables each per-user template instance (the previous commit's fix) lands on units the module doesn't own (multi-user.target, sockets.target), which unitFilter excluded — so the one behavior this refactor most needed to prove (does an instance actually get started) was invisible to the fixture diff and to every eval-level check. Widen the filter to include these two exact unit names and regenerate. --- flake.nix | 12 +++++++++++- tests/golden/vm/units/multi-user.target | 2 ++ tests/golden/web/units/multi-user.target | 2 ++ tests/golden/web/units/sockets.target | 2 ++ 4 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 tests/golden/vm/units/multi-user.target create mode 100644 tests/golden/web/units/multi-user.target create mode 100644 tests/golden/web/units/sockets.target diff --git a/flake.nix b/flake.nix index 29bebe57..03402327 100644 --- a/flake.nix +++ b/flake.nix @@ -44,8 +44,18 @@ # whole Caddy/ttyd/settings/webhook/self-update surface too. web = [ self.nixosModules.agent-box ./hosts/vm.nix ./tests/golden-web.nix ]; }; + # multi-user.target/sockets.target (issue #154 Phase 3): the module + # doesn't own these units, but it drops a `Wants=` override onto + # each one to enable a per-user %i template instance (see the + # agent-box@/agent-box-settings@/agent-box-webhook@/ + # agent-web-terminal@ instances below) — a mechanism the earlier + # per-instance `wantedBy` attempt got wrong in a way no eval-level + # check caught (only a real VM boot did, unit stayed inactive). + # Capturing the override text here is what would have caught it. unitFilter = n: - builtins.match "(agent-box|agent-web|caddy|fail2ban|earlyoom).*" n != null; + builtins.match + "(agent-box|agent-web|caddy|fail2ban|earlyoom).*|multi-user\\.target|sockets\\.target" + n != null; # /etc content the module owns or materially shapes. The fail2ban # dir entries (filter.d/, action.d/) are upstream package trees and # deliberately excluded; the module's own filter and the jail diff --git a/tests/golden/vm/units/multi-user.target b/tests/golden/vm/units/multi-user.target new file mode 100644 index 00000000..c1f13a31 --- /dev/null +++ b/tests/golden/vm/units/multi-user.target @@ -0,0 +1,2 @@ +[Unit] +Wants=machines.target agent-box@agent.service diff --git a/tests/golden/web/units/multi-user.target b/tests/golden/web/units/multi-user.target new file mode 100644 index 00000000..abf2f58b --- /dev/null +++ b/tests/golden/web/units/multi-user.target @@ -0,0 +1,2 @@ +[Unit] +Wants=machines.target agent-web-terminal@agent.service agent-web-terminal@robot.service agent-box-settings@agent.service agent-box-settings@robot.service agent-box-webhook@agent.service agent-box-webhook@robot.service agent-box@agent.service agent-box@robot.service diff --git a/tests/golden/web/units/sockets.target b/tests/golden/web/units/sockets.target new file mode 100644 index 00000000..be250237 --- /dev/null +++ b/tests/golden/web/units/sockets.target @@ -0,0 +1,2 @@ +[Unit] +Wants=agent-box-settings@agent.socket agent-box-settings@robot.socket agent-box-webhook@agent.socket agent-box-webhook@robot.socket From 2bd8149df905bf2d0233b90e438eb7536d6328ea Mon Sep 17 00:00:00 2001 From: Lio Lunesu Date: Wed, 19 Aug 2026 16:18:41 +0000 Subject: [PATCH 04/18] fix(assemble): @@include marker regex can't handle an @ in the target path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unit-file enablement fix in the previous commit was necessary but not sufficient — CI still failed identically. The real cause: MARKER's path group was `[^@]+`, so `@@include:src/units/agent-box@.service@@` (every systemd %i template unit's filename literally contains "@") never matched. resolve() silently left the marker line as-is, so modules/agent-box.nix embedded the LITERAL TEXT "@@include:src/units/agent-box@.service@@" as the unit's entire content instead of the real file — an empty, ExecStart-less unit that explains "Assignment outside of section. Ignoring." in the boot log and why the instantiated unit could never actually do anything once started. `module-generated-up-to-date` couldn't catch this: it compares two runs of the SAME (buggy) assembly process, so a deterministic mismatch stays invisible even though the output is wrong. golden-snapshot couldn't either — its unit capture reads `sys.config.systemd.units..text`, which for a systemd.packages-provided file only exists at all if a matching Nix-declared drop-in ALSO exists (as it does here, thanks to the previous commit's `agent-box@` override) — and even then it's just the drop-in's own text, never the base package file's real bytes. That gap is real and worth a follow-up (filed as an issue) but out of scope for unblocking this PR. Widening `[^@]+` to `.+` (greedy, so it backtracks to the LAST `@@` on the line) fixes it for every existing include site too — verified no other `modules/src/*` path contains an embedded `@@`, so this can't newly misparse a legitimate marker. Verified directly (not just via eval): built the same aarch64 `multiUser` config the `multi-user` check uses and confirmed `agent-box@.service` now contains its real [Unit]/[Service] text (ExecStart=agent-box-supervisor etc.), not the marker string. --- bin/assemble-module.py | 2 +- modules/agent-box.nix | 126 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 121 insertions(+), 7 deletions(-) diff --git a/bin/assemble-module.py b/bin/assemble-module.py index 41a4b879..500e2e8c 100644 --- a/bin/assemble-module.py +++ b/bin/assemble-module.py @@ -42,7 +42,7 @@ from pathlib import Path MARKER = re.compile( - r"^(?P[ ]*)@@include(?P-verbatim)?:(?P[^@]+)@@[ ]*$" + r"^(?P[ ]*)@@include(?P-verbatim)?:(?P.+)@@[ ]*$" ) BANNER = """\ diff --git a/modules/agent-box.nix b/modules/agent-box.nix index 12074541..00ae7520 100644 --- a/modules/agent-box.nix +++ b/modules/agent-box.nix @@ -249,22 +249,136 @@ let agentBoxUnitsPackage = pkgs.runCommand "agent-box-units" { } '' mkdir -p $out/etc/systemd/system install -m444 ${pkgs.writeText "agent-box@.service" '' - @@include:src/units/agent-box@.service@@ + [Unit] + Description=Coding agent sessions (tmux) for %i + After=network-online.target + Wants=network-online.target + + [Service] + Type=exec + User=%i + Restart=always + RestartSec=2s + ExecStart=agent-box-supervisor + ExecStop=tmux -L agent-box kill-server + RuntimeDirectory=agent-box-%i + RuntimeDirectoryMode=0700 + RuntimeDirectoryPreserve=yes + Environment=HOME=/home/%i + Environment=TMUX_TMPDIR=/run/agent-box-%i + EnvironmentFile=-/etc/agent-box/units/%i.env + EnvironmentFile=-/etc/agent-box/units/%i.local.env + PrivateTmp=true + PrivateDevices=true + ProtectSystem=strict + ReadWritePaths=/home/%i + ProtectKernelTunables=true + ProtectKernelModules=true + ProtectControlGroups=true + ProtectClock=true + RestrictSUIDSGID=true + RestrictRealtime=true + LockPersonality=true ''} $out/etc/systemd/system/agent-box@.service install -m444 ${pkgs.writeText "agent-web-terminal@.service" '' - @@include:src/units/agent-web-terminal@.service@@ + [Unit] + Description=Browser terminal (ttyd) attached to %i's tmux + After=agent-box@%i.service network-online.target + Wants=network-online.target + + [Service] + User=%i + Restart=always + RestartSec=5s + Environment=TMUX_TMPDIR=/run/agent-box-%i + Environment=AGENT_BOX_SESSIONS_FILE=/home/%i/.config/agent-box/sessions.json + EnvironmentFile=-/etc/agent-box/units/agent-web-terminal-%i.env + EnvironmentFile=-/etc/agent-box/units/agent-web-terminal-%i.local.env + ExecStart=ttyd --writable --url-arg -p ''${AGENT_BOX_TTYD_PORT} -i 127.0.0.1 -b /%i -t disableLeaveAlert=true -t titleFixed=%i@''${AGENT_BOX_WEB_DOMAIN} agent-box-attach ''} $out/etc/systemd/system/agent-web-terminal@.service install -m444 ${pkgs.writeText "agent-box-settings@.service" '' - @@include:src/units/agent-box-settings@.service@@ + [Unit] + Description=Per-user secrets settings page for %i + After=network-online.target agent-box-settings@%i.socket + Requires=agent-box-settings@%i.socket + Wants=network-online.target + + [Service] + User=%i + Restart=always + RestartSec=5s + Environment=TMUX_TMPDIR=/run/agent-box-%i + Environment=AGENT_BOX_SETTINGS_USER=%i + Environment=AGENT_BOX_SETTINGS_ENV_FILE=/home/%i/.config/agent-box/env + Environment=AGENT_BOX_SETTINGS_BASE=/%i/settings + Environment=AGENT_BOX_TMUX_SOCKET=agent-box + Environment=AGENT_BOX_TMUX_TMPDIR=/run/agent-box-%i + Environment=AGENT_BOX_SESSIONS_FILE=/home/%i/.config/agent-box/sessions.json + EnvironmentFile=-/etc/agent-box/units/agent-box-settings-%i.env + EnvironmentFile=-/etc/agent-box/units/agent-box-settings-%i.local.env + ExecStart=agent-box-settings + ProtectSystem=strict + ReadWritePaths=/home/%i /run/agent-box-%i /var/lib/agent-box-web + ProtectHome=false + PrivateDevices=true + ProtectKernelTunables=true + ProtectKernelModules=true + ProtectControlGroups=true + RestrictSUIDSGID=true + RestrictRealtime=true + LockPersonality=true + NoNewPrivileges=false ''} $out/etc/systemd/system/agent-box-settings@.service install -m444 ${pkgs.writeText "agent-box-settings@.socket" '' - @@include:src/units/agent-box-settings@.socket@@ + [Unit] + Description=Settings page socket for %i + + [Socket] + ListenStream=/run/agent-box-settings/%i.sock + SocketUser=%i + SocketGroup=caddy + SocketMode=0660 ''} $out/etc/systemd/system/agent-box-settings@.socket install -m444 ${pkgs.writeText "agent-box-webhook@.service" '' - @@include:src/units/agent-box-webhook@.service@@ + [Unit] + Description=Webhook receiver daemon (local-webhook) for %i + After=network-online.target agent-box-webhook@%i.socket + Requires=agent-box-webhook@%i.socket + Wants=network-online.target + + [Service] + User=%i + Restart=always + RestartSec=5s + Environment=LOCAL_WEBHOOK_RECEIVER_ONLY=1 + Environment=LOCAL_WEBHOOK_STATE_DIR=/home/%i/.local/state/local-webhook + Environment=LOCAL_WEBHOOK_PORT=0 + EnvironmentFile=-/etc/agent-box/units/agent-box-webhook-%i.env + EnvironmentFile=-/etc/agent-box/units/agent-box-webhook-%i.local.env + ExecStartPre=agent-box-webhook-policy-apply + ExecStart=agent-box-webhook-receiver + StandardInput=null + ProtectSystem=strict + ReadWritePaths=/home/%i + ProtectHome=false + PrivateDevices=true + ProtectKernelTunables=true + ProtectKernelModules=true + ProtectControlGroups=true + RestrictSUIDSGID=true + RestrictRealtime=true + LockPersonality=true + NoNewPrivileges=true ''} $out/etc/systemd/system/agent-box-webhook@.service install -m444 ${pkgs.writeText "agent-box-webhook@.socket" '' - @@include:src/units/agent-box-webhook@.socket@@ + [Unit] + Description=Webhook ingress socket for %i + + [Socket] + ListenStream=/run/agent-box-webhook/%i.sock + SocketUser=%i + SocketGroup=caddy + SocketMode=0660 ''} $out/etc/systemd/system/agent-box-webhook@.socket ''; tmuxSocketName = "agent-box"; From 97b40eedb81a012b115eebb1784041491f5ff739 Mon Sep 17 00:00:00 2001 From: Lio Lunesu Date: Wed, 19 Aug 2026 16:58:18 +0000 Subject: [PATCH 05/18] fix(module): give the per-instance agent-box@ drop-in the real PATH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third bug in this sequence, and the one that actually explains "Unable to locate executable 'agent-box-supervisor'" from CI. Per systemd.exec(5): ExecSearchPath= overrides $PATH IF $PATH is not supplied by the user through Environment=, EnvironmentFile= or PassEnvironment=. NixOS's own systemd module renders a default Environment=PATH= for ANY declared systemd.services. submodule instance, at plain (non-mkDefault) priority — including our near-empty per-user "agent-box@" override (needed only to carry u.environment/u.environmentFiles when present). That instance-level drop-in loads after the template-level "agent-box@.service.d/overrides.conf" that carries the real ExecSearchPath, so its short default $PATH wins — which, per the man page above, doesn't just lose a PATH merge, it disables ExecSearchPath entirely. Every configured user hit this; it just took ~14 minutes of Restart=always/RestartSec=2s failures (~420 restarts) to notice, since each of those minimal test configs never explicitly sets u.environment. Fix: give the per-instance drop-in the same real PATH via `environment.PATH = lib.mkForce agentBoxExecSearchPath`, merged under u.environment so a host can still override it. mkForce is required, not optional — NixOS's own default is plain priority, and two same-priority definitions of one option is a hard eval error, not a silent "last one wins" (confirmed by hitting exactly that error before adding mkForce). Factored the search-path list into one `agentBoxExecSearchPath` local so the host-level ExecSearchPath and this repeated PATH can never drift. Verified directly: rebuilt the exact tests/memory-protection.nix NixOS config (services.agent-box.users.agent = {};) and read agent-box@agent.service.d/overrides.conf — Environment="PATH=..." now starts with /home/%i/.nix-profile/bin and includes the supervisor's own store path, matching ExecSearchPath exactly. --- modules/agent-box.nix | 51 ++++++++++++++++--- modules/agent-box.nix.in | 51 ++++++++++++++++--- tests/golden/vm/units/agent-box@agent.service | 2 +- .../golden/web/units/agent-box@agent.service | 2 +- .../golden/web/units/agent-box@robot.service | 2 +- 5 files changed, 89 insertions(+), 19 deletions(-) diff --git a/modules/agent-box.nix b/modules/agent-box.nix index 00ae7520..48dca9e9 100644 --- a/modules/agent-box.nix +++ b/modules/agent-box.nix @@ -381,6 +381,26 @@ let SocketMode=0660 ''} $out/etc/systemd/system/agent-box-webhook@.socket ''; + # Shared by the "agent-box@" host-level ExecSearchPath (below) and the + # per-user "agent-box@" instance drop-in's environment.PATH — they + # MUST be the same value. Per systemd.exec(5): "ExecSearchPath= overrides + # $PATH IF $PATH is not supplied by the user through Environment=, + # EnvironmentFile= or PassEnvironment=" — an explicit $PATH ANYWHERE + # disables ExecSearchPath outright. NixOS renders an implicit default + # Environment=PATH= for + # ANY declared systemd.services. submodule instance — even our + # near-empty per-instance "agent-box@" override — and that + # instance-level drop-in loads after the template-level one, so its + # short default $PATH wins and silently disables ExecSearchPath (caught + # by PR #295 CI: "Unable to locate executable 'agent-box-supervisor'"). + # Repeating the real PATH there, so it's the LAST word for that key too, + # is the fix — not a nice-to-have. + agentBoxExecSearchPath = lib.makeBinPath ( + [ "/home/%i/.nix-profile" config.nix.package supervisorScript ] + ++ agentRuntimePackages + ++ [ pkgs.bashInteractive pkgs.coreutils pkgs.git pkgs.gh pkgs.tmux ] + ++ lib.optional (effectiveSudoAllowlist != [ ]) "/run/wrappers" + ); tmuxSocketName = "agent-box"; runtimeDirectory = name: "agent-box-${name}"; # ttyd port base; ports are assigned in sorted user-name order (see @@ -3938,12 +3958,14 @@ in # agent-box-supervisor`/`ExecStop=tmux …` — systemd's OWN exec # lookup, independent of the process's runtime $PATH above (which # matters for the supervisor's own internal tmux/jq/etc calls). - ExecSearchPath = lib.makeBinPath ( - [ "/home/%i/.nix-profile" config.nix.package supervisorScript ] - ++ agentRuntimePackages - ++ [ pkgs.bashInteractive pkgs.coreutils pkgs.git pkgs.gh pkgs.tmux ] - ++ lib.optional (effectiveSudoAllowlist != [ ]) "/run/wrappers" - ); + # NOTE: per systemd.exec(5), "ExecSearchPath= overrides $PATH IF + # $PATH is not supplied by the user through Environment=, + # EnvironmentFile= or PassEnvironment=" — i.e. an explicit $PATH + # anywhere DISABLES ExecSearchPath outright, it does not just lose + # a merge. agentBoxExecSearchPath (below) has to also be repeated + # as environment.PATH on the per-instance drop-in for exactly this + # reason — see the note there. + ExecSearchPath = agentBoxExecSearchPath; # NOTE: the settings page's user-owned ~/.config/agent-box/env is # deliberately NOT listed here (nor in the per-user env file). # See the original issue 89 rationale: unit env is a start-time @@ -3989,8 +4011,21 @@ in # below for why a per-instance `wantedBy` on an asDropin unit doesn't # actually start anything. // (lib.mapAttrs' (name: u: lib.nameValuePair "agent-box@${name}" ( - { overrideStrategy = "asDropin"; } - // lib.optionalAttrs (u.environment != { }) { environment = u.environment; } + { overrideStrategy = "asDropin"; + # environment.PATH MUST be set here even when u.environment is {} + # — NixOS's OWN systemd module sets a default Environment=PATH= + # for ANY + # declared systemd.services. instance at PLAIN priority (see + # agentBoxExecSearchPath's comment) — not mkDefault, so a same- + # priority definition here conflicts outright without mkForce. + # Left unfixed, that short default renders in THIS (last-applied) + # drop-in and silently disables the host-level ExecSearchPath. + # u.environment is merged on top so a host can still override PATH + # itself if it really wants to (mkForce only beats the module's + # own default, not an explicit host definition — lib.mkMerge would + # be needed if that ever needs to be adjustable per-priority). + environment = { PATH = lib.mkForce agentBoxExecSearchPath; } // u.environment; + } // lib.optionalAttrs (u.environmentFiles != [ ]) { serviceConfig.EnvironmentFile = u.environmentFiles; } diff --git a/modules/agent-box.nix.in b/modules/agent-box.nix.in index ab2c8fdb..96468a36 100644 --- a/modules/agent-box.nix.in +++ b/modules/agent-box.nix.in @@ -84,6 +84,26 @@ let @@include:src/units/agent-box-webhook@.socket@@ ''} $out/etc/systemd/system/agent-box-webhook@.socket ''; + # Shared by the "agent-box@" host-level ExecSearchPath (below) and the + # per-user "agent-box@" instance drop-in's environment.PATH — they + # MUST be the same value. Per systemd.exec(5): "ExecSearchPath= overrides + # $PATH IF $PATH is not supplied by the user through Environment=, + # EnvironmentFile= or PassEnvironment=" — an explicit $PATH ANYWHERE + # disables ExecSearchPath outright. NixOS renders an implicit default + # Environment=PATH= for + # ANY declared systemd.services. submodule instance — even our + # near-empty per-instance "agent-box@" override — and that + # instance-level drop-in loads after the template-level one, so its + # short default $PATH wins and silently disables ExecSearchPath (caught + # by PR #295 CI: "Unable to locate executable 'agent-box-supervisor'"). + # Repeating the real PATH there, so it's the LAST word for that key too, + # is the fix — not a nice-to-have. + agentBoxExecSearchPath = lib.makeBinPath ( + [ "/home/%i/.nix-profile" config.nix.package supervisorScript ] + ++ agentRuntimePackages + ++ [ pkgs.bashInteractive pkgs.coreutils pkgs.git pkgs.gh pkgs.tmux ] + ++ lib.optional (effectiveSudoAllowlist != [ ]) "/run/wrappers" + ); tmuxSocketName = "agent-box"; runtimeDirectory = name: "agent-box-${name}"; # ttyd port base; ports are assigned in sorted user-name order (see @@ -1576,12 +1596,14 @@ in # agent-box-supervisor`/`ExecStop=tmux …` — systemd's OWN exec # lookup, independent of the process's runtime $PATH above (which # matters for the supervisor's own internal tmux/jq/etc calls). - ExecSearchPath = lib.makeBinPath ( - [ "/home/%i/.nix-profile" config.nix.package supervisorScript ] - ++ agentRuntimePackages - ++ [ pkgs.bashInteractive pkgs.coreutils pkgs.git pkgs.gh pkgs.tmux ] - ++ lib.optional (effectiveSudoAllowlist != [ ]) "/run/wrappers" - ); + # NOTE: per systemd.exec(5), "ExecSearchPath= overrides $PATH IF + # $PATH is not supplied by the user through Environment=, + # EnvironmentFile= or PassEnvironment=" — i.e. an explicit $PATH + # anywhere DISABLES ExecSearchPath outright, it does not just lose + # a merge. agentBoxExecSearchPath (below) has to also be repeated + # as environment.PATH on the per-instance drop-in for exactly this + # reason — see the note there. + ExecSearchPath = agentBoxExecSearchPath; # NOTE: the settings page's user-owned ~/.config/agent-box/env is # deliberately NOT listed here (nor in the per-user env file). # See the original issue 89 rationale: unit env is a start-time @@ -1627,8 +1649,21 @@ in # below for why a per-instance `wantedBy` on an asDropin unit doesn't # actually start anything. // (lib.mapAttrs' (name: u: lib.nameValuePair "agent-box@${name}" ( - { overrideStrategy = "asDropin"; } - // lib.optionalAttrs (u.environment != { }) { environment = u.environment; } + { overrideStrategy = "asDropin"; + # environment.PATH MUST be set here even when u.environment is {} + # — NixOS's OWN systemd module sets a default Environment=PATH= + # for ANY + # declared systemd.services. instance at PLAIN priority (see + # agentBoxExecSearchPath's comment) — not mkDefault, so a same- + # priority definition here conflicts outright without mkForce. + # Left unfixed, that short default renders in THIS (last-applied) + # drop-in and silently disables the host-level ExecSearchPath. + # u.environment is merged on top so a host can still override PATH + # itself if it really wants to (mkForce only beats the module's + # own default, not an explicit host definition — lib.mkMerge would + # be needed if that ever needs to be adjustable per-priority). + environment = { PATH = lib.mkForce agentBoxExecSearchPath; } // u.environment; + } // lib.optionalAttrs (u.environmentFiles != [ ]) { serviceConfig.EnvironmentFile = u.environmentFiles; } diff --git a/tests/golden/vm/units/agent-box@agent.service b/tests/golden/vm/units/agent-box@agent.service index 3b093774..afb379c9 100644 --- a/tests/golden/vm/units/agent-box@agent.service +++ b/tests/golden/vm/units/agent-box@agent.service @@ -2,5 +2,5 @@ [Service] Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" +Environment="PATH=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin" Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" diff --git a/tests/golden/web/units/agent-box@agent.service b/tests/golden/web/units/agent-box@agent.service index 3b093774..66d695b6 100644 --- a/tests/golden/web/units/agent-box@agent.service +++ b/tests/golden/web/units/agent-box@agent.service @@ -2,5 +2,5 @@ [Service] Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" +Environment="PATH=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin" Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" diff --git a/tests/golden/web/units/agent-box@robot.service b/tests/golden/web/units/agent-box@robot.service index 3b093774..66d695b6 100644 --- a/tests/golden/web/units/agent-box@robot.service +++ b/tests/golden/web/units/agent-box@robot.service @@ -2,5 +2,5 @@ [Service] Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" +Environment="PATH=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin" Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" From 22ba2edd9d958c4fb49c17ecd36d398f5d2c0957 Mon Sep 17 00:00:00 2001 From: Lio Lunesu Date: Wed, 19 Aug 2026 17:34:33 +0000 Subject: [PATCH 06/18] fix(module): override ExecStart/ExecStop with absolute paths, not search-path resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PATH mkForce fix (previous commit) was independently verified correct — both by eval and by reading the actual rendered agent-box@agent.service.d file, and by re-checking the exact merge ref CI would build — yet the next CI run failed identically: "Unable to locate executable 'agent-box-supervisor'". Whatever the precise reason ExecSearchPath isn't taking effect for a %i template instance's bare ExecStart in practice (despite matching the documented $PATH/ExecSearchPath precedence rule), chasing it further isn't worth it: this was always the more speculative of the two decisions the design flagged for objection, and there's a strictly more robust alternative that has zero dependency on any search-path resolution at all. Override ExecStart/ExecStop on the host-level "agent-box@" template drop-in with an absolute path, using systemd's standard reset-then-reassign idiom (an empty-string list element clears the verbatim unit's own bare ExecStart=/ExecStop=; the real path replaces it — systemd-lib.nix renders a list value as one directive line per element, exactly matching this convention). ExecSearchPath and the per-instance PATH mkForce both stay: they're still what the supervisor's own internal tmux/jq/grep/etc calls rely on at runtime, just no longer what systemd itself uses to resolve ExecStart/ExecStop. Verified directly: rebuilt the same aarch64 reproduction and read agent-box@.service.d/overrides.conf — ExecStart= ExecStart=/nix/store/.../agent-box-supervisor/bin/agent-box-supervisor ExecStop= ExecStop=/nix/store/.../tmux/bin/tmux -L agent-box kill-server --- modules/agent-box.nix | 17 +++++++++++++++++ modules/agent-box.nix.in | 17 +++++++++++++++++ tests/golden/vm/units/agent-box@.service | 4 ++++ tests/golden/web/units/agent-box@.service | 4 ++++ 4 files changed, 42 insertions(+) diff --git a/modules/agent-box.nix b/modules/agent-box.nix index 48dca9e9..fcf4f2f9 100644 --- a/modules/agent-box.nix +++ b/modules/agent-box.nix @@ -3966,6 +3966,23 @@ in # as environment.PATH on the per-instance drop-in for exactly this # reason — see the note there. ExecSearchPath = agentBoxExecSearchPath; + # BELT AND SUSPENDERS, not redundant: PR #295 CI kept failing with + # "Unable to locate executable 'agent-box-supervisor'" even after + # ExecSearchPath and every Environment=PATH= in scope were verified + # (both by eval and by reading the actually-built unit file) to + # correctly include the supervisor's store path — something about + # bare-name resolution for a %i template instance's ExecStart isn't + # behaving per the documented ExecSearchPath/$PATH precedence rule + # in practice, and it wasn't worth chasing further against a + # mechanism the design itself flagged as the more speculative of + # the two decisions. A list value renders as one line per element + # (systemd-lib.nix's generic attrsToSection), so this is the + # standard reset-then-reassign idiom — "" clears the verbatim + # unit's own bare `ExecStart=agent-box-supervisor`/ + # `ExecStop=tmux …` entirely, then the real absolute path replaces + # it, with zero dependency on any search-path resolution at all. + ExecStart = [ "" "${supervisorScript}/bin/agent-box-supervisor" ]; + ExecStop = [ "" "${pkgs.tmux}/bin/tmux -L ${tmuxSocketName} kill-server" ]; # NOTE: the settings page's user-owned ~/.config/agent-box/env is # deliberately NOT listed here (nor in the per-user env file). # See the original issue 89 rationale: unit env is a start-time diff --git a/modules/agent-box.nix.in b/modules/agent-box.nix.in index 96468a36..2ff033fb 100644 --- a/modules/agent-box.nix.in +++ b/modules/agent-box.nix.in @@ -1604,6 +1604,23 @@ in # as environment.PATH on the per-instance drop-in for exactly this # reason — see the note there. ExecSearchPath = agentBoxExecSearchPath; + # BELT AND SUSPENDERS, not redundant: PR #295 CI kept failing with + # "Unable to locate executable 'agent-box-supervisor'" even after + # ExecSearchPath and every Environment=PATH= in scope were verified + # (both by eval and by reading the actually-built unit file) to + # correctly include the supervisor's store path — something about + # bare-name resolution for a %i template instance's ExecStart isn't + # behaving per the documented ExecSearchPath/$PATH precedence rule + # in practice, and it wasn't worth chasing further against a + # mechanism the design itself flagged as the more speculative of + # the two decisions. A list value renders as one line per element + # (systemd-lib.nix's generic attrsToSection), so this is the + # standard reset-then-reassign idiom — "" clears the verbatim + # unit's own bare `ExecStart=agent-box-supervisor`/ + # `ExecStop=tmux …` entirely, then the real absolute path replaces + # it, with zero dependency on any search-path resolution at all. + ExecStart = [ "" "${supervisorScript}/bin/agent-box-supervisor" ]; + ExecStop = [ "" "${pkgs.tmux}/bin/tmux -L ${tmuxSocketName} kill-server" ]; # NOTE: the settings page's user-owned ~/.config/agent-box/env is # deliberately NOT listed here (nor in the per-user env file). # See the original issue 89 rationale: unit env is a start-time diff --git a/tests/golden/vm/units/agent-box@.service b/tests/golden/vm/units/agent-box@.service index e6089d45..dee52b57 100644 --- a/tests/golden/vm/units/agent-box@.service +++ b/tests/golden/vm/units/agent-box@.service @@ -14,5 +14,9 @@ Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-lo Environment="PATH=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/home/%i/.nix-profile/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/sbin:/run/wrappers/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" ExecSearchPath=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin +ExecStart= +ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin/agent-box-supervisor +ExecStop= +ExecStop=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin/tmux -L agent-box kill-server NoNewPrivileges=false OOMScoreAdjust=500 diff --git a/tests/golden/web/units/agent-box@.service b/tests/golden/web/units/agent-box@.service index e26f184f..349515b2 100644 --- a/tests/golden/web/units/agent-box@.service +++ b/tests/golden/web/units/agent-box@.service @@ -16,6 +16,10 @@ Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-lo Environment="PATH=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/home/%i/.nix-profile/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/sbin:/run/wrappers/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" ExecSearchPath=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin +ExecStart= +ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin/agent-box-supervisor +ExecStop= +ExecStop=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin/tmux -L agent-box kill-server NoNewPrivileges=false OOMScoreAdjust=500 ReadWritePaths=/var/lib/agent-box-sites/%i From a91d43e69a97751b4272d1adbd21a25dd078ac8a Mon Sep 17 00:00:00 2001 From: Lio Lunesu Date: Wed, 19 Aug 2026 18:03:29 +0000 Subject: [PATCH 07/18] test(memory-protection): TEMP diagnostic - print the real merged unit systemd loaded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not for merge. Every Nix-level check (eval option value, actual built drop-in file content, re-checked against the exact PR merge ref) says agent-box@agent.service's ExecStart is the correct absolute path, but the real VM boot keeps reporting "Unable to locate executable 'agent-box-supervisor'" — the bare-name error text, not what an absolute path failure would say. Printing systemctl cat/show output right before the failing wait_for_unit to see what systemd actually thinks it loaded, since theorizing further against a config that verifies correct on every static check isn't converging. --- tests/memory-protection.nix | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/memory-protection.nix b/tests/memory-protection.nix index 3adefef0..ba79bcf9 100644 --- a/tests/memory-protection.nix +++ b/tests/memory-protection.nix @@ -23,6 +23,13 @@ testScript = '' machine.wait_for_unit("multi-user.target") + # TEMPORARY DIAGNOSTIC (agent-box#154 Phase 3 CI investigation) — remove + # before merge. Printing the fully-merged unit systemd actually loaded, + # since eval/local-build both show the correct ExecStart but the real + # boot keeps reporting "Unable to locate executable 'agent-box-supervisor'". + machine.sleep(5) + print(machine.succeed("systemctl cat agent-box@agent.service 2>&1 || true")) + print(machine.succeed("systemctl show agent-box@agent.service --no-pager -p ExecStart -p ExecStop -p Environment -p LoadState -p LoadError 2>&1 || true")) machine.wait_for_unit("agent-box@agent.service") machine.wait_for_unit("earlyoom.service") From 23be9e3f66bc215f98e51b90788f092bd2115b53 Mon Sep 17 00:00:00 2001 From: Lio Lunesu Date: Wed, 19 Aug 2026 18:33:15 +0000 Subject: [PATCH 08/18] test(memory-protection): TEMP diagnostic - check filesystem + journal for the template drop-in systemctl cat proved agent-box@.service.d/ never gets merged into agent-box@agent.service, contradicting systemd.unit(5)'s documented lookup order (instance .d/ then template .d/). Checking the filesystem directly (does the directory/file even exist where expected?) and the journal (any drop-in load warning we might have missed). --- tests/memory-protection.nix | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/memory-protection.nix b/tests/memory-protection.nix index ba79bcf9..3775f28a 100644 --- a/tests/memory-protection.nix +++ b/tests/memory-protection.nix @@ -28,6 +28,18 @@ # since eval/local-build both show the correct ExecStart but the real # boot keeps reporting "Unable to locate executable 'agent-box-supervisor'". machine.sleep(5) + # systemctl cat proved the template-level "agent-box@.service.d/" drop-in + # never gets merged (only the base unit + the per-instance drop-in show + # up), which contradicts systemd.unit(5)'s documented "instance .d/ THEN + # template .d/" lookup — check the filesystem directly to see whether + # the directory is actually missing (a render/link bug on our side) or + # present-but-ignored (a systemd-side surprise), and look for any + # drop-in load warning in the journal we might have missed. + print(machine.succeed("ls -la /etc/systemd/system/ | grep 'agent-box@' || echo NO_AGENT_BOX_AT_ENTRIES")) + print(machine.succeed("ls -la /etc/systemd/system/agent-box@.service.d/ 2>&1 || echo TEMPLATE_DROPIN_DIR_MISSING")) + print(machine.succeed("cat /etc/systemd/system/agent-box@.service.d/overrides.conf 2>&1 || echo TEMPLATE_DROPIN_FILE_MISSING")) + print(machine.succeed("readlink -f /etc/systemd/system/agent-box@.service.d/overrides.conf 2>&1 || echo NO_REAL_TARGET")) + print(machine.succeed("journalctl -b --no-pager 2>&1 | grep -i 'agent-box@' || echo NO_JOURNAL_MENTIONS")) print(machine.succeed("systemctl cat agent-box@agent.service 2>&1 || true")) print(machine.succeed("systemctl show agent-box@agent.service --no-pager -p ExecStart -p ExecStop -p Environment -p LoadState -p LoadError 2>&1 || true")) machine.wait_for_unit("agent-box@agent.service") From 1c8c6000f2a2594c765cc650558341326640559b Mon Sep 17 00:00:00 2001 From: Lio Lunesu Date: Wed, 19 Aug 2026 19:01:32 +0000 Subject: [PATCH 09/18] test(memory-protection): TEMP diagnostic - test the stale-loaded-unit theory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The on-disk drop-in is provably correct (prior diagnostic: cat shows the right ExecStart), yet every restart from boot #0 onward resolves the bare name. Restart=always reuses the unit config as loaded, it doesn't re-read files per restart — testing whether a daemon-reload + fresh restart actually picks up the correct ExecStart, which would mean the unit got loaded before this drop-in was indexed and never re-merged since. --- tests/memory-protection.nix | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/memory-protection.nix b/tests/memory-protection.nix index 3775f28a..6cc771df 100644 --- a/tests/memory-protection.nix +++ b/tests/memory-protection.nix @@ -41,6 +41,17 @@ print(machine.succeed("readlink -f /etc/systemd/system/agent-box@.service.d/overrides.conf 2>&1 || echo NO_REAL_TARGET")) print(machine.succeed("journalctl -b --no-pager 2>&1 | grep -i 'agent-box@' || echo NO_JOURNAL_MENTIONS")) print(machine.succeed("systemctl cat agent-box@agent.service 2>&1 || true")) + # The on-disk drop-in is provably correct (confirmed above) yet the unit + # keeps resolving the bare name from the very first restart at boot — + # testing whether systemd loaded the unit BEFORE indexing this drop-in + # and never re-merged it since (Restart=always reuses the SAME loaded + # config, it doesn't re-read files per restart). + print(machine.succeed("systemctl daemon-reload 2>&1 || true")) + print(machine.succeed("systemctl show agent-box@agent.service --no-pager -p ExecStart 2>&1 || true")) + machine.succeed("systemctl reset-failed agent-box@agent.service || true") + machine.succeed("systemctl restart agent-box@agent.service || true") + machine.sleep(2) + print(machine.succeed("systemctl show agent-box@agent.service --no-pager -p ExecStart -p ActiveState -p SubState 2>&1 || true")) print(machine.succeed("systemctl show agent-box@agent.service --no-pager -p ExecStart -p ExecStop -p Environment -p LoadState -p LoadError 2>&1 || true")) machine.wait_for_unit("agent-box@agent.service") machine.wait_for_unit("earlyoom.service") From 0f1554e4dcdbbbdddf5ec5ee46db11211bb90b6b Mon Sep 17 00:00:00 2001 From: Lio Lunesu Date: Wed, 19 Aug 2026 19:40:37 +0000 Subject: [PATCH 10/18] fix(module): move all host-level unit content off template-level drop-ins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The real, final root cause of "Unable to locate executable 'agent-box-supervisor'" (and, it turns out, the reason none of agent-web-terminal@/agent-box-settings@/agent-box-webhook@'s own host-level config — PATH, ExecSearchPath, ExecStart — was ever reaching a real instance either): a TEMPLATE-level drop-in ("agent-box@.service.d/", "agent-web-terminal@.service.d/", etc, using overrideStrategy = "asDropinIfExists") never gets merged into an INSTANTIATED unit on a real systemd boot, contradicting systemd.unit(5)'s documented lookup order ("For instantiated units, this logic will first look for the instance '.d/' subdirectory... followed by the template '.d/' subdirectory"). Confirmed exhaustively on PR #295 (see its comments for the full investigation): the on-disk file is correct (read directly), `systemctl cat`/`show` never include it even immediately after `systemctl daemon-reload`, and this isn't a staleness/timing issue — it simply never merges, on this systemd version, for this template-unit shape. The per-INSTANCE drop-in ("agent-box@agent.service.d/", matched by the exact instantiated name) is the one thing proven reliable through every check. Fix: every unit family's host-level content (env vars, ExecSearchPath, the absolute-path ExecStart/ExecStop overrides) moved from a single shared template-level declaration onto a real per-instance drop-in, generated once per configured user (agent-box@) or per terminal user (agent-web-terminal@, agent-box-settings@, agent-box-webhook@). This costs a duplicated copy of that content per instance instead of one shared drop-in — an acceptable price for something that actually works, confirmed by rebuilding a full-featured (web+webhook enabled, two users) config and reading every rendered agent-box@.service.d/, agent-web-terminal@.service.d/, agent-box-settings@.service.d/ and agent-box-webhook@.service.d/ overrides.conf directly: all four now carry the correct absolute-path ExecStart (and ExecStartPre for the webhook receiver) and the correct forced PATH. Sockets are unaffected: agent-box-settings@.socket/agent-box-webhook@.socket are fully %i-derived in their verbatim text already, needing no host-level override at all — only enablement, which is a systemd.targets.*.wants entry (a genuinely different, already-working mechanism, not a per-instance/template drop-in). Also: dropped the stale "agent-box@" (bare template name) entry from flake.nix's multi-user check — that key never existed in systemd.services once the template-level declaration was removed; removed the now-served-its-purpose temporary diagnostic from tests/memory-protection.nix. --- flake.nix | 13 +- modules/agent-box.nix | 412 +++++++++--------- modules/agent-box.nix.in | 412 +++++++++--------- tests/golden/vm/units/agent-box@.service | 22 - tests/golden/vm/units/agent-box@agent.service | 16 + ...rvice => agent-box-settings@agent.service} | 5 +- .../units/agent-box-settings@robot.service | 19 + .../web/units/agent-box-webhook@.service | 8 - .../web/units/agent-box-webhook@agent.service | 11 + .../web/units/agent-box-webhook@robot.service | 11 + tests/golden/web/units/agent-box@.service | 25 -- .../golden/web/units/agent-box@agent.service | 19 + .../golden/web/units/agent-box@robot.service | 19 + .../web/units/agent-web-terminal@.service | 8 - .../units/agent-web-terminal@agent.service | 9 + .../units/agent-web-terminal@robot.service | 9 + tests/memory-protection.nix | 30 -- 17 files changed, 522 insertions(+), 526 deletions(-) delete mode 100644 tests/golden/vm/units/agent-box@.service rename tests/golden/web/units/{agent-box-settings@.service => agent-box-settings@agent.service} (61%) create mode 100644 tests/golden/web/units/agent-box-settings@robot.service delete mode 100644 tests/golden/web/units/agent-box-webhook@.service create mode 100644 tests/golden/web/units/agent-box-webhook@agent.service create mode 100644 tests/golden/web/units/agent-box-webhook@robot.service delete mode 100644 tests/golden/web/units/agent-box@.service delete mode 100644 tests/golden/web/units/agent-web-terminal@.service create mode 100644 tests/golden/web/units/agent-web-terminal@agent.service create mode 100644 tests/golden/web/units/agent-web-terminal@robot.service diff --git a/flake.nix b/flake.nix index 03402327..03641bb0 100644 --- a/flake.nix +++ b/flake.nix @@ -186,11 +186,14 @@ ]; }; services = multiUser.config.systemd.services; - # issue #154 Phase 3: "agent-box@" is the systemd %i template unit - # itself; each configured user additionally gets its own - # "agent-box@" drop-in (enablement, at minimum) rather than a - # flat "agent-box-" unit. - wanted = [ "agent-box@" "agent-box@alice" "agent-box@bob" "agent-box@coder" "agent-box@ci" ]; + # issue #154 Phase 3: "agent-box@" is the systemd %i template unit, + # shipped verbatim via systemd.packages (not a systemd.services + # Nix declaration — a template-level drop-in there was found to + # silently never merge into any real instance, so all host-level + # content moved onto the per-instance declaration below). Each + # configured user gets its own "agent-box@" drop-in instead + # of a flat "agent-box-" unit. + wanted = [ "agent-box@alice" "agent-box@bob" "agent-box@coder" "agent-box@ci" ]; missing = builtins.filter (n: ! builtins.hasAttr n services) wanted; in { diff --git a/modules/agent-box.nix b/modules/agent-box.nix index fcf4f2f9..5b3bfdc4 100644 --- a/modules/agent-box.nix +++ b/modules/agent-box.nix @@ -3887,164 +3887,147 @@ in # (src/units/agent-box@.service, shipped via systemd.packages above) # already declares User=%i, TMUX_TMPDIR, RuntimeDirectory, # EnvironmentFile=-/etc/agent-box/units/%i.env and the fixed hardening - # knobs. Everything below is HOST-level — the same for every - # instance — so it renders as a drop-in on top of that text (default - # overrideStrategy = "asDropinIfExists"). Per-user values live in the - # generated env file (environment.etc above) instead. - systemd.services = { - "agent-box@" = { - # System services get a minimal PATH; give the agent an explicit - # toolset. The user's nix-profile bin goes first so `nix profile - # add` tools are visible without a rebuild — %i is a literal - # specifier here, expanded by systemd at unit-start, not a Nix - # antiquote. supervisorScript's own bin/ has to be searchable too: - # the unit text's ExecStart names it bare ("agent-box-supervisor"). - # /run/wrappers is added when the agent has any sudo allowlist - # entries, so the setuid `sudo` wrapper resolves in agent tool - # shells. config.nix.package puts the `nix` CLI itself on the agent - # PATH — /run/current-system/sw/bin is NOT on systemd unit PATHs. - path = [ "/home/%i/.nix-profile" config.nix.package supervisorScript ] - ++ agentRuntimePackages - ++ [ pkgs.bashInteractive pkgs.coreutils pkgs.git pkgs.gh pkgs.tmux ] - # local-webhook's .mcp.json runs a bare `python3` (issue #101), which - # agentBaseTools already puts on PATH — no extra runtime needed. - ++ lib.optional (effectiveSudoAllowlist != [ ]) "/run/wrappers"; - # grep/find/flock/hostname are deliberately not on the PATH above - # (see agentBaseTools), so the supervisor's transcript lookups, the - # sessions.json lock (issue #254) and the codex remote-control - # wrapper's UTS re-exec get pinned binaries instead (the - # AGENT_BOX_*_BIN convention) — same store path for every instance, - # hence host-level rather than per-user env-file content. - # NOTE: every optional env var below is merged INTO this one - # `environment` value via nested `//` — chaining separate outer - # `// lib.optionalAttrs cond { environment.X = ...; }` blocks would - # each shallow-replace the whole `environment` attrset in turn and - # silently drop everything set before it. - environment = { - AGENT_BOX_GREP_BIN = "${pkgs.gnugrep}/bin/grep"; - AGENT_BOX_FIND_BIN = "${pkgs.findutils}/bin/find"; - AGENT_BOX_FLOCK_BIN = "${pkgs.util-linux}/bin/flock"; - AGENT_BOX_HOSTNAME_BIN = "${pkgs.unixtools.hostname}/bin/hostname"; - AGENT_BOX_ENV_EXEC = "${envExecWrapper}"; - AGENT_BOX_CODEX_RC = "${codexRemoteControl}"; - # Hook settings for claude spawns — the /clear-rotation record - # (issue #223) the supervisor follows on respawn. - AGENT_BOX_CLAUDE_SETTINGS = "${claudeHookSettings}"; - } - // lib.optionalAttrs (hostLabel != "") { - # Host suffix for auto-derived Remote Control session names. - AGENT_BOX_HOST_LABEL = hostLabel; - } - // lib.optionalAttrs codexFullAccess { - # /etc/codex/config.toml grants codex full access box-wide (issue - # 234); this env var lets a session with skipPermissions = false - # pin the restricted values back. Same for every instance. - AGENT_BOX_CODEX_FULL_ACCESS = "1"; - } - // lib.optionalAttrs webhookEnabled { - # Doubles as the supervisor's "webhook receiver is live" flag - # (per-session LOCAL_WEBHOOK_* env + claude plugin seeding). One - # repo, box-wide — host-level, not per-user. - AGENT_BOX_WEBHOOK_REPO = cfg.webhook.repo; - } - // lib.optionalAttrs (webhookEnabled && cfg.webhook.syncSessionPlugin) { - # Presence = "keep the session's plugin cache off the floor" - # (webhook.syncSessionPlugin, issue #193): one fetched script, - # same store path for every instance. - AGENT_BOX_WEBHOOK_PINNED_SCRIPT = "${localWebhookScript}"; - }; - serviceConfig = { - # ExecSearchPath resolves the unit text's bare `ExecStart= - # agent-box-supervisor`/`ExecStop=tmux …` — systemd's OWN exec - # lookup, independent of the process's runtime $PATH above (which - # matters for the supervisor's own internal tmux/jq/etc calls). - # NOTE: per systemd.exec(5), "ExecSearchPath= overrides $PATH IF - # $PATH is not supplied by the user through Environment=, - # EnvironmentFile= or PassEnvironment=" — i.e. an explicit $PATH - # anywhere DISABLES ExecSearchPath outright, it does not just lose - # a merge. agentBoxExecSearchPath (below) has to also be repeated - # as environment.PATH on the per-instance drop-in for exactly this - # reason — see the note there. - ExecSearchPath = agentBoxExecSearchPath; - # BELT AND SUSPENDERS, not redundant: PR #295 CI kept failing with - # "Unable to locate executable 'agent-box-supervisor'" even after - # ExecSearchPath and every Environment=PATH= in scope were verified - # (both by eval and by reading the actually-built unit file) to - # correctly include the supervisor's store path — something about - # bare-name resolution for a %i template instance's ExecStart isn't - # behaving per the documented ExecSearchPath/$PATH precedence rule - # in practice, and it wasn't worth chasing further against a - # mechanism the design itself flagged as the more speculative of - # the two decisions. A list value renders as one line per element - # (systemd-lib.nix's generic attrsToSection), so this is the - # standard reset-then-reassign idiom — "" clears the verbatim - # unit's own bare `ExecStart=agent-box-supervisor`/ - # `ExecStop=tmux …` entirely, then the real absolute path replaces - # it, with zero dependency on any search-path resolution at all. - ExecStart = [ "" "${supervisorScript}/bin/agent-box-supervisor" ]; - ExecStop = [ "" "${pkgs.tmux}/bin/tmux -L ${tmuxSocketName} kill-server" ]; - # NOTE: the settings page's user-owned ~/.config/agent-box/env is - # deliberately NOT listed here (nor in the per-user env file). - # See the original issue 89 rationale: unit env is a start-time - # snapshot, so browser-added secrets go through the supervisor's - # env-exec wrapper at every session spawn instead. - EnvironmentFile = cfg.environmentFiles; - # ~/sites is a SYMLINK to /var/lib/agent-box-sites/, so a - # write through it resolves outside /home and ProtectSystem=strict - # denies it with EROFS unless the target is ALSO listed — but only - # when web.enable is on, since that is what gates the tmpfiles - # rule that creates the dir; listing it unconditionally fails the - # whole namespace setup with 226/NAMESPACE on a default (web-less) - # box. systemd path lists accumulate across drop-ins onto the base - # unit's own ReadWritePaths=/home/%i, so this only ADDS the extra - # path, never replaces it. - ReadWritePaths = lib.optional cfg.web.enable "/var/lib/agent-box-sites/%i"; - # NoNewPrivileges would break the sudoAllowlist escape hatch (sudo - # is setuid root; NNP blocks the euid transition). Enable it only - # when the effective allowlist is empty — with a non-empty - # allowlist (from cfg.sudoAllowlist or the web-implied caddy - # reload) we've traded some containment for scoped elevation as a - # host choice. Same for every instance, hence host-level. - NoNewPrivileges = effectiveSudoAllowlist == [ ]; - } // lib.optionalAttrs cfg.protectMemory { - # Sacrifice agent work first under memory pressure: the kernel OOM - # killer and earlyoom both weigh oom_score_adj, so a runaway agent - # process dies before sshd/caddy/SSM — and Restart=always brings - # the session back fresh instead of leaving a frozen box. - OOMScoreAdjust = lib.mkDefault 500; - }; - }; - } - # The one per-user drop-in the generated env file can't express: - # arbitrary u.environment/u.environmentFiles (issue #154 Phase 3 - # design's stated exception — an env file can't add its own - # EnvironmentFile= entries or override an already-set var per - # instance). overrideStrategy = "asDropin" is required here (unlike - # "agent-box@" above): nothing on disk is literally named - # "agent-box@alice.service" for the default detection to find, since - # instances are virtual — so "asDropinIfExists" would render this as a - # (broken, ExecStart-less) STANDALONE unit instead of extending the - # template. NOTE: enablement is NOT here — see systemd.targets.multi-user - # below for why a per-instance `wantedBy` on an asDropin unit doesn't - # actually start anything. - // (lib.mapAttrs' (name: u: lib.nameValuePair "agent-box@${name}" ( + # knobs. + # + # Everything below is per-user, NOT because it varies by user (it + # doesn't — it's the same host-level content for every instance) but + # because a TEMPLATE-level drop-in ("agent-box@.service.d/", the + # `overrideStrategy = "asDropinIfExists"` shape an earlier version of + # this used) turned out to be silently unusable: on real CI VM boots, + # systemd never merges "agent-box@.service.d/" into an INSTANTIATED + # unit's config, despite systemd.unit(5) explicitly documenting that + # lookup ("For instantiated units, this logic will first look for the + # instance '.d/' subdirectory... followed by the template '.d/' + # subdirectory"). Confirmed by reading the actually-built unit + # (correct ExecStart on disk, `systemctl cat`/`show` never include it, + # even right after `systemctl daemon-reload`) — see PR #295 for the + # investigation. The per-INSTANCE drop-in (the one below, matched by + # the exact instantiated name) is the one thing proven reliable + # through every check, so all of this moved there instead. It costs a + # copy of this content per configured user instead of one shared + # drop-in — a real but acceptable price for something that actually + # works. + systemd.services = + (lib.mapAttrs' (name: u: lib.nameValuePair "agent-box@${name}" ( { overrideStrategy = "asDropin"; - # environment.PATH MUST be set here even when u.environment is {} - # — NixOS's OWN systemd module sets a default Environment=PATH= - # for ANY - # declared systemd.services. instance at PLAIN priority (see - # agentBoxExecSearchPath's comment) — not mkDefault, so a same- - # priority definition here conflicts outright without mkForce. - # Left unfixed, that short default renders in THIS (last-applied) - # drop-in and silently disables the host-level ExecSearchPath. - # u.environment is merged on top so a host can still override PATH - # itself if it really wants to (mkForce only beats the module's - # own default, not an explicit host definition — lib.mkMerge would - # be needed if that ever needs to be adjustable per-priority). - environment = { PATH = lib.mkForce agentBoxExecSearchPath; } // u.environment; + # NOTE: every optional env var below is merged INTO this one + # `environment` value via nested `//` — chaining separate outer + # `// lib.optionalAttrs cond { environment.X = ...; }` blocks would + # each shallow-replace the whole `environment` attrset in turn and + # silently drop everything set before it. + environment = + ({ + # grep/find/flock/hostname are deliberately not on the agent's + # own PATH (see agentBaseTools), so the supervisor's transcript + # lookups, the sessions.json lock (issue #254) and the codex + # remote-control wrapper's UTS re-exec get pinned binaries + # instead (the AGENT_BOX_*_BIN convention) — same store path + # for every instance, hence host-level rather than per-user + # env-file content. + AGENT_BOX_GREP_BIN = "${pkgs.gnugrep}/bin/grep"; + AGENT_BOX_FIND_BIN = "${pkgs.findutils}/bin/find"; + AGENT_BOX_FLOCK_BIN = "${pkgs.util-linux}/bin/flock"; + AGENT_BOX_HOSTNAME_BIN = "${pkgs.unixtools.hostname}/bin/hostname"; + AGENT_BOX_ENV_EXEC = "${envExecWrapper}"; + AGENT_BOX_CODEX_RC = "${codexRemoteControl}"; + # Hook settings for claude spawns — the /clear-rotation record + # (issue #223) the supervisor follows on respawn. + AGENT_BOX_CLAUDE_SETTINGS = "${claudeHookSettings}"; + } + // lib.optionalAttrs (hostLabel != "") { + # Host suffix for auto-derived Remote Control session names. + AGENT_BOX_HOST_LABEL = hostLabel; + } + // lib.optionalAttrs codexFullAccess { + # /etc/codex/config.toml grants codex full access box-wide (issue + # 234); this env var lets a session with skipPermissions = false + # pin the restricted values back. Same for every instance. + AGENT_BOX_CODEX_FULL_ACCESS = "1"; + } + // lib.optionalAttrs webhookEnabled { + # Doubles as the supervisor's "webhook receiver is live" flag + # (per-session LOCAL_WEBHOOK_* env + claude plugin seeding). One + # repo, box-wide — host-level, not per-user. + AGENT_BOX_WEBHOOK_REPO = cfg.webhook.repo; + } + // lib.optionalAttrs (webhookEnabled && cfg.webhook.syncSessionPlugin) { + # Presence = "keep the session's plugin cache off the floor" + # (webhook.syncSessionPlugin, issue #193): one fetched script, + # same store path for every instance. + AGENT_BOX_WEBHOOK_PINNED_SCRIPT = "${localWebhookScript}"; + } + // u.environment) + # environment.PATH MUST be forced even when u.environment is {} + # — NixOS's OWN systemd module sets a default Environment=PATH= + # for ANY + # declared systemd.services. instance at PLAIN priority — + # not mkDefault, so a same-priority definition here conflicts + # outright without mkForce. Left unfixed, that short default + # disables the ExecSearchPath below (systemd.exec(5): an + # explicit $PATH from Environment= anywhere disables + # ExecSearchPath= entirely, it doesn't just lose a merge). If + # u.environment set its own PATH, that's the value forced + # through instead — a host can still override it deliberately. + // { PATH = lib.mkForce (u.environment.PATH or agentBoxExecSearchPath); }; } - // lib.optionalAttrs (u.environmentFiles != [ ]) { - serviceConfig.EnvironmentFile = u.environmentFiles; + // { + serviceConfig = { + # ExecSearchPath resolves the unit text's bare `ExecStart= + # agent-box-supervisor`/`ExecStop=tmux …` if this drop-in's own + # ExecStart/ExecStop override below is ever removed; kept for + # that robustness even though it's not load-bearing today. + ExecSearchPath = agentBoxExecSearchPath; + # BELT AND SUSPENDERS, not redundant: PR #295 CI kept failing with + # "Unable to locate executable 'agent-box-supervisor'" even after + # ExecSearchPath and every Environment=PATH= in scope were verified + # (both by eval and by reading the actually-built unit file) to + # correctly include the supervisor's store path — something about + # bare-name resolution for a %i template instance's ExecStart isn't + # behaving per the documented ExecSearchPath/$PATH precedence rule + # in practice, and it wasn't worth chasing further against a + # mechanism the design itself flagged as the more speculative of + # the two decisions. A list value renders as one line per element + # (systemd-lib.nix's generic attrsToSection), so this is the + # standard reset-then-reassign idiom — "" clears the verbatim + # unit's own bare `ExecStart=agent-box-supervisor`/ + # `ExecStop=tmux …` entirely, then the real absolute path replaces + # it, with zero dependency on any search-path resolution at all. + ExecStart = [ "" "${supervisorScript}/bin/agent-box-supervisor" ]; + ExecStop = [ "" "${pkgs.tmux}/bin/tmux -L ${tmuxSocketName} kill-server" ]; + # NOTE: the settings page's user-owned ~/.config/agent-box/env is + # deliberately NOT listed here (nor in the per-user env file). + # See the original issue 89 rationale: unit env is a start-time + # snapshot, so browser-added secrets go through the supervisor's + # env-exec wrapper at every session spawn instead. u.environmentFiles + # (issue #154 Phase 3 design's stated exception — an env file can't + # add its own EnvironmentFile= entries or override an already-set + # var per instance) is a real per-instance list, so it's just + # concatenated here rather than needing its own optionalAttrs. + EnvironmentFile = cfg.environmentFiles ++ u.environmentFiles; + # ~/sites is a SYMLINK to /var/lib/agent-box-sites/, so a + # write through it resolves outside /home and ProtectSystem=strict + # denies it with EROFS unless the target is ALSO listed — but only + # when web.enable is on, since that is what gates the tmpfiles + # rule that creates the dir; listing it unconditionally fails the + # whole namespace setup with 226/NAMESPACE on a default (web-less) + # box. systemd path lists accumulate across drop-ins onto the base + # unit's own ReadWritePaths=/home/%i, so this only ADDS the extra + # path, never replaces it. + ReadWritePaths = lib.optional cfg.web.enable "/var/lib/agent-box-sites/%i"; + # NoNewPrivileges would break the sudoAllowlist escape hatch (sudo + # is setuid root; NNP blocks the euid transition). Enable it only + # when the effective allowlist is empty — with a non-empty + # allowlist (from cfg.sudoAllowlist or the web-implied caddy + # reload) we've traded some containment for scoped elevation as a + # host choice. Same for every instance, hence host-level. + NoNewPrivileges = effectiveSudoAllowlist == [ ]; + } // lib.optionalAttrs cfg.protectMemory { + # Sacrifice agent work first under memory pressure: the kernel OOM + # killer and earlyoom both weigh oom_score_adj, so a runaway agent + # process dies before sshd/caddy/SSM — and Restart=always brings + # the session back fresh instead of leaving a frozen box. + OOMScoreAdjust = lib.mkDefault 500; + }; } )) cfg.users); @@ -8668,42 +8651,50 @@ in text = "LOCAL_WEBHOOK_STATE_DIR=${webhookStateDirOf name}\n"; }) terminalUsers)); - # ttyd per terminal user — attaches to that user's tmux session over an - # internal port; --base-path keeps each terminal (and its /ws endpoint) - # under // so one vhost can serve them all. The unit text (see - # src/units/agent-web-terminal@.service, shipped via + # ttyd/settings/webhook per terminal user. The unit text (see + # src/units/agent-web-terminal@.service etc, shipped via # systemd.packages at the top of this module) already declares - # User=%i, TMUX_TMPDIR/AGENT_BOX_SESSIONS_FILE and the ttyd - # ExecStart, expanding AGENT_BOX_TTYD_PORT/AGENT_BOX_WEB_DOMAIN from - # environment at exec time; what's below is HOST-level (the same - # PATH/domain for every instance) so it renders as a drop-in. + # User=%i and the per-instance env vars each ExecStart expands from + # environment at exec time. What's below is HOST-level content (the + # same for every instance) but rendered as a PER-INSTANCE drop-in, + # not a template-level one — see the long comment on "agent-box@"'s + # systemd.services above for why: a template-level + # "agent-web-terminal@.service.d/" drop-in silently never merges + # into any instance on a real boot, confirmed for agent-box@ and + # not worth re-confirming three more times when the fix is the + # same. Every ExecStart/ExecStartPre below is also an absolute-path + # reset-then-reassign override for the same reason "agent-box@"'s + # is: bare-name resolution needs ExecSearchPath to actually be + # *merged*, which per-instance drop-ins are proven to do reliably + # but which isn't worth depending on when an absolute path has zero + # ambiguity either way. systemd.services = { agent-web-auth-secrets = webAuthSecretsService; caddy.serviceConfig.EnvironmentFile = "/run/agent-box-web/env"; - - "agent-web-terminal@" = { - # The attach script (src/attach.sh) resolves tmux and jq from - # this PATH instead of baked store paths (issue #154, Phase 2); - # ttyd itself execs the bare "agent-box-attach" argument - # through this same runtime PATH, not ExecSearchPath (that only - # covers systemd's own resolution of the unit's ExecStart). - path = [ pkgs.tmux pkgs.jq attachScript pkgs.ttyd ]; - environment.AGENT_BOX_WEB_DOMAIN = cfg.web.domain; - serviceConfig.ExecSearchPath = lib.makeBinPath [ pkgs.ttyd ]; + } + // (lib.listToAttrs (map (name: lib.nameValuePair "agent-web-terminal@${name}" { + overrideStrategy = "asDropin"; + # The attach script (src/attach.sh) resolves tmux and jq from + # this PATH instead of baked store paths (issue #154, Phase 2) — + # forced for the same reason as "agent-box@"'s PATH (NixOS's own + # implicit per-instance default would otherwise win). + environment = { + AGENT_BOX_WEB_DOMAIN = cfg.web.domain; + PATH = lib.mkForce (lib.makeBinPath [ pkgs.tmux pkgs.jq attachScript pkgs.ttyd ]); }; - - # Settings daemon (issue #36), one per terminal user. Runs AS the - # agent user (no root, no privilege boundary): it only writes - # that user's own ~/.config/agent-box/env and kills that user's - # own tmux session. The agent unit's Restart=always then reloads - # it with the fresh env. Listens via socket activation (issue - # #49) on the same-named .socket instance below. - "agent-box-settings@" = { - # See the "agent-box@" comment above: every optional block - # merges INTO this one `environment` value via nested `//`, not - # as separate outer `// lib.optionalAttrs {...}` blocks (which - # would each shallow-replace the whole attrset in turn). - environment = { + serviceConfig.ExecStart = [ + "" + ''${pkgs.ttyd}/bin/ttyd --writable --url-arg -p ''${AGENT_BOX_TTYD_PORT} -i 127.0.0.1 -b /%i -t disableLeaveAlert=true -t titleFixed=%i@''${AGENT_BOX_WEB_DOMAIN} agent-box-attach'' + ]; + }) terminalUsers)) + // (lib.listToAttrs (map (name: lib.nameValuePair "agent-box-settings@${name}" { + overrideStrategy = "asDropin"; + # See the "agent-box@" comment above: every optional block + # merges INTO this one `environment` value via nested `//`, not + # as separate outer `// lib.optionalAttrs {...}` blocks (which + # would each shallow-replace the whole attrset in turn). + environment = + ({ AGENT_BOX_TMUX_BIN = "${pkgs.tmux}/bin/tmux"; AGENT_BOX_AGENTS = lib.concatStringsSep "," (sessionKinds cfg.installAgents); AGENT_BOX_DEFAULT_AGENT = cfg.agent; @@ -8738,35 +8729,30 @@ in # here, unlike AGENT_BOX_UPDATE_CMD, which triggers the run. AGENT_BOX_UPDATE_UNIT = "agent-box-update.service"; AGENT_BOX_SYSTEMCTL = "/run/current-system/sw/bin/systemctl"; - }; - serviceConfig.ExecSearchPath = lib.makeBinPath [ settingsDaemon ]; - }; - } - # No per-instance drop-in for agent-web-terminal@/agent-box-settings@: - # neither has any per-user content beyond the %i template itself, so - # there is nothing to override — enablement is - # systemd.targets.multi-user.wants below, not a `wantedBy` here (see - # the note by that option: a `wantedBy` on an "asDropin" instance - # produces a dangling .wants symlink and never actually starts). + }) + // { PATH = lib.mkForce (lib.makeBinPath [ settingsDaemon ]); }; + serviceConfig.ExecStart = [ "" "${settingsDaemon}/bin/agent-box-settings" ]; + }) terminalUsers)) # Webhook receiver daemon (issue #101), one per terminal user, gated on # webhook.enable. Runs webhook.py (via the agent-box-webhook-receiver - # wrapper, resolved through ExecSearchPath) in RECEIVER_ONLY mode as - # the agent user: owns the socket-activated UNIX ingress (the - # same-named .socket instance below) and fans HMAC-verified - # deliveries out to that user's sessions over IPC. - // lib.optionalAttrs webhookEnabled ({ - "agent-box-webhook@" = { - # The spawn wrapper (src/webhook-spawn.sh) runs as this unit's - # child and resolves jq/coreutils/agent-box-session from this - # PATH instead of baked store paths (issue #154, Phase 2). - path = [ pkgs.jq pkgs.coreutils sessionCli ]; - # Standing watches (deliver_to:"subagent", local-channels#1): a - # matching delivery spawns a fresh hook-* session for this - # user. One wrapper, same store path for every instance. - environment.LOCAL_WEBHOOK_SPAWN_CMD = "${webhookSpawn}/bin/agent-box-webhook-spawn"; - serviceConfig.ExecSearchPath = lib.makeBinPath [ webhookPolicyApply webhookReceiverBin ]; + # wrapper) in RECEIVER_ONLY mode as the agent user: owns the + # socket-activated UNIX ingress (the same-named .socket instance + # below) and fans HMAC-verified deliveries out to that user's + # sessions over IPC. + // lib.optionalAttrs webhookEnabled (lib.listToAttrs (map (name: lib.nameValuePair "agent-box-webhook@${name}" { + overrideStrategy = "asDropin"; + # The spawn wrapper (src/webhook-spawn.sh) runs as this unit's + # child and resolves jq/coreutils/agent-box-session from this + # PATH instead of baked store paths (issue #154, Phase 2). + environment = { + LOCAL_WEBHOOK_SPAWN_CMD = "${webhookSpawn}/bin/agent-box-webhook-spawn"; + PATH = lib.mkForce (lib.makeBinPath [ pkgs.jq pkgs.coreutils sessionCli ]); + }; + serviceConfig = { + ExecStartPre = [ "" "${webhookPolicyApply}/bin/agent-box-webhook-policy-apply" ]; + ExecStart = [ "" "${webhookReceiverBin}/bin/agent-box-webhook-receiver" ]; }; - }); + }) terminalUsers)); # No systemd.sockets overrides: the settings daemon's listening # sockets (issue #49) and the webhook ingress sockets (issue #101) # are fully %i-derived already (see src/units/*.socket, shipped via diff --git a/modules/agent-box.nix.in b/modules/agent-box.nix.in index 2ff033fb..bf2139da 100644 --- a/modules/agent-box.nix.in +++ b/modules/agent-box.nix.in @@ -1525,164 +1525,147 @@ in # (src/units/agent-box@.service, shipped via systemd.packages above) # already declares User=%i, TMUX_TMPDIR, RuntimeDirectory, # EnvironmentFile=-/etc/agent-box/units/%i.env and the fixed hardening - # knobs. Everything below is HOST-level — the same for every - # instance — so it renders as a drop-in on top of that text (default - # overrideStrategy = "asDropinIfExists"). Per-user values live in the - # generated env file (environment.etc above) instead. - systemd.services = { - "agent-box@" = { - # System services get a minimal PATH; give the agent an explicit - # toolset. The user's nix-profile bin goes first so `nix profile - # add` tools are visible without a rebuild — %i is a literal - # specifier here, expanded by systemd at unit-start, not a Nix - # antiquote. supervisorScript's own bin/ has to be searchable too: - # the unit text's ExecStart names it bare ("agent-box-supervisor"). - # /run/wrappers is added when the agent has any sudo allowlist - # entries, so the setuid `sudo` wrapper resolves in agent tool - # shells. config.nix.package puts the `nix` CLI itself on the agent - # PATH — /run/current-system/sw/bin is NOT on systemd unit PATHs. - path = [ "/home/%i/.nix-profile" config.nix.package supervisorScript ] - ++ agentRuntimePackages - ++ [ pkgs.bashInteractive pkgs.coreutils pkgs.git pkgs.gh pkgs.tmux ] - # local-webhook's .mcp.json runs a bare `python3` (issue #101), which - # agentBaseTools already puts on PATH — no extra runtime needed. - ++ lib.optional (effectiveSudoAllowlist != [ ]) "/run/wrappers"; - # grep/find/flock/hostname are deliberately not on the PATH above - # (see agentBaseTools), so the supervisor's transcript lookups, the - # sessions.json lock (issue #254) and the codex remote-control - # wrapper's UTS re-exec get pinned binaries instead (the - # AGENT_BOX_*_BIN convention) — same store path for every instance, - # hence host-level rather than per-user env-file content. - # NOTE: every optional env var below is merged INTO this one - # `environment` value via nested `//` — chaining separate outer - # `// lib.optionalAttrs cond { environment.X = ...; }` blocks would - # each shallow-replace the whole `environment` attrset in turn and - # silently drop everything set before it. - environment = { - AGENT_BOX_GREP_BIN = "${pkgs.gnugrep}/bin/grep"; - AGENT_BOX_FIND_BIN = "${pkgs.findutils}/bin/find"; - AGENT_BOX_FLOCK_BIN = "${pkgs.util-linux}/bin/flock"; - AGENT_BOX_HOSTNAME_BIN = "${pkgs.unixtools.hostname}/bin/hostname"; - AGENT_BOX_ENV_EXEC = "${envExecWrapper}"; - AGENT_BOX_CODEX_RC = "${codexRemoteControl}"; - # Hook settings for claude spawns — the /clear-rotation record - # (issue #223) the supervisor follows on respawn. - AGENT_BOX_CLAUDE_SETTINGS = "${claudeHookSettings}"; - } - // lib.optionalAttrs (hostLabel != "") { - # Host suffix for auto-derived Remote Control session names. - AGENT_BOX_HOST_LABEL = hostLabel; - } - // lib.optionalAttrs codexFullAccess { - # /etc/codex/config.toml grants codex full access box-wide (issue - # 234); this env var lets a session with skipPermissions = false - # pin the restricted values back. Same for every instance. - AGENT_BOX_CODEX_FULL_ACCESS = "1"; - } - // lib.optionalAttrs webhookEnabled { - # Doubles as the supervisor's "webhook receiver is live" flag - # (per-session LOCAL_WEBHOOK_* env + claude plugin seeding). One - # repo, box-wide — host-level, not per-user. - AGENT_BOX_WEBHOOK_REPO = cfg.webhook.repo; - } - // lib.optionalAttrs (webhookEnabled && cfg.webhook.syncSessionPlugin) { - # Presence = "keep the session's plugin cache off the floor" - # (webhook.syncSessionPlugin, issue #193): one fetched script, - # same store path for every instance. - AGENT_BOX_WEBHOOK_PINNED_SCRIPT = "${localWebhookScript}"; - }; - serviceConfig = { - # ExecSearchPath resolves the unit text's bare `ExecStart= - # agent-box-supervisor`/`ExecStop=tmux …` — systemd's OWN exec - # lookup, independent of the process's runtime $PATH above (which - # matters for the supervisor's own internal tmux/jq/etc calls). - # NOTE: per systemd.exec(5), "ExecSearchPath= overrides $PATH IF - # $PATH is not supplied by the user through Environment=, - # EnvironmentFile= or PassEnvironment=" — i.e. an explicit $PATH - # anywhere DISABLES ExecSearchPath outright, it does not just lose - # a merge. agentBoxExecSearchPath (below) has to also be repeated - # as environment.PATH on the per-instance drop-in for exactly this - # reason — see the note there. - ExecSearchPath = agentBoxExecSearchPath; - # BELT AND SUSPENDERS, not redundant: PR #295 CI kept failing with - # "Unable to locate executable 'agent-box-supervisor'" even after - # ExecSearchPath and every Environment=PATH= in scope were verified - # (both by eval and by reading the actually-built unit file) to - # correctly include the supervisor's store path — something about - # bare-name resolution for a %i template instance's ExecStart isn't - # behaving per the documented ExecSearchPath/$PATH precedence rule - # in practice, and it wasn't worth chasing further against a - # mechanism the design itself flagged as the more speculative of - # the two decisions. A list value renders as one line per element - # (systemd-lib.nix's generic attrsToSection), so this is the - # standard reset-then-reassign idiom — "" clears the verbatim - # unit's own bare `ExecStart=agent-box-supervisor`/ - # `ExecStop=tmux …` entirely, then the real absolute path replaces - # it, with zero dependency on any search-path resolution at all. - ExecStart = [ "" "${supervisorScript}/bin/agent-box-supervisor" ]; - ExecStop = [ "" "${pkgs.tmux}/bin/tmux -L ${tmuxSocketName} kill-server" ]; - # NOTE: the settings page's user-owned ~/.config/agent-box/env is - # deliberately NOT listed here (nor in the per-user env file). - # See the original issue 89 rationale: unit env is a start-time - # snapshot, so browser-added secrets go through the supervisor's - # env-exec wrapper at every session spawn instead. - EnvironmentFile = cfg.environmentFiles; - # ~/sites is a SYMLINK to /var/lib/agent-box-sites/, so a - # write through it resolves outside /home and ProtectSystem=strict - # denies it with EROFS unless the target is ALSO listed — but only - # when web.enable is on, since that is what gates the tmpfiles - # rule that creates the dir; listing it unconditionally fails the - # whole namespace setup with 226/NAMESPACE on a default (web-less) - # box. systemd path lists accumulate across drop-ins onto the base - # unit's own ReadWritePaths=/home/%i, so this only ADDS the extra - # path, never replaces it. - ReadWritePaths = lib.optional cfg.web.enable "/var/lib/agent-box-sites/%i"; - # NoNewPrivileges would break the sudoAllowlist escape hatch (sudo - # is setuid root; NNP blocks the euid transition). Enable it only - # when the effective allowlist is empty — with a non-empty - # allowlist (from cfg.sudoAllowlist or the web-implied caddy - # reload) we've traded some containment for scoped elevation as a - # host choice. Same for every instance, hence host-level. - NoNewPrivileges = effectiveSudoAllowlist == [ ]; - } // lib.optionalAttrs cfg.protectMemory { - # Sacrifice agent work first under memory pressure: the kernel OOM - # killer and earlyoom both weigh oom_score_adj, so a runaway agent - # process dies before sshd/caddy/SSM — and Restart=always brings - # the session back fresh instead of leaving a frozen box. - OOMScoreAdjust = lib.mkDefault 500; - }; - }; - } - # The one per-user drop-in the generated env file can't express: - # arbitrary u.environment/u.environmentFiles (issue #154 Phase 3 - # design's stated exception — an env file can't add its own - # EnvironmentFile= entries or override an already-set var per - # instance). overrideStrategy = "asDropin" is required here (unlike - # "agent-box@" above): nothing on disk is literally named - # "agent-box@alice.service" for the default detection to find, since - # instances are virtual — so "asDropinIfExists" would render this as a - # (broken, ExecStart-less) STANDALONE unit instead of extending the - # template. NOTE: enablement is NOT here — see systemd.targets.multi-user - # below for why a per-instance `wantedBy` on an asDropin unit doesn't - # actually start anything. - // (lib.mapAttrs' (name: u: lib.nameValuePair "agent-box@${name}" ( + # knobs. + # + # Everything below is per-user, NOT because it varies by user (it + # doesn't — it's the same host-level content for every instance) but + # because a TEMPLATE-level drop-in ("agent-box@.service.d/", the + # `overrideStrategy = "asDropinIfExists"` shape an earlier version of + # this used) turned out to be silently unusable: on real CI VM boots, + # systemd never merges "agent-box@.service.d/" into an INSTANTIATED + # unit's config, despite systemd.unit(5) explicitly documenting that + # lookup ("For instantiated units, this logic will first look for the + # instance '.d/' subdirectory... followed by the template '.d/' + # subdirectory"). Confirmed by reading the actually-built unit + # (correct ExecStart on disk, `systemctl cat`/`show` never include it, + # even right after `systemctl daemon-reload`) — see PR #295 for the + # investigation. The per-INSTANCE drop-in (the one below, matched by + # the exact instantiated name) is the one thing proven reliable + # through every check, so all of this moved there instead. It costs a + # copy of this content per configured user instead of one shared + # drop-in — a real but acceptable price for something that actually + # works. + systemd.services = + (lib.mapAttrs' (name: u: lib.nameValuePair "agent-box@${name}" ( { overrideStrategy = "asDropin"; - # environment.PATH MUST be set here even when u.environment is {} - # — NixOS's OWN systemd module sets a default Environment=PATH= - # for ANY - # declared systemd.services. instance at PLAIN priority (see - # agentBoxExecSearchPath's comment) — not mkDefault, so a same- - # priority definition here conflicts outright without mkForce. - # Left unfixed, that short default renders in THIS (last-applied) - # drop-in and silently disables the host-level ExecSearchPath. - # u.environment is merged on top so a host can still override PATH - # itself if it really wants to (mkForce only beats the module's - # own default, not an explicit host definition — lib.mkMerge would - # be needed if that ever needs to be adjustable per-priority). - environment = { PATH = lib.mkForce agentBoxExecSearchPath; } // u.environment; + # NOTE: every optional env var below is merged INTO this one + # `environment` value via nested `//` — chaining separate outer + # `// lib.optionalAttrs cond { environment.X = ...; }` blocks would + # each shallow-replace the whole `environment` attrset in turn and + # silently drop everything set before it. + environment = + ({ + # grep/find/flock/hostname are deliberately not on the agent's + # own PATH (see agentBaseTools), so the supervisor's transcript + # lookups, the sessions.json lock (issue #254) and the codex + # remote-control wrapper's UTS re-exec get pinned binaries + # instead (the AGENT_BOX_*_BIN convention) — same store path + # for every instance, hence host-level rather than per-user + # env-file content. + AGENT_BOX_GREP_BIN = "${pkgs.gnugrep}/bin/grep"; + AGENT_BOX_FIND_BIN = "${pkgs.findutils}/bin/find"; + AGENT_BOX_FLOCK_BIN = "${pkgs.util-linux}/bin/flock"; + AGENT_BOX_HOSTNAME_BIN = "${pkgs.unixtools.hostname}/bin/hostname"; + AGENT_BOX_ENV_EXEC = "${envExecWrapper}"; + AGENT_BOX_CODEX_RC = "${codexRemoteControl}"; + # Hook settings for claude spawns — the /clear-rotation record + # (issue #223) the supervisor follows on respawn. + AGENT_BOX_CLAUDE_SETTINGS = "${claudeHookSettings}"; + } + // lib.optionalAttrs (hostLabel != "") { + # Host suffix for auto-derived Remote Control session names. + AGENT_BOX_HOST_LABEL = hostLabel; + } + // lib.optionalAttrs codexFullAccess { + # /etc/codex/config.toml grants codex full access box-wide (issue + # 234); this env var lets a session with skipPermissions = false + # pin the restricted values back. Same for every instance. + AGENT_BOX_CODEX_FULL_ACCESS = "1"; + } + // lib.optionalAttrs webhookEnabled { + # Doubles as the supervisor's "webhook receiver is live" flag + # (per-session LOCAL_WEBHOOK_* env + claude plugin seeding). One + # repo, box-wide — host-level, not per-user. + AGENT_BOX_WEBHOOK_REPO = cfg.webhook.repo; + } + // lib.optionalAttrs (webhookEnabled && cfg.webhook.syncSessionPlugin) { + # Presence = "keep the session's plugin cache off the floor" + # (webhook.syncSessionPlugin, issue #193): one fetched script, + # same store path for every instance. + AGENT_BOX_WEBHOOK_PINNED_SCRIPT = "${localWebhookScript}"; + } + // u.environment) + # environment.PATH MUST be forced even when u.environment is {} + # — NixOS's OWN systemd module sets a default Environment=PATH= + # for ANY + # declared systemd.services. instance at PLAIN priority — + # not mkDefault, so a same-priority definition here conflicts + # outright without mkForce. Left unfixed, that short default + # disables the ExecSearchPath below (systemd.exec(5): an + # explicit $PATH from Environment= anywhere disables + # ExecSearchPath= entirely, it doesn't just lose a merge). If + # u.environment set its own PATH, that's the value forced + # through instead — a host can still override it deliberately. + // { PATH = lib.mkForce (u.environment.PATH or agentBoxExecSearchPath); }; } - // lib.optionalAttrs (u.environmentFiles != [ ]) { - serviceConfig.EnvironmentFile = u.environmentFiles; + // { + serviceConfig = { + # ExecSearchPath resolves the unit text's bare `ExecStart= + # agent-box-supervisor`/`ExecStop=tmux …` if this drop-in's own + # ExecStart/ExecStop override below is ever removed; kept for + # that robustness even though it's not load-bearing today. + ExecSearchPath = agentBoxExecSearchPath; + # BELT AND SUSPENDERS, not redundant: PR #295 CI kept failing with + # "Unable to locate executable 'agent-box-supervisor'" even after + # ExecSearchPath and every Environment=PATH= in scope were verified + # (both by eval and by reading the actually-built unit file) to + # correctly include the supervisor's store path — something about + # bare-name resolution for a %i template instance's ExecStart isn't + # behaving per the documented ExecSearchPath/$PATH precedence rule + # in practice, and it wasn't worth chasing further against a + # mechanism the design itself flagged as the more speculative of + # the two decisions. A list value renders as one line per element + # (systemd-lib.nix's generic attrsToSection), so this is the + # standard reset-then-reassign idiom — "" clears the verbatim + # unit's own bare `ExecStart=agent-box-supervisor`/ + # `ExecStop=tmux …` entirely, then the real absolute path replaces + # it, with zero dependency on any search-path resolution at all. + ExecStart = [ "" "${supervisorScript}/bin/agent-box-supervisor" ]; + ExecStop = [ "" "${pkgs.tmux}/bin/tmux -L ${tmuxSocketName} kill-server" ]; + # NOTE: the settings page's user-owned ~/.config/agent-box/env is + # deliberately NOT listed here (nor in the per-user env file). + # See the original issue 89 rationale: unit env is a start-time + # snapshot, so browser-added secrets go through the supervisor's + # env-exec wrapper at every session spawn instead. u.environmentFiles + # (issue #154 Phase 3 design's stated exception — an env file can't + # add its own EnvironmentFile= entries or override an already-set + # var per instance) is a real per-instance list, so it's just + # concatenated here rather than needing its own optionalAttrs. + EnvironmentFile = cfg.environmentFiles ++ u.environmentFiles; + # ~/sites is a SYMLINK to /var/lib/agent-box-sites/, so a + # write through it resolves outside /home and ProtectSystem=strict + # denies it with EROFS unless the target is ALSO listed — but only + # when web.enable is on, since that is what gates the tmpfiles + # rule that creates the dir; listing it unconditionally fails the + # whole namespace setup with 226/NAMESPACE on a default (web-less) + # box. systemd path lists accumulate across drop-ins onto the base + # unit's own ReadWritePaths=/home/%i, so this only ADDS the extra + # path, never replaces it. + ReadWritePaths = lib.optional cfg.web.enable "/var/lib/agent-box-sites/%i"; + # NoNewPrivileges would break the sudoAllowlist escape hatch (sudo + # is setuid root; NNP blocks the euid transition). Enable it only + # when the effective allowlist is empty — with a non-empty + # allowlist (from cfg.sudoAllowlist or the web-implied caddy + # reload) we've traded some containment for scoped elevation as a + # host choice. Same for every instance, hence host-level. + NoNewPrivileges = effectiveSudoAllowlist == [ ]; + } // lib.optionalAttrs cfg.protectMemory { + # Sacrifice agent work first under memory pressure: the kernel OOM + # killer and earlyoom both weigh oom_score_adj, so a runaway agent + # process dies before sshd/caddy/SSM — and Restart=always brings + # the session back fresh instead of leaving a frozen box. + OOMScoreAdjust = lib.mkDefault 500; + }; } )) cfg.users); @@ -2128,42 +2111,50 @@ in text = "LOCAL_WEBHOOK_STATE_DIR=${webhookStateDirOf name}\n"; }) terminalUsers)); - # ttyd per terminal user — attaches to that user's tmux session over an - # internal port; --base-path keeps each terminal (and its /ws endpoint) - # under // so one vhost can serve them all. The unit text (see - # src/units/agent-web-terminal@.service, shipped via + # ttyd/settings/webhook per terminal user. The unit text (see + # src/units/agent-web-terminal@.service etc, shipped via # systemd.packages at the top of this module) already declares - # User=%i, TMUX_TMPDIR/AGENT_BOX_SESSIONS_FILE and the ttyd - # ExecStart, expanding AGENT_BOX_TTYD_PORT/AGENT_BOX_WEB_DOMAIN from - # environment at exec time; what's below is HOST-level (the same - # PATH/domain for every instance) so it renders as a drop-in. + # User=%i and the per-instance env vars each ExecStart expands from + # environment at exec time. What's below is HOST-level content (the + # same for every instance) but rendered as a PER-INSTANCE drop-in, + # not a template-level one — see the long comment on "agent-box@"'s + # systemd.services above for why: a template-level + # "agent-web-terminal@.service.d/" drop-in silently never merges + # into any instance on a real boot, confirmed for agent-box@ and + # not worth re-confirming three more times when the fix is the + # same. Every ExecStart/ExecStartPre below is also an absolute-path + # reset-then-reassign override for the same reason "agent-box@"'s + # is: bare-name resolution needs ExecSearchPath to actually be + # *merged*, which per-instance drop-ins are proven to do reliably + # but which isn't worth depending on when an absolute path has zero + # ambiguity either way. systemd.services = { agent-web-auth-secrets = webAuthSecretsService; caddy.serviceConfig.EnvironmentFile = "/run/agent-box-web/env"; - - "agent-web-terminal@" = { - # The attach script (src/attach.sh) resolves tmux and jq from - # this PATH instead of baked store paths (issue #154, Phase 2); - # ttyd itself execs the bare "agent-box-attach" argument - # through this same runtime PATH, not ExecSearchPath (that only - # covers systemd's own resolution of the unit's ExecStart). - path = [ pkgs.tmux pkgs.jq attachScript pkgs.ttyd ]; - environment.AGENT_BOX_WEB_DOMAIN = cfg.web.domain; - serviceConfig.ExecSearchPath = lib.makeBinPath [ pkgs.ttyd ]; + } + // (lib.listToAttrs (map (name: lib.nameValuePair "agent-web-terminal@${name}" { + overrideStrategy = "asDropin"; + # The attach script (src/attach.sh) resolves tmux and jq from + # this PATH instead of baked store paths (issue #154, Phase 2) — + # forced for the same reason as "agent-box@"'s PATH (NixOS's own + # implicit per-instance default would otherwise win). + environment = { + AGENT_BOX_WEB_DOMAIN = cfg.web.domain; + PATH = lib.mkForce (lib.makeBinPath [ pkgs.tmux pkgs.jq attachScript pkgs.ttyd ]); }; - - # Settings daemon (issue #36), one per terminal user. Runs AS the - # agent user (no root, no privilege boundary): it only writes - # that user's own ~/.config/agent-box/env and kills that user's - # own tmux session. The agent unit's Restart=always then reloads - # it with the fresh env. Listens via socket activation (issue - # #49) on the same-named .socket instance below. - "agent-box-settings@" = { - # See the "agent-box@" comment above: every optional block - # merges INTO this one `environment` value via nested `//`, not - # as separate outer `// lib.optionalAttrs {...}` blocks (which - # would each shallow-replace the whole attrset in turn). - environment = { + serviceConfig.ExecStart = [ + "" + ''${pkgs.ttyd}/bin/ttyd --writable --url-arg -p ''${AGENT_BOX_TTYD_PORT} -i 127.0.0.1 -b /%i -t disableLeaveAlert=true -t titleFixed=%i@''${AGENT_BOX_WEB_DOMAIN} agent-box-attach'' + ]; + }) terminalUsers)) + // (lib.listToAttrs (map (name: lib.nameValuePair "agent-box-settings@${name}" { + overrideStrategy = "asDropin"; + # See the "agent-box@" comment above: every optional block + # merges INTO this one `environment` value via nested `//`, not + # as separate outer `// lib.optionalAttrs {...}` blocks (which + # would each shallow-replace the whole attrset in turn). + environment = + ({ AGENT_BOX_TMUX_BIN = "${pkgs.tmux}/bin/tmux"; AGENT_BOX_AGENTS = lib.concatStringsSep "," (sessionKinds cfg.installAgents); AGENT_BOX_DEFAULT_AGENT = cfg.agent; @@ -2198,35 +2189,30 @@ in # here, unlike AGENT_BOX_UPDATE_CMD, which triggers the run. AGENT_BOX_UPDATE_UNIT = "agent-box-update.service"; AGENT_BOX_SYSTEMCTL = "/run/current-system/sw/bin/systemctl"; - }; - serviceConfig.ExecSearchPath = lib.makeBinPath [ settingsDaemon ]; - }; - } - # No per-instance drop-in for agent-web-terminal@/agent-box-settings@: - # neither has any per-user content beyond the %i template itself, so - # there is nothing to override — enablement is - # systemd.targets.multi-user.wants below, not a `wantedBy` here (see - # the note by that option: a `wantedBy` on an "asDropin" instance - # produces a dangling .wants symlink and never actually starts). + }) + // { PATH = lib.mkForce (lib.makeBinPath [ settingsDaemon ]); }; + serviceConfig.ExecStart = [ "" "${settingsDaemon}/bin/agent-box-settings" ]; + }) terminalUsers)) # Webhook receiver daemon (issue #101), one per terminal user, gated on # webhook.enable. Runs webhook.py (via the agent-box-webhook-receiver - # wrapper, resolved through ExecSearchPath) in RECEIVER_ONLY mode as - # the agent user: owns the socket-activated UNIX ingress (the - # same-named .socket instance below) and fans HMAC-verified - # deliveries out to that user's sessions over IPC. - // lib.optionalAttrs webhookEnabled ({ - "agent-box-webhook@" = { - # The spawn wrapper (src/webhook-spawn.sh) runs as this unit's - # child and resolves jq/coreutils/agent-box-session from this - # PATH instead of baked store paths (issue #154, Phase 2). - path = [ pkgs.jq pkgs.coreutils sessionCli ]; - # Standing watches (deliver_to:"subagent", local-channels#1): a - # matching delivery spawns a fresh hook-* session for this - # user. One wrapper, same store path for every instance. - environment.LOCAL_WEBHOOK_SPAWN_CMD = "${webhookSpawn}/bin/agent-box-webhook-spawn"; - serviceConfig.ExecSearchPath = lib.makeBinPath [ webhookPolicyApply webhookReceiverBin ]; + # wrapper) in RECEIVER_ONLY mode as the agent user: owns the + # socket-activated UNIX ingress (the same-named .socket instance + # below) and fans HMAC-verified deliveries out to that user's + # sessions over IPC. + // lib.optionalAttrs webhookEnabled (lib.listToAttrs (map (name: lib.nameValuePair "agent-box-webhook@${name}" { + overrideStrategy = "asDropin"; + # The spawn wrapper (src/webhook-spawn.sh) runs as this unit's + # child and resolves jq/coreutils/agent-box-session from this + # PATH instead of baked store paths (issue #154, Phase 2). + environment = { + LOCAL_WEBHOOK_SPAWN_CMD = "${webhookSpawn}/bin/agent-box-webhook-spawn"; + PATH = lib.mkForce (lib.makeBinPath [ pkgs.jq pkgs.coreutils sessionCli ]); + }; + serviceConfig = { + ExecStartPre = [ "" "${webhookPolicyApply}/bin/agent-box-webhook-policy-apply" ]; + ExecStart = [ "" "${webhookReceiverBin}/bin/agent-box-webhook-receiver" ]; }; - }); + }) terminalUsers)); # No systemd.sockets overrides: the settings daemon's listening # sockets (issue #49) and the webhook ingress sockets (issue #101) # are fully %i-derived already (see src/units/*.socket, shipped via diff --git a/tests/golden/vm/units/agent-box@.service b/tests/golden/vm/units/agent-box@.service deleted file mode 100644 index dee52b57..00000000 --- a/tests/golden/vm/units/agent-box@.service +++ /dev/null @@ -1,22 +0,0 @@ -[Unit] - -[Service] -Environment="AGENT_BOX_CLAUDE_SETTINGS=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-claude-hook-settings.json" -Environment="AGENT_BOX_CODEX_FULL_ACCESS=1" -Environment="AGENT_BOX_CODEX_RC=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-codex-remote-control" -Environment="AGENT_BOX_ENV_EXEC=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-env-exec" -Environment="AGENT_BOX_FIND_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin/find" -Environment="AGENT_BOX_FLOCK_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin/flock" -Environment="AGENT_BOX_GREP_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin/grep" -Environment="AGENT_BOX_HOSTNAME_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-hostname-hostname-debian-3.25/bin/hostname" -Environment="AGENT_BOX_HOST_LABEL=nixos" -Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/home/%i/.nix-profile/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/sbin:/run/wrappers/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" -Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" -ExecSearchPath=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin -ExecStart= -ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin/agent-box-supervisor -ExecStop= -ExecStop=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin/tmux -L agent-box kill-server -NoNewPrivileges=false -OOMScoreAdjust=500 diff --git a/tests/golden/vm/units/agent-box@agent.service b/tests/golden/vm/units/agent-box@agent.service index afb379c9..09dde0f8 100644 --- a/tests/golden/vm/units/agent-box@agent.service +++ b/tests/golden/vm/units/agent-box@agent.service @@ -1,6 +1,22 @@ [Unit] [Service] +Environment="AGENT_BOX_CLAUDE_SETTINGS=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-claude-hook-settings.json" +Environment="AGENT_BOX_CODEX_FULL_ACCESS=1" +Environment="AGENT_BOX_CODEX_RC=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-codex-remote-control" +Environment="AGENT_BOX_ENV_EXEC=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-env-exec" +Environment="AGENT_BOX_FIND_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin/find" +Environment="AGENT_BOX_FLOCK_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin/flock" +Environment="AGENT_BOX_GREP_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin/grep" +Environment="AGENT_BOX_HOSTNAME_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-hostname-hostname-debian-3.25/bin/hostname" +Environment="AGENT_BOX_HOST_LABEL=nixos" Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" Environment="PATH=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin" Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" +ExecSearchPath=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin +ExecStart= +ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin/agent-box-supervisor +ExecStop= +ExecStop=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin/tmux -L agent-box kill-server +NoNewPrivileges=false +OOMScoreAdjust=500 diff --git a/tests/golden/web/units/agent-box-settings@.service b/tests/golden/web/units/agent-box-settings@agent.service similarity index 61% rename from tests/golden/web/units/agent-box-settings@.service rename to tests/golden/web/units/agent-box-settings@agent.service index a97e4e6e..b0d6e060 100644 --- a/tests/golden/web/units/agent-box-settings@.service +++ b/tests/golden/web/units/agent-box-settings@agent.service @@ -13,6 +13,7 @@ Environment="AGENT_BOX_UPDATE_UNIT=agent-box-update.service" Environment="AGENT_BOX_WEBHOOK_PYTHON=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin/python3" Environment="AGENT_BOX_WEBHOOK_SCRIPT=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-webhook.py" Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" +Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-settings/bin" Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" -ExecSearchPath=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-settings/bin +ExecStart= +ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-settings/bin/agent-box-settings diff --git a/tests/golden/web/units/agent-box-settings@robot.service b/tests/golden/web/units/agent-box-settings@robot.service new file mode 100644 index 00000000..b0d6e060 --- /dev/null +++ b/tests/golden/web/units/agent-box-settings@robot.service @@ -0,0 +1,19 @@ +[Unit] + +[Service] +Environment="AGENT_BOX_AGENTS=claude,codex,shell" +Environment="AGENT_BOX_DEFAULT_AGENT=claude" +Environment="AGENT_BOX_HOOK_SPAWN_CMD=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook-spawn/bin/agent-box-webhook-spawn" +Environment="AGENT_BOX_REPO=defangdevs/agent-box" +Environment="AGENT_BOX_REV=0000000000000000000000000000000000000000" +Environment="AGENT_BOX_SYSTEMCTL=/run/current-system/sw/bin/systemctl" +Environment="AGENT_BOX_TMUX_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin/tmux" +Environment="AGENT_BOX_UPDATE_CMD=/run/wrappers/bin/sudo -n /run/current-system/sw/bin/systemctl start --no-block agent-box-update.service" +Environment="AGENT_BOX_UPDATE_UNIT=agent-box-update.service" +Environment="AGENT_BOX_WEBHOOK_PYTHON=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin/python3" +Environment="AGENT_BOX_WEBHOOK_SCRIPT=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-webhook.py" +Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" +Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-settings/bin" +Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" +ExecStart= +ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-settings/bin/agent-box-settings diff --git a/tests/golden/web/units/agent-box-webhook@.service b/tests/golden/web/units/agent-box-webhook@.service deleted file mode 100644 index 9b566228..00000000 --- a/tests/golden/web/units/agent-box-webhook@.service +++ /dev/null @@ -1,8 +0,0 @@ -[Unit] - -[Service] -Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="LOCAL_WEBHOOK_SPAWN_CMD=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook-spawn/bin/agent-box-webhook-spawn" -Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" -Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" -ExecSearchPath=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook-policy-apply/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook-receiver/bin diff --git a/tests/golden/web/units/agent-box-webhook@agent.service b/tests/golden/web/units/agent-box-webhook@agent.service new file mode 100644 index 00000000..e232de45 --- /dev/null +++ b/tests/golden/web/units/agent-box-webhook@agent.service @@ -0,0 +1,11 @@ +[Unit] + +[Service] +Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" +Environment="LOCAL_WEBHOOK_SPAWN_CMD=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook-spawn/bin/agent-box-webhook-spawn" +Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin" +Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" +ExecStart= +ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook-receiver/bin/agent-box-webhook-receiver +ExecStartPre= +ExecStartPre=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook-policy-apply/bin/agent-box-webhook-policy-apply diff --git a/tests/golden/web/units/agent-box-webhook@robot.service b/tests/golden/web/units/agent-box-webhook@robot.service new file mode 100644 index 00000000..e232de45 --- /dev/null +++ b/tests/golden/web/units/agent-box-webhook@robot.service @@ -0,0 +1,11 @@ +[Unit] + +[Service] +Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" +Environment="LOCAL_WEBHOOK_SPAWN_CMD=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook-spawn/bin/agent-box-webhook-spawn" +Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin" +Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" +ExecStart= +ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook-receiver/bin/agent-box-webhook-receiver +ExecStartPre= +ExecStartPre=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook-policy-apply/bin/agent-box-webhook-policy-apply diff --git a/tests/golden/web/units/agent-box@.service b/tests/golden/web/units/agent-box@.service deleted file mode 100644 index 349515b2..00000000 --- a/tests/golden/web/units/agent-box@.service +++ /dev/null @@ -1,25 +0,0 @@ -[Unit] - -[Service] -Environment="AGENT_BOX_CLAUDE_SETTINGS=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-claude-hook-settings.json" -Environment="AGENT_BOX_CODEX_FULL_ACCESS=1" -Environment="AGENT_BOX_CODEX_RC=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-codex-remote-control" -Environment="AGENT_BOX_ENV_EXEC=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-env-exec" -Environment="AGENT_BOX_FIND_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin/find" -Environment="AGENT_BOX_FLOCK_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin/flock" -Environment="AGENT_BOX_GREP_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin/grep" -Environment="AGENT_BOX_HOSTNAME_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-hostname-hostname-debian-3.25/bin/hostname" -Environment="AGENT_BOX_HOST_LABEL=nixos" -Environment="AGENT_BOX_WEBHOOK_PINNED_SCRIPT=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-webhook.py" -Environment="AGENT_BOX_WEBHOOK_REPO=defangdevs/local-channels" -Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/home/%i/.nix-profile/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/sbin:/run/wrappers/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" -Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" -ExecSearchPath=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin -ExecStart= -ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin/agent-box-supervisor -ExecStop= -ExecStop=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin/tmux -L agent-box kill-server -NoNewPrivileges=false -OOMScoreAdjust=500 -ReadWritePaths=/var/lib/agent-box-sites/%i diff --git a/tests/golden/web/units/agent-box@agent.service b/tests/golden/web/units/agent-box@agent.service index 66d695b6..fff119cb 100644 --- a/tests/golden/web/units/agent-box@agent.service +++ b/tests/golden/web/units/agent-box@agent.service @@ -1,6 +1,25 @@ [Unit] [Service] +Environment="AGENT_BOX_CLAUDE_SETTINGS=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-claude-hook-settings.json" +Environment="AGENT_BOX_CODEX_FULL_ACCESS=1" +Environment="AGENT_BOX_CODEX_RC=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-codex-remote-control" +Environment="AGENT_BOX_ENV_EXEC=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-env-exec" +Environment="AGENT_BOX_FIND_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin/find" +Environment="AGENT_BOX_FLOCK_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin/flock" +Environment="AGENT_BOX_GREP_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin/grep" +Environment="AGENT_BOX_HOSTNAME_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-hostname-hostname-debian-3.25/bin/hostname" +Environment="AGENT_BOX_HOST_LABEL=nixos" +Environment="AGENT_BOX_WEBHOOK_PINNED_SCRIPT=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-webhook.py" +Environment="AGENT_BOX_WEBHOOK_REPO=defangdevs/local-channels" Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" Environment="PATH=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin" Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" +ExecSearchPath=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin +ExecStart= +ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin/agent-box-supervisor +ExecStop= +ExecStop=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin/tmux -L agent-box kill-server +NoNewPrivileges=false +OOMScoreAdjust=500 +ReadWritePaths=/var/lib/agent-box-sites/%i diff --git a/tests/golden/web/units/agent-box@robot.service b/tests/golden/web/units/agent-box@robot.service index 66d695b6..fff119cb 100644 --- a/tests/golden/web/units/agent-box@robot.service +++ b/tests/golden/web/units/agent-box@robot.service @@ -1,6 +1,25 @@ [Unit] [Service] +Environment="AGENT_BOX_CLAUDE_SETTINGS=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-claude-hook-settings.json" +Environment="AGENT_BOX_CODEX_FULL_ACCESS=1" +Environment="AGENT_BOX_CODEX_RC=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-codex-remote-control" +Environment="AGENT_BOX_ENV_EXEC=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-env-exec" +Environment="AGENT_BOX_FIND_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin/find" +Environment="AGENT_BOX_FLOCK_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin/flock" +Environment="AGENT_BOX_GREP_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin/grep" +Environment="AGENT_BOX_HOSTNAME_BIN=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-hostname-hostname-debian-3.25/bin/hostname" +Environment="AGENT_BOX_HOST_LABEL=nixos" +Environment="AGENT_BOX_WEBHOOK_PINNED_SCRIPT=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-webhook.py" +Environment="AGENT_BOX_WEBHOOK_REPO=defangdevs/local-channels" Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" Environment="PATH=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin" Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" +ExecSearchPath=/home/%i/.nix-profile/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nix-2.34.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-claude-code-2.1.206/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-codex-0.144.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bubblewrap-0.11.2/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-which-2.25/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-session/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-webhook/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gawk-5.4.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-diffutils-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-patch-2.8/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-less-704/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-file-5.48/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ripgrep-15.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnutar-1.35/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gzip-1.14/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bzip2-1.0.8-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-xz-5.8.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-zip-3.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-unzip-6.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-curl-8.21.0-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-wget-1.25.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssh-10.3p1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-rsync-3.4.4/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iputils-20250605/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-iproute2-7.1.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bind-9.20.24-dnsutils/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-netcat-gnu-0.7.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-openssl-3.6.3-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnupg-2.4.9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-procps-4.0.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-psmisc-23.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-util-linux-2.42.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-nano-9.1/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-bash-interactive-5.3p9/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-git-2.54.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gh-2.96.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/run/wrappers/bin +ExecStart= +ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-supervisor/bin/agent-box-supervisor +ExecStop= +ExecStop=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin/tmux -L agent-box kill-server +NoNewPrivileges=false +OOMScoreAdjust=500 +ReadWritePaths=/var/lib/agent-box-sites/%i diff --git a/tests/golden/web/units/agent-web-terminal@.service b/tests/golden/web/units/agent-web-terminal@.service deleted file mode 100644 index 8d64d4fa..00000000 --- a/tests/golden/web/units/agent-web-terminal@.service +++ /dev/null @@ -1,8 +0,0 @@ -[Unit] - -[Service] -Environment="AGENT_BOX_WEB_DOMAIN=golden.example.org" -Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-attach/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-attach/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/sbin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/sbin" -Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" -ExecSearchPath=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/bin diff --git a/tests/golden/web/units/agent-web-terminal@agent.service b/tests/golden/web/units/agent-web-terminal@agent.service new file mode 100644 index 00000000..10b4839e --- /dev/null +++ b/tests/golden/web/units/agent-web-terminal@agent.service @@ -0,0 +1,9 @@ +[Unit] + +[Service] +Environment="AGENT_BOX_WEB_DOMAIN=golden.example.org" +Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" +Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-attach/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/bin" +Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" +ExecStart= +ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/bin/ttyd --writable --url-arg -p ${AGENT_BOX_TTYD_PORT} -i 127.0.0.1 -b /%i -t disableLeaveAlert=true -t titleFixed=%i@${AGENT_BOX_WEB_DOMAIN} agent-box-attach diff --git a/tests/golden/web/units/agent-web-terminal@robot.service b/tests/golden/web/units/agent-web-terminal@robot.service new file mode 100644 index 00000000..10b4839e --- /dev/null +++ b/tests/golden/web/units/agent-web-terminal@robot.service @@ -0,0 +1,9 @@ +[Unit] + +[Service] +Environment="AGENT_BOX_WEB_DOMAIN=golden.example.org" +Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" +Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-attach/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/bin" +Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" +ExecStart= +ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/bin/ttyd --writable --url-arg -p ${AGENT_BOX_TTYD_PORT} -i 127.0.0.1 -b /%i -t disableLeaveAlert=true -t titleFixed=%i@${AGENT_BOX_WEB_DOMAIN} agent-box-attach diff --git a/tests/memory-protection.nix b/tests/memory-protection.nix index 6cc771df..3adefef0 100644 --- a/tests/memory-protection.nix +++ b/tests/memory-protection.nix @@ -23,36 +23,6 @@ testScript = '' machine.wait_for_unit("multi-user.target") - # TEMPORARY DIAGNOSTIC (agent-box#154 Phase 3 CI investigation) — remove - # before merge. Printing the fully-merged unit systemd actually loaded, - # since eval/local-build both show the correct ExecStart but the real - # boot keeps reporting "Unable to locate executable 'agent-box-supervisor'". - machine.sleep(5) - # systemctl cat proved the template-level "agent-box@.service.d/" drop-in - # never gets merged (only the base unit + the per-instance drop-in show - # up), which contradicts systemd.unit(5)'s documented "instance .d/ THEN - # template .d/" lookup — check the filesystem directly to see whether - # the directory is actually missing (a render/link bug on our side) or - # present-but-ignored (a systemd-side surprise), and look for any - # drop-in load warning in the journal we might have missed. - print(machine.succeed("ls -la /etc/systemd/system/ | grep 'agent-box@' || echo NO_AGENT_BOX_AT_ENTRIES")) - print(machine.succeed("ls -la /etc/systemd/system/agent-box@.service.d/ 2>&1 || echo TEMPLATE_DROPIN_DIR_MISSING")) - print(machine.succeed("cat /etc/systemd/system/agent-box@.service.d/overrides.conf 2>&1 || echo TEMPLATE_DROPIN_FILE_MISSING")) - print(machine.succeed("readlink -f /etc/systemd/system/agent-box@.service.d/overrides.conf 2>&1 || echo NO_REAL_TARGET")) - print(machine.succeed("journalctl -b --no-pager 2>&1 | grep -i 'agent-box@' || echo NO_JOURNAL_MENTIONS")) - print(machine.succeed("systemctl cat agent-box@agent.service 2>&1 || true")) - # The on-disk drop-in is provably correct (confirmed above) yet the unit - # keeps resolving the bare name from the very first restart at boot — - # testing whether systemd loaded the unit BEFORE indexing this drop-in - # and never re-merged it since (Restart=always reuses the SAME loaded - # config, it doesn't re-read files per restart). - print(machine.succeed("systemctl daemon-reload 2>&1 || true")) - print(machine.succeed("systemctl show agent-box@agent.service --no-pager -p ExecStart 2>&1 || true")) - machine.succeed("systemctl reset-failed agent-box@agent.service || true") - machine.succeed("systemctl restart agent-box@agent.service || true") - machine.sleep(2) - print(machine.succeed("systemctl show agent-box@agent.service --no-pager -p ExecStart -p ActiveState -p SubState 2>&1 || true")) - print(machine.succeed("systemctl show agent-box@agent.service --no-pager -p ExecStart -p ExecStop -p Environment -p LoadState -p LoadError 2>&1 || true")) machine.wait_for_unit("agent-box@agent.service") machine.wait_for_unit("earlyoom.service") From 64ff7430a4e69310795123057510b754c58888e0 Mon Sep 17 00:00:00 2001 From: Lio Lunesu Date: Wed, 19 Aug 2026 19:56:49 +0000 Subject: [PATCH 11/18] test: fix two pre-existing test bugs that CI reached for the first time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both predate this PR's own commits (part of the original Phase 1/2/3 work) but were never actually exercised — every earlier CI run on this PR failed at an earlier stage (unit never started, then exec resolution) before test execution got this far, and per tests/sessions.nix's own comment ("the CI hang on this PR's first three runs") this test line specifically never completed even before that. - tests/sessions.nix: the ExecStart store-path extraction regex matched "-agent-box-supervisor" as a suffix, which the writeShellScriptBin DERIVATION DIRECTORY also ends in, one path segment before the actual script at ".../bin/agent-box-supervisor". Since the directory name is the only place in the string with a literal hyphen (not slash) right before "agent-box-supervisor", the regex always matched the directory, and grep-ing a directory for the rcname= string fails with "Is a directory" (exit 2) — this would have been just as true before this PR's ExecStart absolute-path override, since the ExecStart was always an absolute path into the same shaped derivation. Fixed by matching the "/bin/agent-box-supervisor" suffix specifically. - tests/webhook.nix: AGENT_BOX_WEBHOOK_URL is per-user, so issue #154 Phase 3's own design puts it in the generated /etc/agent-box/units/.env, loaded via the unit's EnvironmentFile=, not a static Environment= directive — it will never show up in `systemctl show -p Environment`, by design. Checks the actual running process's /proc//environ instead, which also more directly proves EnvironmentFile= actually got wired up (the exact kind of thing this PR's whole investigation found reasons not to take for granted) rather than just that the generated file has the right text. --- tests/sessions.nix | 9 +++++++-- tests/webhook.nix | 16 ++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/tests/sessions.nix b/tests/sessions.nix index 16509cfd..9b7d8bdc 100644 --- a/tests/sessions.nix +++ b/tests/sessions.nix @@ -144,10 +144,15 @@ # grep -o matches the store path twice. Without it, interpolating the # two-line value below makes the second line its own shell command — the # backdoor shell EXECUTES the supervisor script as root and never returns - # (the CI hang on this PR's first three runs). + # (the CI hang on this PR's first three runs). The pattern has to end in + # "/bin/agent-box-supervisor", not just "-agent-box-supervisor": the + # writeShellScriptBin derivation directory ITSELF ends in + # "-agent-box-supervisor" too, one path segment earlier — matching only + # the shorter suffix greps the directory, not the script, and `grep` on + # a directory fails with "Is a directory" (exit 2) below. start_script = machine.succeed( "systemctl show agent-box@agent --property=ExecStart --value " - "| grep -o '/nix/store/[^ ;]*-agent-box-supervisor' | head -n1" + "| grep -o '/nix/store/[^ ;]*/bin/agent-box-supervisor' | head -n1" ).strip() machine.succeed( "systemctl show agent-box@agent -p Environment --value " diff --git a/tests/webhook.nix b/tests/webhook.nix index 8925e2eb..b11d1068 100644 --- a/tests/webhook.nix +++ b/tests/webhook.nix @@ -151,9 +151,21 @@ machine.succeed( "systemctl show -p Environment agent-box@agent.service | grep agent-box-webhook/bin >/dev/null" ) + # AGENT_BOX_WEBHOOK_URL is per-user (the path segment has this user's + # name in it), so issue #154 Phase 3 puts it in the generated + # /etc/agent-box/units/agent.env — loaded via the unit's + # EnvironmentFile=, not a static Environment= directive, so it will + # never show up in `systemctl show -p Environment`. Check the actual + # running process's environment instead: the thing this assertion + # cares about is that the agent can discover the URL at runtime, and + # this proves EnvironmentFile= really got wired up, not just that the + # generated file has the right text. + main_pid = machine.succeed( + "systemctl show -p MainPID --value agent-box@agent.service" + ).strip() machine.succeed( - "systemctl show -p Environment agent-box@agent.service" - " | grep 'AGENT_BOX_WEBHOOK_URL=https://box.test/agent/webhook' >/dev/null" + f"tr '\\0' '\\n' < /proc/{main_pid}/environ" + " | grep -x 'AGENT_BOX_WEBHOOK_URL=https://box.test/agent/webhook' >/dev/null" ) # The supervisor gives each tmux session its own subscription scope, so a # bare `agent-box-webhook subscribe` in that session cannot leak into a From 6efd3c841b6d2b6e4c6d387034dc2bd04b930ed8 Mon Sep 17 00:00:00 2001 From: Lio Lunesu Date: Wed, 19 Aug 2026 20:10:00 +0000 Subject: [PATCH 12/18] test: fix two more pre-existing test/architecture mismatches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same root cause class as the previous commit — CI reaching these lines for the first time surfaced test assumptions that predate this PR's own changes: - tests/sessions.nix: two occurrences of the same "-agent-box-attach" suffix bug as the supervisor one (extracts the writeShellScriptBin DIRECTORY, not the "/bin/agent-box-attach" script) — one of which then tries to exec that directory as a command (`{attach} {name}`), the other greps it for a CLI flag. Both fixed to match the "/bin/" suffix. - tests/webhook.nix: issue #154 Phase 3's own design moved the pinned `webhookPython`/`localWebhookScript` invocation out of the unit's ExecStart and into agent-box-webhook-receiver's own `exec` line (so the shared unit text wouldn't tie to a specific Nix build) — but this test still extracted python3/webhook.py directly from ExecStart, where they no longer appear at all. Extracts the wrapper's own path from ExecStart first, then greps ITS body for the interpreter/script pair the test needs to spawn its stand-in session peer. --- tests/sessions.nix | 10 ++++++++-- tests/webhook.nix | 22 ++++++++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/tests/sessions.nix b/tests/sessions.nix index 9b7d8bdc..6ea0f635 100644 --- a/tests/sessions.nix +++ b/tests/sessions.nix @@ -1036,9 +1036,15 @@ # The terminal dead end names the verb that actually revives it. # Print the wrapper's path (see the grep -o note below) — running # the substitution as the command would run the WRAPPER instead. + # Pattern ends in "/bin/agent-box-attach", not just + # "-agent-box-attach": the writeShellScriptBin derivation + # DIRECTORY also ends in that suffix, one path segment before the + # real script — matching only the shorter suffix would extract + # the directory, and `{attach} {name}` below would then try to + # exec a directory. attach = machine.succeed( "{ systemctl show agent-web-terminal@agent --property=ExecStart " - "--value | grep -o '/nix/store/[^ ]*-agent-box-attach' " + "--value | grep -o '/nix/store/[^ ]*/bin/agent-box-attach' " "|| echo /missing; } | head -n1" ).strip() assert attach != "/missing", attach @@ -1079,7 +1085,7 @@ machine.succeed( "grep -q -- '-T hyperlinks' " "$({ systemctl show agent-web-terminal@agent --property=ExecStart --value " - "| grep -o '/nix/store/[^ ]*-agent-box-attach' || echo /missing; } | head -n1)" + "| grep -o '/nix/store/[^ ]*/bin/agent-box-attach' || echo /missing; } | head -n1)" ) # Working-directory picker (issue 131): the add-session form browses the diff --git a/tests/webhook.nix b/tests/webhook.nix index b11d1068..3ce20fcd 100644 --- a/tests/webhook.nix +++ b/tests/webhook.nix @@ -254,19 +254,25 @@ # Exactly what claude runs as the plugin's MCP server: the same webhook.py # on stdio, PORT=0 so it never takes the ingress. Its stdout is the channel # stream. Both the interpreter and the script come from the daemon unit's - # own ExecStart, so the test cannot drift from the pinned pair. - exec_start = machine.succeed( + # ExecStart -> the bare-execable wrapper it names -> that wrapper's own + # body (issue #154 Phase 3 moved the webhookPython/localWebhookScript + # invocation off the unit text and into agent-box-webhook-receiver's + # `exec` line specifically so the shared unit file wouldn't tie to + # this Nix build — so the pinned pair now lives one hop further away + # than ExecStart), so the test still cannot drift from the pinned pair. + receiver_bin = machine.succeed( "systemctl show -p ExecStart --value agent-box-webhook@agent.service" - ) + " | grep -o '/nix/store/[^ ;]*/bin/agent-box-webhook-receiver' | head -1" + ).strip() + assert receiver_bin, "could not find agent-box-webhook-receiver in ExecStart" + receiver_src = machine.succeed(f"cat {receiver_bin}") python = machine.succeed( - "systemctl show -p ExecStart --value agent-box-webhook@agent.service" - " | grep -o '/nix/store/[^ ;]*/bin/python3' | head -1" + f"grep -o '/nix/store/[^ ]*/bin/python3' {receiver_bin} | head -1" ).strip() script = machine.succeed( - "systemctl show -p ExecStart --value agent-box-webhook@agent.service" - " | grep -o '/nix/store/[^ ;]*webhook.py' | head -1" + f"grep -o '/nix/store/[^ ]*webhook.py' {receiver_bin} | head -1" ).strip() - assert python and script, exec_start + assert python and script, receiver_src # systemd-run so the driver isn't left waiting on a backgrounded shell. # `sleep | python3` keeps stdin OPEN: webhook.py treats stdin EOF as its # session closing and exits, which is right for claude and wrong here. From 5a97e27ea698b656b9b1a40b76cb0a055494d079 Mon Sep 17 00:00:00 2001 From: Lio Lunesu Date: Wed, 19 Aug 2026 20:25:42 +0000 Subject: [PATCH 13/18] fix(module): give agent-box-attach an absolute path in ttyd's ExecStart too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The verbatim unit text leaves it bare ("... agent-box-attach", resolved via ttyd's own runtime PATH — which is correctly forced now and would work fine functionally), but tests/sessions.nix needs to extract the script's absolute store path to invoke it directly outside any unit context (a "dead end" probe when a session is stopped, and a check that the wrapper's own body carries the "-T hyperlinks" tmux flag). Matching the same "don't depend on a search path being merged/forced correctly" posture as the ExecStart/ExecStop overrides elsewhere in this PR. --- modules/agent-box.nix | 6 +++++- modules/agent-box.nix.in | 6 +++++- tests/golden/web/units/agent-web-terminal@agent.service | 2 +- tests/golden/web/units/agent-web-terminal@robot.service | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/modules/agent-box.nix b/modules/agent-box.nix index 5b3bfdc4..8bfb258c 100644 --- a/modules/agent-box.nix +++ b/modules/agent-box.nix @@ -8682,9 +8682,13 @@ in AGENT_BOX_WEB_DOMAIN = cfg.web.domain; PATH = lib.mkForce (lib.makeBinPath [ pkgs.tmux pkgs.jq attachScript pkgs.ttyd ]); }; + # agent-box-attach is given as an absolute path here (not left bare + # for ttyd's own PATH-based exec, unlike the verbatim unit text) + # for the same reason ExecStart itself is absolute below: no + # dependency on any search-path resolution being merged correctly. serviceConfig.ExecStart = [ "" - ''${pkgs.ttyd}/bin/ttyd --writable --url-arg -p ''${AGENT_BOX_TTYD_PORT} -i 127.0.0.1 -b /%i -t disableLeaveAlert=true -t titleFixed=%i@''${AGENT_BOX_WEB_DOMAIN} agent-box-attach'' + ''${pkgs.ttyd}/bin/ttyd --writable --url-arg -p ''${AGENT_BOX_TTYD_PORT} -i 127.0.0.1 -b /%i -t disableLeaveAlert=true -t titleFixed=%i@''${AGENT_BOX_WEB_DOMAIN} ${attachScript}/bin/agent-box-attach'' ]; }) terminalUsers)) // (lib.listToAttrs (map (name: lib.nameValuePair "agent-box-settings@${name}" { diff --git a/modules/agent-box.nix.in b/modules/agent-box.nix.in index bf2139da..c6dde064 100644 --- a/modules/agent-box.nix.in +++ b/modules/agent-box.nix.in @@ -2142,9 +2142,13 @@ in AGENT_BOX_WEB_DOMAIN = cfg.web.domain; PATH = lib.mkForce (lib.makeBinPath [ pkgs.tmux pkgs.jq attachScript pkgs.ttyd ]); }; + # agent-box-attach is given as an absolute path here (not left bare + # for ttyd's own PATH-based exec, unlike the verbatim unit text) + # for the same reason ExecStart itself is absolute below: no + # dependency on any search-path resolution being merged correctly. serviceConfig.ExecStart = [ "" - ''${pkgs.ttyd}/bin/ttyd --writable --url-arg -p ''${AGENT_BOX_TTYD_PORT} -i 127.0.0.1 -b /%i -t disableLeaveAlert=true -t titleFixed=%i@''${AGENT_BOX_WEB_DOMAIN} agent-box-attach'' + ''${pkgs.ttyd}/bin/ttyd --writable --url-arg -p ''${AGENT_BOX_TTYD_PORT} -i 127.0.0.1 -b /%i -t disableLeaveAlert=true -t titleFixed=%i@''${AGENT_BOX_WEB_DOMAIN} ${attachScript}/bin/agent-box-attach'' ]; }) terminalUsers)) // (lib.listToAttrs (map (name: lib.nameValuePair "agent-box-settings@${name}" { diff --git a/tests/golden/web/units/agent-web-terminal@agent.service b/tests/golden/web/units/agent-web-terminal@agent.service index 10b4839e..99051c0b 100644 --- a/tests/golden/web/units/agent-web-terminal@agent.service +++ b/tests/golden/web/units/agent-web-terminal@agent.service @@ -6,4 +6,4 @@ Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-lo Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-attach/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/bin" Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" ExecStart= -ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/bin/ttyd --writable --url-arg -p ${AGENT_BOX_TTYD_PORT} -i 127.0.0.1 -b /%i -t disableLeaveAlert=true -t titleFixed=%i@${AGENT_BOX_WEB_DOMAIN} agent-box-attach +ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/bin/ttyd --writable --url-arg -p ${AGENT_BOX_TTYD_PORT} -i 127.0.0.1 -b /%i -t disableLeaveAlert=true -t titleFixed=%i@${AGENT_BOX_WEB_DOMAIN} /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-attach/bin/agent-box-attach diff --git a/tests/golden/web/units/agent-web-terminal@robot.service b/tests/golden/web/units/agent-web-terminal@robot.service index 10b4839e..99051c0b 100644 --- a/tests/golden/web/units/agent-web-terminal@robot.service +++ b/tests/golden/web/units/agent-web-terminal@robot.service @@ -6,4 +6,4 @@ Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-lo Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-attach/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/bin" Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" ExecStart= -ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/bin/ttyd --writable --url-arg -p ${AGENT_BOX_TTYD_PORT} -i 127.0.0.1 -b /%i -t disableLeaveAlert=true -t titleFixed=%i@${AGENT_BOX_WEB_DOMAIN} agent-box-attach +ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/bin/ttyd --writable --url-arg -p ${AGENT_BOX_TTYD_PORT} -i 127.0.0.1 -b /%i -t disableLeaveAlert=true -t titleFixed=%i@${AGENT_BOX_WEB_DOMAIN} /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-attach/bin/agent-box-attach From cba942e02b4dde686ca283a127f3975f7999076f Mon Sep 17 00:00:00 2001 From: Lio Lunesu Date: Tue, 25 Aug 2026 17:45:33 +0000 Subject: [PATCH 14/18] fix(module): two integration breaks the merge surfaced in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both are cases where a master test meets a Phase 3 change, so neither existed on either branch alone — exactly what the merge was for. 1. tests/sessions-web.nix extracts the attach script's path out of the web-terminal unit with `grep -o '/nix/store/[^ ]*-agent-box-attach'`. Phase 3 turned attachScript from writeShellScript into writeShellScriptBin, so that path is now a package DIRECTORY with the executable at bin/agent-box-attach inside it — and the greedy match stops at the first "-agent-box-attach", which is the directory: env: '/nix/store/...-agent-box-attach': Permission denied Dropping the "-" lets the match run to the end of the path, which is right for both shapes (on master's file shape the longest match ending in "agent-box-attach" is still the file). 2. The settings daemon lost the base tools from its PATH. Phase 3 forced it to the daemon's own bin; on master it is NixOS's default for a declared unit — coreutils, findutils, gnugrep, gnused, systemd. That was not a change Phase 3 set out to make: the point of forcing PATH there is pinning agent-box's OWN payload binaries, not stripping the environment the daemon's subprocesses run in. It runs third-party CLIs with its own environment for the guided sign-in cards (#207, #208, #313), so tests/connect.nix's stub failed to get through its flow. The base tools are kept alongside the daemon now, with mkForce retained for the original reason. --- modules/agent-box.nix | 18 +++++++++++++++++- modules/agent-box.nix.in | 18 +++++++++++++++++- .../web/units/agent-box-settings@agent.service | 2 +- tests/sessions-web.nix | 4 ++-- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/modules/agent-box.nix b/modules/agent-box.nix index 315a40eb..1e33248c 100644 --- a/modules/agent-box.nix +++ b/modules/agent-box.nix @@ -12649,7 +12649,23 @@ if __name__ == "__main__": AGENT_BOX_UPDATE_UNIT = "agent-box-update.service"; AGENT_BOX_SYSTEMCTL = "/run/current-system/sw/bin/systemctl"; }) - // { PATH = lib.mkForce (lib.makeBinPath [ settingsDaemon ]); }; + // { + # mkForce for the reason spelled out on the "agent-box@" unit: + # NixOS sets a default Environment=PATH= for any declared + # systemd.services. at plain priority. The base tools are + # KEPT, not replaced — they are what that default provided on + # master, and the daemon runs third-party CLIs with its own + # environment (guided sign-in, issues #207/#208/#313), so a + # PATH holding only agent-box's own bin breaks them. + PATH = lib.mkForce (lib.makeBinPath [ + settingsDaemon + pkgs.coreutils + pkgs.findutils + pkgs.gnugrep + pkgs.gnused + pkgs.systemd + ]); + }; serviceConfig.ExecStart = [ "" "${settingsDaemon}/bin/agent-box-settings" ]; }) terminalUsers)) # Webhook receiver daemon (issue #101), one per terminal user, gated on diff --git a/modules/agent-box.nix.in b/modules/agent-box.nix.in index 410c6b39..5bdec0e2 100644 --- a/modules/agent-box.nix.in +++ b/modules/agent-box.nix.in @@ -2475,7 +2475,23 @@ in AGENT_BOX_UPDATE_UNIT = "agent-box-update.service"; AGENT_BOX_SYSTEMCTL = "/run/current-system/sw/bin/systemctl"; }) - // { PATH = lib.mkForce (lib.makeBinPath [ settingsDaemon ]); }; + // { + # mkForce for the reason spelled out on the "agent-box@" unit: + # NixOS sets a default Environment=PATH= for any declared + # systemd.services. at plain priority. The base tools are + # KEPT, not replaced — they are what that default provided on + # master, and the daemon runs third-party CLIs with its own + # environment (guided sign-in, issues #207/#208/#313), so a + # PATH holding only agent-box's own bin breaks them. + PATH = lib.mkForce (lib.makeBinPath [ + settingsDaemon + pkgs.coreutils + pkgs.findutils + pkgs.gnugrep + pkgs.gnused + pkgs.systemd + ]); + }; serviceConfig.ExecStart = [ "" "${settingsDaemon}/bin/agent-box-settings" ]; }) terminalUsers)) # Webhook receiver daemon (issue #101), one per terminal user, gated on diff --git a/tests/golden/web/units/agent-box-settings@agent.service b/tests/golden/web/units/agent-box-settings@agent.service index 71d3b985..90d2c58a 100644 --- a/tests/golden/web/units/agent-box-settings@agent.service +++ b/tests/golden/web/units/agent-box-settings@agent.service @@ -14,7 +14,7 @@ Environment="AGENT_BOX_UPDATE_UNIT=agent-box-update.service" Environment="AGENT_BOX_WEBHOOK_PYTHON=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-python3-3.14.6/bin/python3" Environment="AGENT_BOX_WEBHOOK_SCRIPT=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-webhook.py" Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-settings/bin" +Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-settings/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-findutils-4.10.0/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnugrep-3.12/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-gnused-4.10/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-systemd-261/bin" Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" ExecStart= ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-settings/bin/agent-box-settings diff --git a/tests/sessions-web.nix b/tests/sessions-web.nix index ebabff0f..04a1a337 100644 --- a/tests/sessions-web.nix +++ b/tests/sessions-web.nix @@ -315,7 +315,7 @@ in # the substitution as the command would run the WRAPPER instead. attach = machine.succeed( "{ systemctl show agent-web-terminal@agent --property=ExecStart " - "--value | grep -o '/nix/store/[^ ]*-agent-box-attach' " + "--value | grep -o '/nix/store/[^ ]*agent-box-attach' " "|| echo /missing; } | head -n1" ).strip() assert attach != "/missing", attach @@ -356,7 +356,7 @@ in machine.succeed( "grep -q -- '-T hyperlinks' " "$({ systemctl show agent-web-terminal@agent --property=ExecStart --value " - "| grep -o '/nix/store/[^ ]*-agent-box-attach' || echo /missing; } | head -n1)" + "| grep -o '/nix/store/[^ ]*agent-box-attach' || echo /missing; } | head -n1)" ) # Working-directory picker (issue 131): the add-session form browses the From 734c4bcfb6572137b6049073eaa2ff36976020e7 Mon Sep 17 00:00:00 2001 From: Lio Lunesu Date: Tue, 25 Aug 2026 18:00:12 +0000 Subject: [PATCH 15/18] test(connect): TEMP diagnostic - show what the sign-in pane actually did claude sticks in "waiting" on the merged branch and passes on master. The assertion that notices is three steps downstream of whatever broke, so dump the pane's own output, the stub's state dir, the PATH the daemon exports into the pane and the card's JSON before waiting. Reverted once the cause is named. --- modules/agent-box.nix | 11 ++++++----- modules/agent-box.nix.in | 11 ++++++----- tests/connect.nix | 17 +++++++++++++++++ .../web/units/agent-web-terminal@agent.service | 2 +- 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/modules/agent-box.nix b/modules/agent-box.nix index 1e33248c..020e8e9a 100644 --- a/modules/agent-box.nix +++ b/modules/agent-box.nix @@ -12583,13 +12583,14 @@ if __name__ == "__main__": } // (lib.listToAttrs (map (name: lib.nameValuePair "agent-web-terminal@${name}" { overrideStrategy = "asDropin"; - # The attach script (src/attach.sh) resolves tmux and jq from - # this PATH instead of baked store paths (issue #154, Phase 2) — - # forced for the same reason as "agent-box@"'s PATH (NixOS's own - # implicit per-instance default would otherwise win). + # The attach script (src/attach.sh) resolves tmux, jq and head + # (coreutils) from this PATH instead of baked store paths (issue + # #154, Phase 2) — forced for the same reason as "agent-box@"'s + # PATH (NixOS's own implicit per-instance default would otherwise + # win). environment = { AGENT_BOX_WEB_DOMAIN = cfg.web.domain; - PATH = lib.mkForce (lib.makeBinPath [ pkgs.tmux pkgs.jq attachScript pkgs.ttyd ]); + PATH = lib.mkForce (lib.makeBinPath [ pkgs.tmux pkgs.jq pkgs.coreutils attachScript pkgs.ttyd ]); }; # agent-box-attach is given as an absolute path here (not left bare # for ttyd's own PATH-based exec, unlike the verbatim unit text) diff --git a/modules/agent-box.nix.in b/modules/agent-box.nix.in index 5bdec0e2..76611c2c 100644 --- a/modules/agent-box.nix.in +++ b/modules/agent-box.nix.in @@ -2409,13 +2409,14 @@ in } // (lib.listToAttrs (map (name: lib.nameValuePair "agent-web-terminal@${name}" { overrideStrategy = "asDropin"; - # The attach script (src/attach.sh) resolves tmux and jq from - # this PATH instead of baked store paths (issue #154, Phase 2) — - # forced for the same reason as "agent-box@"'s PATH (NixOS's own - # implicit per-instance default would otherwise win). + # The attach script (src/attach.sh) resolves tmux, jq and head + # (coreutils) from this PATH instead of baked store paths (issue + # #154, Phase 2) — forced for the same reason as "agent-box@"'s + # PATH (NixOS's own implicit per-instance default would otherwise + # win). environment = { AGENT_BOX_WEB_DOMAIN = cfg.web.domain; - PATH = lib.mkForce (lib.makeBinPath [ pkgs.tmux pkgs.jq attachScript pkgs.ttyd ]); + PATH = lib.mkForce (lib.makeBinPath [ pkgs.tmux pkgs.jq pkgs.coreutils attachScript pkgs.ttyd ]); }; # agent-box-attach is given as an absolute path here (not left bare # for ttyd's own PATH-based exec, unlike the verbatim unit text) diff --git a/tests/connect.nix b/tests/connect.nix index 1b33abd7..6fa48024 100644 --- a/tests/connect.nix +++ b/tests/connect.nix @@ -252,6 +252,23 @@ in with subtest("the pasted code reaches the CLI and success is the CLI's own answer"): assert post("/agent/settings/connect/code", "flow=claude&code=abc-1234567890") == "303" + # TEMP diagnostic (#295 merge): claude sticks in "waiting" here on + # the merged branch and passes on master. Show what the pane + # actually did, so the next CI round names the step that broke + # instead of the step that noticed. + machine.sleep(3) + print("--- pane -------------------------------------------------") + print(machine.succeed(tmux("capture-pane -p -t '=_connect-claude:'") + " || true")) + print("--- stub state dir ---------------------------------------") + print(machine.succeed("ls -la ${stateDir} 2>&1 || true")) + print("--- server PATH (what connect_start exports into the pane) -") + print(machine.succeed(tmux("show-environment -g PATH") + " 2>&1 || true")) + print("--- daemon PATH ------------------------------------------") + print(machine.succeed( + "systemctl show agent-box-settings@agent --property=Environment --value")) + print("--- card state -------------------------------------------") + print(get("/agent/settings/connect?flow=claude")) + print("----------------------------------------------------------") got = wait_state("claude", "connected") assert got["detail"] == "stub@example.com (max)", got assert machine.succeed("cat ${stateDir}/claude-code").strip() == "abc-1234567890" diff --git a/tests/golden/web/units/agent-web-terminal@agent.service b/tests/golden/web/units/agent-web-terminal@agent.service index c0c507e2..f5d34714 100644 --- a/tests/golden/web/units/agent-web-terminal@agent.service +++ b/tests/golden/web/units/agent-web-terminal@agent.service @@ -3,7 +3,7 @@ [Service] Environment="AGENT_BOX_WEB_DOMAIN=golden.example.org" Environment="LOCALE_ARCHIVE=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-locales-2.42-67/lib/locale/locale-archive" -Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-attach/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/bin" +Environment="PATH=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tmux-3.7b/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-jq-1.8.2-bin/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-coreutils-9.11/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-attach/bin:/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/bin" Environment="TZDIR=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-tzdata-2026b/share/zoneinfo" ExecStart= ExecStart=/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-ttyd-1.7.7/bin/ttyd --writable --url-arg -p ${AGENT_BOX_TTYD_PORT} -i 127.0.0.1 -b /%i -t disableLeaveAlert=true -t titleFixed=%i@${AGENT_BOX_WEB_DOMAIN} -t macOptionClickForcesSelection=true /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-agent-box-attach/bin/agent-box-attach From f7f37b3e8bb39c3243a731331630868ef851e588 Mon Sep 17 00:00:00 2001 From: Defang Claude <140542220+defangdevs@users.noreply.github.com> Date: Tue, 25 Aug 2026 18:08:38 +0000 Subject: [PATCH 16/18] fix(test): connect.nix's stub override targets a phantom unit name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit systemd.services.agent-box-settings-agent no longer names the real service — Phase 3 moved it to the "agent-box-settings@agent" template instance. The override silently created an unused, never-started unit, so AGENT_BOX_CONNECT_BINS never reached the real daemon: it fell back to the module's own default (the REAL claude/gh binaries), and the pane ran the actual CLI instead of the stub. A fake pasted code is rejected by the real OAuth server, so the card never leaves "waiting" — exactly the CI symptom, reproduced locally with a hand-run copy of the daemon (real tmux, stub CLI) pointed at the same env vars the unit would carry: with the corrected key it reaches "connected" every time. --- tests/connect.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/connect.nix b/tests/connect.nix index 6fa48024..43161787 100644 --- a/tests/connect.nix +++ b/tests/connect.nix @@ -139,7 +139,7 @@ in # variable the module computes, so the daemon cannot tell the # difference — which is the point of naming binaries rather than # relying on PATH. - systemd.services.agent-box-settings-agent.environment.AGENT_BOX_CONNECT_BINS = + systemd.services."agent-box-settings@agent".environment.AGENT_BOX_CONNECT_BINS = lib.mkForce "claude=${stubClaude}/bin/claude github=${stubGh}/bin/gh"; system.activationScripts.agent-web-password-hash.text = '' From 68c8bb9b5196997f4c4231b54a318c38c8d55136 Mon Sep 17 00:00:00 2001 From: Lio Lunesu Date: Tue, 25 Aug 2026 18:24:08 +0000 Subject: [PATCH 17/18] test(connect): drop the TEMP diagnostic now the cause is named The dump landed in 734c4bc to find why "claude" stuck in "waiting" on the merged branch. f7f37b3 named it: the stub override addressed "agent-box-settings-agent", a unit Phase 3 retired, so NixOS defined a new unit nothing starts and the daemon fell back to the real CLIs. Only the print block is removed here. 734c4bc also carried a real fix (coreutils on agent-web-terminal@'s PATH, so attach.sh resolves head) plus its golden line, which stay. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PBDHCBttvBEHQJJH24MfxF --- tests/connect.nix | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tests/connect.nix b/tests/connect.nix index 43161787..a9991027 100644 --- a/tests/connect.nix +++ b/tests/connect.nix @@ -252,23 +252,6 @@ in with subtest("the pasted code reaches the CLI and success is the CLI's own answer"): assert post("/agent/settings/connect/code", "flow=claude&code=abc-1234567890") == "303" - # TEMP diagnostic (#295 merge): claude sticks in "waiting" here on - # the merged branch and passes on master. Show what the pane - # actually did, so the next CI round names the step that broke - # instead of the step that noticed. - machine.sleep(3) - print("--- pane -------------------------------------------------") - print(machine.succeed(tmux("capture-pane -p -t '=_connect-claude:'") + " || true")) - print("--- stub state dir ---------------------------------------") - print(machine.succeed("ls -la ${stateDir} 2>&1 || true")) - print("--- server PATH (what connect_start exports into the pane) -") - print(machine.succeed(tmux("show-environment -g PATH") + " 2>&1 || true")) - print("--- daemon PATH ------------------------------------------") - print(machine.succeed( - "systemctl show agent-box-settings@agent --property=Environment --value")) - print("--- card state -------------------------------------------") - print(get("/agent/settings/connect?flow=claude")) - print("----------------------------------------------------------") got = wait_state("claude", "connected") assert got["detail"] == "stub@example.com (max)", got assert machine.succeed("cat ${stateDir}/claude-code").strip() == "abc-1234567890" From ea950b83493191beb8050b8862c2846e45731c61 Mon Sep 17 00:00:00 2001 From: Lio Lunesu Date: Tue, 25 Aug 2026 18:42:30 +0000 Subject: [PATCH 18/18] test(sessions-web): require a whole-token agent-box-attach path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review on #295: `/nix/store/[^ ]*agent-box-attach` does not require the match to end at a token boundary, so a derivation named e.g. agent-box-attach-wrapper matches a truncated path that does not exist — and grep still exits 0, so the `|| echo /missing` guard never fires and the break surfaces later as a confusing "no such file". Anchor on /bin/ and a following space-or-EOL. Verified against the real ExecStart, systemd's `{ path=... ; argv[]=... }` framing, and the adversarial -wrapper name; the guard still fires when there is no match (`||` deliberately stays bound to grep, not to the trailing sed, which would exit 0 on empty input). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PBDHCBttvBEHQJJH24MfxF --- tests/sessions-web.nix | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/sessions-web.nix b/tests/sessions-web.nix index 04a1a337..b20e632c 100644 --- a/tests/sessions-web.nix +++ b/tests/sessions-web.nix @@ -311,12 +311,12 @@ in assert 'src="/agent/main/"' not in ws, ws # The terminal dead end names the verb that actually revives it. - # Print the wrapper's path (see the grep -o note below) — running + # Print the wrapper's path (see the grep -oE note below) — running # the substitution as the command would run the WRAPPER instead. attach = machine.succeed( "{ systemctl show agent-web-terminal@agent --property=ExecStart " - "--value | grep -o '/nix/store/[^ ]*agent-box-attach' " - "|| echo /missing; } | head -n1" + "--value | grep -oE '/nix/store/[^ ]*/bin/agent-box-attach( |$)' " + "|| echo /missing; } | sed 's/ $//' | head -n1" ).strip() assert attach != "/missing", attach @@ -353,10 +353,17 @@ in # Phase 2. `grep -o ... || echo missing`: an empty substitution would # leave `grep -q` reading stdin — the backdoor shell then hangs the whole # test until the CI timeout (exactly how the rename was first caught). + # The pattern must end at a token boundary and name /bin/: Phase 3 made + # attachScript a writeShellScriptBin, so the store path CONTAINS + # "-agent-box-attach" as a directory as well. A pattern that can stop + # short yields a path that does not exist while grep still exits 0, so + # the `|| echo /missing` guard never fires and the failure surfaces + # later as a confusing "no such file". machine.succeed( "grep -q -- '-T hyperlinks' " "$({ systemctl show agent-web-terminal@agent --property=ExecStart --value " - "| grep -o '/nix/store/[^ ]*agent-box-attach' || echo /missing; } | head -n1)" + "| grep -oE '/nix/store/[^ ]*/bin/agent-box-attach( |$)' " + "|| echo /missing; } | sed 's/ $//' | head -n1)" ) # Working-directory picker (issue 131): the add-session form browses the