Skip to content
Open
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,12 @@ Read more about E2B on the [E2B website](https://e2b.dev/?utm_source=github&utm_
<td><a href="./examples/agentkit-coding-agent">TypeScript</a></td>
</tr>
<tr>
<td><a href="https://agentclientprotocol.com">Agent Client Protocol</a></td>
<td>Drive OpenCode, goose or Claude Code inside a Sandbox with an ACP client</td>
<td><a href="./examples/acp-agents-python">Python</a></td>
<td><a href="./examples/acp-agents-js">TypeScript</a></td>
</tr>
<tr>
<td><a href="https://sandboxagent.dev/docs/sdk-overview">Sandbox Agent SDK</a></td>
<td>Run Sandbox Agent inside E2B and connect with the SDK</td>
<td>-</td>
Expand Down
5 changes: 5 additions & 0 deletions examples/acp-agents-js/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
E2B_API_KEY=
# For the opencode and goose agents
OPENAI_API_KEY=
# For the claude agent
ANTHROPIC_API_KEY=
38 changes: 38 additions & 0 deletions examples/acp-agents-js/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# ACP coding agents in E2B Sandbox (TypeScript)

Run a coding agent that speaks the [Agent Client Protocol](https://agentclientprotocol.com) (ACP), such as OpenCode, goose or Claude Code, inside an E2B sandbox and drive it from your own code with the official ACP client.

The same client works for all three agents: only the sandbox template and the command that starts the agent change.

| `AGENT` | Template | Command | Key |
|---|---|---|---|
| `opencode` (default) | `opencode` | `opencode acp` | `OPENAI_API_KEY` |
| `goose` | `goose` | `goose acp --with-builtin developer` | `OPENAI_API_KEY` |
| `claude` | `claude` | `claude-agent-acp` | `ANTHROPIC_API_KEY` |

## How it works

The example creates a sandbox from the agent's template, then `ACPAgent.start()` from [`@e2b/acp`](https://www.npmjs.com/package/@e2b/acp) starts the agent inside it and connects your ACP client, built with `client()` from `@agentclientprotocol/sdk`, over the agent's stdin and stdout:

```ts
const agent = await ACPAgent.start(sandbox, 'opencode acp', app, { cwd, envs })

const acp = agent.connection.agent
await acp.request(methods.agent.initialize, { protocolVersion: PROTOCOL_VERSION })
const { sessionId } = await acp.request(methods.agent.session.new, { cwd, mcpServers: [] })
await acp.request(methods.agent.session.prompt, { sessionId, prompt: [{ type: 'text', text: '...' }] })

await agent.stop()
```

`ACPAgent.start()` turns off the client filesystem and terminal capabilities in `initialize` and rejects agent requests for them, so the agent edits files and runs commands with its own tools inside the sandbox, never on the machine running your client. It does not restrict the agent's network access or the MCP servers you pass to it.

Your client still answers permission requests and receives the agent's messages and tool calls. This example approves each request with its `allow_once` option and prints the agent's progress.

## How to run

1. Copy `.env.template` to `.env` and fill in `E2B_API_KEY` and the key for the agent you use.
2. Install dependencies with `npm install`.
3. Run `npm start`, or pick another agent with `AGENT=goose npm start` or `AGENT=claude npm start`.

The agent writes and runs `fizzbuzz.py` in the sandbox, then the example runs the same file again and checks that it prints `FizzBuzz`, which shows the result lives in the sandbox.
19 changes: 19 additions & 0 deletions examples/acp-agents-js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "acp-agents",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "tsx src/index.ts"
},
"dependencies": {
"@agentclientprotocol/sdk": "^1.8.0",
"@e2b/acp": "^0.1.0",
"dotenv": "^17.4.2",
"e2b": "^2.56.1",
"tsx": "^4.19.4"
},
"devDependencies": {
"@types/node": "^22.10.2",
"typescript": "^5.9.3"
}
}
96 changes: 96 additions & 0 deletions examples/acp-agents-js/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import 'dotenv/config'
import { Sandbox } from 'e2b'
import { ACPAgent } from '@e2b/acp'
import { client, methods, PROTOCOL_VERSION } from '@agentclientprotocol/sdk'

