Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/sandbox/bundle/assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,29 @@
"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": "",
"dest": "/usr/local/share/superset/media/superset-1.29.0",
"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",
Expand Down
4 changes: 2 additions & 2 deletions packages/sandbox/bundle/rootfs/etc/profile.d/superset.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion packages/sandbox/bundle/steps.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
]
49 changes: 49 additions & 0 deletions packages/sandbox/bundle/steps/install-agent-clis.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/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/<sha>/: 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" "${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
dir="${dir%/}"
[ "$dir" = "$target" ] || rm -rf "$dir"
done

claude --version
codex --version
77 changes: 75 additions & 2 deletions packages/sandbox/src/assets/produce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<sha256><suffix>, then
Expand Down Expand Up @@ -398,14 +400,85 @@ function cli(): void {
console.log(`cli ${pkg.version} sha256 ${hash}`);
}

export const producers: Record<string, () => Promise<void> | 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<string> {
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<void> {
const pinned = version ?? (await fetchText(`${CLAUDE_RELEASES}/stable`));
const manifest = JSON.parse(
await fetchText(`${CLAUDE_RELEASES}/${pinned}/manifest.json`),
) as { platforms: Record<string, { checksum: string }> };
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<void> {
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> | void
> = {
chrome,
fonts,
themes,
wallpapers,
"host-service": hostService,
go,
cli,
claude,
codex,
};

if (import.meta.main) {
Expand All @@ -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]);
}
}
3 changes: 0 additions & 3 deletions packages/sandbox/src/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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.
Expand Down
Loading