Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
aed70da
feat(acp-python): add e2b-acp package with ACP framing helpers
max-sudolabs Oct 15, 2026
48b0157
feat(acp-python): add connect_acp_agent
max-sudolabs Oct 15, 2026
7704b93
test(acp-python): cover connect_acp_agent against a scripted agent
max-sudolabs Oct 15, 2026
358e386
ci: test and release the e2b-acp package
max-sudolabs Oct 23, 2026
0530696
chore: add changeset for e2b-acp
max-sudolabs Oct 23, 2026
6ba181b
chore: add acp-python to the pnpm lockfile
max-sudolabs Oct 16, 2026
8b2c413
fix(acp-python): raise the e2b floor to the released core version
max-sudolabs Oct 16, 2026
dd7ba13
fix(acp-python): raise from close() when the agent could not be killed
max-sudolabs Oct 16, 2026
564d44f
docs(acp-python): describe how requests fail when the agent exits
max-sudolabs Oct 16, 2026
660e76a
fix(acp-python): drop the unused xdist flag from make test
max-sudolabs Oct 16, 2026
52ecfa8
test(acp-python): drop live tests the offline tests already cover
max-sudolabs Oct 16, 2026
785d3c7
fix(acp-python): let kill() stop output that waits for the client
max-sudolabs Oct 19, 2026
2e49ccd
fix(acp-python): split agent output without rescanning a pending line
max-sudolabs Oct 19, 2026
114176d
refactor(acp-python): make options keyword-only, rename close() to st…
max-sudolabs Oct 19, 2026
4517842
chore(acp-python): refresh uv.lock for the current Python SDK
max-sudolabs Oct 23, 2026
3135ded
fix(acp-python): kill the agent when its output stream fails
max-sudolabs Oct 23, 2026
e3050c9
refactor(acp-python): rename AcpAgentExit to AcpAgentResult, point th…
max-sudolabs Oct 23, 2026
e0073d2
refactor(acp-python): start the agent with the public retain_output o…
mishushakov Oct 28, 2026
c5734e6
chore(acp-python): refresh uv.lock for the current Python SDK
mishushakov Oct 28, 2026
ff66763
ci: pass the release plan to is_release_for_package for e2b-acp
max-sudolabs Oct 29, 2026
a2ad48f
refactor(acp-python)!: start agents with AcpAgent.start
max-sudolabs Oct 29, 2026
613120a
feat(acp-python): accept a client factory in AcpAgent.start
max-sudolabs Oct 29, 2026
5ebc17e
docs(acp-python): show AcpAgent.start
max-sudolabs Oct 29, 2026
df0b1dd
docs(acp-python): say that a client factory takes the agent connection
max-sudolabs Oct 29, 2026
8c1bde4
Merge main and relock acp-python against e2b 2.62.0
OndrejDrapalik Oct 29, 2026
6679eff
refactor(acp-python)!: rename to AsyncACPAgent and ACPAgentResult
OndrejDrapalik Oct 29, 2026
265e519
feat(acp-python): pass request_timeout from stop() to close_stdin
max-sudolabs Oct 29, 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
5 changes: 5 additions & 0 deletions .changeset/acp-python-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@e2b/acp-python": minor
---

Add `e2b-acp` for running [Agent Client Protocol](https://agentclientprotocol.com) agents such as goose, OpenCode or Claude Code (through its ACP adapter) inside an E2B sandbox. `AsyncACPAgent.start(sandbox, command, client)` starts the agent over stdin/stdout and returns the ACP client connection; `client` can also be a function that takes the connection and returns the client. 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.
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 }}
17 changes: 17 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ 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_python: ${{ steps.acp_python.outputs.release }}
itinerary: ${{ steps.itinerary.outputs.itinerary }}
steps:
- name: Check the ref
Expand Down Expand Up @@ -148,6 +149,13 @@ jobs:
IS_RELEASE=$(./.github/scripts/is_release_for_package.sh "@e2b/dockerfile-utils-python" "$RUNNER_TEMP/release-plan.json")
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" "$RUNNER_TEMP/release-plan.json")
echo "release=$IS_RELEASE" >> "$GITHUB_OUTPUT"

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

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 @@ -236,6 +251,7 @@ jobs:
- desktop-python-tests
- dockerfile-utils-js-tests
- dockerfile-utils-python-tests
- acp-python-tests
if: (!cancelled()) && !contains(needs.*.result, 'failure') && needs.preflight.outputs.release == 'true'
uses: ./.github/workflows/publish_packages.yml
with:
Expand Down Expand Up @@ -275,6 +291,7 @@ jobs:
- desktop-python-tests
- dockerfile-utils-js-tests
- dockerfile-utils-python-tests
- acp-python-tests
- publish
if: failure()
name: Release Failed - Slack Notification
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/sdk_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ 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_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 +72,12 @@ jobs:
- 'packages/cli/**/!(*.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 +143,14 @@ jobs:
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 +284,7 @@ jobs:
- js-tests
- python-tests
- cli-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-python/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.
13 changes: 13 additions & 0 deletions packages/acp-python/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
typecheck:
ty check e2b_acp

lint:
ruff check .
ruff format --check .

format:
ruff format .

.PHONY: test
test:
uv run pytest --verbose
77 changes: 77 additions & 0 deletions packages/acp-python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# 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-python](https://github.com/e2b-dev/E2B/tree/main/packages/acp-python)

## Install

```
pip install e2b e2b-acp agent-client-protocol
```

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. The ACP Python SDK is asynchronous, so use `AsyncSandbox`.

```python
import asyncio
import os

import acp
from acp import schema
from e2b import AsyncSandbox
from e2b_acp import AsyncACPAgent


class Client:
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])
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="")


async def main():
sandbox = await AsyncSandbox.create("goose", timeout=600)
try:
agent = await AsyncACPAgent.start(
sandbox,
"goose acp --with-builtin developer",
Client(),
envs={
"GOOSE_PROVIDER": "openai",
"GOOSE_MODEL": "gpt-5.6-luna",
"OPENAI_API_KEY": os.environ["OPENAI_API_KEY"],
},
)
await agent.connection.initialize(protocol_version=acp.PROTOCOL_VERSION)
session = await agent.connection.new_session(cwd="/home/user", mcp_servers=[])
await agent.connection.prompt(
session_id=session.session_id,
prompt=[acp.text_block("Create fizzbuzz.py for 1 to 15 and run it.")],
)
await agent.stop()
finally:
await sandbox.kill()


asyncio.run(main())
```

## Where the agent's tools run

The agent talks ACP over its stdin and stdout. `AsyncACPAgent.start` 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 `timeout` (5 seconds by default) of the call. Messages the agent sends after that are not delivered.
- `agent.wait()` returns the exit code once the agent exits.
- If the agent exits, pending requests on the connection fail with a connection error. Use `agent.wait()` for the exit code and pass `on_stderr` to receive the agent's logs.
- The agent runs as long as the sandbox does, so set the sandbox timeout for the longest task you expect.
Loading
Loading