Skip to content

Commit 167bcf2

Browse files
committed
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.
1 parent ae7755c commit 167bcf2

File tree

18 files changed

+10117
-3
lines changed

18 files changed

+10117
-3
lines changed
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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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+
# Download script first, then run - this shows errors instead of failing silently
8+
RUN curl -fsSL https://opencode.ai/install -o /tmp/install-opencode.sh \
9+
&& bash /tmp/install-opencode.sh \
10+
&& rm /tmp/install-opencode.sh \
11+
&& opencode --version
12+
13+
# Expose OpenCode server port
14+
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.0"
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+
}

examples/opencode/src/index.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* OpenCode + Sandbox SDK Example
3+
*
4+
* This example demonstrates both ways to use OpenCode with Sandbox:
5+
* 1. Web UI - Browse to / for the full OpenCode web experience
6+
* 2. Programmatic - POST to /api/test for SDK-based automation
7+
*/
8+
import { getSandbox } from '@cloudflare/sandbox';
9+
import { createOpencode, proxyToOpencode } from '@cloudflare/sandbox/opencode';
10+
import type { OpencodeClient } from '@opencode-ai/sdk';
11+
12+
export default {
13+
async fetch(request: Request, env: Env): Promise<Response> {
14+
const url = new URL(request.url);
15+
const sandbox = getSandbox(env.Sandbox, 'opencode');
16+
17+
// Programmatic SDK test endpoint
18+
if (request.method === 'POST' && url.pathname === '/api/test') {
19+
return handleSdkTest(sandbox, env);
20+
}
21+
22+
// Everything else: Web UI proxy
23+
return proxyToOpencode(request, sandbox, {
24+
config: {
25+
provider: {
26+
anthropic: {
27+
apiKey: env.ANTHROPIC_API_KEY
28+
}
29+
}
30+
}
31+
});
32+
}
33+
};
34+
35+
/**
36+
* Test the programmatic SDK access
37+
*/
38+
async function handleSdkTest(
39+
sandbox: ReturnType<typeof getSandbox>,
40+
env: Env
41+
): Promise<Response> {
42+
try {
43+
// Clone a repo to give the agent something to work with
44+
await sandbox.gitCheckout('https://github.com/cloudflare/agents.git', {
45+
targetDir: '/home/user/agents'
46+
});
47+
48+
// Get typed SDK client
49+
const { client } = await createOpencode<OpencodeClient>(sandbox, {
50+
config: {
51+
provider: {
52+
anthropic: {
53+
apiKey: env.ANTHROPIC_API_KEY
54+
}
55+
}
56+
}
57+
});
58+
59+
// Create a session
60+
const session = await client.session.create({
61+
body: { title: 'Test Session' },
62+
query: { directory: '/home/user/agents' }
63+
});
64+
65+
if (!session.data) {
66+
throw new Error(`Failed to create session: ${JSON.stringify(session)}`);
67+
}
68+
69+
// Send a prompt using the SDK
70+
const promptResult = await client.session.prompt({
71+
path: { id: session.data.id },
72+
query: { directory: '/home/user/agents' },
73+
body: {
74+
model: {
75+
providerID: 'anthropic',
76+
modelID: 'claude-haiku-4-5'
77+
},
78+
parts: [
79+
{
80+
type: 'text',
81+
text: 'Summarize the README.md file in 2-3 sentences. Be concise.'
82+
}
83+
]
84+
}
85+
});
86+
87+
// Extract text response from result
88+
const parts = promptResult.data?.parts ?? [];
89+
const textPart = parts.find((p: { type: string }) => p.type === 'text') as
90+
| { text?: string }
91+
| undefined;
92+
93+
return new Response(textPart?.text ?? 'No response', {
94+
headers: { 'Content-Type': 'text/plain' }
95+
});
96+
} catch (error) {
97+
console.error('SDK test error:', error);
98+
const message = error instanceof Error ? error.message : 'Unknown error';
99+
const stack = error instanceof Error ? error.stack : undefined;
100+
return Response.json(
101+
{ success: false, error: message, stack },
102+
{ status: 500 }
103+
);
104+
}
105+
}
106+
107+
// Export Sandbox DO for wrangler
108+
export { Sandbox } from '@cloudflare/sandbox';

0 commit comments

Comments
 (0)