// Each agent is a sandbox template plus the command that starts it in ACP mode.
const agents = {
opencode: {
template: 'opencode',
command: 'opencode acp',
envs: {
OPENAI_API_KEY: process.env.OPENAI_API_KEY ?? '',
OPENCODE_CONFIG_CONTENT: JSON.stringify({ model: 'openai/gpt-5.6-luna' }),
},
},
goose: {
template: 'goose',
command: 'goose acp --with-builtin developer',
envs: {
OPENAI_API_KEY: process.env.OPENAI_API_KEY ?? '',
GOOSE_PROVIDER: 'openai',
GOOSE_MODEL: 'gpt-5.6-luna',
},
},
claude: {
template: 'claude',
command: 'claude-agent-acp',
envs: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY ?? '' },
},
}

const name = (process.env.AGENT ?? 'opencode') as keyof typeof agents
const agentConfig = agents[name]
if (!agentConfig) {
throw new Error(`Unknown AGENT "${name}", use one of: ${Object.keys(agents).join(', ')}`)
}

// The ACP client: it shows the agent's progress and answers permission requests.
// File edits and shell commands happen inside the sandbox, not on this machine.
const app = client({ name: 'e2b-acp-cookbook' })
.onRequest(methods.client.session.requestPermission, async ({ params: { toolCall, options } }) => {
const allow = options.find((o) => o.kind === 'allow_once') ?? options[0]
console.log(`\n[permission] ${toolCall.title} -> ${allow.name}`)
return { outcome: { outcome: 'selected', optionId: allow.optionId } }
})
.onNotification(methods.client.session.update, ({ params: { update } }) => {
if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
process.stdout.write(update.content.text)
} else if (update.sessionUpdate === 'tool_call') {
console.log(`\n[tool] ${update.title}`)
}
})

const sandbox = await Sandbox.create(agentConfig.template, { timeoutMs: 10 * 60_000 })
console.log(`Sandbox ${sandbox.sandboxId} with ${name}`)

try {
const cwd = '/home/user/project'
await sandbox.files.makeDir(cwd)

// Start the agent inside the sandbox and connect an ACP client to it.
const agent = await ACPAgent.start(sandbox, agentConfig.command, app, {
cwd,
envs: agentConfig.envs,
})
// Requests to the agent go through agent.connection.agent.
const acp = agent.connection.agent

const { agentInfo } = await acp.request(methods.agent.initialize, {
protocolVersion: PROTOCOL_VERSION,
})
console.log(`Connected to ${agentInfo?.name} ${agentInfo?.version}`)

const { sessionId } = await acp.request(methods.agent.session.new, { cwd, mcpServers: [] })
const { stopReason } = await acp.request(methods.agent.session.prompt, {
sessionId,
prompt: [
{
type: 'text',
text: 'Create fizzbuzz.py that prints FizzBuzz for 1 to 15, run it with python3 and show the output.',
},
],
})
console.log(`\n\nTurn finished: ${stopReason}`)

// The agent worked inside the sandbox, so its result is there.
const result = await sandbox.commands.run(`python3 ${cwd}/fizzbuzz.py`)
console.log(`fizzbuzz.py in the sandbox prints:\n${result.stdout}`)
if (!result.stdout.includes('FizzBuzz')) {
throw new Error('fizzbuzz.py did not print FizzBuzz')
}

await agent.stop()
} finally {
await sandbox.kill()
}
5 changes: 5 additions & 0 deletions examples/acp-agents-python/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
E2B_API_KEY=
# For the opencode and goose agents
OPENAI_API_KEY=
# For the claude agent
ANTHROPIC_API_KEY=
10 changes: 10 additions & 0 deletions examples/acp-agents-python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Virtual environments
.venv

