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, session messaging, MCP, Codex accounts, OpenAI Fast,
Cursor subscription provider, AGENTS.md /init (pi-agentsmd), upstream Goal
(parent sessions only), 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, interpreter -c/-e payloads, heredoc bodies, and embedded
literal paths. Core integration and final policy share the sensitive-path matcher:
.env and .env.* (including custom suffixes) are denied before any approval,
while .env.example, .env.sample, and .env.template remain usable. Literal
secret references are checked independently of the executable name, including
Python, Node, Ruby, Perl, and PHP. Shell parsing is nevertheless 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. RPC sessions additionally use a per-profile Bubblewrap filesystem
sandbox by default in the container modules; see
RPC filesystem sandbox
for grants, authentication limitations, and Linux namespace prerequisites.
Interactive Pi launches are not covered by that RPC sandbox.
The outer 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.
The image bundles upstream pi-agentsmd 0.2.1,
pinned by hash and force-loaded for CLI, browser, RPC, and delegated sessions.
Install telemetry from the published package is stripped so the reviewed extension
does not phone home. No runtime pi install is needed. In a trusted project:
/init
/init --force
/init asks the active model to inspect the repository and create AGENTS.md.
If the file already exists, pass --force (or -f) to reconcile it. The package
itself only injects a prompt; the model writes the file through normal tools.
Project trust is required first (/trust). Rebuild/redeploy the image and start a
new Pi process to pick this up; pi update does not change the immutable bundle.
Run nix build .#pi-agentsmd-test .#pi-codex-wrapper-test to verify offline load,
command registration, and wrapper force-loading without model calls.
The image bundles @rahularya01/pi-cursor
1.4.39 and its protobuf runtime, pinned by hash and force-loaded for CLI and
browser sessions. No runtime package install or separate model API key is needed.
In Pi, run:
/login cursor
/cursor.models
Open the sign-in URL in your browser; the extension polls Cursor to complete
login, so no callback port or host Cursor credentials need to be mounted into
the container. Credentials persist in Pi's existing ~/.pi/agent/auth.json.
Select an available model with /model cursor/<id>. /cursor.usage shows plan
usage, and /cursor.doctor shows provider diagnostics.
This is an unofficial integration using Cursor's reverse-engineered protocol;
Cursor can change it independently. Available models and usage limits depend on
the signed-in account. Run nix build .#pi-cursor-test to verify offline provider
loading alongside the Agentbox guards and pi-post, without credentials or model
calls.
The image also bundles pi-post 0.8.1, pinned by hash and force-loaded alongside the Agentbox extension for CLI and browser sessions. No runtime install is needed. Pi gets two model tools:
list_sessionsdiscovers registered sessions, their addresses, presence, and resume handles.send_messagesends text to a session name, address, session ID, or directory. Live sessions receive it automatically; offline sessions receive queued mail when resumed. Ambiguous targets fail instead of guessing.
Archived sessions are excluded, including with all: true or /sessions all.
Agentbox patches pi-post to honor the native Pi UI's .archived-<session-id>
marker beside the JSONL session file. Sending by name, ID, directory, or a known
address fails for an archive; queued or in-flight mail cannot wake it, including
if its file is opened again. The CLI applies the same filter. Ordinary closed
sessions can still receive queued messages. Records from an older messaging
installation without a session-file location stay hidden until the session
re-registers, so unknown archive state cannot bypass this rule.
/sessions lists peers and /inbox previews queued messages. Shell tools and
hooks can use the bundled pi-post CLI. Registry and mailbox data persist in
~/.pi/agent/post on the existing Pi volume (or the supervisor's PI_POST_DIR).
For sandboxed RPC sessions, a supervisor-owned broker accesses that storage via a
private inherited channel. No shared messaging directory or sibling history is
mounted into the agent sandbox. Presence uses supervisor connection leases, not
PIDs from isolated namespaces. A sandbox-local relay supports child agents and
the CLI; its raw sends are restricted to the same main session. Cross-main
messages must use the main session's send_message tool and its approval path.
Interactive/unsandboxed sessions retain the original local backend. Messaging
does not create subagents.
After upgrading, restart the RPC service and open each affected session once to register it with the broker. A name absent from the isolated pre-upgrade registry is not evidence that its session is archived. Private tmpfs-only registrations from old sandbox processes cannot be recovered after those processes exit. Archived sessions remain excluded; the broker never opens/unarchives or resumes a session just to send mail. Broker failure is an error, not a fallback to a private local registry.
Agentbox's Pi instructions allow a main session and its child, sibling, and
nested subagents to communicate and reply without asking for approval by
default. Agents are instructed not to spam: no acknowledgment-only,
standing-by, or progress-ping messages; prefer one message when coordination
starts or finishes. Agentbox tightens pi-post's inbound loop guard (identical
body within 30s dropped; at most 4 deliveries per sender per 60s) and adds
outbound caps that fail send_message before deposit (8 messages per 60s,
4 per target). Communication with unrelated main sessions always asks a
human for approval — Auto mode and the classifier do not silently approve
cross-main messaging, so concurrent sessions cannot wake each other under
Auto-on. The managed policy still permits same-main messages by default when
every target shares the sender's main session ID, inherited by workflow
children. Explicit policy asks and denials still apply. Incoming messages are
labeled as peer context and cannot grant permissions. Receipt is automatic
by default (PI_POST_INBOUND=accept); users can explicitly select ask or
refuse. Set PI_POST_RESUME=stage to hold offline mail for the first user
prompt instead of waking the session on resume.
Run nix build .#pi-agentbox-extension-test .#pi-post-test to check the policy
and live/queued delivery using the bundled Pi loader without model calls.
Agentbox bundles upstream @narumitw/pi-goal 0.53.1,
characterized against Pi 0.87.1, without reimplementing its workflow.
It continues one session-owned objective at settled idle boundaries until complete,
paused, blocked, waiting, or limited. It does not start a goal automatically in a
fresh session.
/goal --tokens 100k fix the failing test and verify it
/goal status
/goal pause
/goal resume
/goal edit verify the smaller fix first
/goal clear
Bare /goal opens the TUI manager (including Settings). In native Pi RPC/chat,
bare /goal and status emit notifications instead; direct commands work via
RPC prompt, with replacement confirmation through the existing UI protocol.
Print/JSON status routes are unsupported upstream. pause aborts current Goal
work, resume preserves cumulative usage but starts a new automatic-work epoch,
edit preserves usage, and clear removes the goal without cancelling unrelated
work. Replacing an unfinished goal requires confirmation. Completion requires
goal_complete with the current goal ID and evidence; goal_blocked reports a
repeated impasse and goal_wait waits for an arranged external wake source.
These checks are model guardrails, not independent proof of task completion.
Defaults are 25 automatic model responses per epoch and 3 repeated
empty/identical tool-free automatic runs before pausing. The response cap does
not charge user-triggered kickoff/resume/edit or ordinary user runs. An attempted
tool call resets the no-progress heuristic. Optional --tokens is cumulative
assistant token usage on the current session branch, including cached tokens;
it can overshoot by the final model call and is not a dollar or whole-task cost
cap. Delegated task usage and other nested/tool usage are not included. Goal
state persists with the session across reload/resume and compatible forks; a new
session does not inherit it. Review restored active goals before leaving Pi
unattended.
Optional settings live in ~/.pi/agent/pi-goal.json (or Pi's configured agent
directory), not project configuration. The absent file remains absent. Settings
use toolVisibility: "after-first-goal", rpc.enabled: false, and
continuationLimits: { automaticTurns: 25, noProgressTurns: 3 } by default.
External edits require /reload; the TUI Settings menu applies changes live.
Explicit null removes the corresponding limit—use cautiously. Invalid settings
are retained, warned about, and fall back to defaults. The default-off Managed
run RPC setting is an extension event-bus cooperation protocol, not the native
Pi RPC transport, authentication, or permission authorization.
Permissions and scope: Goal never enables /auto or grants tool approval.
All its model tool calls, including goal_*, still pass the final managed policy
and may ask or be denied. No policy allow rules are added. Workflow children
(PI_WORKFLOW_CHILD=1 or any PI_WORKFLOW_APPROVAL_VERSION presence) do not load
Goal at all: their role tool scopes, recursion guard, step limits, and parent
approval channel remain unchanged. Parent RPC sessions do load it. Agentbox's
workflow is not an upstream Workflow Mutex participant; no cross-extension
mutual-exclusion guarantee is claimed. Goal budgets do not replace task limits.
Like all Pi extensions, this trusted code runs with full process permissions:
policy tool hooks do not sandbox extension code. Reviewed behavior includes
session entries, local settings reads/atomic menu saves, legacy
pi-goal-state.json cleanup on clear, timers, prompt injection, and active-tool
visibility changes. The reviewed Goal runtime has no direct shell execution,
network client, or credential access; model work uses existing Pi tools and
providers. Its TUI dependency also runs in-process. Container isolation and
least-privilege credentials remain the security boundary.
Installation/source: goal-runtime/ uses buildNpmPackage, the checked-in
npm lockfile and fixed npmDepsHash; no runtime pi install, floating npm
resolution, or dependency lifecycle scripts run. The published Goal dist/
(including lazy chunks) is used unchanged, with @narumitw/pi-tui-kit 0.57.0,
grok-mermaid 0.2.3, and highlight.js 11.12.0 locked transitively. Pi and
TypeBox peers come from Pi's loader, not another npm Pi installation.
- Upstream Goal commit:
86a5eda01aaf271ff0865b36d1a711dc972232e9. - npm source:
https://registry.npmjs.org/@narumitw/pi-goal/-/pi-goal-0.53.1.tgz. - Goal tarball SHA-256:
sha256-t9acGqk7ZsLc/cl+fJ8jSNtc3/ulcnaCOSDPww9N43U=. - Complete npm dependency cache:
sha256-ccT5bpqIaD5rNobSuF7G+5GEbJjILM/XzivJbPEtgBQ=; each tarball's SHA-512 integrity is recorded ingoal-runtime/package-lock.json.
Rebuild/redeploy the image and start a new Pi process to install this integration;
pi update does not update the immutable bundle. The host package.nix CLI
already launches the container wrapper and needs no separate installer.
Focused offline checks (also run in CI):
nix build .#agentbox-pi-goal-runtime .#pi-goal-extension-test .#pi-agentsmd-test .#pi-codex-wrapper-testThe extension check uses Pi 0.87.1's real Jiti loader, lazy TUI/RPC routes, managed policy approvals, stale goal IDs, pause/resume/clear, token exhaustion and restored response caps. The wrapper check verifies policy-last ordering, immutable entry paths, rejection of user extensions, and exclusion from child sessions.
Ask Pi to call task with child-only overrides, for example:
{"role":"general","skill":"review","prompt":"Review the current diff","provider":"anthropic","model":"claude-sonnet-4-5"}skill is the bare name of a skill discovered by the current Pi session (not a
path or /skill:name command). Its full file is loaded into the child's prompt,
with its source directory for relative references; prompt supplies optional
skill arguments. Without skill, a non-empty prompt is required. Unknown,
unreadable, empty, or oversized (>1 MiB) skill files fail before children start.
Discovery uses Pi's existing trust and name-collision decisions, including
package/project skills, rather than rescanning directories. Only use trusted
skills: they can instruct the child to run tools with your permissions.
Each field resolves independently: job override → role config → active parent. For a provider change, usually supply both provider and model to avoid inheriting an incompatible model. Pi in the child validates model availability/authentication. The parent model and thinking selection are never changed; role thinking, step limits, approval handling, and recursion restrictions remain in force.
Batch jobs accept the same fields inside each entry (top-level single-job fields
cannot be mixed with jobs):
{"jobs":[{"role":"explore","prompt":"Find relevant tests","model":"gpt-5.4"},{"role":"general","skill":"review","provider":"anthropic","model":"claude-sonnet-4-5"}],"concurrency":2}resume also accepts these fields. Overrides apply only to that invocation;
omitting them on a later resume resolves role/parent defaults again. Resume still
requires the same role, working directory, and current-branch task ID. To run a
skill again on resume, explicitly pass skill again.
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.
For two Codex accounts in one Pi, use the built-in ChatGPT Plus/Pro (Codex)
(openai-codex) for personal, then run /login for Codex Work (codex-work). Use the
appropriate browser account/profile for each OAuth flow; headless device-code
login is also available. Select /model openai-codex/gpt-6-sol or
/model codex-work/gpt-6-sol (or another model from /model). The work alias has its
own stored login and native OAuth refresh; it neither replaces nor copies the
existing openai-codex login. Use interactive /login and /logout for the
work alias, not the wrapper's extension-free pi auth command.
The work alias loads the native provider's local catalog (cached remote models
and models.json metadata) before CLI/default/session model resolution, then
mirrors the live registry after startup. Credentials and configured auth headers
remain separate; catalog bootstrap performs no network requests or token refresh.
Agentbox patches Pi 0.87.1 to batch startup provider registrations before one
awaited catalog/auth refresh, preventing intermittent account fallback on resume
and missing aliases in new-session model lists. The web model picker loads after
startup synchronization, retries transient read failures, and reloads on Refresh
or reconnect without restarting Pi. Agentbox also patches RPC catalog reads to
reload the local model cache: run agentbox exec pi update --models on the host,
then click Refresh in the web UI to discover newly published models in an existing
process. The picker itself does not force a network catalog refresh. Rebuild and redeploy the image to pick up the
patched executable and web assets; browser refresh alone cannot update Pi in an
older image. Start a new session or resume into a new process after deployment.
Reply badges show the model/provider recorded on each old reply.
The Pi-only nixpkgs-unstable pin supplies Pi 0.87.1. Agentbox also backports
GPT-6.1 Sol and GPT-6.1 Terra into its offline OpenAI API and Codex catalogs,
plus OpenAI's chat-latest, gpt-5.6-cyber, gpt-daybreak-blue-latest, and
gpt-daybreak-red-latest, using provider-specific metadata from Pi's OpenAI catalog
and Codex catalog. Native catalog
refreshes remain authoritative. Select /model openai/gpt-6.1-sol,
/model openai-codex/gpt-6.1-sol, or /model codex-work/gpt-6.1-sol.
Codex Sol 6.1 and Terra 6.1 use the published 500,000-token context; the OpenAI
API variants use 272,000. The CLI and RPC UI share this catalog; RPC Refresh
fetches current published metadata. Codex Work mirrors the native Codex catalog,
so no separate model list is needed. Authentication and
account entitlements still determine which models you can use. Rebuild/redeploy
the image to upgrade Pi; pi update --models refreshes metadata only.
When customizing agentboxImage.override, retain its bundled pi-coding-agent:
replacing it with another nixpkgs package bypasses Agentbox's Pi pin and model/RPC
patches, even if the Agentbox flake input itself is current.
OpenAI Fast mode is a
paid service tier, not a lower thinking level or a different model. Enable it
while idle using Fast beside Auto in the active session's toolbar, or use /fast on, /fast off,
and /fast status in Pi. The managed extension sets service_tier: "priority"
in the actual request payload for native OpenAI, personal Codex, and Codex Work;
custom/proxy providers are excluded. It defaults off, is not a saved default,
and resets on model selection, session start/switch/fork. Each managed subagent
inherits the parent's effective Fast mode when it actually spawns, including
resumed jobs; queued jobs read the live mode when their slot opens. Unsupported
child providers ignore it. Changing the parent does not retroactively change
already-running children. Each session reports its own mode; no global/browser
default is changed. The web control waits for reported metadata rather than assuming
a command succeeded. Fast mode may consume extra money/credits; availability,
actual tier, and speed remain server/account dependent. Pi's token-cost
estimates may not reflect the premium tier—consult provider billing.
Privacy: switching accounts in a conversation sends its existing context,
including prior messages and tool results, to the newly selected account.
Separate credentials are not separate workspaces or conversation sandboxes.
Start a new session before crossing personal/work boundaries. The work alias is
a managed extension, also used by delegated and RPC sessions; do not install
user extensions or duplicate credentials in models.json.
Pi slash skills (/skill:commit optional arguments) embed the skill instructions
once and substitute $ARGUMENTS (also $ARGUMENT) literally with the supplied
arguments, or an empty string when omitted. Arguments are also appended for
skills without placeholders. The agent is instructed to use the embedded skill
without rereading its Markdown; referenced files can still be loaded as needed.
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 = 1800000; # default: 30 minutes
# 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.
The task tool runs independent child Pi processes concurrently, with role-based
models/reasoning, progress, cancellation, step limits, approval forwarding, and
branch-scoped resume IDs. Children share the working directory, not the
parent conversation; give explicit context and assign disjoint files to writers.
Prefer parallel exploration/review/tests, then reconcile and verify the results.
Recursive delegation is disabled.
workflow.maxConcurrency (default 4, range 1..16) is the ceiling across
all overlapping task calls in one session, excluding the parent. Excess jobs
wait in a cancellation-aware FIFO queue. task.concurrency can further lower a
single batch's parallelism; maxJobs (default 8) limits batch size, not the
session's lifetime task count.
Use web Settings → Session subagent concurrency, or:
/agentbox-subagents: show effective cap, managed ceiling, and running/queued jobs./agentbox-subagents 2: lower this session branch's cap while idle./agentbox-subagents reset: restore the managed default while idle.
The override is saved in the conversation branch and restored on resume/tree navigation; it is not a global default. Deploy/rebuild Agentbox and start a new Pi process to load these changes.
This is not an account-wide provider throttle: other chats and the parent can still make requests, and all children on one subscription share its usage. Reduce concurrency if rate-limited; a higher cap does not grant provider capacity. There is no automatic workspace/worktree isolation or cross-process file lock, so never assign overlapping writes. More agents only help when work is independent; use narrow tasks and concise evidence-based summaries to limit cost and context.
settings.piConfig.permissions types and bounds ordered
tools/patterns/decision rules, generated policy size, and approval timeout.
Human policy approvals, including forwarded child approvals, default to 30
minutes (timeoutMs, range 1..1800000 milliseconds); expiry still denies the
call. The question tool has no reply timeout and waits until answered or
cancelled. These settings do not change classifier or MCP call timeouts.
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.
/auto on means auto-approve, not “ask a classifier.” Like
OpenCode auto mode, it allows
both default and explicit ask decisions without prompting. No model, login,
conversation evidence, or classifier timeout is involved. Managed deny rules,
immutable safety guards, invalid policy, and cancellation still block execution.
Use deny, not ask, for actions that must remain forbidden in auto mode.
Enable availability declaratively:
services.agentbox.settings.piConfig.permissions.auto.enable = true;Fresh sessions start off unless explicitly saved as a global default (see below).
Use /auto on, /auto off, /auto status, or the web header's Auto: off/on button.
An explicit /auto choice (including review) is saved with that native conversation,
not in global settings or browser storage. Restarting Pi or resuming from
PICK UP WHERE YOU LEFT OFF restores its last choice; switching sessions loads
that session's own choice. Tree navigation cannot resurrect an older Auto-on after
an explicit off. Forks/new sessions and workflow children do not inherit a parent's
saved authorization. Old conversations without a saved choice resume off; a global
default alone is not copied into their history. Set Auto once to retain it.
Turning Auto off revokes live authorization immediately and saves off for later resumes; it does not answer an already open approval dialog. Managed policy must still permit Auto. Invalid saved entries fail closed. If saving fails, live Auto is disabled and an error warns that restarting may restore an older saved choice. Pi normally flushes a brand-new conversation to disk after its first assistant reply; retention before that point follows Pi's normal session persistence. This change requires rebuilding/redeploying Agentbox and starting a new Pi process; editing the source does not change a running wrapper.
Workflow child approval requests are automatically answered by the parent while its auto mode is on. MCP approval checks consult the same live mode; MCP children forward required approvals through the policy's existing parent transport. Tool allowlists and denials still apply. Children never independently enable saved auto defaults; turning the parent's live mode off takes effect at the next decision.
Auto is broad authorization, including arbitrary non-denied shell commands
inside Agentbox—not a sandbox or a claim that each action is safe. Scope container
mounts, credentials, network, and Docker access accordingly. Messaging an
unrelated main session is carved out: Auto still allows same-main
send_message, but cross-main sends always prompt.
/agentbox-defaultsor/agentbox-defaults status: show saved model/auto, policy availability, and live auto state./agentbox-defaults model current: save the current provider/model pair./agentbox-defaults auto on: save auto-on after a broad-authorization warning and explicit confirmation. Without a UI or on cancellation, nothing is saved./agentbox-defaults auto off: save auto-off (no confirmation needed).
These commands change future defaults, not the current model or live auto
mode. To stop live auto too, use /auto off.
CLI and web settings share Pi's global ~/.pi/agent/settings.json
(/home/agent/.pi/agent/settings.json inside Agentbox), or
$PI_CODING_AGENT_DIR/settings.json when that directory override is set:
{
"defaultProvider": "openai-codex",
"defaultModel": "gpt-5.4",
"agentboxAutoDefault": true
}Only boolean true enables the auto default; strings/numbers/missing values do
not. The policy reads it at session start, only for fresh startup/new sessions
without restored conversation history or a parent session. Reload, resume,
fork/clone, tree navigation, and workflow children never apply the global auto
default. Restoring an explicit same-session Auto choice is separate. Managed
permissions.auto.enable = true is still required, and all safety gates remain.
Project settings cannot grant this auto default. Pi's normal model resolution
still applies: explicit model choices, restored session models, and trusted
project settings can override the global model default.
Writes preserve unrelated global settings. Model persistence uses SDK
SettingsManager; the custom auto key reuses Pi's internal
FileSettingsStorage lock because the SDK has no arbitrary-key setter. Settings
errors are reported rather than claiming success. This compatibility seam is
covered by an installed-SDK integration test:
node --experimental-strip-types tests/pi-defaults.ts /path/to/pi-monorepoOmit the package path to run only policy/command tests. Rebuild/redeploy Agentbox and start a new Pi process to install the updated extension.
The former classifier behavior is now explicitly selected with /auto review.
It may ask for approval and is not what /auto on or the web toggle enables.
Existing provider/model/timeout configuration remains supported for review only:
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 using review. Unsupported or unconfigured models fall back to human
approval in review only. An explicit review choice is saved and restored with
the same conversation, like on/off; forks and workflow children do not inherit
that saved choice. The web toggle reports auto off during review;
/auto status reports the specific mode. The auto defaults are enable = false,
provider = "", model = "", and timeout = 30000.
In review, only unmatched defaultDecision = "ask" decisions are classified.
An effective explicit matching ask requires a human in review. 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 review nor human
approval can override denials. Rules that resolve to allow bypass classification.
With both auto and review disabled, default asks require human approval except for routine
installed skill Markdown reads. The public decision schema remains
allow / ask / deny.
read of .md files under ~/.pi/agent/skills or ~/.agents/skills does not
prompt when the effective decision is the default ask, even with auto off. This
covers SKILL.md and Markdown references, not executing scripts, shell reads,
writes, or arbitrary project Markdown. Both lexical and resolved paths must stay
within the same installed skill root; symlink escapes do not gain this exemption.
Explicit ask/deny rules, default deny, and sensitive-file guards still apply.
With review on, Bash commands are eligible for classification, not automatically
sent to human approval. Ordinary task-related read-only inspection of clearly
non-sensitive source/docs can be approved without naming each command or knowing
the file contents in advance. Credential files such as .env, private keys, and
auth/token stores remain excluded. Broad searches must clearly exclude sensitive
files; the classifier considers every command segment, flag, pipe, substitution,
and redirect rather than trusting a command name. Unknown scripts or effects
still fall back to human approval. Explicit policy rules and safety guards win.
With review on, the classifier focuses on the latest user instruction and recent follow-up context. A clear request to commit and push a repository makes the normal scoped Git workflow eligible for auto approval: status/diff/log, staging, commit with normal hooks, and push to its configured remote. It no longer rejects that requested push solely as external publication or for lacking hook contents. This is not a blanket Git allow rule: commit-only requests do not authorize push; later restrictions and unrelated tasks supersede earlier intent. Force-push, remote changes, hook bypasses, unrelated repositories/commands, and ambiguous scope still require human approval unless independently allowed by managed rules.
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 up to 12 recent user text messages
and up to 32 prior tool calls across the active session branch, plus the current
tool call. latestUserRequestIndex identifies the latest retained instruction.
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, then older user requests,
may be omitted to fit; counts and a history-omitted flag disclose omissions.
Prior-call request indices are remapped (-1 means absent/omitted). The latest
user instruction and current call are never truncated; if those cannot fit,
human approval is required.
Image bytes are not sent to the text classifier. userRequestsWithAttachments
identifies messages with omitted non-text content. A historical screenshot no
longer disables auto for later clear textual requests, but unseen image contents
cannot establish authorization. An image-only latest request requires a human;
otherwise available text must independently establish the action and scope.
Transcript tool calls remain 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 review 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 review to off. Turning review 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.
Enable services.agentbox.settings.browser.enable for the direct managed Pi
browser tool, independently of the desktop/noVNC. Delegated agents can share
cooperative tabs and cookies within a conversation, or use isolated browsers;
separate conversations are isolated. A domain allowlist forces isolated mode.
Screenshots render inline in chat. The RPC Browser pane adds bounded main-tab
preview and explicit, expiring human takeover; it never starts an unopened browser.
Viewing requires browser:read; takeover also requires browser:control, alongside
session scopes and profile permissions. There are no public CDP/stream/download
endpoints.
See Managed Pi browser for all settings, mandatory namespace
sandbox prerequisites, session/authentication limits, the versioned result
contract and the Phase 2 preview/control boundary. If Docker's default seccomp
policy blocks namespace creation, explicitly opt in to
settings.hardening.seccompProfile = "browser"; this adds narrowly filtered
namespace permissions container-wide, without disabling Chromium's sandbox.
It is not selected automatically and requires container recreation after an
authorized rollout. Managed browser executables are packaged, with no runtime
downloads or automatic streaming. Consumers must update
their pinned Agentbox input, rebuild and recreate the container/Pi processes;
local tests neither deploy the host nor prove a live browser smoke test passed.
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.
With settings.network.enable = true, container loopback is separate from host
loopback on both platforms, and port 4098 is not published by default. For host
browser access, use the same explicit bind and acknowledgement above, add
"127.0.0.1:4098:4098" to network.publishedPorts while retaining any other
ports you need, and allow the browser's exact origin. Alternatively, forward
container loopback through SSH into Agentbox. Publishing a port alone does not
make a loopback-bound RPC listener reachable.
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";
# Keep this outside the discovery prefix tree when auto-discovery is on.
sessionDir = "/home/agent/.pi/agent/sessions/--workspace--";
# Optional git/gh access, only if GH_TOKEN exists in environmentFile.
env.GH_TOKEN = "GH_TOKEN";
};
# Optional: one profile per immediate /workspace child at pi-rpc startup.
autoDiscoverProfiles = {
enable = true;
root = "/workspace";
sessionDirPrefix = "/home/agent/.pi/agent/sessions";
excludePrefixes = [ "tmp-" ];
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 choose a session folder, or leave it Ungrouped. By default the browser keeps the password in page memory only; optional Remember on this device stores it in localStorage until logout. It is never put in a URL. 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 define workspace roots and persistent Pi session directories on the
server. With autoDiscoverProfiles.enable = true, pi-rpc also turns each
immediate child of root into a profile (cwd=<root>/<name>,
sessionDir=<sessionDirPrefix>/--<name>--) at startup. Explicit profiles win on
name conflicts; discovery skips exclude / excludePrefixes, invalid names,
non-directories, and symlink escapes. Restart pi-rpc to pick up new folders.
This is soft project selection inside one container, not hard isolation. New
session chooses a sidebar session-folder label such as Work/Project;
it starts Pi in the configured profile cwd. Folder headings also have a +
action to start a conversation in that group. Folder labels do not select a server
directory. API clients requesting an explicit cwd remain confined to the approved
workspace root. Pi loads project AGENTS.md / CLAUDE.md from the session cwd. Sessions
open in tabs, with independent drafts and background processes; closing a tab
leaves Pi running, while End & archive stops it. Each sidebar row also offers
Archive, including saved conversations without starting Pi. The compact sidebar
retains nested conversation folders and shows running/unread indicators on the right. 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.
Model discovery uses Pi's official provider catalogs with cached/offline fallback;
Refresh bypasses catalog freshness. The same patched Pi package is available as
packages.<system>.pi-coding-agent for native deployments. The UI, runtime,
provider patches and portable workspace check are all maintained in Agentbox;
consumers only configure host paths, authentication, networking and services.
profiles, command allowlists, token scopes, origins, and resource/time limits
are typed. Session-switch/fork/clone/new-session, HTML export, and raw
UI-response commands are always forbidden by the supervisor. User shell (bash)
is allowed so the web composer can run OpenCode-style !command / !!command
without prompting the model (same as Pi's interactive TUI). 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 (opt-in) | 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.
Cloud CLIs are excluded from the base image. On NixOS, set
services.agentbox.settings.enableCloudTools = true to include them in the
module's default image. On Darwin, or when supplying a custom image, build the
Linux image with agentboxImage.override { withCloudTools = true; }. The
docker-client binary is part of this optional bundle; enabling the Docker API
proxy alone does not add it to the image.
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.
Build the OCI images on Linux or with a configured Linux Nix builder. The flake does not provide native Darwin image derivations; copy the Linux-built tarballs to the Mac and configure their paths there.
nix build .#agentboxImage # -> ./result (a docker image tarball)
docker load < result # loads agentbox:latestFrom Darwin with a Linux builder, explicitly select
.#packages.aarch64-linux.agentboxImage (or x86_64-linux) instead of the native
.#agentboxImage target. Select the matching Linux target for
agentboxNetworkImage as well.
Network policies are opt-in. With settings.network.enable = false (the
default), both platforms retain their existing host networking. Restricted
mode currently requires Docker; the NixOS Podman backend is rejected.
To block specific networks while allowing other destinations:
services.agentbox.settings.network = {
enable = true;
defaultAction = "allow";
deniedNetworks = [
{ cidr = "10.20.0.0/16"; } # production network
{ cidr = "169.254.169.254/32"; } # cloud metadata
{ cidr = "fd12:3456::/32"; }
{ cidr = "203.0.113.5"; protocol = "tcp"; ports = [ 5432 ]; }
];
};For an IP allowlist, set defaultAction = "deny" and populate
allowedNetworks with the same rule format. Rules accept IPv4/IPv6 addresses
and CIDRs, protocol = "all" | "tcp" | "udp", and optional destination
ports. Ports require TCP or UDP. Denied rules win over allowed rules.
Loopback, return traffic for existing connections, and IPv6 neighbor discovery
remain available. dnsServers defaults to [ "1.1.1.1" ]; TCP/UDP port 53 to
these resolvers is allowed unless a denied rule blocks it. Set dnsServers = [ ] with an empty allowlist and default deny for no new external connections.
Permitted DNS is an explicit network exception, including arbitrary queries.
For a mandatory HTTP/HTTPS domain allowlist:
services.agentbox.settings.network = {
enable = true;
domains = {
enable = true;
defaultAction = "deny";
allowed = [ ".github.com" "registry.npmjs.org" ];
denied = [ "blocked.example.com" ];
};
deniedNetworks = [
{ cidr = "10.0.0.0/8"; }
{ cidr = "172.16.0.0/12"; }
{ cidr = "192.168.0.0/16"; }
{ cidr = "169.254.0.0/16"; }
{ cidr = "fc00::/7"; }
{ cidr = "fe80::/10"; }
];
};A leading dot matches the domain and all subdomains; other entries match
exactly. Domain denies win. Use domains.defaultAction = "allow" with
domains.denied for a domain blocklist. Both domain modes require the proxy:
direct outbound traffic, direct DNS, QUIC, and SSH are blocked. Use Git over
HTTPS. Add actual provider, login, registry, CDN, and download domains required
by your enabled agents; the example is not a complete agent bootstrap list.
The generated upper/lowercase proxy environment variables configure compatible
clients; clients that ignore the proxy fail closed. Domain mode rejects
allowedNetworks because direct exceptions could bypass it.
Domain filtering checks HTTP request destinations and HTTPS CONNECT hostnames; it does not decrypt TLS or inspect data sent to an allowed service. IP deny rules are also enforced on the proxy's outbound connections after DNS resolution, including when an allowed domain resolves to a denied subnet. Domain blocklists match the supplied hostname; aliases and literal IPs are different destinations. Use a domain allowlist or destination subnet denies when those alternatives must also be restricted.
Restricted mode starts a separate firewall companion before the agent and
removes NET_ADMIN, NET_RAW, and SYS_ADMIN from the agent's capability
bounding set. In-container sudo cannot edit the firewall. Domain mode adds a
separate Squid companion, so becoming root in the agent cannot impersonate a
privileged proxy process. Docker-in-Docker (enableDocker = true) is rejected.
The existing restricted Docker API proxy remains reachable on loopback.
Rules are installed for both IP families, with Docker's embedded DNS blocked
before NAT. A firewall setup error prevents the agent from starting.
The network companion owns published ports. Defaults expose only the UI ports
on host loopback: 127.0.0.1:4096:4096 and 127.0.0.1:4097:4097. Set
network.publishedPorts to explicit Docker publish specs to add SSH (for
example 127.0.0.1:2222:22), the optional Pi chat UI on port 4098, dev servers,
UDP, or remote bindings. An explicit host IP is required, and the target
service must listen on a container interface reachable by Docker port
forwarding. See the Pi RPC and desktop sections for loopback-only listeners.
Existing host-loopback services are no longer directly reachable from the
agent; grant access to their routable IPs if needed.
The helper image builds separately on Linux:
nix build .#agentboxNetworkImageNixOS defaults settings.network.image to this package through the overlay.
On Darwin, set that option to the Linux-built tarball (just as for the main
image). The launcher loads it automatically when creating the companions.
The Docker Linux host/VM must support IPv6 bridge networking and IPv4/IPv6
iptables. Rules are applied inside that VM, independently of macOS PF.
Apply policy changes by recreating the containers: agentbox restart on
Darwin, or rebuild/restart docker-agentbox on NixOS. The NixOS unit starts and
stops the companions in dependency order. On Darwin, restricted containers
do not auto-restart after a Docker daemon restart: run agentbox start to
reinstall/check the policy before starting them. Stopping Agentbox removes its
companions. A policy change cannot replace the namespace of a running agent.
Validation:
python3 tests/network-policy.py scripts/network-policy.py scripts/network-host.py
nix build .#network-policy-test
nix-instantiate --eval --strict --json tests/network-modules.nix --arg nixpkgs /path/to/nixpkgs
docker load < result # after building agentboxNetworkImage
python3 tests/network-integration.pyThe integration test uses isolated, uniquely named containers and the helper image only. It checks IPv4/IPv6 allow/deny rules, HTTP/CONNECT filtering, direct and DNS bypass attempts, root capabilities, and proxy failure without internet access. It requires a running Docker daemon.
.github/workflows/build-image.yml runs for path-filtered pushes to master,
v* tag pushes, path-filtered pull requests, and manual dispatch. README-only
changes are outside its path filters.
The workflow evaluates the flake, builds its listed extension/runtime tests and
main image, then builds the network helper and runs policy, module, and live
Docker enforcement checks. Pull requests do not publish images. Non-PR runs
publish the main image to ghcr.io/<owner>/agentbox:<sha> and either :<tag>
for tag runs or :latest otherwise. The network helper is built and tested but
is not pushed to GHCR. Builds use public pinned inputs, including OpenCode's
upstream flake; publishing 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 delivery service. Agentbox only writes the request: it does not send transcripts and never receives the workflow webhook URL, signing secret, or optional edge access 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. Delivery state and credentials remain
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.
This host-uploader path is separate from the Pi RPC web UI's /archive slash
command and End & archive control, which only stop the supervised child and write
a local .archived-* sidebar marker. Those actions do not create collector
requests.
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: the host service must validate them before reading the selected local conversation. 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. The host service must derive host 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";
};When enabled, 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. With the default host networking, other local processes on the Linux Docker host can access these ports. On macOS, loopback belongs to the Docker runtime's Linux network context; reach that context through the runtime's supported forwarding mechanism. Never expose these listeners publicly or forward them through an unauthenticated proxy.
With network policies enabled, those listeners remain on Agentbox's own
loopback. The host-level SSH forwarding example above no longer reaches them,
and adding port 6080 to network.publishedPorts alone is insufficient. Publish
only Agentbox's SSH listener on host loopback alongside the UI ports:
services.agentbox.settings.network.publishedPorts = [
"127.0.0.1:4096:4096"
"127.0.0.1:4097:4097"
"127.0.0.1:2222:22"
];Then authenticate to the container's SSH server and tunnel its loopback viewer:
ssh -N -p 2222 -L 6080:127.0.0.1:6080 [email protected]Open http://127.0.0.1:6080/vnc.html on that SSH client's machine. For a remote
Docker host, reach the published loopback SSH port through your existing trusted
host connection. Keep the unauthenticated VNC/noVNC listeners private.
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).
The base image also exports LD_LIBRARY_PATH with the shared libraries
Playwright's own Chromium / headless_shell downloads need (libglib-2.0,
libnss3, libdrm, …), taken from the same nixpkgs pin as the image so the
glibc ABI matches. That covers playwright install chromium without requiring
the on-demand desktop. Do not point LD_LIBRARY_PATH at a newer nixpkgs — a
GLIBC symbol mismatch (for example GLIBC_2.43) will keep chrome from starting.
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!.
Enabling the desktop/browser does not implicitly relax those policies, add
privileged mode, or pass --no-sandbox. The opt-in
settings.hardening.seccompProfile = "browser" supplies a pinned, narrowly
filtered Chromium namespace policy; review its container-wide exposure and
rollout requirements, 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. The opt-in managed Pi browser provides structured browser actions and inline screenshots separately from noVNC; it does not provide a live viewer. Agents can also run browser scripts or the bundled screenshot/input commands; other browser MCP or pixel-based computer-use tools require separate configuration. 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, use the following extension points without 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 the agent container's processes while
preserving tmux sessions; agentbox resume continues them. The network firewall
and egress proxy companions remain running. 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.