Skip to content

Consolidate sandbox images: e2b + local docker only, drop K8s SandboxSet#87

Merged
MiniMax-AI-Dev merged 4 commits into
mainfrom
feat/simplify-sandbox-images
Jul 11, 2026
Merged

Consolidate sandbox images: e2b + local docker only, drop K8s SandboxSet#87
MiniMax-AI-Dev merged 4 commits into
mainfrom
feat/simplify-sandbox-images

Conversation

@RyanLee-Dev

@RyanLee-Dev RyanLee-Dev commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Support-scope decision: only e2b.app SaaS templates and the local docker-backed sandbox provider are supported going forward. The self-hosted K8s SandboxSet path is dropped.

Root cause investigation for "sandbox does not start" after ./install.sh led here: PARSAR_SANDBOX_IMAGE defaulted to ghcr.io/minimax-ai-dev/parsar-sandbox:latest, an image that had never been published — its build workflow was gated off since the initial public release because the Dockerfile's COPY --from=e2bdev/base:latest /usr/bin/envd broke (upstream removed envd from that image) and nobody had re-enabled it. Rather than just patch that one Dockerfile, this PR steps back and simplifies the whole sandbox image story.

What changed

  • Dropped the K8s SandboxSet path entirely: infra/sandbox/parsar-daemon-claudecode/{Dockerfile,sandboxset.yaml}, infra/sandbox/parsar-daemon-claudecode-xl/sandboxset.yaml, the old .github/workflows/sandbox-image-release.yml, docs/deploy/sandbox-setup.md — all deleted. It duplicated most of the e2b.app template, needed its own OpenKruise CRD infra, and its publish pipeline had been silently dead since the initial release.
  • Dropped OpenCode from the sandbox images (Claude Code + Codex + Pi only) to keep them minimal. Also deleted the standalone infra/e2b-templates/opencode/e2b.Dockerfile template. Server-side agent_kind=opencode support (DB, API, frontend, the local apps/parsar-daemon daemon runtime) is untouched — this only affects whether a sandbox image has OpenCode preinstalled.
  • One Dockerfile for both remaining targets: infra/sandbox/Dockerfile.local + infra/sandbox/Dockerfile.e2b (themselves already consolidated from 5 original files, deduplicating CLI-install logic into shared scripts) collapsed further into a single infra/sandbox/Dockerfile, selecting the target via --build-arg BASE_IMAGE:
    docker build -f infra/sandbox/Dockerfile -t parsar-sandbox:local .
    docker build -f infra/sandbox/Dockerfile --build-arg BASE_IMAGE=e2bdev/base:latest -t parsar-daemon-claudecode .
    The two prior files had shrunk to a ~2-line real diff (base image + a TARGETARCH default) — not worth maintaining separately when Docker's ARG-before-FROM substitution covers it natively. Verified both targets still produce byte-identical images (full layer-cache hit against the prior two-file build).
  • Both targets now compile parsar-daemon from source in the shared Go builder stage instead of the e2b variant downloading a prebuilt binary from GitHub Releases — removes a dependency on a release existing first, and removes ~25 lines of GitHub-API tag-resolution logic.
  • CLI installs unified into one script: infra/sandbox/scripts/install-agents.sh (previously 4 separate install-*.sh files) installs Node 22, Claude Code, Codex, and Pi, shared by the one Dockerfile. Backfilled Codex + Pi into the e2b target (it was previously Claude-Code-only, inconsistent with the local target) and switched its Claude Code install off claude.ai/install.sh (bot-challenge-prone) onto the CDN method the local target already used.
  • Two real bugs found and fixed while backfilling:
    1. @earendil-works/pi-coding-agent pins engines.node >=22.19.0 and fails at runtime on Node 20 (TypeError: webidl.util.markAsUncloneable is not a function, not just an npm warning) — bumped the shared install script to Node 22, pinned pi to 0.80.6 for reproducibility.
    2. e2bdev/base:latest ships its own stale /usr/local/bin/node (v20.9.0) that shadows a fresh nodesource install on PATHinstall-agents.sh now force-relinks /usr/local/bin/{node,npm,npx} to the dpkg-installed /usr/bin/ binaries.
  • New .github/workflows/sandbox-image-release.yml: a matrix build off the single Dockerfile publishing both ghcr.io/<org>/parsar-sandbox (multi-arch amd64+arm64) and ghcr.io/<org>/parsar-daemon-claudecode (amd64 only) to GHCR, mirroring parsar-server-release.yml's trigger conventions (PR = build only, push to main = :latest + :sha, sandbox-v* tag = release, manual dispatch).
  • docker-compose.yml / install.sh: AGENT_DAEMON_SANDBOX_DOCKER_IMAGE / DEFAULT_SANDBOX_IMAGE now default to parsar-sandbox:local instead of the never-published GHCR image.

