A self-contained AI coding-agent sandbox, packaged as a standalone Nix flake. It ships a full dev toolchain in an OCI container and runs Claude Code, Codex, OpenCode, and Pi inside it, isolated from the host.
agentboxImage— a Nix-built OCI image (no Dockerfile, no Homebrew).agentbox— a host-side CLI to drive the container (status,shell,logs,exec,opencode,pi,pi-web,pi-ui,pi-rpc,claude, on-demand services,pause/resume, andstart/stop/restart).nixosModules.agentbox/darwinModules.agentbox— run it as a systemdoci-containersservice (NixOS) or via manual management (macOS).
It has no private inputs. Codex comes from the stable nixpkgs base, OpenCode is pinned to its upstream flake, Pi comes from unstable nixpkgs, and Claude Code uses Anthropic's runtime installer.
| Agent | How it gets into the box | Toggle (default) |
|---|---|---|
| Codex | bundled in the image from nixpkgs (codex) |
settings.enableCodex (false) |
| OpenCode | bundled from the pinned upstream OpenCode flake | settings.enableOpencode (true) |
| Pi | bundled in the image from nixpkgs (pi-coding-agent) |
settings.enablePi (true; availability toggle) |
| Claude Code | installed at container start by Anthropic's native installer (self-updates) | settings.enableClaudeCode (false) + claudeCodeVersion |
All four are first-class: Codex, OpenCode, and Pi are baked into the image;
Claude Code is runtime-installed so its own updater keeps it current.
Each toggle sets the matching ENABLE_* env the entrypoint reads. Provide
credentials through the secret environmentFile (e.g. OPENAI_API_KEY for
Codex, CLAUDE_CODE_OAUTH_TOKEN for Claude Code).
Codex and Pi are interactive CLIs, so enableCodex and enablePi control
startup availability checks rather than removing their binaries from the image.
enablePiWeb separately controls Pi's ttyd browser TUI. For a native chat UI
without SSH or a terminal, enable piRpcApi and use agentbox pi-ui instead.
Pi also has a browser TUI at http://localhost:4097, enabled by
settings.enablePiWeb (default: true). It runs the exact wrapped Pi CLI in a
persistent tmux session, so terminal and browser use the same native settings,
credentials, sessions, skills, and immutable Agentbox extension. Set
PI_WEB_PASSWORD in the secret environmentFile; OPENCODE_PASSWORD is used
as a fallback. The Basic Auth username is pi. It binds to host loopback by
default on NixOS. The Darwin container binds all interfaces because its Docker
runtime may cross a Linux VM, and refuses to start Pi web without a password;
use a trusted TLS reverse proxy or VPN rather than exposing plain HTTP Basic
Auth directly.
Pi runs through an immutable wrapper that rejects -e/--extension, disables
extension discovery, and force-loads trusted Agentbox extensions in this order:
core integration, workflow, MCP, and final managed policy. Delegated workflow
jobs and RPC sessions re-enter the same wrapper. Pi's positional package/config/
auth management commands remain available, but their installed extensions are
not discovered by agent runs. The final policy fails closed
if /etc/pi/agentbox-policy.json is absent or invalid. Direct file targets are
canonicalized, so safe symlinks whose destinations are allowed work, while
aliases into sensitive paths and writes through aliases into managed/Nix-store
paths remain immutable denials. Obvious sudo/su and privilege-escalation calls,
privileged containers or host-root mounts, root deletion, and filesystem/device
destruction are also immutable denials. These run before configurable rules. Any
matching configured deny wins; otherwise the last matching allow or ask
wins per target. A multi-target call is denied if any target is denied, or asks
if none is denied but any target asks. The defaults allow workspace file work
plus todo/questions. Shell, web, MCP, and delegated tasks fall through to the
default ask, making them auto eligible when auto is enabled. The broad default
ask rule for bash, web_*, mcp__*, and task is included only when
defaultDecision != "ask", preserving their explicit asks if the default is
changed to allow or deny. Task prompts
and bounded string arguments from MCP/custom tools are policy targets. The
policy, workflow, and shared runtime JSON files are generated declaratively,
mounted read-only, and cannot be replaced by Pi or project configuration.
A Pi bash approval is approval of the submitted command string, not a command
sandbox. The policy rejects listed immutable operations and inspects obvious
nested sh/bash and interpreter -c/-e payloads, but shell parsing is
necessarily heuristic: generated scripts, alternate interpreters, expansion,
obfuscation, subprocesses, and time-of-check/time-of-use changes can evade
string inspection. Treat an approved shell call as arbitrary code execution
inside Agentbox. The security boundary is the container and the credentials,
host mounts, network access, Docker access, and cloud permissions granted to it;
scope each of those to the least privilege needed.
Pi adds web_search, web_fetch, code_diagnostics, code_navigation,
todo, question, and task. Diagnostics and navigation lazily start and reuse
language servers for Go, Nix, TypeScript/JavaScript, JSON, YAML, HTML, and CSS;
diagnostics fall back to the existing deterministic type/syntax checks and still
run automatically after write/edit alongside project tests. LSP output and
targets with disallowed file URIs are removed; free-form diagnostic and hover
text is bounded but is not a sensitive-content filter. Search uses anonymous
DuckDuckGo HTML or Jina Search with JINA_API_KEY; fetch only accepts public
HTTPS URLs and output is bounded.
Workflow defaults provide simple-task, explore, and general roles without
pinning a provider or model. Roles inherit the active Pi selection unless
configured otherwise. Todo/task state follows the current session branch,
delegation has concurrency/job/step/output limits, and question works in both
the TUI and an RPC-provided UI.
For headless ChatGPT/Codex login, run /login, select ChatGPT Plus/Pro (Codex), then choose Device code login (headless). Pi stores and refreshes
the result in ~/.pi/agent/auth.json.
services.agentbox = {
enable = true;
environmentFile = config.sops.templates."agentbox.env".path;
settings = {
enableClaudeCode = true;
enableCodex = true;
enableOpencode = true; # on by default
enablePi = true; # on by default
enablePiWeb = true; # browser TUI on http://localhost:4097
piConfig.workflow.maxConcurrency = 4;
piConfig.permissions.timeoutMs = 30000;
# Empty by default. Values below are interpreted as environment variable
# identifiers; keep the corresponding secret values in environmentFile.
piConfig.mcpServers.docs = {
transport = {
type = "http";
url = "https://mcp.example.com/rpc";
headers.Authorization = "DOCS_MCP_AUTH";
};
allowedTools = [ "search_*" ];
approval = "destructive";
};
};
};settings.piConfig.workflow types roles and delegation limits.
settings.piConfig.permissions types and bounds ordered
tools/patterns/decision rules, generated policy size, and approval timeout.
settings.piConfig.mcpServers supports stdio
(command, args, cwd, env references) and Streamable HTTP (url, header
references), per-server allow/deny tool globs, approval mode, and connection,
call, close, response, output, and tool-count limits. HTTP endpoints must use
public HTTPS unless transport.allowInsecureLoopback = true explicitly enables
a loopback endpoint. Identifier-shaped values cannot be distinguished from
literals by the module schema, so MCP secret values belong in environmentFile.
No MCP server is enabled by default.
Automatic permission classification is opt-in and starts off in every session.
Configure a classifier to make the toggle available, for example
openai-codex/gpt-5.3-codex-spark:
services.agentbox.settings.piConfig.permissions = {
defaultDecision = "ask";
auto = {
enable = true;
provider = "openai-codex";
model = "gpt-5.3-codex-spark";
timeout = 30000; # milliseconds, allowed range 1..300000
};
};The openai-codex provider uses Pi's ChatGPT/Codex login. For an OpenAI API key,
use the openai provider and an available model such as gpt-4.1-mini instead.
Model catalog presence does not guarantee account access; verify a live request
before deployment. Unsupported models immediately fall back to human approval.
Setting auto.enable = true makes auto mode available; it does not activate it.
Use the web header's Auto: off/on button or /auto on and /auto off in Pi.
/auto toggles, and /auto status reports the current mode. With the toggle off,
Pi follows the original managed permission rules without calling the classifier.
The setting belongs to one Pi process, is not inherited by workflow children,
and is not persisted. Children continue forwarding required human approvals.
The auto defaults are enable = false, provider = "", model = "", and
timeout = 30000; select a provider/model available in your Pi model registry.
Only unmatched defaultDecision = "ask" decisions are auto eligible. An effective
explicit matching ask requires a human even when auto is enabled. The last
matching allow or ask still wins per target, and any matching deny always
wins. A multi-target call with an effective explicit ask requires a human unless
any target is denied. Existing allows and denials remain unchanged, including
immutable sensitive-path and managed-write denials; neither auto nor human
approval can override denials. Rules that resolve to allow bypass classification.
With auto disabled, default asks also require human approval. The public decision
schema remains allow / ask / deny.
Classification uses a direct, tool-free ctx.modelRegistry.complete call with
existing Pi authentication, not a delegated agent or separate auth setup. Evidence
shared with the classifier provider includes all user text and up to 32 recent
prior tool calls across the active session branch, plus the current tool call.
The history cutoff uses toolCallId to exclude the pending current call and any
siblings after it. Assistant prose/thinking and tool results are excluded.
Prior tool-call history has a 12000-character subbudget within the fixed
32000-character evidence maximum. Whole oldest calls are omitted to fit, and a
history-omitted flag discloses omissions to the classifier. User text and the
current call are never truncated; evidence that cannot fit requires human approval.
Non-text user history (including screenshots) also falls back to human approval;
it is never silently discarded as evidence or treated as permission to proceed.
Transcript tool calls are untrusted proposals, not proof of execution or
authorization.
The classifier requests a 128-output-token maxTokens limit.
Pi's Codex provider ignores that token limit; Codex requests use low reasoning
effort instead. The timeout still applies, and verdict text over 256 characters
is rejected after completion, not stopped during generation. Failure, timeout,
malformed output, oversized input or output, and missing context all fail
classification. Any classifier denial or inability to classify requests human
approval for that same call immediately; there is no silent-denial budget or
recovery threshold. The classifier cannot turn a default ask into a hard deny.
A human answer applies to that call only. Denied, cancelled, timed-out, or
unavailable human approval still blocks execution; headless calls without an
approval transport fail closed. Explicit asks and auto fallbacks use the same
local approval UI or existing delegated-child approval transport. Rule denials
are never overridden, and caller/session cancellation does not open a new prompt.
Session start, switch, fork, tree navigation, and shutdown cancel stale checks and reset auto to off. Turning auto off cancels an in-flight classification; a late classifier allow cannot authorize the call after the toggle changes. Changing the toggle does not answer or dismiss an existing human approval.
This is not a sandbox and does not provide a security guarantee equivalent to Claude mode. MCP retains its independent per-server approval checks even when the Pi policy classifier allows a call.
The optional settings.piRpcApi service exposes authenticated HTTP/SSE
supervision and a native static chat UI at / on the same port (default: 4098).
The UI is bundled from rpc-runtime/web and always served when RPC is enabled;
there is no separate UI service, port, or enable option. agentbox pi-ui shows
the chat URL; agentbox pi-rpc continues to check /health/ready.
agentbox pi-web still refers to the shipped ttyd TUI, not this chat UI. If
moving RPC to port 4097, set enablePiWeb = false to avoid a port collision.
RPC is disabled by default and binds to 127.0.0.1 on
both NixOS and Darwin. Some Darwin Docker runtimes cannot route host requests to
container loopback; agentbox pi-rpc still checks readiness inside the
container. To make it host-accessible there, set bindAddress = "0.0.0.0" and
explicitly acknowledge plain-HTTP exposure with
allowInsecureRemoteAccess = true, then restrict access with a trusted TLS
reverse proxy or VPN.
Only an environment variable name for a bearer token or its hash is placed in
managed JSON. To reuse an existing runtime password, configure
auth.tokens = [ { tokenEnv = "OPENCODE_PASSWORD"; } ];. The supervisor hashes
that raw value at startup; it is not a Nix string containing the password.
tokenEnv defaults to null. When set, the modules omit sha256Env entirely;
otherwise the existing sha256Env = "PI_RPC_TOKEN_SHA256" default is unchanged.
For the hash-based alternative, put the lowercase SHA-256 digest in the secret
environmentFile:
TOKEN="$(openssl rand -hex 32)"
printf 'PI_RPC_TOKEN_SHA256=%s\n' "$(printf %s "$TOKEN" | sha256sum | cut -d' ' -f1)"services.agentbox.settings.piRpcApi = {
enable = true;
auth.tokens = [
{
sha256Env = "PI_RPC_TOKEN_SHA256";
scopes = [ "*" ];
}
];
allowedOrigins = [
"http://localhost:4098"
"http://127.0.0.1:4098"
"https://agent.example.com"
];
profiles.default = {
cwd = "/workspace";
sessionDir = "/home/agent/.pi/agent/sessions";
# Optional git/gh access, only if GH_TOKEN exists in environmentFile.
env.GH_TOKEN = "GH_TOKEN";
};
limits.maxBodyBytes = 8 * 1024 * 1024; # JSON includes base64 image overhead.
limits.maxRecordBytes = 16 * 1024 * 1024;
};Open the UI directly in a browser, enter the raw bearer token/password (not its digest), and select an administrator-approved profile. The browser keeps the password in page memory, not localStorage or a URL; reload or log out to clear it. Allow the exact browser origin, including its scheme and port, even when the UI and API share an origin. Use TLS or a trusted VPN for remote access.
Profiles fix the working directory and persistent Pi session directory on the
server. The browser cannot choose arbitrary filesystem paths. Native Pi history
is separate from OpenCode: there is no OpenCode conversation conversion or
automatic credential migration. Existing Pi auth/settings under ~/.pi/agent
remain available. Provider environment credentials, if needed, must be explicitly
mapped in the profile; a Claude Code token is not assumed to be a Pi provider key.
RPC authentication secrets are excluded from the Pi child environment.
profiles, command allowlists, token scopes, origins, and resource/time limits
are typed. Session-switch/fork/clone/new-session, direct bash, HTML export, and
raw UI-response commands are always forbidden by the supervisor. Extension UI
dialogs use the checked /ui endpoint. Run agentbox pi-rpc to show the URL and
check health; see rpc-runtime/README.md for routes and scopes.
The container entrypoint bounds the API log and caps each backoff-controlled
restart run; the API runtime separately supervises and bounds each
pi --mode rpc child.
Bearer authentication does not isolate same-UID code inside the container:
agents with that trust level can inspect processes, credentials, and session
files. Use separate containers and UIDs for mutually untrusted tenants.
Alternatives considered: agegr/pi-web and
jmfederico/pi-web are fuller applications
with their own Pi SDK/session backends, not drop-in clients for Agentbox's
authenticated RPC supervisor. The former upstream packages/web-ui was removed;
this integration instead keeps Agentbox's pinned, wrapped pi --mode rpc path
and managed policy boundary. See upstream Pi.
| Group | Packages |
|---|---|
| Dev tools | git, neovim (pre-configured), tmux, htop, tree, ripgrep, fd, fzf, jq, yq-go, curl, wget, unzip, gnumake, pkg-config, gcc, nix |
| Languages | go, nodejs 22, bun, python 3.12, uv |
| AI CLIs | codex, opencode, pi, Pi MCP runtime, Pi RPC supervisor (Claude Code is runtime-installed) |
| Language servers | gopls, nil, typescript-language-server, yaml-language-server, vscode-langservers-extracted |
| Formatters | nixfmt (RFC), prettier |
| Cloud CLIs | awscli2, kubectl, kubernetes-helm, google-cloud-sdk (+gke-gcloud-auth-plugin), docker-client |
| VCS / scanning | gh, gitleaks, openssh |
Base core/system utilities are tagged REQUIRED vs CONVENIENCE inline in
image.nix — readline and expect were dropped from the base as unneeded.
The image has no apt/dpkg (it isn't Debian). Two knobs cover the usual "let me just install a thing" workflow, both on by default:
-
settings.hardening.enableSudo— the entrypoint stages a setuid-rootsudounder/run/wrappers/binat boot (Nix store paths can't carry setuid bits, and there's nosecurity.wrappersinside the container, so plain/bin/sudocan't elevate).agentgets passwordless sudo. Turning onhardening.noNewPrivilegesdisables it at the kernel level regardless. -
settings.enableNix— bakes thenixCLI in, registers the store DB (buildLayeredImageWithNixDb), and starts a rootnix-daemonat boot so the unprivileged agent can install throwaway packages:nix profile install nixpkgs#ripgrep # persists for the container's life nix shell nixpkgs#hello -c hello # ephemeral, one command
These land in the container's writable layer and are lost on recreate — bake anything permanent into the image instead (
extraPackages, or the package lists inimage.nix). Language-level installs (bun add -g,go install,uv tool install,cargo install) work without either knob.
nix build .#agentboxImage # -> ./result (a docker image tarball)
docker load < result # loads agentbox:latest.github/workflows/build-image.yml evaluates the flake, builds every extension
and runtime test derivation, then builds the image on every push and PR. On
pushes to main / v* tags it publishes to GHCR
(ghcr.io/<owner>/agentbox:latest and :<sha> / :<tag>). Building needs no
secrets (nixpkgs-only); only the push uses the built-in GITHUB_TOKEN.
# flake.nix (consumer)
{
inputs.agentbox.url = "github:<you>/agentbox"; # or path:./agentbox
outputs = { self, nixpkgs, agentbox, ... }: {
nixosConfigurations.host = nixpkgs.lib.nixosSystem {
modules = [
{ nixpkgs.overlays = [ agentbox.overlays.default ]; }
agentbox.nixosModules.agentbox
{
services.agentbox = {
enable = true;
user = "you";
settings.enableCodex = true;
# image/package default to pkgs.agentboxImage / pkgs.agentbox
};
}
];
};
};
}For macOS swap nixosModules → darwinModules and use darwin.lib.darwinSystem
(set image to a Linux-built image copied onto the Mac).
Both platform modules share one option schema (modules/common-options.nix,
covering hardening, enableDocker, enableNotification, cloud creds, the
dockerProxy, autoCloneRepos, …). Each platform module adds only its
OS-specific top-level options and config body (systemd vs launchd/manual).
Agentbox can expose an opt-in request inbox for a separate host collector. It does not upload transcripts and never receives object-storage credentials.
services.agentbox.settings.historyArchive = {
enable = true;
hostId = "home-macbook";
};This creates only <dataDir>/history-sync/requests and mounts that directory at
/home/agent/.agent-history/requests. The rest of a collector's spool should be
host-only and must not be mounted into Agentbox.
Explicitly invoke /archive-conversation in Claude Code or OpenCode, or
/skill:archive-conversation in Pi. The command
creates a bounded intent; the next Claude Stop or terminal OpenCode idle event
resolves it with the native session ID. The request timestamp is the content
cutoff, so delayed resolution must not include later messages. Archiving more of
the session requires another explicit request. Codex support is intentionally
deferred until its skill invocation can be bound to a native thread ID before
the completion callback.
Pi's extension resolves its intent on the next agent_end event using Pi's
native session ID and JSONL session path.
Resolved files contain identifiers, timestamps, event type, and local source context, but no message content. They are untrusted input: a host collector must still validate repository remotes and allowlists before reading or uploading a transcript. The request is an opt-in user interface, not an authentication boundary: code running inside Agentbox can forge or delete inbox files, just as it can alter its local transcript. A collector must derive host and trust-domain identity from host configuration rather than trusting request fields.
An optional virtual desktop bundles Xvfb, Openbox, Chromium, fonts, D-Bus,
xdotool, scrot, x11vnc, and noVNC. It controls applications inside the
container, not applications on the host desktop. Nothing starts at boot.
services.agentbox.settings.desktop = {
enable = true;
# Defaults; override if these ports are already in use.
webPort = 6080;
vncPort = 5900;
resolution = "1440x900x24";
shmSize = "1g";
};The NixOS module includes the desktop in its default image. If you supply a
custom image, build it with agentboxImage.override { withDesktop = true; }.
Darwin also requires a prebuilt Linux image with that flag. Rebuild and recreate
the container after changing image or container settings; service start alone
does not install packages. Consumers pinned to an older Agentbox revision must
update that input before enabling the option.
agentbox service start desktop
agentbox service status desktop
agentbox service restart desktop
agentbox service stop desktopService status reports the tmux session, not desktop readiness. The launcher
prints agentbox-desktop: ready after X, D-Bus, VNC, and noVNC are reachable;
inspect its output inside the container with
tmux capture-pane -pt service-desktop. Stop tears down the supervised desktop
process groups; restart waits for cleanup. Browser windows close when their X
display disappears, but independently launched applications are not supervised
by this service and may retain background processes.
On NixOS, open http://127.0.0.1:6080/vnc.html on the host, or forward it from
your own machine:
ssh -N -L 6080:127.0.0.1:6080 your-nixos-hostAccess trusts local users. VNC and noVNC have no password and bind only to IPv4 loopback; X11 uses a private authentication cookie and disables TCP. Agentbox uses host networking, so other local host processes can access these ports. Never publish them publicly or forward them through an unauthenticated proxy. On macOS, loopback belongs to the Docker runtime's Linux network context; reach that context through the runtime's supported forwarding mechanism.
The module exports DISPLAY=:10, XAUTHORITY, and DBUS_SESSION_BUS_ADDRESS
in the container environment. Pi RPC profiles deliberately do not inherit that
environment; explicitly grant desktop access to each desired profile:
services.agentbox.settings.piRpcApi.profiles.default.env = {
DISPLAY = "DISPLAY";
XAUTHORITY = "XAUTHORITY";
DBUS_SESSION_BUS_ADDRESS = "DBUS_SESSION_BUS_ADDRESS";
};From an Agentbox shell, for example:
chromium --user-data-dir=/workspace/.browser-profile http://127.0.0.1:3000
xdotool getactivewindow
scrot /workspace/desktop.pngKeep browser profiles out of version control; they may contain authentication
data. Chromium is not automatically launched. Headless tests do not require
starting the desktop; Playwright can use executablePath: "/bin/chromium".
The existing frontend smoke test accepts CHROMIUM_EXECUTABLE=/bin/chromium.
Install Playwright separately and verify compatibility with packaged Chromium;
this option does not supply Playwright, Firefox, or WebKit. In Playwright, set
chromiumSandbox: true to retain sandboxing (its default is false).
Chromium requires working unprivileged user namespaces. The wrapper disables
only the unavailable Nix-store setuid-helper fallback, not Chromium's namespace
or seccomp-BPF sandbox. Check unshare -Ur true as the agent user in the actual
container. If the host kernel, Docker seccomp profile, or other security policy
denies namespace creation, Chromium will fail with No usable sandbox!.
This option does not relax those policies, add privileged mode, or pass
--no-sandbox. Review a narrowly scoped browser-compatible policy on your host,
or use a separate sandbox-capable browser container. The bundled desktop tools
can still operate without Chromium.
noVNC is a human viewer, not an LLM tool integration. Agents can run browser scripts or the bundled screenshot/input commands; structured browser MCP or pixel-based computer-use tools must be configured separately. Browsing untrusted sites in a container that also holds agent credentials is less isolated than a dedicated browser container.
Runtime verification (the desktop smoke test needs an unused display :10):
nix build .#agentbox-desktop
bash tests/desktop.sh
bash tests/desktop.sh --real ./result
bash tests/desktop.sh --sandbox ./resultThe sandbox check reports a rendering skip when user namespaces are unavailable.
Beyond the four bundled agents, this standalone ships no others. To add your own you have four generic hooks, none of which require forking:
| Hook | Purpose |
|---|---|
agentboxImage.override { extraPackages = [ … ]; } |
bake the binary into the image |
services.agentbox.extraEnvironment |
pass its config/env vars |
services.agentbox.extraVolumes |
mount its data |
services.agentbox.extraActivation |
stage files on the host at activation |
services.agentbox.bootScripts |
run it at container boot (as the agent, backgrounded) |
services.agentbox.onDemandScripts |
install a service that starts only through the Agentbox CLI |
bootScripts is what lets a daemon-style agent actually start — the entrypoint
launches each snippet in the background and keeps the container alive while it
runs (it also honors a single AGENTBOX_BOOT_CMD env var, or any executables
mounted into ~/.agentbox/boot.d).
nixpkgs.overlays = [
agentbox.overlays.default
(final: prev: {
agentboxImage = prev.agentboxImage.override { extraPackages = [ myAgent ]; };
})
];
services.agentbox = {
extraEnvironment.MY_AGENT_PORT = "1234";
onDemandScripts.my-agent = ''
exec my-agent serve --port "$MY_AGENT_PORT"
'';
};Manage the service without restarting the container:
agentbox service list
agentbox service start my-agent
agentbox service status my-agent
agentbox service restart my-agent
agentbox service stop my-agentagentbox service list includes both boot-time and on-demand services, showing
each service's startup mode and current active or inactive status. The other
service actions manage on-demand services only.
For a short break, agentbox pause freezes every process while preserving tmux
sessions; agentbox resume continues them. agentbox stop tears the container
down and, on NixOS, also stops Docker when no other containers are running.
The NixOS image-load unit records the source store path and skips importing an
unchanged agentbox:latest image after reboot.
The generic hooks keep additional agents out of this repository; everything specific to an added agent lives in your own config.