Skip to content

Commit c4faae8

Browse files
Merge pull request #3 from bespokelabsai/feature/preset-images
Add preset images with pinned tags and Tensorlake support
2 parents fafad9b + 9493c61 commit c4faae8

13 files changed

Lines changed: 268 additions & 6 deletions

File tree

‎.github/workflows/build-images.yml‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
name: Build and push preset images
2+
3+
# Two modes of operation:
4+
#
5+
# * Push to main -> build+push :main (mutable dev tag). Library code
6+
# never references :main; it exists for manual testing.
7+
#
8+
# * workflow_dispatch -> build+push a pinned, immutable tag (e.g. :v1)
9+
# matching PRESET_IMAGE_TAG in src/bespokelabs/sandbox/presets.py.
10+
# This is how you cut a new image release: bump the constant in
11+
# Python, then run this workflow with the same tag value.
12+
#
13+
# :sha-<short> is always emitted for audit/rollback.
14+
15+
on:
16+
push:
17+
branches: [main]
18+
paths:
19+
- "images/**"
20+
workflow_dispatch:
21+
inputs:
22+
image:
23+
description: "Specific image to build (e.g. claude-sdk), or 'all'"
24+
required: false
25+
default: "all"
26+
tag:
27+
description: "Pinned tag to publish (e.g. v1). Must match PRESET_IMAGE_TAG."
28+
required: true
29+
default: "v1"
30+
31+
env:
32+
REGISTRY: ghcr.io
33+
IMAGE_PREFIX: ghcr.io/bespokelabsai/sandbox
34+
35+
jobs:
36+
detect-changes:
37+
runs-on: ubuntu-latest
38+
outputs:
39+
images: ${{ steps.set-matrix.outputs.images }}
40+
steps:
41+
- uses: actions/checkout@v4
42+
with:
43+
fetch-depth: 2
44+
45+
- id: set-matrix
46+
run: |
47+
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
48+
INPUT="${{ github.event.inputs.image }}"
49+
if [[ "$INPUT" == "all" || -z "$INPUT" ]]; then
50+
IMAGES=$(ls -d images/*/ | xargs -I{} basename {} | jq -R -s -c 'split("\n") | map(select(. != ""))')
51+
else
52+
IMAGES=$(echo "[\"$INPUT\"]")
53+
fi
54+
else
55+
# On push, only build images whose Dockerfile changed
56+
CHANGED=$(git diff --name-only HEAD~1 HEAD -- images/ | cut -d/ -f2 | sort -u | jq -R -s -c 'split("\n") | map(select(. != ""))')
57+
if [[ "$CHANGED" == "[]" || "$CHANGED" == '[""]' ]]; then
58+
IMAGES="[]"
59+
else
60+
IMAGES="$CHANGED"
61+
fi
62+
fi
63+
echo "images=$IMAGES" >> "$GITHUB_OUTPUT"
64+
65+
build:
66+
needs: detect-changes
67+
if: needs.detect-changes.outputs.images != '[]'
68+
runs-on: ubuntu-latest
69+
permissions:
70+
contents: read
71+
packages: write
72+
strategy:
73+
fail-fast: false
74+
matrix:
75+
image: ${{ fromJson(needs.detect-changes.outputs.images) }}
76+
steps:
77+
- uses: actions/checkout@v4
78+
79+
- uses: docker/login-action@v3
80+
with:
81+
registry: ${{ env.REGISTRY }}
82+
username: ${{ github.actor }}
83+
password: ${{ secrets.GITHUB_TOKEN }}
84+
85+
- uses: docker/setup-buildx-action@v3
86+
87+
- uses: docker/metadata-action@v5
88+
id: meta
89+
with:
90+
images: ${{ env.IMAGE_PREFIX }}/${{ matrix.image }}
91+
# On workflow_dispatch -> the pinned tag input (e.g. :v1).
92+
# On push to main -> :main (dev tag, never referenced by library).
93+
# Always emit :sha-<short> for auditability.
94+
tags: |
95+
type=raw,value=${{ github.event.inputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' }}
96+
type=raw,value=main,enable=${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
97+
type=sha,prefix=sha-
98+
99+
- uses: docker/build-push-action@v5
100+
with:
101+
context: images/${{ matrix.image }}
102+
push: true
103+
tags: ${{ steps.meta.outputs.tags }}
104+
labels: ${{ steps.meta.outputs.labels }}
105+
cache-from: type=gha
106+
cache-to: type=gha,mode=max

‎.gitignore‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ __pycache__/
33
*.pyo
44
dist/
55
examples/.sandbox_workdir/
6-
examples/__pycache__/
6+
examples/__pycache__/
7+
.claude/

‎images/claude-code/Dockerfile‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FROM python:3.12-slim
2+
3+
RUN apt-get update && apt-get install -y --no-install-recommends \
4+
curl \
5+
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
6+
&& apt-get install -y --no-install-recommends nodejs \
7+
&& npm install -g @anthropic-ai/claude-code \
8+
&& apt-get clean && rm -rf /var/lib/apt/lists/*

‎images/claude-sdk/Dockerfile‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FROM python:3.12-slim
2+
3+
RUN pip install --no-cache-dir claude-agent-sdk

‎images/codex/Dockerfile‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FROM python:3.12-slim
2+
3+
RUN apt-get update && apt-get install -y --no-install-recommends \
4+
curl \
5+
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
6+
&& apt-get install -y --no-install-recommends nodejs \
7+
&& npm install -g @openai/codex \
8+
&& apt-get clean && rm -rf /var/lib/apt/lists/*
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
FROM python:3.12-slim
2+
3+
RUN pip install --no-cache-dir \
4+
numpy \
5+
pandas \
6+
matplotlib \
7+
scikit-learn

‎images/python-ml/Dockerfile‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
FROM python:3.12-slim
2+
3+
RUN pip install --no-cache-dir \
4+
torch \
5+
transformers \
6+
datasets \
7+
accelerate

‎images/web-dev/Dockerfile‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FROM node:22-slim
2+
3+
RUN npm install -g typescript ts-node prettier eslint

‎src/bespokelabs/sandbox/backends/tensorlake.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ def create(self, config: SandboxConfig) -> None:
3333
"timeout_secs": config.timeout_secs,
3434
"allow_internet_access": config.allow_internet,
3535
}
36+
if config.image:
37+
kwargs["image"] = config.image
3638
if config.snapshot_id:
3739
kwargs["snapshot_id"] = config.snapshot_id
3840
self._sandbox = self._client.create_and_connect(**kwargs)

‎src/bespokelabs/sandbox/presets.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ class SandboxPreset:
1010
Attributes:
1111
name: Human-readable preset name.
1212
description: What this preset provides.
13+
image: Pre-built OCI container image for docker/daytona/modal backends
14+
(skips setup_commands if the backend supports images).
15+
tensorlake_image: Tensorlake project-scoped image name. Tensorlake
16+
doesn't accept arbitrary OCI URLs, so this is a separate field.
17+
Users register a project image with `tl sbx image build` (our
18+
Dockerfiles under images/ can be used as-is) and then set this.
19+
When set, setup_commands are skipped on the Tensorlake backend.
1320
setup_commands: Shell commands run after sandbox creation (in order).
1421
cpu: Minimum recommended vCPUs.
1522
memory_mb: Minimum recommended RAM in MB.
@@ -20,6 +27,8 @@ class SandboxPreset:
2027

2128
name: str
2229
description: str
30+
image: str | None = None
31+
tensorlake_image: str | None = None
2332
setup_commands: list[str] = field(default_factory=list)
2433
cpu: float = 1.0
2534
memory_mb: int = 1024
@@ -49,11 +58,21 @@ def get_preset(name: str) -> SandboxPreset:
4958
return PRESETS[name]
5059

5160

61+
# -- Constants -------------------------------------------------------------
62+
63+
IMAGE_REGISTRY = "ghcr.io/bespokelabsai/sandbox"
64+
65+
# Pinned tag for preset images. Bump to cut a new immutable image set,
66+
# then run the build-images workflow_dispatch with the same tag value.
67+
# Never use ":latest" here — it's not reproducible.
68+
PRESET_IMAGE_TAG = "v1"
69+
5270
# -- Built-in presets ------------------------------------------------------
5371

5472
register_preset(SandboxPreset(
5573
name="claude-code",
5674
description="Sandbox with Claude Code (Anthropic CLI) installed",
75+
image=f"{IMAGE_REGISTRY}/claude-code:{PRESET_IMAGE_TAG}",
5776
setup_commands=[
5877
"npm install -g @anthropic-ai/claude-code",
5978
],
@@ -64,6 +83,7 @@ def get_preset(name: str) -> SandboxPreset:
6483
register_preset(SandboxPreset(
6584
name="claude-sdk",
6685
description="Sandbox with Claude Agent SDK and bundled Claude Code CLI",
86+
image=f"{IMAGE_REGISTRY}/claude-sdk:{PRESET_IMAGE_TAG}",
6787
setup_commands=[
6888
"pip install claude-agent-sdk",
6989
],
@@ -74,6 +94,7 @@ def get_preset(name: str) -> SandboxPreset:
7494
register_preset(SandboxPreset(
7595
name="codex",
7696
description="Sandbox with Codex CLI installed",
97+
image=f"{IMAGE_REGISTRY}/codex:{PRESET_IMAGE_TAG}",
7798
setup_commands=[
7899
"npm install -g @openai/codex",
79100
],
@@ -84,6 +105,7 @@ def get_preset(name: str) -> SandboxPreset:
84105
register_preset(SandboxPreset(
85106
name="python-data-science",
86107
description="Python with numpy, pandas, matplotlib, scikit-learn",
108+
image=f"{IMAGE_REGISTRY}/python-data-science:{PRESET_IMAGE_TAG}",
87109
setup_commands=[
88110
"pip install numpy pandas matplotlib scikit-learn",
89111
],
@@ -101,6 +123,7 @@ def get_preset(name: str) -> SandboxPreset:
101123
register_preset(SandboxPreset(
102124
name="web-dev",
103125
description="Node.js with common web development tools",
126+
image=f"{IMAGE_REGISTRY}/web-dev:{PRESET_IMAGE_TAG}",
104127
setup_commands=[
105128
"npm install -g typescript ts-node prettier eslint",
106129
],
@@ -110,6 +133,7 @@ def get_preset(name: str) -> SandboxPreset:
110133
register_preset(SandboxPreset(
111134
name="python-ml",
112135
description="Python with PyTorch and common ML libraries",
136+
image=f"{IMAGE_REGISTRY}/python-ml:{PRESET_IMAGE_TAG}",
113137
setup_commands=[
114138
"pip install torch transformers datasets accelerate",
115139
],

0 commit comments

Comments
 (0)