Not included

  • server/internal/sandbox/e2b/podip.go (K8s in-cluster pod IP resolution) is now dead code but wasn't touched — removing it means editing server Go code beyond this PR's infra/Dockerfile scope.
  • Found but did not fix (pre-existing, unrelated): deploy/compose/compose.selfhost.yml / deploy/compose/.env.example set PARSAR_E2B_TEMPLATE_ID / PARSAR_E2B_TIMEOUT_SECONDS, but server/cmd/server/main.go actually reads AGENT_DAEMON_SANDBOX_TEMPLATE / AGENT_DAEMON_SANDBOX_TEMPLATE_XL — the self-host e2b sandbox path can never enable itself with those files as shipped.

Test plan

  • docker build -f infra/sandbox/Dockerfile -t parsar-sandbox:local . and the e2b variant (--build-arg BASE_IMAGE=e2bdev/base:latest) both succeed end to end
  • docker run --rm <image> bash -c 'node/claude/codex/pi --version' confirms all four CLIs resolve to pinned versions on both images (Node 22.23.1, Claude 2.1.207, Codex 0.141.0, Pi 0.80.6)
  • Rebuilt parsar-sandbox:local, recreated parsar-server, confirmed the startup log reports "agent_daemon docker sandbox provider wired","image":"parsar-sandbox:local"
  • Started a real sandbox container on the parsar_default compose network with the exact invocation server/internal/sandbox/docker/client.go uses (docker run -d --entrypoint sleep ... infinity) and confirmed it can curl parsar-server:8080/healthz successfully
  • actionlint clean on the new workflow; locally simulated both matrix legs' exact build invocations
  • Repo-wide grep confirms no leftover references to any of the deleted/renamed paths (infra/e2b-templates, Dockerfile.local/Dockerfile.e2b, the 4 separate install scripts, docker-compose.local.yml, parsar-init, old seed-dev, docker-image-info, parsar-paths.sh)
  • bash -n install.sh, docker compose config --quiet on docker-compose.yml both pass

🤖 Generated with Claude Code

yuanhe and others added 4 commits July 11, 2026 15:40
… path

Support scope decision: only e2b.app SaaS templates and the local
docker-backed sandbox provider are supported going forward. The
self-hosted K8s SandboxSet path is dropped — it required its own
OpenKruise CRD infrastructure, duplicated most of the e2b.app template,
and its publish pipeline had been silently broken since the initial
public release (envd COPY failure, caught while investigating why
ghcr.io/minimax-ai-dev/parsar-sandbox never published).

- Delete infra/sandbox/parsar-daemon-claudecode/{Dockerfile,sandboxset.yaml},
  infra/sandbox/parsar-daemon-claudecode-xl/sandboxset.yaml,
  .github/workflows/sandbox-image-release.yml, and
  docs/deploy/sandbox-setup.md (entirely K8s-specific).
- Consolidate the two local-docker sandbox Dockerfiles (amd64
  infra/sandbox/Dockerfile.local + arm64
  infra/e2b-templates/parsar-sandbox-local/local.Dockerfile, which had
  drifted — the arm64 variant was missing OpenCode/Codex/Pi) into one
  multi-arch Dockerfile using TARGETARCH, deleting the arm64-only
  variant and its companion build.sh staging script.
