Skip to content

Commit d3997a8

Browse files
Add OpenCode integration for Sandbox SDK (#282)
* Add OpenCode integration for Sandbox SDK Adds support for running OpenCode (AI coding agent) inside Sandbox containers with two usage patterns: 1. Programmatic SDK access via createOpencode(): - Returns typed client from @opencode-ai/sdk - Custom fetch adapter routes requests through container - Automatic server lifecycle management 2. Web UI proxy via proxyToOpencode(): - Exposes OpenCode's web interface through Workers - Handles localhost redirect for proper API routing Features: - Process reuse: detects existing OpenCode server on port - Provider auth: extracts API keys from config to env vars - Peer dependency on @opencode-ai/sdk (optional install) Includes example worker demonstrating both patterns. * Add changeset * Improve type safety and test coverage for OpenCode integration - Use `unknown` instead of `any` for dynamic SDK import - Add typed mock interfaces for Process and Sandbox in tests - Add tests for process reuse logic and API key extraction * Add OpenCode container variant and improve startup reliability - Add -opencode Docker image variant with OpenCode CLI pre-installed - Handle concurrent startup attempts gracefully with retry logic - Add error handling in proxyToOpencode with proper error responses - Add OPENCODE_STARTUP_FAILED error code to shared error codes - Add logging throughout OpenCode server lifecycle - Use exact command matching for process detection - Add E2E tests for OpenCode CLI availability and server lifecycle - Update CI workflows to build -opencode image variant * Improve OpenCode type safety and add internal logging - Use @opencode-ai/sdk Config type instead of Record<string, unknown> - Fix API key extraction path (options.apiKey) - Add OpencodeStartupContext for structured error context - Remove Logger from public options (internal concern) - Add internal logging for debugging without exposing in API - Fix Sandbox<any> to Sandbox<unknown> * Remove createSandboxFetch and inline the one-liner * Align OpenCode API with SDK conventions Refactors the integration to match OpenCode's own SDK structure with createOpencodeServer for server lifecycle and proxyToOpencode for web UI requests. Adds directory option and pre-clones sample project. * Add config validation and test coverage for OpenCode integration Validate that config.provider is an object before iterating, preventing crashes when users pass malformed config like strings or arrays. Add tests for proxyToOpencode redirect logic and malformed config handling. * Add error handling for concurrent server retry path
1 parent 67100d0 commit d3997a8

30 files changed

+10658
-15
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@cloudflare/sandbox': patch
3+
---
4+
5+
Add OpenCode integration with createOpencode() and proxyToOpencode() helpers

.github/workflows/pkg-pr-new.yml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,22 @@ jobs:
109109
build-args: |
110110
SANDBOX_VERSION=${{ steps.package-version.outputs.version }}
111111
112+
- name: Build and push Docker image (opencode)
113+
uses: docker/build-push-action@v6
114+
with:
115+
context: .
116+
file: packages/sandbox/Dockerfile
117+
target: opencode
118+
platforms: linux/amd64
119+
push: true
120+
tags: cloudflare/sandbox:${{ steps.package-version.outputs.version }}-opencode
121+
cache-from: |
122+
type=gha,scope=preview-pr-${{ github.event.pull_request.number }}-opencode
123+
type=gha,scope=release-opencode
124+
cache-to: type=gha,mode=max,scope=preview-pr-${{ github.event.pull_request.number }}-opencode
125+
build-args: |
126+
SANDBOX_VERSION=${{ steps.package-version.outputs.version }}
127+
112128
- name: Publish to pkg.pr.new
113129
run: npx pkg-pr-new publish './packages/sandbox'
114130

@@ -119,7 +135,8 @@ jobs:
119135
const version = '${{ steps.package-version.outputs.version }}';
120136
const defaultTag = `cloudflare/sandbox:${version}`;
121137
const pythonTag = `cloudflare/sandbox:${version}-python`;
122-
const body = `### 🐳 Docker Images Published\n\n**Default (no Python):**\n\`\`\`dockerfile\nFROM ${defaultTag}\n\`\`\`\n\n**With Python:**\n\`\`\`dockerfile\nFROM ${pythonTag}\n\`\`\`\n\n**Version:** \`${version}\`\n\nUse the \`-python\` variant if you need Python code execution.`;
138+
const opencodeTag = `cloudflare/sandbox:${version}-opencode`;
139+
const body = `### 🐳 Docker Images Published\n\n**Default (no Python):**\n\`\`\`dockerfile\nFROM ${defaultTag}\n\`\`\`\n\n**With Python:**\n\`\`\`dockerfile\nFROM ${pythonTag}\n\`\`\`\n\n**With OpenCode:**\n\`\`\`dockerfile\nFROM ${opencodeTag}\n\`\`\`\n\n**Version:** \`${version}\`\n\nUse the \`-python\` variant for Python code execution, or \`-opencode\` for the OpenCode AI coding agent.`;
123140
124141
// Find existing comment
125142
const { data: comments } = await github.rest.issues.listComments({

.github/workflows/pullrequest.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,18 @@ jobs:
116116
- name: Set up Docker Buildx
117117
uses: docker/setup-buildx-action@v3
118118

119-
- name: Build test worker Docker images (base + python)
119+
- name: Build test worker Docker images (base + python + opencode)
120120
run: |
121121
VERSION=${{ needs.unit-tests.outputs.version || '0.0.0' }}
122-
# Build base image (no Python) - used by SandboxBase binding
122+
# Build base image (no Python) - used by Sandbox binding
123123
docker build -f packages/sandbox/Dockerfile --target default --platform linux/amd64 \
124124
--build-arg SANDBOX_VERSION=$VERSION -t cloudflare/sandbox-test:$VERSION .
125-
# Build python image - used by Sandbox binding
125+
# Build python image - used by SandboxPython binding
126126
docker build -f packages/sandbox/Dockerfile --target python --platform linux/amd64 \
127127
--build-arg SANDBOX_VERSION=$VERSION -t cloudflare/sandbox-test:$VERSION-python .
128+
# Build opencode image - used by SandboxOpencode binding
129+
docker build -f packages/sandbox/Dockerfile --target opencode --platform linux/amd64 \
130+
--build-arg SANDBOX_VERSION=$VERSION -t cloudflare/sandbox-test:$VERSION-opencode .
128131
129132
# Deploy test worker using official Cloudflare action
130133
- name: Deploy test worker

.github/workflows/release.yml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ jobs:
108108
- name: Set up Docker Buildx
109109
uses: docker/setup-buildx-action@v3
110110

111-
- name: Build test worker Docker images (base + python)
111+
- name: Build test worker Docker images (base + python + opencode)
112112
run: |
113113
VERSION=${{ needs.unit-tests.outputs.version }}
114114
# Build base image (no Python) - used by Sandbox binding
@@ -117,6 +117,9 @@ jobs:
117117
# Build python image - used by SandboxPython binding
118118
docker build -f packages/sandbox/Dockerfile --target python --platform linux/amd64 \
119119
--build-arg SANDBOX_VERSION=$VERSION -t cloudflare/sandbox-test:$VERSION-python .
120+
# Build opencode image - used by SandboxOpencode binding
121+
docker build -f packages/sandbox/Dockerfile --target opencode --platform linux/amd64 \
122+
--build-arg SANDBOX_VERSION=$VERSION -t cloudflare/sandbox-test:$VERSION-opencode .
120123
121124
- name: Deploy test worker
122125
uses: cloudflare/wrangler-action@v3
@@ -213,6 +216,20 @@ jobs:
213216
build-args: |
214217
SANDBOX_VERSION=${{ needs.unit-tests.outputs.version }}
215218
219+
- name: Build and push Docker image (opencode)
220+
uses: docker/build-push-action@v6
221+
with:
222+
context: .
223+
file: packages/sandbox/Dockerfile
224+
target: opencode
225+
platforms: linux/amd64
226+
push: true
227+
tags: cloudflare/sandbox:${{ needs.unit-tests.outputs.version }}-opencode
228+
cache-from: type=gha,scope=release-opencode
229+
cache-to: type=gha,mode=max,scope=release-opencode
230+
build-args: |
231+
SANDBOX_VERSION=${{ needs.unit-tests.outputs.version }}
232+
216233
- id: changesets
217234
uses: changesets/action@v1
218235
with:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ANTHROPIC_API_KEY=<YOUR-KEY-HERE>

examples/opencode/.gitignore

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
# Created by https://www.toptal.com/developers/gitignore/api/macos,node,git
2+
# Edit at https://www.toptal.com/developers/gitignore?templates=macos,node,git
3+
4+
### Git ###
5+
# Created by git for backups. To disable backups in Git:
6+
# $ git config --global mergetool.keepBackup false
7+
*.orig
8+
9+
# Created by git when using merge tools for conflicts
10+
*.BACKUP.*
11+
*.BASE.*
12+
*.LOCAL.*
13+
*.REMOTE.*
14+
*_BACKUP_*.txt
15+
*_BASE_*.txt
16+
*_LOCAL_*.txt
17+
*_REMOTE_*.txt
18+
19+
### macOS ###
20+
# General
21+
.DS_Store
22+
.AppleDouble
23+
.LSOverride
24+
25+
# Icon must end with two \r
26+
Icon
27+
28+
29+
# Thumbnails
30+
._*
31+
32+
# Files that might appear in the root of a volume
33+
.DocumentRevisions-V100
34+
.fseventsd
35+
.Spotlight-V100
36+
.TemporaryItems
37+
.Trashes
38+
.VolumeIcon.icns
39+
.com.apple.timemachine.donotpresent
40+
41+
# Directories potentially created on remote AFP share
42+
.AppleDB
43+
.AppleDesktop
44+
Network Trash Folder
45+
Temporary Items
46+
.apdisk
47+
48+
### macOS Patch ###
49+
# iCloud generated files
50+
*.icloud
51+
52+
### Node ###
53+
# Logs
54+
logs
55+
*.log
56+
npm-debug.log*
57+
yarn-debug.log*
58+
yarn-error.log*
59+
lerna-debug.log*
60+
.pnpm-debug.log*
61+
62+
# Diagnostic reports (https://nodejs.org/api/report.html)
63+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
64+
65+
# Runtime data
66+
pids
67+
*.pid
68+
*.seed
69+
*.pid.lock
70+
71+
# Directory for instrumented libs generated by jscoverage/JSCover
72+
lib-cov
73+
74+
# Coverage directory used by tools like istanbul
75+
coverage
76+
*.lcov
77+
78+
# nyc test coverage
79+
.nyc_output
80+
81+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
82+
.grunt
83+
84+
# Bower dependency directory (https://bower.io/)
85+
bower_components
86+
87+
# node-waf configuration
88+
.lock-wscript
89+
90+
# Compiled binary addons (https://nodejs.org/api/addons.html)
91+
build/Release
92+
93+
# Dependency directories
94+
node_modules/
95+
jspm_packages/
96+
97+
# Snowpack dependency directory (https://snowpack.dev/)
98+
web_modules/
99+
100+
# TypeScript cache
101+
*.tsbuildinfo
102+
103+
# Optional npm cache directory
104+
.npm
105+
106+
# Optional eslint cache
107+
.eslintcache
108+
109+
# Optional stylelint cache
110+
.stylelintcache
111+
112+
# Microbundle cache
113+
.rpt2_cache/
114+
.rts2_cache_cjs/
115+
.rts2_cache_es/
116+
.rts2_cache_umd/
117+
118+
# Optional REPL history
119+
.node_repl_history
120+
121+
# Output of 'npm pack'
122+
*.tgz
123+
124+
# Yarn Integrity file
125+
.yarn-integrity
126+
127+
# dotenv environment variable files
128+
.env
129+
.env.development.local
130+
.env.test.local
131+
.env.production.local
132+
.env.local
133+
134+
# parcel-bundler cache (https://parceljs.org/)
135+
.cache
136+
.parcel-cache
137+
138+
# Next.js build output
139+
.next
140+
out
141+
142+
# Nuxt.js build / generate output
143+
.nuxt
144+
dist
145+
146+
# Gatsby files
147+
.cache/
148+
# Comment in the public line in if your project uses Gatsby and not Next.js
149+
# https://nextjs.org/blog/next-9-1#public-directory-support
150+
# public
151+
152+
# vuepress build output
153+
.vuepress/dist
154+
155+
# vuepress v2.x temp and cache directory
156+
.temp
157+
158+
# Docusaurus cache and generated files
159+
.docusaurus
160+
161+
# Serverless directories
162+
.serverless/
163+
164+
# FuseBox cache
165+
.fusebox/
166+
167+
# DynamoDB Local files
168+
.dynamodb/
169+
170+
# TernJS port file
171+
.tern-port
172+
173+
# Stores VSCode versions used for testing VSCode extensions
174+
.vscode-test
175+
176+
# yarn v2
177+
.yarn/cache
178+
.yarn/unplugged
179+
.yarn/build-state.yml
180+
.yarn/install-state.gz
181+
.pnp.*
182+
183+
### Node Patch ###
184+
# Serverless Webpack directories
185+
.webpack/
186+
187+
# Optional stylelint cache
188+
189+
# SvelteKit build / generate output
190+
.svelte-kit
191+
192+
# End of https://www.toptal.com/developers/gitignore/api/macos,node,git
193+
194+
### Wrangler ###
195+
.wrangler/
196+
.env*
197+
!.env.example
198+
.dev.vars*
199+
!.dev.vars.example

examples/opencode/Dockerfile

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
FROM docker.io/cloudflare/sandbox:0.6.3
2+
3+
# Add opencode install location to PATH before installation
4+
ENV PATH="/root/.opencode/bin:${PATH}"
5+
6+
# Install OpenCode CLI
7+
RUN curl -fsSL https://opencode.ai/install -o /tmp/install-opencode.sh \
8+
&& bash /tmp/install-opencode.sh \
9+
&& rm /tmp/install-opencode.sh \
10+
&& opencode --version
11+
12+
# Clone sample project for the web UI to work with
13+
RUN git clone --depth 1 https://github.com/cloudflare/agents.git /home/user/agents
14+
15+
# Start in the sample project directory
16+
WORKDIR /home/user/agents
17+
18+
# Expose OpenCode server port
19+
EXPOSE 4096

examples/opencode/README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# OpenCode + Sandbox SDK
2+
3+
Run OpenCode inside Cloudflare Sandboxes! Just open the worker URL in your browser to get the full OpenCode web experience.
4+
5+
## Quick Start
6+
7+
1. Copy `.dev.vars.example` to `.dev.vars` and add your Anthropic API key:
8+
9+
```bash
10+
cp .dev.vars.example .dev.vars
11+
# Edit .dev.vars with your ANTHROPIC_API_KEY
12+
```
13+
14+
2. Install dependencies and run:
15+
16+
```bash
17+
npm install
18+
npm run dev
19+
```
20+
21+
3. Open http://localhost:8787 in your browser - you'll see the OpenCode web UI!
22+
23+
## How It Works
24+
25+
The worker acts as a transparent proxy to OpenCode running in the container:
26+
27+
```
28+
Browser → Worker → Sandbox DO → Container :4096 → OpenCode Server
29+
30+
Proxies UI from desktop.dev.opencode.ai
31+
```
32+
33+
OpenCode handles everything:
34+
35+
- API routes (`/session/*`, `/event`, etc.)
36+
- Web UI (proxied from `desktop.dev.opencode.ai`)
37+
- WebSocket for terminal
38+
39+
## Key Benefits
40+
41+
- **Web UI** - Full browser-based OpenCode experience
42+
- **Isolated execution** - Code runs in secure sandbox containers
43+
- **Persistent sessions** - Sessions survive across requests
44+
45+
Happy hacking!

examples/opencode/package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "@cloudflare/sandbox-opencode-example",
3+
"version": "1.0.0",
4+
"type": "module",
5+
"private": true,
6+
"description": "Example of running OpenCode web UI inside Sandbox",
7+
"scripts": {
8+
"deploy": "wrangler deploy",
9+
"dev": "wrangler dev",
10+
"start": "wrangler dev",
11+
"types": "wrangler types",
12+
"typecheck": "tsc --noEmit"
13+
},
14+
"dependencies": {
15+
"@opencode-ai/sdk": "^1.0.137"
16+
},
17+
"devDependencies": {
18+
"@cloudflare/sandbox": "*",
19+
"@types/node": "^24.10.1",
20+
"typescript": "^5.9.3",
21+
"wrangler": "^4.50.0"
22+
},
23+
"author": "",
24+
"license": "MIT"
25+
}

0 commit comments

Comments
 (0)