Skip to content

Latest commit

 

History

History
20 lines (18 loc) · 24.8 KB

File metadata and controls

20 lines (18 loc) · 24.8 KB

Where state lives

Part of the AgentBox docs. Start at CLAUDE.md. For cloud-specific state (per-org credential volumes, snapshot manifests under ~/.agentbox/cloud-checkpoints/, sshfs mount root, Daytona credentials) see cloud-providers.md.

  • ~/.agentbox/config.yaml — global user config (layered defaults). Same shape as the per-project file and the defaults: block in agentbox.yaml. Owned by @agentbox/config (read by every apps/cli command at startup). Manage via agentbox config get|set|unset|list|edit|path|list-projectsset/unset/edit only target --global and --project (default), the workspace defaults: block is hand-edited. Precedence (highest wins): CLI flag > workspace defaults: > per-project (~/.agentbox/projects/<sha1-16-of-abs-path>/config.yaml) > global > built-in. Engine override (engine.kind) is the only key applied at CLI startup via setEngineOverride() in host-export.ts; everything else flows through loadEffectiveConfig() per command. The full key set + types live in KEY_REGISTRY (packages/config/src/types.ts) — single source of truth for the parser, the JSON schema, and config set coercion. The ctl parser accepts top-level defaults: as a permissive passthrough (no dep on @agentbox/config); the host re-validates strictly when loading.
  • ~/.agentbox/state.json — registry of created boxes. Mutated only through recordBox/removeBoxRecord/mutateState (@agentbox/sandbox-core), which take a cross-process lock (state.json.lock, with stale-break) around the read-modify-write and write atomically (temp file + rename). This is load-bearing for parallel agentbox create/destroy: without it, concurrent processes lost each other's records (last-writer-wins) and could leave a half-written file, which in turn wedged the queue's running-count and made boxes vanish from agentbox list. recordBox also de-duplicates a clashing per-project projectIndex under the lock (two concurrent creates can allocate the same index before either records). Box dir segments are <id>-<n>-<mnemonic>, id-prefixed, so on-disk paths stay unique regardless of n. BoxRecord.lastAgent (claude/codex/opencode) records which agent the box was last launched with — written on every agentbox claude/codex/opencode launch (foreground + queued, via recordLastAgent). It's durable, unlike the in-box session pointers which are cleared when an agent stops, so agentbox recover reads it to relaunch (or attach) the right agent — and it's the only such signal for an adopted box whose state was rebuilt from a live sandbox (recover --provider <cloud> --adopt). BoxRecord.displayName is an optional cosmetic label set by agentbox status <box> --set-name (via setBoxDisplayName, a locked read-modify-write; empty/blank clears it). Unlike name it does not drive the container / git branch / Portless URL — those stay on the original name — so a rename is a pure, provider-agnostic state edit. Display surfaces (list, the hub, the tray) prefer displayName ?? name, and findBox resolves a box by its displayName too (lowest-precedence exact match, after id/name, so a label can't shadow another box's id/name).
  • ~/.agentbox/auth.json (mode 0600) — long-lived Claude OAuth token captured on first agentbox claude via claude setup-token. Forwarded to every box as CLAUDE_CODE_OAUTH_TOKEN. Host env vars (ANTHROPIC_API_KEY, CLAUDE_CODE_OAUTH_TOKEN) override it. CLAUDE_EFFORT and ANTHROPIC_MODEL are also forwarded when set on the host — Claude Code stores the user's model selection (Opus/Sonnet/Haiku via /model or --effort) only in the parent claude's process env, not in ~/.claude.json or ~/.claude/settings.json. Both create-time (docker run -e) and agentbox claude start exec-time (docker exec -e) re-read these from the current host shell, so launching from inside a host claude session propagates that session's model to the box (packages/sandbox-docker/src/claude.tsFORWARDED_ENV_KEYS).
  • ~/.agentbox/snapshots/<id>/ — frozen APFS clones of host workspaces (the --host-snapshot path; config key box.hostSnapshot, renamed from box.snapshot — the old key now errors with a migration hint via RENAMED_KEYS in packages/config/src/parse.ts)
  • ~/.agentbox/checkpoints/<sha1-16-of-project-root>/<box-name>-<n>/manifest.json — per-project warm-state checkpoint metadata only (schema: 2, type, image, parents, lineage). The captured filesystem itself is a local Docker image tag named agentbox-ckpt-<sha1-16-of-project-root>:<name> (CHECKPOINT_IMAGE_PREFIX + checkpointImageTag() in packages/sandbox-docker/src/checkpoint.ts). Capture is docker exec /usr/local/bin/agentbox-checkpoint-cleanup (apt + tmp + logs + bash history) → docker commit <ctr> <tag> for layered; for flattened (--merged, or auto when chainDepth >= checkpoint.maxLayers, default 3) the intermediate commit is then docker create/docker export-ed to a rootfs tarball and rebuilt via a tiny FROM scratch Dockerfile that ADDs the tarball and replays the base image's Env/Cmd/Entrypoint/WorkingDir/User/ExposedPorts (everything docker export discards). A box created from a checkpoint just uses that image as its base — runBox(image=<ckpt-tag>) — so /workspace is already populated and there's no seedWorkspace step. BoxRecord.checkpointImage mirrors record.image for plain-vs-checkpoint disambiguation; checkpointSource = { ref, type, chain } carries lineage for the auto-flatten rule. Tags are durable project assets: they survive box destroy and are allowlisted in prune --all by the agentbox-ckpt- prefix (parallel to SHARED_CLAUDE_VOLUME); agentbox checkpoint rm removes the manifest and docker image rms the tag. Project default ref is the per-project config key box.defaultCheckpoint; agentbox create/claude --snapshot <ref> overrides it. Capture/restore is host-side; the in-box agent triggers it through the relay (agentbox-ctl checkpoint/rpc checkpoint.create → relay spawns the host agentbox checkpoint create CLI via AGENTBOX_CLI_ENTRY).
  • ~/.agentbox/boxes/<id>/run/ctl.sock — host-side view of the in-box ctl socket (bind-mounted to /run/agentbox/ in the container)
  • ~/.agentbox/boxes/<id>/status.json — durable snapshot the in-box supervisor pushes through the relay (services, tasks, listening ports, the web-service expose:, claude activity). Prefer reading this via readBoxStatus() (host-export.ts) as the host-side source of box runtime facts — it works when the box is paused/stopped and avoids a docker exec round-trip. Reach for docker exec agentbox-ctl … live reads only when you need fresher-than-snapshot data and the container is running (e.g. agentbox status's live TASKS/SERVICES tables). getBoxEndpoints follows this rule: snapshot first, host agentbox.yaml only as fallback. Additive schema — old snapshots lack newer fields (treated as absent); schema stays 1. The snapshot's claude block (activity state + tmux sessionTitle) is joined by optional codex / opencode blocks: the ctl daemon's StatusReporter probes all three agent tmux sessions each snapshot (probeAgentSession in packages/ctl/src/tmux.tstmux #{pane_title}), so a codex/opencode box surfaces its session title in list / status --inspect. codex also has an activity state fed by agentbox-ctl codex-state — wired end-to-end, but codex's own ~/.codex/hooks.json lifecycle hooks don't fire in codex-cli 0.133.0 (verified: features.hooks is stable/on, --dangerously-bypass-hook-trust, both JSON shapes, exec + TUI — none execute the hook), so codex.state stays unknown in practice and agentSummary falls back to a plain codex; the pipeline lights up automatically if a future codex build runs the seeded hooks. OpenCode has no activity hooks (its plugin-based integration was deferred).
  • ~/.agentbox/boxes/<id>/workspace — per-box host export target for agentbox open (and agentbox open --path). Empty until refresh runs — by design. createBox mkdirs it at create-time and bind-mounts it into the container at /host-export (CONTAINER_EXPORT_MERGED); refreshExport in host-export.ts does docker exec rsync /workspace/ /host-export/ (with --exclude=node_modules by default). The container /workspace lives in the container's writable layer and is invisible to macOS directly, which is why a copy is needed. agentbox open --path (canonical spelling; --print is a kept alias) prints the host path and still refreshes (use --no-refresh to skip), so scripts that pipe the path get a fresh snapshot in one call. The old agentbox path command was folded into agentbox open --path (logic in path.ts's runPath, called by open.ts); agentbox path no longer exists. The --upper flag retired with the FUSE overlay — the merged export is the only view now. agentbox url (web app URL — http://<container>.orb.local on OrbStack, http://127.0.0.1:<webHostPort> otherwise; --loopback forces loopback) and agentbox screen (noVNC viewer) are separate top-level commands. agentbox url works even when no service declares expose: — only the create-time web-port reservation (BoxRecord.webContainerPort) is required; pre-feature boxes error with a recreate hint. Boxes created before the bind-mount existed (/host-export absent) fall back to a docker exec tar | tar -x pipe into the host dir — slower but doesn't need a container restart.
  • Per-box git worktrees live inside the container at /workspace (root repo) and /workspace/<sub> (1st-level nested repos in a monorepo). There is no host-side worktree dir under ~/.agentbox/boxes/<id>/worktrees/ anymore — the worktree's working tree is the container's writable layer. agentbox create runs git worktree add -b agentbox/<box-name>[--<sub>] <containerPath> HEAD inside the container via seedWorkspace (packages/sandbox-docker/src/in-box-git.ts), against the bind-mounted host .git/ (RW at its identical absolute host path). extensions.worktreeConfig=true is set on the main repo so commit.gpgsign=false (per-worktree) can disable signing for the box's commits — boxes carry no SSH keys. The host's tracked-uncommitted + untracked state is carried over at create via host-side git stash create (commit lands in the shared .git) + in-container git stash apply --index + a host→container tar pipe for untracked files; the user's main checkout is never modified. The host's .git/worktrees/<branch>/gitdir points to the container-only /workspace path — cosmetically visible in git worktree list on the host, otherwise inert. On destroy, git -C <hostMainRepo> worktree remove --force <containerPath> (best-effort, falls back to worktree prune) deregisters from the main repo so the host's worktree list doesn't accumulate orphans.
  • ~/.agentbox/relay.pid and ~/.agentbox/relay.log — pidfile + log for the host relay node process spawned by ensureRelay(). See host-relay.md.
  • Per-box agentbox-docker-<id> (or shared agentbox-docker-cache when box.dockerCacheShared is set) — the in-box dockerd's /var/lib/docker data root. Per-box variant removed on destroy; shared variant preserved (allowlisted in prune --all). Holds images, layers, inner containers, networks, and volumes pulled/created by the agent inside the box.
  • Docker objects: containers agentbox-<id|name> (per-box; /workspace lives in the container's writable layer, not a volume) + the Claude Code config volume (agentbox-claude-config shared by default, or agentbox-claude-config-<id> when --isolate-claude-config is set). /workspace/node_modules (and .next, target, .venv, …) lives in the container's writable layer alongside the rest of the working tree; isolated per box, captured by agentbox checkpoint create (docker commit), wiped on destroy. Host darwin node_modules is never copied into the box (the seedWorkspace tar pipe in the no-git case + the worktree-add carry-over in the git case both originate from git-tracked content); the wizard-generated agentbox.yaml install task force-rebuilds Linux-native deps on first box start (marker-guarded). The relay is no longer a docker containerprune --all reaps any leftover agentbox-relay container + agentbox/relay:dev image + agentbox-net network from previous versions. Host's ~/.claude is the authoritative source: every create / claude rsyncs host -> volume (additive — host wins on overlap, box-only files like session logs are preserved). Host's ~/.claude.json (file, not directory) also syncs into the volume as _claude.json; an image-baked symlink at /home/vscode/.claude.json -> /home/vscode/.claude/_claude.json routes claude's reads/writes through the volume. Hook commands referencing host-absolute paths under $HOME/ are filtered out during sync (packages/sandbox-docker/src/claude-hooks-filter.ts) so the in-box claude doesn't spam cc-status: not found errors. The same module's clearInstallMethod strips the top-level installMethod field from the synced _claude.json so the in-box claude (installed via Anthropic's native installer at /home/vscode/.local/bin/claude) redetects rather than trip an integrity warning when the host recorded a different install method. The rsync also runs with --copy-unsafe-links so user-skill symlinks (e.g. ~/.claude/skills/<x> -> ../../.agents/skills/<x>) are dereferenced into real directories inside the volume — ~/.agents is not bind-mounted. The rsync excludes node_modules so the host's darwin-arm64 plugin binaries (fsevents.node, @esbuild/darwin-arm64, @rollup/rollup-darwin-arm64, sharp, …) never reach the linux box; rebuildPluginNativeDeps() then re-runs npm install for each plugin cache the first time agentbox claude launches in a fresh box (idempotent — gated by a per-plugin .agentbox-installed marker, since some plugins have empty dep lists that don't produce a node_modules/ dir). Installs run in parallel (bounded, MAX=4) and share a persistent npm cache at /home/vscode/.claude/.agentbox-npm-cache (in the claude-config volume, so a package@version is fetched once globally and reused by every later box/plugin-version) with --prefer-offline. A failed install writes a .agentbox-install-failed marker (mtime = failure time); that plugin is skipped rather than retried on every launch until the marker ages past 6h (PLUGIN_INSTALL_BACKOFF_MS), then it retries. The host pre-scan scanPluginCacheForRebuild() mirrors the same accept/skip rules (incl. the fail-backoff window) so it never disagrees with the in-box script. A one-shot migration in the rsync helper wipes pre-existing node_modules/ from the shared volume the first time a post-upgrade sync runs (sentinel ~/.claude/.agentbox-cleaned-nm-v1); without it, existing users would keep darwin binaries indefinitely. Every top-level JSON under ~/.claude/plugins/ (currently installed_plugins.json + known_marketplaces.json) has its host-home prefix rewritten to /home/vscode via an inline sed sweep in the helper container — without rewriting known_marketplaces.json.installLocation, claude can't load the marketplaces (it falls back to a <org>-<repo> slug derived from source.repo like microsoft-playwright-cli, which doesn't exist on disk, masquerading as "Plugin X not found in marketplace Y" for every plugin in the marketplace). claude-hooks-filter.ts's addProjectAlias duplicates _claude.json.projects[<host-cwd>] to projects['/workspace'] so project-scoped MCP servers / trust / history apply inside the box (workspace is always /workspace regardless of host path). The shared volume is never auto-removed by destroy or prune (it holds user identity); per-box isolated volumes are removed with their box.
  • The Codex CLI config volume mirrors the claude one: agentbox-codex-config shared by default, agentbox-codex-config-<id> under --isolate-codex-config (config key box.isolateCodexConfig), mounted at /home/vscode/.codex, recorded as BoxRecord.codexConfigVolume. createBox mounts + host→volume rsync-syncs it (additive, host-authoritative) when either the caller is agentbox codex or the host has a ~/.codex (so a plain agentbox create for a Codex user still gets a working box). The rsync excludes sessions/ / log/ / history.jsonl (rollout transcripts) and hooks.json — the latter is box-owned: seedCodexHooks copies the image-baked agentbox-codex-hooks.json to ~/.codex/hooks.json for activity reporting (see the status.json note above re: codex 0.133.0 not firing them). It also excludes Codex's session-state DBs / indexes (state_*.sqlite*, logs_*.sqlite*, session_index.jsonl, external_agent_session_imports.json, shell_snapshots/) and rm -rfs any that a pre-fix sync already left in the shared volume. state_*.sqlite is the threads INDEX over the rollout files — Codex reads the resume cwd from threads.cwd, so seeding the host copy made a teleported (agentbox fork --agent codex) session resume at its host cwd and pop Codex's "Choose working directory" prompt, overriding the cwd we rewrite in the rollout. The index is a derived cache Codex backfills from the box's rollouts (the backfill_state table), so the box rebuilds it from the one teleported rollout (already rewritten to /workspace). Excluding it also stops the host's entire cross-project Codex history (all indexed threads) from leaking into every box. config.toml is not rsynced at all (anchored --exclude=/config.toml) — the box copy is reconciled instead (reconcileVolumeCodexConfig): the host config is sanitized (sanitizeCodexConfigForBoxmcp_servers / top-level notify / local-source marketplaces (and their dependent plugins) whose paths point at host-only absolute locations (the host home, /Applications, /opt/homebrew, …) are stripped, so in-box codex doesn't try to exec macOS-only paths like the desktop Codex.app's node_repl MCP server, which otherwise prints MCP client \node_repl` failed to start on every launch), then the box's existing config is **merged in additively** (mergeCodexConfigForBox): box-only [marketplaces.]/[plugins.]/[mcp_servers.]/[projects.]entries and box-only top-level keys survive, so in-boxcodex plugin add/codex mcp addstate isn't wiped by the nextagentbox codexstart — but the host wins on every overlapping key (host-authoritative: an in-box enable flip of a host-listed plugin reverts, and a marketplace deleted on the host lives on in the box until removed there or the volume is isolated/recreated). The sanitizer also **pre-trusts/workspace** (injects [projects."/workspace"] trust_level = "trusted") so codex doesn't pop the "trust this folder?" prompt on attach — the host config only trusts host-absolute project paths, never /workspace(this is the stable path-based trust, distinct from the brittle hash-based hook trust). For a Codex user with **no** hostconfig.toml, the minimal /workspace-trusted config (MINIMAL_TRUSTED_CODEX_CONFIG) is merged into whatever the box has. Best-effort otherwise (codex-config.ts; the cloud path sanitizes the staged tarball in host-stage.ts — no merge there, cloud boxes seed a fresh FS at prepare). **Codex plugins ride this sync**: the git-marketplace snapshots (.tmp/marketplaces/) are carved back into the rsync/staging via --includerules ahead of the.tmpexclude (without them, in-boxcodex plugin list/addfails with "marketplace root does not contain a supported manifest", and the box can't re-clone — the git shim blocksclone), while the rest of .tmp(the ~200 MB desktop-appbundled-marketplaces+pluginspayloads) stays excluded; and marketplace dirs underplugins/cache/+.tmp/marketplaces/ that the **merged** config no longer references are purged (buildCodexOrphanPurgeScripton docker,purgeOrphanCodexMarketplaceDirson the cloud staging path). The codex sync **excludesskills/**: /.codex/skills/are symlinks into/.agents/skills/, which the box mounts from its own volume (see the agents-config bullet below), so codex reads /.agents/skillsdirectly and the per-codex copies are redundant; excluding them also avoids anrsync"could not make way for new symlink" failure when the shared volume already holds them as real dirs from an earlier deref (a trailingfindpurges those stale dirs, keeping codex's runtime-managed.system). The cloud path's equivalent CODEX_RSYNC_EXCLUDES (host-stage.ts) globs the same DBs (version-proof across state_5state_6schema bumps)./.codex used to be a RW host bind-mount (buildIdentityMounts); that was replaced by the volume so boxes can't corrupt host state. Codex plugins need no native-deps rebuild (marketplace payloads are JS/markdown; the platform-specific plugins/.plugin-appserverruntime is excluded). The shared codex volume is allowlisted indestroy/prune --allexactly like the shared claude volume; per-box isolated codex volumes are removed with their box.packages/sandbox-docker/src/codex.tsownsensureCodexVolume/buildCodexMounts/startCodexSession/pullCodexConfig`.
  • The Agent Skills volume agentbox-agents-config (always shared — skills only, no auth, so no per-box isolation) is mounted at /home/vscode/.agents, recorded as BoxRecord.agentsConfigVolume. It carries the host's ~/.agents — the cross-agent "Agent Skills" dir (~/.agents/skills/<name>, managed by the open skills installer + .skill-lock.json). Codex discovers skills from ~/.agents/skills directly (not just ~/.codex/skills, which only symlinks a subset back into it), so without this volume an in-box codex saw only its 5 runtime .system skills, never the user's full set (and the agentbox fork skill). createBox mounts + host→volume rsync-syncs it (additive, host-authoritative, rsync -a --copy-unsafe-links to materialize each skill's symlinks, broken/out-of-tree ones --excluded via findUnsyncableSymlinks) whenever the host has a ~/.agents. Allowlisted in destroy / prune --all like the other shared agent volumes. Cloud bakes it into the prepare snapshot via stageAgentsStaticForUpload (host-stage.ts, rsync -a -L deref → tarball → extracted at /home/vscode/.agents), wired into both sandbox-daytona/sandbox-hetzner prepare. packages/sandbox-docker/src/agents.ts owns ensureAgentsVolume/buildAgentsMounts/resolveAgentsVolume.
  • The OpenCode config volume (agentbox-opencode-config, box.isolateOpencodeConfig, BoxRecord.opencodeConfigVolume) follows the codex pattern with one twist: OpenCode splits its state across three dirs — ~/.config/opencode (config), ~/.local/share/opencode (data + the auth.json provider credentials), and ~/.local/state/opencode (state — incl. model.json, the last-selected model). A single volume holds all three: it is mounted at the data dir /home/vscode/.local/share/opencode; the config dir is relocated into that volume's config/ subdir via the box-global env OPENCODE_CONFIG_DIR, and the state dir into .state/opencode via the box-global env XDG_STATE_HOME (both OpenCode-derivable paths, so safe box-global — unlike XDG_DATA_HOME; OpenCode has no dedicated OPENCODE_STATE_DIR). ensureOpencodeVolume rsyncs host ~/.local/share/opencode → volume root (excl. storage/log/project/cache/bin), host ~/.config/opencode → volume config/, and host ~/.local/state/opencode → volume .state/opencode (excl. locks, newest-wins --update since the selected model is two-way state, not host-authoritative config). createBox mounts it when the caller is agentbox opencode or the host has any OpenCode dir. agentbox opencode login runs the interactive opencode auth login (provider picker) in a throwaway container. On the cloud providers there is no persistent per-box data store, so model.json is instead seeded host→box per-create at the default ~/.local/state/opencode path (seedOpencodeModelState in packages/sandbox-cloud/src/agent-credentials.ts, called from the shared cloud create flow). Allowlisted in destroy/prune --all like the other shared agent volumes. packages/sandbox-docker/src/opencode.ts owns the helpers.
  • The box image is agentbox/box:dev. On first use the CLI pulls it from GHCR (ghcr.io/madarco/agentbox/box, tagged sha-<build-context fingerprint>) and retags it locally; on a pull miss it builds from packages/sandbox-docker/Dockerfile.box. Build context is the monorepo root (so the Dockerfile can COPY packages/ctl/dist/bin.cjs); see BUILD_CONTEXT_DIR, BOX_IMAGE_REGISTRY, registryRefForSha, and pullOrBuild in image.ts. Pulling is governed by the box.imageRegistry config key (empty = always build) and the --build flag on create/prepare. Publishing is automated in .github/workflows/box-image.yml; the GHCR package must be made public once for anonymous pulls to work.