- The consolidated Dockerfile compiles parsar-daemon + the parsar CLI
  from source in its own Go builder stage, removing the prior
  dependency on the parsar:local server image being built first.
- Drop OpenCode from the local sandbox image (Claude Code + Codex + Pi
  only) to keep it minimal; final image is 1.21GB vs 2.08GB before.
- docker-compose.yml / install.sh: AGENT_DAEMON_SANDBOX_DOCKER_IMAGE /
  DEFAULT_SANDBOX_IMAGE now default to parsar-sandbox:local instead of
  the never-published GHCR image — this was the actual root cause of
  "sandbox does not start" after a fresh ./install.sh.

Verified: full docker build succeeds (amd64; arm64 download URLs
confirmed to exist but not build-tested, no arm64 emulation available
here), and `docker run -d --entrypoint sleep parsar-sandbox:local
infinity` + `docker exec ... bash -l -c` (the exact invocation
server/internal/sandbox/docker/client.go uses) confirms claude/codex/
pi/parsar-daemon/parsar all resolve on PATH.

Follow-up not included here: server/internal/sandbox/e2b/podip.go
(K8s in-cluster pod IP resolution) is now dead code now that the
self-hosted K8s path is gone; left in place since removing it means
touching server Go code beyond the infra/Dockerfile scope of this
change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
infra/e2b-templates now has a single template
(parsar-daemon-claudecode/) instead of two. Deletes the standalone
opencode/e2b.Dockerfile (ran opencode serve directly, no
parsar-daemon) and the OpenCode hook scripts baked into the
parsar-daemon-claudecode template's hooks/ dir, narrowing that
template's COPY to hooks/claude only.

Scope explicitly limited to the e2b template/image layer: OpenCode
remains a fully supported agent_kind everywhere else (DB schema, API
routes, web frontend, and the local parsar-daemon runtime at
apps/parsar-daemon/internal/agent/opencode/ are all untouched — that
support spans ~35 server files plus a standalone npm package, too
large to fold into this change). The only user-visible effect: an
Agent configured with agent_kind=opencode + daemon_mode=sandbox on the
e2b backend has no image to run against anymore. Local-daemon-mode
OpenCode and every other agent_kind are unaffected.

