Consolidate sandbox images: e2b + local docker only, drop K8s SandboxSet#87
Merged
Conversation
… 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>
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.shled here:PARSAR_SANDBOX_IMAGEdefaulted toghcr.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'sCOPY --from=e2bdev/base:latest /usr/bin/envdbroke (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
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.infra/e2b-templates/opencode/e2b.Dockerfiletemplate. Server-sideagent_kind=opencodesupport (DB, API, frontend, the localapps/parsar-daemondaemon runtime) is untouched — this only affects whether a sandbox image has OpenCode preinstalled.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 singleinfra/sandbox/Dockerfile, selecting the target via--build-arg BASE_IMAGE:TARGETARCHdefault) — not worth maintaining separately when Docker'sARG-before-FROMsubstitution covers it natively. Verified both targets still produce byte-identical images (full layer-cache hit against the prior two-file build).parsar-daemonfrom 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.infra/sandbox/scripts/install-agents.sh(previously 4 separateinstall-*.shfiles) 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 offclaude.ai/install.sh(bot-challenge-prone) onto the CDN method the local target already used.@earendil-works/pi-coding-agentpinsengines.node >=22.19.0and 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 to0.80.6for reproducibility.e2bdev/base:latestships its own stale/usr/local/bin/node(v20.9.0) that shadows a fresh nodesource install onPATH—install-agents.shnow force-relinks/usr/local/bin/{node,npm,npx}to the dpkg-installed/usr/bin/binaries..github/workflows/sandbox-image-release.yml: a matrix build off the single Dockerfile publishing bothghcr.io/<org>/parsar-sandbox(multi-arch amd64+arm64) andghcr.io/<org>/parsar-daemon-claudecode(amd64 only) to GHCR, mirroringparsar-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_IMAGEnow default toparsar-sandbox:localinstead 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.deploy/compose/compose.selfhost.yml/deploy/compose/.env.examplesetPARSAR_E2B_TEMPLATE_ID/PARSAR_E2B_TIMEOUT_SECONDS, butserver/cmd/server/main.goactually readsAGENT_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 enddocker 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)parsar-sandbox:local, recreatedparsar-server, confirmed the startup log reports"agent_daemon docker sandbox provider wired","image":"parsar-sandbox:local"parsar_defaultcompose network with the exact invocationserver/internal/sandbox/docker/client.gouses (docker run -d --entrypoint sleep ... infinity) and confirmed it cancurl parsar-server:8080/healthzsuccessfullyactionlintclean on the new workflow; locally simulated both matrix legs' exact build invocationsinfra/e2b-templates,Dockerfile.local/Dockerfile.e2b, the 4 separate install scripts,docker-compose.local.yml,parsar-init, oldseed-dev,docker-image-info,parsar-paths.sh)bash -n install.sh,docker compose config --quietondocker-compose.ymlboth pass🤖 Generated with Claude Code