Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
abd1178
phase(e2b-adk-plugin): Foundation & Scaffolding
tomasvarga Jul 3, 2026
cc5ad1e
fix: address Codex review findings (phase e2b-adk-plugin checkpoint)
tomasvarga Jul 3, 2026
63ac12a
fix gitignore after renaming planning folder
max-sudolabs Jul 6, 2026
c0ab0cd
ignore .env and add python-dotenv for examples
max-sudolabs Jul 6, 2026
b0bff6f
phase(e2b-adk-plugin): Execution tools + plugin
max-sudolabs Jul 7, 2026
0662846
fix: harden run_code and sandbox lifecycle
max-sudolabs Jul 7, 2026
c3da715
test: cover SandboxManager concurrency
max-sudolabs Jul 7, 2026
ef9da50
phase(e2b-adk-plugin): File & background tools
max-sudolabs Jul 7, 2026
1b1ff91
fix: harden tools against malformed calls and preview-URL failures
max-sudolabs Jul 7, 2026
34d1a19
fix: keep sandbox alive across tool calls and scope plugin callbacks
max-sudolabs Jul 7, 2026
6c273e7
test: add live E2B smoke suite behind a 'live' marker
max-sudolabs Jul 8, 2026
e615054
test: assert before_tool_callback returns None for owned tools
max-sudolabs Jul 8, 2026
f86ff34
test: cover failure paths in the live smoke suite
max-sudolabs Jul 8, 2026
cf1f3b9
add data-analysis example and default examples to gemini-2.5-flash
max-sudolabs Jul 8, 2026
98d0ff1
add developer README
max-sudolabs Jul 8, 2026
954bfa6
add CI workflow and finalize packaging metadata
max-sudolabs Jul 8, 2026
2e82fb9
feat: expose full sandbox create() options as neutral passthroughs
max-sudolabs Jul 8, 2026
098e0de
docs: correct README truncation marker to match code
max-sudolabs Jul 8, 2026
16886cf
ci: install with --locked so CI fails on lockfile drift
max-sudolabs Jul 9, 2026
3ecb833
docs: document the full sandbox passthrough options in README
max-sudolabs Jul 9, 2026
e86fb4e
build: allowlist sdist contents so local builds can't leak untracked …
max-sudolabs Jul 9, 2026
ba42e1e
fix: read the default sandbox timeout from the public AsyncSandbox class
max-sudolabs Jul 9, 2026
8967d1e
docs: document keep-alive and lifecycle semantics, drop redundant exa…
max-sudolabs Jul 9, 2026
257805e
docs: add the E2B website page for the Google ADK integration
max-sudolabs Jul 9, 2026
e6dd8ff
docs: spell out the two-tier result contract and output bound
max-sudolabs Jul 9, 2026
9ff5121
docs: add documentatio page for ADK site
max-sudolabs Jul 10, 2026
fefd3c6
refactor: split tools into one module per tool under e2b_adk/tools/
max-sudolabs Jul 13, 2026
fdc68f3
docs: correct copyright holder and trim redundant README prose
max-sudolabs Jul 13, 2026
54d49e3
ci: pin action SHAs, add least-privilege token and uv cache
max-sudolabs Jul 13, 2026
18dd04d
chore: stop tracking docs/ (staged locally for separate docs-repo PRs)
max-sudolabs Jul 13, 2026
543cc6f
ci: restore dev-extra install in the build job
max-sudolabs Jul 13, 2026
22c8218
fix: redact command arguments in debug logs
max-sudolabs Jul 14, 2026
e23bc76
fix: normalize malformed UTF-8 tool output
max-sudolabs Jul 15, 2026
602b3b8
fix: validate background command preview ports
max-sudolabs Jul 15, 2026
10cff0d
fix: treat background commands as regular ADK tool calls
max-sudolabs Jul 15, 2026
881e9ef
fix: keep sandboxes alive during active operations
max-sudolabs Jul 15, 2026
60c21f7
chore: harden v0.1.0 release
max-sudolabs Jul 16, 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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
E2B_API_KEY=
GOOGLE_API_KEY=
53 changes: 53 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: CI
Comment thread
tomasvarga marked this conversation as resolved.

on:
push:
branches: [main]
pull_request:

jobs:
lint-type-test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: uv sync --locked --extra dev

- name: Lint
run: uv run ruff check e2b_adk tests examples

- name: Type check
run: uv run mypy e2b_adk

- name: Test
# Tests mock the E2B sandbox — no credentials or network access needed.
run: uv run pytest

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"

- name: Install dependencies
run: uv sync --locked --extra dev

- name: Build sdist and wheel
run: uv run python -m build

