Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7f12ee0
feat(js-sdk): start streaming commands without retaining output
max-sudolabs Oct 15, 2026
5aaa037
feat(python-sdk): start streaming commands without retaining output
max-sudolabs Oct 15, 2026
4919434
feat(acp-js): add @e2b/acp package with ACP framing helpers
max-sudolabs Oct 15, 2026
2dc86db
feat(acp-js): add connectAcpAgent
max-sudolabs Oct 15, 2026
fd2fca6
test(acp-js): cover connectAcpAgent against a scripted agent
max-sudolabs Oct 15, 2026
e93ecfa
feat(acp-python): add e2b-acp package with ACP framing helpers
max-sudolabs Oct 15, 2026
978f70f
feat(acp-python): add connect_acp_agent
max-sudolabs Oct 15, 2026
106f768
test(acp-python): cover connect_acp_agent against a scripted agent
max-sudolabs Oct 15, 2026
3dd3d3d
ci: test and release the ACP packages
max-sudolabs Oct 15, 2026
5e904b2
chore: add changeset for the ACP packages
max-sudolabs Oct 15, 2026
c0736a5
chore: add acp-python to the pnpm lockfile
max-sudolabs Oct 16, 2026
b9850e6
fix(acp-python): raise the e2b floor to the released core version
max-sudolabs Oct 16, 2026
35981bf
fix(acp-js): reject close() when the agent could not be killed
max-sudolabs Oct 16, 2026
b5489cd
fix(acp-python): raise from close() when the agent could not be killed
max-sudolabs Oct 16, 2026
ce191b8
fix(acp-js): kill the agent when the client factory throws
max-sudolabs Oct 16, 2026
fb38bea
test(acp-js): cover dropping in-flight lines after close
max-sudolabs Oct 16, 2026
d44f4b4
docs(acp-python): describe how requests fail when the agent exits
max-sudolabs Oct 16, 2026
b46abda
fix(acp-python): drop the unused xdist flag from make test
max-sudolabs Oct 16, 2026
6e469da
test(acp-js): drop live tests the offline tests already cover
max-sudolabs Oct 16, 2026
fe004fe
test(acp-python): drop live tests the offline tests already cover
max-sudolabs Oct 16, 2026
cb4bd67
fix(acp-js): let kill() and early output errors stop the agent
max-sudolabs Oct 19, 2026
e18515b
fix(acp-js): split agent output without rescanning a pending line
max-sudolabs Oct 19, 2026
431618e
fix(acp-python): let kill() stop output that waits for the client
max-sudolabs Oct 19, 2026
d1c9d86
fix(acp-python): split agent output without rescanning a pending line
max-sudolabs Oct 19, 2026
833990b
refactor(acp-js): take command and client as arguments, rename close(…
max-sudolabs Oct 19, 2026
6c1a1a4
refactor(acp-python): make options keyword-only, rename close() to st…
max-sudolabs Oct 19, 2026
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
8 changes: 8 additions & 0 deletions .changeset/acp-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@e2b/acp": minor
"@e2b/acp-python": minor
"e2b": patch
"@e2b/python-sdk": patch
---

Add `@e2b/acp` (JavaScript) and `e2b-acp` (Python) for running [Agent Client Protocol](https://agentclientprotocol.com) agents such as goose, OpenCode or Claude Code (through its ACP adapter) inside an E2B sandbox. `connectAcpAgent(sandbox, command, client)` / `connect_acp_agent(sandbox, command, client)` starts the agent over stdin/stdout and returns the ACP client connection. Client filesystem and terminal capabilities are turned off and agent requests for them are rejected, so the agent edits files and runs commands with its own tools in the sandbox. The core SDKs can now stream a background command's output without keeping it in memory, which these packages use.
68 changes: 68 additions & 0 deletions .github/workflows/acp_js_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Test ACP JS SDK

on:
workflow_call:
inputs:
E2B_DOMAIN:
required: false
type: string
default: ''
secrets:
E2B_API_KEY:
required: true

permissions:
contents: read

jobs:
test:
defaults:
run:
working-directory: ./packages/acp-js
shell: bash
name: ACP JS SDK - Build and test
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Parse .tool-versions
uses: wistia/parse-tool-versions@32f568a4ffd4bfa7720ebf93f171597d1ebc979a # v2.1.1
with:
filename: '.tool-versions'
uppercase: 'true'
prefix: 'tool_version_'

- name: Install pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
with:
version: '${{ env.TOOL_VERSION_PNPM }}'

- name: Setup Node
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: '${{ env.TOOL_VERSION_NODEJS }}'
registry-url: 'https://registry.npmjs.org'
cache: pnpm
cache-dependency-path: pnpm-lock.yaml

- name: Configure pnpm
run: |
pnpm config set auto-install-peers true
pnpm config set exclude-links-from-lockfile true

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build base JS SDK
run: pnpm --dir ../js-sdk build

- name: Test build
run: pnpm build

- name: Run tests
run: pnpm test
env:
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
E2B_DOMAIN: ${{ inputs.E2B_DOMAIN }}
E2B_TEST_MAX_WORKERS: 1
53 changes: 53 additions & 0 deletions .github/workflows/acp_python_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Test ACP Python SDK

on:
workflow_call:
inputs:
E2B_DOMAIN:
required: false
type: string
default: ''
secrets:
E2B_API_KEY:
required: true

permissions:
contents: read

jobs:
test:
defaults:
run:
working-directory: ./packages/acp-python
shell: bash
name: ACP Python SDK - Build and test
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Parse .tool-versions
uses: wistia/parse-tool-versions@32f568a4ffd4bfa7720ebf93f171597d1ebc979a # v2.1.1
with:
filename: '.tool-versions'
uppercase: 'true'
prefix: 'tool_version_'

- name: Install uv
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with:
version: '${{ env.TOOL_VERSION_UV }}'
python-version: '${{ env.TOOL_VERSION_PYTHON }}'
enable-cache: true

- name: Install dependencies
run: uv sync --locked --package e2b-acp

- name: Test build
run: uv build

- name: Run tests
run: uv run pytest --verbose
env:
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
E2B_DOMAIN: ${{ inputs.E2B_DOMAIN }}
34 changes: 34 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ jobs:
desktop_python: ${{ steps.desktop_python.outputs.release }}
dockerfile_utils_js: ${{ steps.dockerfile_utils_js.outputs.release }}
dockerfile_utils_python: ${{ steps.dockerfile_utils_python.outputs.release }}
acp_js: ${{ steps.acp_js.outputs.release }}
acp_python: ${{ steps.acp_python.outputs.release }}
itinerary: ${{ steps.itinerary.outputs.itinerary }}
steps:
- name: Check the ref
Expand Down Expand Up @@ -138,6 +140,20 @@ jobs:
IS_RELEASE=$(./.github/scripts/is_release_for_package.sh "@e2b/dockerfile-utils-python")
echo "release=$IS_RELEASE" >> "$GITHUB_OUTPUT"

- name: Check ACP JavaScript SDK Release
id: acp_js
if: steps.version.outputs.release == 'true'
run: |
IS_RELEASE=$(./.github/scripts/is_release_for_package.sh "@e2b/acp")
echo "release=$IS_RELEASE" >> "$GITHUB_OUTPUT"

- name: Check ACP Python SDK Release
id: acp_python
if: steps.version.outputs.release == 'true'
run: |
IS_RELEASE=$(./.github/scripts/is_release_for_package.sh "@e2b/acp-python")
echo "release=$IS_RELEASE" >> "$GITHUB_OUTPUT"

- name: Build release itinerary
id: itinerary
if: steps.version.outputs.release == 'true'
Expand Down Expand Up @@ -215,6 +231,20 @@ jobs:
if: needs.preflight.outputs.dockerfile_utils_python == 'true'
uses: ./.github/workflows/dockerfile_utils_python_tests.yml

acp-js-tests:
name: ACP JS SDK Tests
needs: [preflight]
if: needs.preflight.outputs.acp_js == 'true'
uses: ./.github/workflows/acp_js_tests.yml
secrets: inherit

acp-python-tests:
name: ACP Python SDK Tests
needs: [preflight]
if: needs.preflight.outputs.acp_python == 'true'
uses: ./.github/workflows/acp_python_tests.yml
secrets: inherit

publish:
name: Publish
needs:
Expand All @@ -228,6 +258,8 @@ jobs:
- desktop-python-tests
- dockerfile-utils-js-tests
- dockerfile-utils-python-tests
- acp-js-tests
- acp-python-tests
if: (!cancelled()) && !contains(needs.*.result, 'failure') && needs.preflight.outputs.release == 'true'
uses: ./.github/workflows/publish_packages.yml
secrets: inherit
Expand Down Expand Up @@ -265,6 +297,8 @@ jobs:
- desktop-python-tests
- dockerfile-utils-js-tests
- dockerfile-utils-python-tests
- acp-js-tests
- acp-python-tests
- publish
if: failure()
name: Release Failed - Slack Notification
Expand Down
35 changes: 35 additions & 0 deletions .github/workflows/sdk_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ jobs:
desktop_python: ${{ steps.filter.outputs.desktop_python }}
dockerfile_utils_js: ${{ steps.filter.outputs.dockerfile_utils_js }}
dockerfile_utils_python: ${{ steps.filter.outputs.dockerfile_utils_python }}
acp_js: ${{ steps.filter.outputs.acp_js }}
acp_python: ${{ steps.filter.outputs.acp_python }}
steps:
- name: Filter changed paths
# On workflow_dispatch there is no PR diff and paths-filter would fall
Expand Down Expand Up @@ -71,6 +73,21 @@ jobs:
- 'packages/cli/**/!(*.md)'
- 'packages/js-sdk/**/!(*.md)'
- 'packages/dockerfile-utils-js/**/!(*.md)'
acp_js:
- *shared
- '.github/workflows/acp_js_tests.yml'
- 'tsconfig.sdk.json'
- 'tsdown.sdk.config.mts'
- 'vitest.sdk.config.mts'
- 'packages/acp-js/**/!(*.md)'
- 'packages/js-sdk/**/!(*.md)'
- 'packages/dockerfile-utils-js/**/!(*.md)'
acp_python:
- *shared
- '.github/workflows/acp_python_tests.yml'
- 'packages/acp-python/**/!(*.md)'
- 'packages/python-sdk/**/!(*.md)'
- 'packages/dockerfile-utils-python/**/!(*.md)'
code_interpreter_js:
- *shared
- '.github/workflows/code_interpreter_js_tests.yml'
Expand Down Expand Up @@ -136,6 +153,22 @@ jobs:
secrets:
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}

acp-js-tests:
name: Production / ACP JS SDK Tests
needs: changes
if: ${{ needs.changes.outputs.acp_js == 'true' || github.event_name == 'workflow_dispatch' }}
uses: ./.github/workflows/acp_js_tests.yml
secrets:
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}

acp-python-tests:
name: Production / ACP Python SDK Tests
needs: changes
if: ${{ needs.changes.outputs.acp_python == 'true' || github.event_name == 'workflow_dispatch' }}
uses: ./.github/workflows/acp_python_tests.yml
secrets:
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}

code-interpreter-js-tests:
name: Production / Code Interpreter JS SDK Tests
needs: changes
Expand Down Expand Up @@ -269,6 +302,8 @@ jobs:
- js-tests
- python-tests
- cli-tests
- acp-js-tests
- acp-python-tests
- code-interpreter-js-tests
- code-interpreter-python-tests
- desktop-js-tests
Expand Down
9 changes: 9 additions & 0 deletions packages/acp-js/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
MIT License

Copyright (c) 2025 FOUNDRYLABS, INC.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
68 changes: 68 additions & 0 deletions packages/acp-js/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# E2B ACP

Run [Agent Client Protocol](https://agentclientprotocol.com) (ACP) agents such as goose, OpenCode or Claude Code inside an [E2B](https://e2b.dev) sandbox, and drive them from your code with the official ACP client.

Source: [packages/acp-js](https://github.com/e2b-dev/E2B/tree/main/packages/acp-js)

## Install

```
npm i e2b @e2b/acp @agentclientprotocol/sdk
```

Set `E2B_API_KEY` to your [E2B API key](https://e2b.dev/dashboard?tab=keys), and the provider key the agent needs.

## Usage

Create a sandbox from a template with the agent installed, then start the agent and connect your ACP client to it:

```ts
import { Sandbox } from 'e2b'
import { connectAcpAgent } from '@e2b/acp'
import { PROTOCOL_VERSION, type Client } from '@agentclientprotocol/sdk'

const client: Client = {
async requestPermission({ options }) {
const allow = options.find((o) => o.kind === 'allow_once') ?? options[0]
return { outcome: { outcome: 'selected', optionId: allow.optionId } }
},
async sessionUpdate({ update }) {
if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
process.stdout.write(update.content.text)
}
},
}

const sandbox = await Sandbox.create('goose', { timeoutMs: 10 * 60_000 })
try {
const agent = await connectAcpAgent(sandbox, 'goose acp --with-builtin developer', client, {
envs: {
GOOSE_PROVIDER: 'openai',
GOOSE_MODEL: 'gpt-5.6-luna',
OPENAI_API_KEY: process.env.OPENAI_API_KEY!,
},
})

await agent.connection.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await agent.connection.newSession({ cwd: '/home/user', mcpServers: [] })
await agent.connection.prompt({
sessionId,
prompt: [{ type: 'text', text: 'Create fizzbuzz.py for 1 to 15 and run it.' }],
})

await agent.stop()
} finally {
await sandbox.kill()
}
```

## Where the agent's tools run

The agent talks ACP over its stdin and stdout. `connectAcpAgent` turns off the client filesystem and terminal capabilities in `initialize` and answers agent requests for `fs/*` or `terminal/*` with an error, so the agent edits files and runs commands with its own tools inside the sandbox. It does not restrict the agent's network access or the MCP servers you pass to it.

## Lifecycle

- `agent.stop()` sends EOF to the agent and kills it if it has not exited within `timeoutMs` (5 seconds by default) of the call. Messages the agent sends after that are not delivered.
- `agent.wait()` resolves with the exit code once the agent exits.
- If the agent exits with an error, the connection fails with the exit code and the end of the agent's stderr. Pass `onStderr` to receive its logs.
- The agent runs as long as the sandbox does, so set the sandbox timeout for the longest task you expect.
Loading