Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
29ba52f
feat(acp-js): add @e2b/acp package with ACP framing helpers
max-sudolabs Oct 15, 2026
de11987
feat(acp-js): add connectAcpAgent
max-sudolabs Oct 15, 2026
0bbd233
test(acp-js): cover connectAcpAgent against a scripted agent
max-sudolabs Oct 15, 2026
0d5d3bd
ci: test and release the @e2b/acp package
max-sudolabs Oct 23, 2026
b482d38
chore: add changeset for @e2b/acp
max-sudolabs Oct 23, 2026
c46b413
fix(acp-js): reject close() when the agent could not be killed
max-sudolabs Oct 16, 2026
e2328cc
fix(acp-js): kill the agent when the client factory throws
max-sudolabs Oct 16, 2026
2ab0801
test(acp-js): cover dropping in-flight lines after close
max-sudolabs Oct 16, 2026
0658e27
test(acp-js): drop live tests the offline tests already cover
max-sudolabs Oct 16, 2026
bc2cca1
fix(acp-js): let kill() and early output errors stop the agent
max-sudolabs Oct 19, 2026
a763be1
fix(acp-js): split agent output without rescanning a pending line
max-sudolabs Oct 19, 2026
695effa
refactor(acp-js): take command and client as arguments, rename close(…
max-sudolabs Oct 19, 2026
80f19ca
fix(acp-js): kill the agent when its output stream fails
max-sudolabs Oct 23, 2026
2d56496
refactor(acp-js): rename AcpAgentExit to AcpAgentResult, point the si…
max-sudolabs Oct 23, 2026
22d1536
refactor(acp-js): start the agent with the public retainOutput option
mishushakov Oct 28, 2026
9f142b0
chore: refresh the pnpm lockfile for the rebased branch
mishushakov Oct 28, 2026
7842147
ci: pass the release plan to is_release_for_package for @e2b/acp
max-sudolabs Oct 29, 2026
6f6250e
refactor(acp-js)!: start agents with AcpAgent.start, rename option types
max-sudolabs Oct 29, 2026
0b900ae
refactor(acp-js)!: connect a ClientApp instead of the deprecated Clie…
max-sudolabs Oct 29, 2026
b43f150
docs(acp-js): show AcpAgent.start with a client() app
max-sudolabs Oct 29, 2026
336eea0
docs(acp-js): send session requests directly in the usage example
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-js-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@e2b/acp": 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. `AcpAgent.start(sandbox, command, client)` starts the agent over stdin/stdout and connects your ACP `client()` app to it. 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.
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
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_js: ${{ steps.acp_js.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 JavaScript SDK Release
id: acp_js
if: steps.version.outputs.release == 'true'
run: |
IS_RELEASE=$(./.github/scripts/is_release_for_package.sh "@e2b/acp" "$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-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

publish:
name: Publish
needs:
Expand All @@ -236,6 +251,7 @@ jobs:
- desktop-python-tests
- dockerfile-utils-js-tests
- dockerfile-utils-python-tests
- acp-js-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-js-tests
- publish
if: failure()
name: Release Failed - Slack Notification
Expand Down
19 changes: 19 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_js: ${{ steps.filter.outputs.acp_js }}
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,15 @@ 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)'
code_interpreter_js:
- *shared
- '.github/workflows/code_interpreter_js_tests.yml'
Expand Down Expand Up @@ -136,6 +146,14 @@ 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 }}

code-interpreter-js-tests:
name: Production / Code Interpreter JS SDK Tests
needs: changes
Expand Down Expand Up @@ -269,6 +287,7 @@ jobs:
- js-tests
- python-tests
- cli-tests
- acp-js-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.
71 changes: 71 additions & 0 deletions packages/acp-js/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# 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

Build your ACP client with `client()` from `@agentclientprotocol/sdk`, create a sandbox from a template with the agent installed, then start the agent and connect the client to it:

```ts
import { Sandbox } from 'e2b'
import { AcpAgent } from '@e2b/acp'
import { client, methods, PROTOCOL_VERSION } from '@agentclientprotocol/sdk'

const app = client({ name: 'my-app' })
.onRequest(methods.client.session.requestPermission, async ({ params }) => {
const allow = params.options.find((o) => o.kind === 'allow_once') ?? params.options[0]
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)
}
})

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

await agent.connection.agent.request(methods.agent.initialize, { protocolVersion: PROTOCOL_VERSION })
const { sessionId } = await agent.connection.agent.request(methods.agent.session.new, {
cwd: '/home/user',
mcpServers: [],
})
await agent.connection.agent.request(methods.agent.session.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. `AcpAgent.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 `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.
- Closing the connection with `agent.connection.close()` also stops the agent, in the background. Use `agent.stop()` to wait for the agent to stop and to see kill errors.
- 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.
69 changes: 69 additions & 0 deletions packages/acp-js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"name": "@e2b/acp",
"version": "0.0.0",
"packageManager": "pnpm@10.34.5",
"description": "Run Agent Client Protocol (ACP) agents in E2B sandboxes",
"homepage": "https://e2b.dev/docs/agents/acp?utm_source=npm&utm_medium=referral&utm_campaign=package_homepage&utm_content=e2b-acp",
"license": "MIT",
"author": {
"name": "FoundryLabs, Inc.",
"email": "hello@e2b.dev",
"url": "https://e2b.dev"
},
"bugs": "https://github.com/e2b-dev/e2b/issues",
"repository": {
"type": "git",
"url": "https://github.com/e2b-dev/e2b",
"directory": "packages/acp-js"
},
"publishConfig": {
"access": "public"
},
"sideEffects": false,
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"scripts": {
"prepublishOnly": "pnpm build",
"build": "tsc --noEmit && tsdown",
"dev": "tsdown --watch",
"test": "vitest run",
"typecheck": "tsc --noEmit",
"lint": "oxlint --config ../../.oxlintrc.json src tests",
"format": "prettier --write src/ tests/"
},
"devDependencies": {
"@agentclientprotocol/sdk": "1.8.0",
"@types/node": "catalog:",
"@typescript/native": "catalog:",
"dotenv": "^16.4.5",
"tsdown": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
},
"files": [
"dist",
"README.md",
"package.json"
],
"keywords": [
"e2b",
"acp",
"agent-client-protocol",
"ai-agents",
"coding-agents",
"sandbox"
],
"engines": {
"node": ">=22.19.0"
},
"browserslist": [
"defaults"
],
"dependencies": {
"e2b": "workspace:^"
},
"peerDependencies": {
"@agentclientprotocol/sdk": "^1.8.0"
}
}
Loading
Loading