docs/spec-memory-module.md still describes OpenCode's hook design
referencing the now-deleted infra/e2b-templates/.../hooks/opencode/
path — left as-is since that doc covers the still-supported OpenCode
architecture broadly, not just the e2b template.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
infra/ now has a single directory for everything sandbox-image-related
instead of two top-level dirs that each reached across into the other
(Dockerfile.e2b pulling scripts from infra/sandbox/scripts/,
Dockerfile.local pulling hooks from infra/e2b-templates/.../hooks/):

  infra/sandbox/
    Dockerfile.local          (was already here)
    Dockerfile.e2b             (moved from infra/e2b-templates/parsar-daemon-claudecode/e2b.Dockerfile)
    scripts/install-agents.sh  (was 4 separate install-*.sh, now 1)
    hooks/claude/*.py          (moved from infra/e2b-templates/.../hooks/claude/)

- Collapsed install-claude-code.sh / install-codex.sh / install-node.sh
  / install-pi.sh into one install-agents.sh (arch + version knobs via
  positional arg + env vars). Both Dockerfiles now COPY + RUN the same
  single script instead of four.
- Backfilled Dockerfile.e2b with Codex + Pi (previously Claude-Code-only,
  inconsistent with Dockerfile.local) and swapped its Claude Code install
  from the claude.ai/install.sh bot-challenge-prone path to the CDN
  method Dockerfile.local already used.
- Fixed a real bug installing Pi found while backfilling: Node 20 is not
  enough — @earendil-works/pi-coding-agent pins engines.node
  >=22.19.0 and fails at *runtime* on 20.x (TypeError:
  webidl.util.markAsUncloneable is not a function, from undici's
  Node-22-only Cache API), not just an npm EBADENGINE warning. Bumped
  the shared script to Node 22 and pinned pi to 0.80.6 for reproducibility
  (matching the existing Codex/Claude pin pattern).
- Fixed a second bug the Node bump surfaced on Dockerfile.e2b only:
  e2bdev/base:latest ships its own /usr/local/bin/node (v20.9.0) baked
  into the image. nodesource's .deb actually installs to /usr/bin/node,
  but /usr/local/bin wins on PATH, so every npm-installed CLI was
  silently running on the stale bundled Node. install-agents.sh now
  force-relinks /usr/local/bin/{node,npm,npx} to the dpkg-installed
  /usr/bin/ binaries after install (hardcoded path, not `command -v` —
  that still resolves the stale shadow).
- Updated the few remaining path references: .github/workflows/
  parsar-daemon-release.yml, server/internal/connector/agentdaemon/
  sandbox_seed.go (comment only), docs/spec-memory-module.md.

Verified: both Dockerfile.local and Dockerfile.e2b build clean end to
end; `docker run --rm <image> bash -c 'node/claude/codex/pi --version'`
confirms all four CLIs resolve to the intended pinned versions on both
images (Node 22.23.1, Claude 2.1.207, Codex 0.141.0, Pi 0.80.6).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Collapses infra/sandbox/Dockerfile.local + Dockerfile.e2b into a single
infra/sandbox/Dockerfile, selecting the local-docker vs e2b.app target
via --build-arg BASE_IMAGE (defaults to ubuntu:22.04; pass
e2bdev/base:latest for the e2b template). The two files had shrunk to a
~2-line real diff (base image + a TARGETARCH default) after the prior
commit's cleanup — not worth maintaining as two files when Docker's
ARG-before-FROM substitution covers it natively. Verified both build
targets still produce byte-identical images (full cache hit against the
prior two-file build).

- docker build -f infra/sandbox/Dockerfile -t parsar-sandbox:local .
- docker build -f infra/sandbox/Dockerfile --build-arg BASE_IMAGE=e2bdev/base:latest -t parsar-daemon-claudecode .

Updated every remaining reference to the old two-file paths:
docker-compose.yml, install.sh, INSTALL.md, docs/deploy/lan-deploy.md,
docs/spec-memory-module.md, .github/workflows/parsar-daemon-release.yml
(also fixed stale comments there claiming the sandbox image consumes
its published releases — it compiles parsar-daemon from source now,
those releases are only for the device-pairing connect command),
server/internal/connector/agentdaemon/sandbox_seed.go (comment only).

Added .github/workflows/sandbox-image-release.yml: a matrix build off
the single Dockerfile publishing both ghcr.io/<org>/parsar-sandbox
(multi-arch amd64+arm64) and ghcr.io/<org>/parsar-daemon-claudecode
(amd64 only) to GHCR, mirroring parsar-server-release.yml's trigger
conventions (PR = build only, push to main = :latest + :sha, sandbox-v*
tag = release, manual dispatch). actionlint clean; locally simulated
both matrix legs' exact build invocations successfully.

Verified end to end: rebuilt parsar-sandbox:local, recreated
parsar-server against it, confirmed the startup log reports the correct
wired image, then started a real container on the parsar_default
compose network with the exact invocation
server/internal/sandbox/docker/client.go uses and confirmed it can curl
parsar-server:8080/healthz and all four agent CLIs resolve.

Found but did not fix (pre-existing, unrelated to this change):
deploy/compose/compose.selfhost.yml and deploy/compose/.env.example set
PARSAR_E2B_TEMPLATE_ID / PARSAR_E2B_TIMEOUT_SECONDS, but
server/cmd/server/main.go actually reads AGENT_DAEMON_SANDBOX_TEMPLATE /
AGENT_DAEMON_SANDBOX_TEMPLATE_XL — the self-host e2b sandbox path can
never enable itself with those files as shipped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@MiniMax-AI-Dev MiniMax-AI-Dev merged commit b9ffffe into main Jul 11, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants