Skip to content

perf(sandbox): stop re-shipping the runtime image on every release - #4538

Merged
yannickmonney merged 3 commits into
mainfrom
perf/sandbox-image-layers
Oct 9, 2026
Merged

yannickmonney merged 3 commits into
mainfrom
perf/sandbox-image-layers

Conversation

@yannickmonney

@yannickmonney yannickmonney commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Every host downloads and stores each runtime-image layer whose bytes change. Measured from the registry manifests, almost every release re-shipped the whole ~2 GB (compressed) image; new compressed bytes a host pulled per upgrade, amd64: 0.5.70→71 1.88 GB, 71→72 2.17 GB, 72→73 2.17 GB, 73→74 2.03 GB, 74→75 1.25 GB, 75→76 1.98 GB. Each of those also added ~6 GB on disk (old versions are never removed; #4533 fixes that half).

Two causes:

  1. Cache lineage. The amd64 release build imported main's GHA cache besides its own registry cache. That cache is evicted often (~10.7 GB in a 10 GB repository cache); a main build at 04:55 on Oct 7 ran with 0 cached steps, rebuilt everything, and the v0.5.76 release then published those new bytes although no image input had changed. arm64, built from the same source with the registry cache alone, shipped 0 MB new for that same release.
  2. One chain. Every harness, browser and vision stage was FROM tooling-base (a guard required it), the one chain that also held OS packages, Office, TeX, the document libraries, the Docker engine and the CLAUDE_CODE_* env defaults. Adding two apt packages (fix(platform): synchronize external task lifecycles #4395) or a sharp security bump (fix(deps): update dependency sharp to v0.35.5 [security] #4450) re-ran every harness install and re-shipped unrelated layers.

Change

  • CI (ci: commit): releases read only their own per-arch registry cache. PR and main builds read it too (after GHA, never writing it), so a build whose GHA cache was evicted reuses released layers instead of a cold ~14 min rebuild.
  • Layout (perf: commit 2): a small harness-base (Python slim + Node + download tools) builds the harnesses, the browser and the document Node libraries; the document Python libraries install into their own prefix and Hermes builds on them; everything arrives as COPY --link layers. The OS chain runs from the largest, stablest apt set to the most often extended, and holds no ENV. The guard now encodes this (no stage but runtime reaches the OS chain; no ENV there; apt order; document libraries as their own layers).
  • Payloads (perf: commit 3): drop Cursor's two macOS-only SEA builds (~265 MB unpacked), Codex's realtime voice host (~77 MB), and Qwen's ripgrep for other platforms; bake the Python stdlib's bytecode (the base image strips it and the read-only root can't cache it, so every Python process recompiled its stdlib imports: ~0.1 s per wrapper start, up to ~1 s for broad imports); carry the node image's yarn and fix the dangling yarn/yarnpkg/nodejs links on PATH; pin python, node, debian and uv by digest (like oven/bun already is) so base refreshes come through Renovate.

Expected effect

  • Releases that don't touch image inputs: ~0 MB download per host instead of ~2 GB, and ~0 new disk instead of ~6 GB.
  • A document-library or apt change: re-ships its own layer (tens to ~250 MB) instead of everything.
  • Image 339 MB smaller unpacked, measured by this PR's image validation (5,141 MB against main's 5,480 MB); Python process starts cheaper.

Proof

  • Every stage and the full runtime target built locally (arm64, throwaway builder), including the step that launches the real Playwright MCP server to record its manifests. Probes on the built image: no dangling links; yarn 1.22.22, nodejs, bunx run; stdlib .pyc present; cursor, qwen, codex, hermes start; codex exec reaches its network phase without the voice host; pip check clean; Hermes installed only its own additions on top of the shared document libraries.
  • dockerfile-fail-closed.guard.test.ts 61/61; the new rules fail 17 cases against the old Dockerfile.
  • CI tests: build-ci, ci-build-optimization, ci-image-pulls, deployment-ci pass.
  • CI on this PR: Build sandbox-runtime (amd64) green; image suite 50/50 (size budgets); runtime conformance suite 88/0, including the new no-dangling-links, yarn/nodejs and stdlib-bytecode assertions.

The first release after this merges re-ships the image once (new layout); later releases reuse it.

@yannickmonney
yannickmonney force-pushed the perf/sandbox-image-layers branch from c32fc54 to 4238f8c Compare October 8, 2026 12:47
The amd64 release build also imported the GHA cache main CI warms. That
cache is evicted often (the repository holds ~10.7 GB against GitHub's
10 GB limit), and a main build that rebuilt from an evicted cache exported
new bytes for unchanged layers; the next release then published them.
v0.5.76's amd64 runtime image changed 1.98 of its 2.03 GB compressed with
no image input changed, while arm64, built from the same source with the
registry cache alone, changed none. Every host downloaded ~2 GB and stored
~6 GB more on such an upgrade.

Releases now read only their own per-architecture registry cache, so a
reused layer keeps the previous release's bytes. Pull request and main
builds read that cache as well, after their GHA cache, so a build whose GHA
cache was evicted reuses the released layers instead of rebuilding all of
them; they still never write it.
Every harness, browser and vision stage was built FROM tooling-base, the
one chain that also held the OS packages, Office, TeX, the document
libraries, the Docker engine and the Claude Code environment defaults, and
a guard required it. Any edit there (two apt packages for #4395, the sharp
security bump in #4450, an ENV tweak) re-ran every harness install and
could re-ship ~2 GB of layers to every host.

- harness-base (Python slim + Node + download tools) now builds the
  harnesses, the browser and the document Node libraries; the document
  Python libraries install into their own prefix, Hermes builds on them,
  and all of these arrive in the runtime stage as `COPY --link` layers.
- The OS chain runs from the largest, stablest apt set to the most often
  extended (fonts and browser libraries, Office, TeX, the Docker engine,
  then the everyday tools), and its environment variables moved to the
  runtime stage, below every install.
- The guard now requires that no stage but the runtime reaches the OS
  chain, that the OS chain holds no ENV and keeps its apt order, and that
  the document libraries ship as layers of their own.

Every stage, and the full runtime image including the Playwright MCP
manifest recording, was built locally (arm64).
- Remove Cursor's two macOS-only single-executable builds (~265 MB): its
  computer-use re-exec downloads and codesign-checks its own copy under
  ~/.cursor, and `cursor-agent` runs the bundled node.
- Remove Codex's realtime voice host (~77 MB with its media libraries); the
  sandbox drives `codex exec`, which starts without it.
- Keep only this architecture's Linux ripgrep among the five Qwen vendors.
- Compile the Python stdlib to hash-based bytecode. The python image deletes
  every stdlib .pyc and sessions run with a read-only root and
  PYTHONDONTWRITEBYTECODE, so each wrapper, launcher, Hermes turn, run_code
  script and document skill compiled its stdlib imports again (~0.1 s per
  wrapper start, up to ~1 s for a broad import set).
- Carry the node image's yarn and repoint its nodejs link: yarn, yarnpkg
  and nodejs were dangling links first on PATH.
- Pin python, node, debian and uv by digest, like oven/bun already is, so
  a base refresh is a Renovate change instead of whatever the tag held when
  a cache missed.

The image suite now asserts no dangling link on PATH, working yarn and
nodejs, and baked stdlib bytecode. Probed locally on the built image:
cursor, qwen, codex and hermes start, `codex exec` reaches its network
phase, and `pip check` is clean.
@yannickmonney
yannickmonney force-pushed the perf/sandbox-image-layers branch from 6f07b7e to 4b161f2 Compare October 9, 2026 02:54
@yannickmonney
yannickmonney merged commit ef8f7c1 into main Oct 9, 2026
1 of 2 checks passed
@yannickmonney
yannickmonney deleted the perf/sandbox-image-layers branch October 9, 2026 03:03
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.

1 participant