Skip to content
awcjackPublic

About

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, and OpenCode inside it, isolated from the host.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

251 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

agentbox

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, and start/stop/restart).
  • nixosModules.agentbox / darwinModules.agentbox — run it as a systemd oci-containers service (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.

The agents

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.

Pi /init for AGENTS.md

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.

Cursor subscription in Pi

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.

Pi session communication

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_sessions discovers registered sessions, their addresses, presence, and resume handles.
  • send_message sends 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.

Pi goal mode

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 in goal-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-test

The 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.

Per-task models and skills

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.

OpenAI models and fast mode

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.

Concurrent Pi subagents

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.

Pi permissions and MCP configuration

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.

Pi auto permissions

/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.

Persistent Pi defaults (CLI)

  • /agentbox-defaults or /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-monorepo

Omit the package path to run only policy/command tests. Rebuild/redeploy Agentbox and start a new Pi process to install the updated extension.

Optional Pi permission review

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.

Managed Pi browser

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.

Pi chat UI and RPC API

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.

What's in the image

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.

In-container sudo & package installs

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-root sudo under /run/wrappers/bin at boot (Nix store paths can't carry setuid bits, and there's no security.wrappers inside the container, so plain /bin/sudo can't elevate). agent gets passwordless sudo. Turning on hardening.noNewPrivileges disables it at the kernel level regardless.

  • settings.enableNix — bakes the nix CLI in, registers the store DB (buildLayeredImageWithNixDb), and starts a root nix-daemon at 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 in image.nix). Language-level installs (bun add -g, go install, uv tool install, cargo install) work without either knob.

Build

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:latest

From 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.

Outbound network policies

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 .#agentboxNetworkImage

NixOS 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.py

The 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.

CI

.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.

Use the modules

# 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).

Selective conversation archive requests

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.

On-demand desktop

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 desktop

Service 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-host

Access 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.png

Keep 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 ./result

The sandbox check reports a rendering skip when user namespaces are unavailable.

Advanced: baking extra agents in

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-agent

agentbox 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.

About

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, and OpenCode inside it, isolated from the host.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages