From b6f1dea3359cf712ca8eff29ef770e7afa4f073c Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Tue, 29 Sep 2026 18:11:58 -0700 Subject: [PATCH 1/2] feat(sandbox): pin claude and codex in the bundle so boxes update on wake The agent CLIs were npm-installed at image build, so a box kept the version it was forked with forever, and codex's own updater fails (npm prefix is root-owned). They are now bundle assets installed by an install-agent-clis step: a version bump is one assets.json row, and every box picks it up on its next boot. - claude: native linux-x64 binary, stable channel (2.1.280) - codex: standalone linux package (0.159.2), unpacked to /opt/codex/ - `bun run assets claude|codex [version]` produces the rows, verified against the vendor checksums - the step removes the old npm installs on existing boxes --- packages/sandbox/bundle/assets.json | 16 ++++ .../bundle/rootfs/etc/profile.d/superset.sh | 4 +- packages/sandbox/bundle/steps.json | 3 +- .../bundle/steps/install-agent-clis.sh | 48 ++++++++++++ packages/sandbox/src/assets/produce.ts | 77 ++++++++++++++++++- packages/sandbox/src/image.ts | 3 - 6 files changed, 143 insertions(+), 8 deletions(-) create mode 100755 packages/sandbox/bundle/steps/install-agent-clis.sh diff --git a/packages/sandbox/bundle/assets.json b/packages/sandbox/bundle/assets.json index 52aad6e4fc3..27770d4dfdb 100644 --- a/packages/sandbox/bundle/assets.json +++ b/packages/sandbox/bundle/assets.json @@ -7,6 +7,14 @@ "source": "https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb", "version": "153.0.8010.36" }, + "claude": { + "sha256": "1e08503dbdf3c2cb0d706d32f3408277388d1c76ef108673e8fe42c1b322925b", + "suffix": "", + "dest": "/usr/local/share/superset/media/claude-2.1.280", + "mode": "0755", + "source": "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/2.1.280/linux-x64/claude", + "version": "2.1.280" + }, "cli": { "sha256": "66be5ff609582623dc943c2a6405553268c9b57d8ad215d2056cde9f746186ab", "suffix": "", @@ -14,6 +22,14 @@ "mode": "0755", "version": "1.29.0" }, + "codex": { + "sha256": "9e2d29a713b94478b240dec2f10e11324cd05fad76dc43e7c639bdf8a1337a6b", + "suffix": ".tar.gz", + "dest": "/usr/local/share/superset/media/codex-0.159.2.tar.gz", + "mode": "0644", + "source": "https://github.com/openai/codex/releases/download/rust-v0.159.2/codex-package-x86_64-unknown-linux-musl.tar.gz", + "version": "0.159.2" + }, "font-bold": { "sha256": "e490660ad75e0b152c93b1604c2ea1a4ea675f1b8a3fe0d005b1998568f99f1f", "suffix": ".ttf", diff --git a/packages/sandbox/bundle/rootfs/etc/profile.d/superset.sh b/packages/sandbox/bundle/rootfs/etc/profile.d/superset.sh index 64c53be4c3c..793e2b54931 100644 --- a/packages/sandbox/bundle/rootfs/etc/profile.d/superset.sh +++ b/packages/sandbox/bundle/rootfs/etc/profile.d/superset.sh @@ -8,6 +8,6 @@ export DISPLAY="${DISPLAY:-${SUPERSET_DISPLAY:-:1}}" # The desktop session's bus, written by superset-desktop-init once it is up. [ -r "${SUPERSET_RUN_DIR:-/run/superset}/desktop.env" ] && . "${SUPERSET_RUN_DIR:-/run/superset}/desktop.env" case ":$PATH:" in *":/usr/local/go/bin:"*) ;; *) export PATH="$PATH:/usr/local/go/bin" ;; esac -# The agent CLIs are installed by root at image build; a self-update from the -# sandbox user has nowhere to write and only prints a warning every session. +# The agent CLIs are pinned by the bundle (install-agent-clis); a self-update +# would drift from the pin and only prints a warning every session. export DISABLE_AUTOUPDATER=1 diff --git a/packages/sandbox/bundle/steps.json b/packages/sandbox/bundle/steps.json index 31b4e784dd1..7fadde4fb8c 100644 --- a/packages/sandbox/bundle/steps.json +++ b/packages/sandbox/bundle/steps.json @@ -32,5 +32,6 @@ ] }, { "name": "install-host", "assets": ["host-service"] }, - { "name": "install-cli", "assets": ["cli"] } + { "name": "install-cli", "assets": ["cli"] }, + { "name": "install-agent-clis", "assets": ["claude", "codex"] } ] diff --git a/packages/sandbox/bundle/steps/install-agent-clis.sh b/packages/sandbox/bundle/steps/install-agent-clis.sh new file mode 100755 index 00000000000..493474c393e --- /dev/null +++ b/packages/sandbox/bundle/steps/install-agent-clis.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Puts the pinned `claude` and `codex` on PATH. Both are rows in assets.json, +# so a version bump is a row change and every box picks it up on its next +# boot. Codex unpacks into /opt/codex//: its binary finds the bundled +# rg, bwrap and code-mode host through the codex-package.json beside it. +set -euo pipefail +. /etc/superset/contract.sh + +staged() { + local pattern="$1" found="" + for candidate in "${SUPERSET_MEDIA_DIR}"/${pattern}; do + [ -f "${candidate}.hash" ] || continue + if grep -qF "$(tr -d '\n\r' < "${candidate}.hash")" "${SUPERSET_BUNDLE_DIR}/assets.tsv"; then + found="$candidate" + fi + done + [ -n "$found" ] || { echo "no ${pattern} of this bundle under ${SUPERSET_MEDIA_DIR}" >&2; exit 1; } + printf '%s' "$found" +} + +link() { + ln -sfn "$2" "$1.tmp" + mv -T "$1.tmp" "$1" +} + +# Images before this step installed both with npm; its links and packages go. +rm -rf /usr/local/lib/node_modules/@anthropic-ai/claude-code /usr/local/lib/node_modules/@openai/codex + +claude="$(staged 'claude-*')" +chmod 0755 "$claude" +link /usr/local/bin/claude "$claude" + +archive="$(staged 'codex-*.tar.gz')" +sha="$(tr -d '\n\r' < "${archive}.hash")" +target="/opt/codex/${sha}" +if [ ! -x "${target}/bin/codex" ]; then + rm -rf "$target" + mkdir -p "$target" + tar -xzf "$archive" -C "$target" +fi +link /usr/local/bin/codex "${target}/bin/codex" +for dir in /opt/codex/*/; do + dir="${dir%/}" + [ "$dir" = "$target" ] || rm -rf "$dir" +done + +claude --version +codex --version diff --git a/packages/sandbox/src/assets/produce.ts b/packages/sandbox/src/assets/produce.ts index 02e512cb6a6..7c031fa69a2 100644 --- a/packages/sandbox/src/assets/produce.ts +++ b/packages/sandbox/src/assets/produce.ts @@ -6,6 +6,8 @@ * bun run assets themes prebuilt GTK, icon and cursor theme tarballs * bun run assets wallpapers eight photos, cropped to the display * bun run assets host-service the runtime tarball from this checkout + * bun run assets claude [version] Claude Code's native binary (default: stable) + * bun run assets codex [version] Codex's standalone package (default: latest) * bun run assets all * * Each producer writes its files to .cache/assets/, then @@ -398,7 +400,76 @@ function cli(): void { console.log(`cli ${pkg.version} sha256 ${hash}`); } -export const producers: Record Promise | void> = { +// --- Agent CLIs ------------------------------------------------------------- +const CLAUDE_RELEASES = + "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases"; + +async function fetchText(url: string): Promise { + return new TextDecoder().decode(await fetchBytes(url)).trim(); +} + +/** Claude Code's native linux-x64 binary; defaults to the `stable` channel. */ +async function claude(version?: string): Promise { + const pinned = version ?? (await fetchText(`${CLAUDE_RELEASES}/stable`)); + const manifest = JSON.parse( + await fetchText(`${CLAUDE_RELEASES}/${pinned}/manifest.json`), + ) as { platforms: Record }; + const expected = manifest.platforms["linux-x64"]?.checksum; + if (!expected) throw new Error(`claude ${pinned}: no linux-x64 build`); + const source = `${CLAUDE_RELEASES}/${pinned}/linux-x64/claude`; + const { sha256: hash } = cache(await fetchBytes(source), ""); + if (hash !== expected) + throw new Error(`claude ${pinned}: sha256 ${hash}, manifest ${expected}`); + rewriteRows({ + claude: { + sha256: hash, + suffix: "", + dest: `${SANDBOX_PATHS.media}/claude-${pinned}`, + mode: "0755", + source, + version: pinned, + }, + }); +} + +/** Codex's standalone package (binary, rg, bwrap); defaults to the latest release. */ +async function codex(version?: string): Promise { + const tag = version ? `tags/rust-v${version}` : "latest"; + const release = JSON.parse( + await fetchText( + `https://api.github.com/repos/openai/codex/releases/${tag}`, + ), + ) as { + tag_name: string; + assets: { name: string; digest: string; browser_download_url: string }[]; + }; + const pinned = release.tag_name.replace(/^rust-v/, ""); + const asset = release.assets.find( + (a) => a.name === "codex-package-x86_64-unknown-linux-musl.tar.gz", + ); + if (!asset) throw new Error(`codex ${pinned}: no linux package`); + const { sha256: hash } = cache( + await fetchBytes(asset.browser_download_url), + ".tar.gz", + ); + if (`sha256:${hash}` !== asset.digest) + throw new Error(`codex ${pinned}: sha256 ${hash}, release ${asset.digest}`); + rewriteRows({ + codex: { + sha256: hash, + suffix: ".tar.gz", + dest: `${SANDBOX_PATHS.media}/codex-${pinned}.tar.gz`, + mode: "0644", + source: asset.browser_download_url, + version: pinned, + }, + }); +} + +export const producers: Record< + string, + (version?: string) => Promise | void +> = { chrome, fonts, themes, @@ -406,6 +477,8 @@ export const producers: Record Promise | void> = { "host-service": hostService, go, cli, + claude, + codex, }; if (import.meta.main) { @@ -419,6 +492,6 @@ if (import.meta.main) { } for (const name of names) { console.log(`--- ${name}`); - await producers[name]?.(); + await producers[name]?.(process.argv[3]); } } diff --git a/packages/sandbox/src/image.ts b/packages/sandbox/src/image.ts index 7974edf1f96..cb49ec9db30 100644 --- a/packages/sandbox/src/image.ts +++ b/packages/sandbox/src/image.ts @@ -49,7 +49,6 @@ const GO_VERSION = "1.27.1"; /** Pinned by `bun run assets go`; verified before the tarball is unpacked. */ const GO_SHA256 = "63d339f0da5ab53635a56f2490a7984dfe12dfcff22ad749f63edaf590168445"; -const AGENT_CLI_VERSIONS = { claudeCode: "2.1.257", codex: "0.152.0" } as const; function aptList(name: string): string { return readFileSync( @@ -91,8 +90,6 @@ RUN curl -fsSL -o /tmp/go.tgz https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar. && echo "${GO_SHA256} /tmp/go.tgz" | sha256sum -c - \\ && tar -C /usr/local -xzf /tmp/go.tgz && rm /tmp/go.tgz RUN npm install -g bun@${BUN_VERSION} --no-audit --no-fund && bun --version -# The agents the sandbox can run; a sandbox has none of the user's local ones. -RUN npm install -g @anthropic-ai/claude-code@${AGENT_CLI_VERSIONS.claudeCode} @openai/codex@${AGENT_CLI_VERSIONS.codex} --no-audit --no-fund && claude --version && codex --version # The sandbox user: the reference and the platform's own images run as one # with passwordless sudo; root is the boot runner only. # The base image ships a \`node\` user on uid 1000; ours takes that uid. From 49ada01f3a5210580bb0cd426dbc105789459e26 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Tue, 29 Sep 2026 18:34:09 -0700 Subject: [PATCH 2/2] fix(sandbox): move the codex package into place only after it unpacks --- packages/sandbox/bundle/steps/install-agent-clis.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/sandbox/bundle/steps/install-agent-clis.sh b/packages/sandbox/bundle/steps/install-agent-clis.sh index 493474c393e..63892269540 100755 --- a/packages/sandbox/bundle/steps/install-agent-clis.sh +++ b/packages/sandbox/bundle/steps/install-agent-clis.sh @@ -34,9 +34,10 @@ archive="$(staged 'codex-*.tar.gz')" sha="$(tr -d '\n\r' < "${archive}.hash")" target="/opt/codex/${sha}" if [ ! -x "${target}/bin/codex" ]; then - rm -rf "$target" - mkdir -p "$target" - tar -xzf "$archive" -C "$target" + rm -rf "$target" "${target}.partial" + mkdir -p "${target}.partial" + tar -xzf "$archive" -C "${target}.partial" + mv -T "${target}.partial" "$target" fi link /usr/local/bin/codex "${target}/bin/codex" for dir in /opt/codex/*/; do