# Environment files
.env

# Python caches
__pycache__
.pytest_cache
.ruff_cache
1 change: 1 addition & 0 deletions examples/acp-agents-python/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
38 changes: 38 additions & 0 deletions examples/acp-agents-python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# ACP coding agents in E2B Sandbox (Python)

Run a coding agent that speaks the [Agent Client Protocol](https://agentclientprotocol.com) (ACP), such as OpenCode, goose or Claude Code, inside an E2B sandbox and drive it from your own code with the official ACP client (`agent-client-protocol`).

The same client works for all three agents: only the sandbox template and the command that starts the agent change.

| `AGENT` | Template | Command | Key |
|---|---|---|---|
| `opencode` (default) | `opencode` | `opencode acp` | `OPENAI_API_KEY` |
| `goose` | `goose` | `goose acp --with-builtin developer` | `OPENAI_API_KEY` |
| `claude` | `claude` | `claude-agent-acp` | `ANTHROPIC_API_KEY` |

## How it works

The example creates a sandbox from the agent's template, then `AsyncACPAgent.start()` from [`e2b-acp`](https://pypi.org/project/e2b-acp/) starts the agent inside it and connects your ACP client over the agent's stdin and stdout:

```python
agent = await AsyncACPAgent.start(sandbox, "opencode acp", client, cwd=cwd, envs=envs)

await agent.connection.initialize(protocol_version=acp.PROTOCOL_VERSION)
session = await agent.connection.new_session(cwd=cwd, mcp_servers=[])
await agent.connection.prompt(session_id=session.session_id, prompt=[acp.text_block("...")])

await agent.stop()
```

`AsyncACPAgent.start()` turns off the client filesystem and terminal capabilities in `initialize` and rejects agent requests for them, so the agent edits files and runs commands with its own tools inside the sandbox, never on the machine running your client. It does not restrict the agent's network access or the MCP servers you pass to it.

Your client still answers permission requests and receives the agent's messages and tool calls. This example approves each request with its `allow_once` option and prints the agent's progress.

The ACP Python SDK is asynchronous, so the example uses `AsyncSandbox`.

## How to run

1. Copy `.env.template` to `.env` and fill in `E2B_API_KEY` and the key for the agent you use.
2. Run `uv run main.py`, or pick another agent with `AGENT=goose uv run main.py` or `AGENT=claude uv run main.py`.

The agent writes and runs `fizzbuzz.py` in the sandbox, then the example runs the same file again and checks that it prints `FizzBuzz`, which shows the result lives in the sandbox.
109 changes: 109 additions & 0 deletions examples/acp-agents-python/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import asyncio
import json
import os

import acp
from acp import schema
from dotenv import load_dotenv

from e2b import AsyncSandbox
from e2b_acp import AsyncACPAgent

load_dotenv()

# Each agent is a sandbox template plus the command that starts it in ACP mode.
AGENTS = {
"opencode": {
"template": "opencode",
"command": "opencode acp",
"envs": {
"OPENAI_API_KEY": os.getenv("OPENAI_API_KEY", ""),
"OPENCODE_CONFIG_CONTENT": json.dumps({"model": "openai/gpt-5.6-luna"}),
},
},
"goose": {
"template": "goose",
"command": "goose acp --with-builtin developer",
"envs": {
"OPENAI_API_KEY": os.getenv("OPENAI_API_KEY", ""),
"GOOSE_PROVIDER": "openai",
"GOOSE_MODEL": "gpt-5.6-luna",
},
},
"claude": {
"template": "claude",
"command": "claude-agent-acp",
"envs": {"ANTHROPIC_API_KEY": os.getenv("ANTHROPIC_API_KEY", "")},
},
}


class Client:
"""The ACP client: it shows the agent's progress and answers permission requests.

File edits and shell commands happen inside the sandbox, not on this machine.
"""

async def request_permission(self, session_id, tool_call, options, **kwargs):
allow = next((o for o in options if o.kind == "allow_once"), options[0])
print(f"\n[permission] {tool_call.title} -> {allow.name}")
return schema.RequestPermissionResponse(
outcome=schema.AllowedOutcome(outcome="selected", option_id=allow.option_id)
)

async def session_update(self, session_id, update, **kwargs):
if (
isinstance(update, schema.AgentMessageChunk)
and update.content.type == "text"
):
print(update.content.text, end="", flush=True)
elif isinstance(update, schema.ToolCallStart):
print(f"\n[tool] {update.title}")


async def main():
name = os.getenv("AGENT", "opencode")
if name not in AGENTS:
raise SystemExit(f'Unknown AGENT "{name}", use one of: {", ".join(AGENTS)}')
config = AGENTS[name]

sandbox = await AsyncSandbox.create(config["template"], timeout=600)
print(f"Sandbox {sandbox.sandbox_id} with {name}")
try:
cwd = "/home/user/project"
await sandbox.files.make_dir(cwd)

# Start the agent inside the sandbox and connect an ACP client to it.
agent = await AsyncACPAgent.start(
sandbox, config["command"], Client(), cwd=cwd, envs=config["envs"]
)
connection = agent.connection

init = await connection.initialize(protocol_version=acp.PROTOCOL_VERSION)
print(f"Connected to {init.agent_info.name} {init.agent_info.version}")

session = await connection.new_session(cwd=cwd, mcp_servers=[])
response = await connection.prompt(
session_id=session.session_id,
prompt=[
acp.text_block(
"Create fizzbuzz.py that prints FizzBuzz for 1 to 15, "
"run it with python3 and show the output."
)
],
)
print(f"\n\nTurn finished: {response.stop_reason}")

# The agent worked inside the sandbox, so its result is there.
result = await sandbox.commands.run(f"python3 {cwd}/fizzbuzz.py")
print(f"fizzbuzz.py in the sandbox prints:\n{result.stdout}")
if "FizzBuzz" not in result.stdout:
raise RuntimeError("fizzbuzz.py did not print FizzBuzz")

await agent.stop()
finally:
await sandbox.kill()


if __name__ == "__main__":
asyncio.run(main())
12 changes: 12 additions & 0 deletions examples/acp-agents-python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[project]
name = "acp-agents-python"
version = "0.1.0"
description = "Drive ACP coding agents (OpenCode, goose, Claude Code) running inside an E2B sandbox"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"agent-client-protocol>=0.12.1,<0.13",
"e2b>=2.56.1",
"e2b-acp>=0.1.0",
"python-dotenv>=1.1,<2",
]
2 changes: 2 additions & 0 deletions tests/run-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ const scripts: {
// openai 7.33 (the first release with openai/helpers/computer) requires Node 22.
{ name: 'openai-computer-use-python', interpreter: 'uv', file: './examples/openai-computer-use-python/', entrypoint: '1-run.py' },
{ name: 'openai-computer-use-js', interpreter: 'npm', file: './examples/openai-computer-use-js/', nodeVersion: '22.22.0' },
{ name: 'acp-agents-js', interpreter: 'npm', file: './examples/acp-agents-js/', nodeVersion: '22.22.0' },
{ name: 'acp-agents-python', interpreter: 'uv', file: './examples/acp-agents-python/' },
{ name: 'playwright-in-e2b', interpreter: 'npm', file: './examples/playwright-in-e2b/' },
{ name: 'anthropic-claude-code-in-sandbox-js', interpreter: 'npm', file: './examples/anthropic-claude-code-in-sandbox-js/' },
{ name: 'anthropic-claude-code-in-sandbox-python', interpreter: 'uv', file: './examples/anthropic-claude-code-in-sandbox-python/', entrypoint: 'python -m anthropic_claude_code_in_sandbox.main' },
Expand Down