- name: Check package metadata
run: uv run twine check dist/*
32 changes: 30 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,30 @@
.claude/
e2b-adk/
/.agents/
/.claude/
/.planning/

# Python
.venv/
__pycache__/
*.pyc
dist/
build/
*.egg-info/
.pytest_cache/
.mypy_cache/
.ruff_cache/

# Features
/features/
/CLAUDE.md
/AGENTS.md
/CLAUDE_ADDITIONS.md
/AGENTS_ADDITIONS.md
/DECISIONS.md

# Environment
.env
!.env.example

# Verification credentials (never commit)
.env.verification
/.claude/verification/auth-state.json
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 e2b-adk contributors
Comment thread
tomasvarga marked this conversation as resolved.
Outdated

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.
158 changes: 157 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,157 @@
# e2b-adk-plugin
# e2b-adk

A [Google ADK](https://google.github.io/adk-docs/) plugin backed by
[E2B](https://e2b.dev) sandboxes. Attach it to an ADK agent and the agent can
generate and run code, execute shell commands, read and write files, and start
long-running processes — all inside an isolated E2B sandbox, with no
infrastructure of your own.

Under the hood every tool call runs in the
[E2B Code Interpreter](https://e2b.dev/docs), so the model's code executes in a
real Python kernel instead of being trusted blind.

## Install

```bash
pip install e2b-adk
```

Or with [uv](https://docs.astral.sh/uv/):

```bash
uv pip install e2b-adk
```

## Credentials

You need an E2B API key for the sandbox and a model key for the agent's LLM
(Gemini is ADK's default):

```bash
export E2B_API_KEY="..."
export GOOGLE_API_KEY="..."
```

Get an E2B key at [e2b.dev/dashboard](https://e2b.dev/dashboard).

## Quickstart

Create the plugin, hand its tools to an `Agent`, and register the plugin on the
`App`. The plugin owns the sandbox lifecycle — you never create or tear one down
yourself.

```python
import asyncio

from google.adk.agents import Agent
from google.adk.apps import App
from google.adk.runners import InMemoryRunner

from e2b_adk import E2BPlugin


async def main() -> None:
plugin = E2BPlugin()
agent = Agent(
model="gemini-2.5-flash",
name="coder",
instruction="Write and run Python to answer the user. Verify with run_code.",
tools=plugin.get_tools(),
)
app = App(name="demo", root_agent=agent, plugins=[plugin])

async with InMemoryRunner(app=app) as runner:
# run_debug prints the conversation as it runs.
await runner.run_debug("Compute the 20th Fibonacci number.")


asyncio.run(main())
```

`get_tools()` returns tool instances that all share the plugin's single sandbox,
so state written by one tool call is visible to the next. When the runner's
`async with` block exits, the plugin's `close()` fires and the sandbox is killed.
Comment thread
tomasvarga marked this conversation as resolved.
Outdated

> The examples use `gemini-2.5-flash`, which runs on a free Gemini key. Swap to
> `gemini-2.5-pro` for stronger reasoning (it needs a paid tier — a free key
> returns HTTP 429).
Comment thread
tomasvarga marked this conversation as resolved.
Outdated

## Tools

`plugin.get_tools()` returns six tools. Each returns a JSON-serializable dict
with a `success` flag — tools report failures in the result rather than raising,
so a bad call never aborts the agent run. `success` means the tool *ran*: code
that raised or a command that exited non-zero still returns `success: True` with
the failure captured in `error` / `exit_code`. Only a call that could not run at
all (sandbox unavailable, timeout) returns `success: False`.

| Tool | Does | Key result fields |
|------|------|-------------------|
| `run_code` | Run code in a stateful kernel (variables persist across calls) | `stdout`, `stderr`, `text`, `error` |
| `run_command` | Run a shell command (each call is independent) | `stdout`, `stderr`, `exit_code` |
| `write_file` | Write a file in the sandbox | `path` |
| `read_file` | Read a file back | `path`, `content` |
| `list_files` | List a directory | `entries` (`[{name, type}]`) |
| `start_background_command` | Start a long-running process; optional preview URL for a port | `pid`, `preview_url` (+ `readiness` when `port` is set) |

### Configuration

`E2BPlugin` takes keyword-only options, all optional. Anything you don't set
falls back to the E2B SDK's own default — the plugin overrides none of them.

```python
E2BPlugin(
# Common options
api_key=None, # defaults to the E2B_API_KEY env var
template=None, # E2B sandbox template
metadata=None, # dict[str, str] attached to the sandbox
envs=None, # dict[str, str] environment variables
timeout=None, # sandbox timeout in seconds (re-applied on every tool call)
plugin_name="e2b_plugin",
# Passed straight through to AsyncSandbox.create() — see the E2B SDK docs
secure=None,
allow_internet_access=None,
mcp=None,
network=None,
lifecycle=None, # e.g. pause/auto-resume; defaults to E2B's behavior
volume_mounts=None,
# **opts: any other AsyncSandbox.create() / connection param (proxy, request_timeout, …)
)
```

## Examples

- [`examples/data_analysis.py`](examples/data_analysis.py) — a data-analysis
agent that saves a dataset and computes real answers with pandas instead of
guessing.
- [`examples/code_generator.py`](examples/code_generator.py) — a code generator
that executes every snippet before returning it.

Run one with the credentials above:

```bash
uv run --extra examples python examples/data_analysis.py
```

## v1 notes

- **Sequential tool calls.** One sandbox is shared per plugin; calls are meant
to run one at a time, not concurrently. Concurrent calls are safe for the
sandbox lifecycle (creation is locked) but share one filesystem and kernel.
- **Text-only output.** Tools return text — `stdout`/`stderr`/file contents.
Rich results (charts, dataframes, images) are not surfaced yet.
- **Lazy, shared sandbox lifecycle.** The sandbox is created on the first tool
call and reused for the agent's lifetime, then killed when the runner exits.
- **Keep-alive while active, expiry when idle.** Every tool call pushes the
sandbox's expiry window forward by `timeout` (E2B's default is 300s), so an
active session never expires mid-run. An idle gap longer than `timeout` still
expires the sandbox under E2B's default lifecycle (`on_timeout: kill`), and
later tool calls return failure results — pass e.g.
`lifecycle={"on_timeout": {"action": "pause"}, "auto_resume": True}` to pause
and auto-resume across idle gaps instead.
- **Bounded output.** Output larger than 10 KB per field is truncated with a
`…[truncated N bytes]` marker so a single call can't flood the model's context.

## License

MIT — see [LICENSE](LICENSE).
Loading
Loading