Skip to content
Closed
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
13 changes: 13 additions & 0 deletions api/oss/src/utils/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,18 @@ class DaytonaConfig(BaseModel):
model_config = ConfigDict(extra="ignore")


# ---------------------------------------------------------------------------
# e2b
# ---------------------------------------------------------------------------


class E2BConfig(BaseModel):
api_key: str | None = os.getenv("E2B_API_KEY")
template: str | None = os.getenv("E2B_TEMPLATE")

model_config = ConfigDict(extra="ignore")


# ---------------------------------------------------------------------------
# docker
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1239,6 +1251,7 @@ class EnvironSettings(BaseModel):
crisp: CrispConfig = CrispConfig()
daytona: DaytonaConfig = DaytonaConfig()
docker: DockerConfig = DockerConfig()
e2b: E2BConfig = E2BConfig()
identity: IdentityConfig = IdentityConfig()
llm: LLMConfig = LLMConfig()
loops: LoopsConfig = LoopsConfig()
Expand Down
97 changes: 97 additions & 0 deletions docs/design/agent-workflows/projects/add-opencode-e2b/research.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# opencode on E2B — investigation

## What this worktree adds

opencode harness on the E2B sandbox. opencode-local (`add-harness-opencode`) is
already done on this branch; this layer adds the E2B sandbox axis.

## Sandbox-agent/e2b provider (sourced from sibling `chore/add-sandbox-e2b`)

The `sandbox-agent@0.4.2` package exports `sandbox-agent/e2b`. It requires
`@e2b/code-interpreter >=1.0.0` as an optional peer dep — add it as a direct dep
so the subpath import resolves. The provider options are:

| Option | Purpose |
|---|---|
| `template` | E2B template ID (env `E2B_TEMPLATE`, default `agenta-sandbox-agent`) |
| `create.envs` | In-sandbox env vars (piExtEnv + secrets merged) |
| `timeoutMs` | Sandbox lifetime backstop; self-reaps a leaked sandbox on process KILL |
| `autoPause` | Pause (not delete) after idle; reduces billing; set `true` |

`E2B_API_KEY` is picked up by the `@e2b/code-interpreter` SDK automatically from env;
the runner does not need to pass it. `SANDBOX_AGENT_PROVIDER=e2b` routes the run.

### Leak backstop

`timeoutMs` is the E2B equivalent of Daytona's `ephemeral + autoStopInterval`. A process
KILL skips the `finally` teardown; `timeoutMs` self-reaps the sandbox after it goes idle.
Default: 30 min (`DEFAULT_E2B_TIMEOUT_MS`). Override with `E2B_TIMEOUT_MS`.
Clamped to >= 1 ms (0 would re-disable the backstop).

### Network policy on E2B

The `sandbox-agent/e2b` wrapper exposes no egress-control API. A restricted
`network` policy on E2B is therefore refused up-front (same rule as local), not silently
accepted. `E2B_NETWORK_UNSUPPORTED_MESSAGE` mirrors `LOCAL_NETWORK_UNSUPPORTED_MESSAGE`.

### Default cwd on E2B

The E2B base image (`e2bdev/code-interpreter`) uses `/root/work` as working dir.
`defaultE2bCwd()` returns `/root/work/agenta-<6-byte-hex>` so each run gets an isolated
subdir without conflicting across concurrent runs.

## opencode credential on E2B

opencode uses a **plain managed provider key** — `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`.
No Zen gateway, no auth file, no cookie fetch. The key is injected through `create.envs`
(the same secrets dict that `plan.secrets` carries from the wire request). The daemon on
the E2B template auto-installs opencode on first use (same as local); no bake is needed
for the binary itself.

### planMode: false

Unchanged from opencode-local: `capabilities.ts` static fallback already excludes opencode
from `session/set_mode`. No E2B-specific change needed here.

### Plain `createAcpFetch`

No Daytona auth cookie — use `createAcpFetch()` directly (same as local). The engine
already selects `createCookieFetch` only for `isDaytona`.

## The arch gotcha (opencode binary on E2B)

The daemon's `install-agent opencode` downloads the linux-**x64** Bun binary even on
arm64 hosts → SIGTRAP (`rosetta error … ld-linux-x86-64.so.2`). E2B runs on real x86-64
cloud hardware, so the default x64 download is **correct** — no arch override needed on
E2B. The gotcha only bites on local arm64 dev machines. Document but do not fix (not
applicable to E2B).

This "E2B is always x86-64" assumption is load-bearing: if E2B ever offers arm64
templates, the opencode arch override must be ported here too (see the daytona
sibling's `opencodeArchEnv` in `daytona.ts`).

## Node version in the E2B template

The `e2bdev/code-interpreter` base image ships Node 20. `pi-acp` requires `>=22.19`. The
template must upgrade to Node 22 before installing the pi-acp adapter. The sibling
`chore/add-sandbox-e2b` Dockerfile already handles this. For opencode the daemon
auto-installs the binary at runtime; no Node constraint applies to the opencode binary
(it is a Bun single-file binary, not a Node script). Node 22 is still required in the
template because the `sandbox-agent` daemon itself is a Node process.

## Overlap with sibling branch `chore/add-sandbox-e2b`

These files are modified in both this branch and `chore/add-sandbox-e2b`:

| File | Overlap nature |
|---|---|
| `services/agent/src/engines/sandbox_agent/provider.ts` | E2B wiring (`buildE2bCreate`, `e2bTimeoutMs`, `e2b` branch) is byte-identical |
| `services/agent/src/engines/sandbox_agent/run-plan.ts` | E2B wiring (`isE2b`, `E2B_NETWORK_UNSUPPORTED_MESSAGE`, `defaultE2bCwd`, `createE2bCwd`) is byte-identical; opencode acpAgent comment differs |
| `services/agent/package.json` | `@e2b/code-interpreter` dep added in both |
Comment on lines +86 to +90
| `api/oss/src/utils/env.py` | `E2bConfig` + `EnvironSettings.e2b` added in both |
| `services/agent/sandbox-images/e2b/e2b.Dockerfile` | Sibling has Pi-only; this branch adds opencode note to README but Dockerfile is same |
| `services/agent/tests/unit/sandbox-agent-provider.test.ts` | E2B tests are identical |
| `services/agent/tests/unit/sandbox-agent-run-plan.test.ts` | E2B tests are identical; opencode tests differ (this branch only) |
Comment on lines +91 to +94

Dedup strategy: when these branches merge, keep one copy of the shared E2B wiring and
deduplicate in a follow-up chore.
86 changes: 86 additions & 0 deletions docs/design/agent-workflows/projects/add-opencode-e2b/specs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# opencode on E2B — spec

## Scope

Add the E2B sandbox provider to this branch (opencode-local already done) so the
`harness=opencode, sandbox=e2b` combination works. E2B-specific wiring is duplicated from
the sibling `chore/add-sandbox-e2b` branch; it will be deduped after both land.

## Provider wiring (`services/agent/src/engines/sandbox_agent/provider.ts`)

```
import { e2b } from "sandbox-agent/e2b";

DEFAULT_E2B_TIMEOUT_MS = 30 * 60 * 1000 // 30 min backstop
e2bTimeoutMs(raw?) // parse E2B_TIMEOUT_MS; clamp >= 1 ms
buildE2bCreate(piExtEnv, secrets) -> { envs, timeoutMs, autoPause: true }

Comment on lines +14 to +17
buildSandboxProvider(...):
if sandboxId === "e2b":
template = E2B_TEMPLATE ?? "agenta-sandbox-agent"
create = buildE2bCreate(piExtEnv, secrets)
return e2b({ template, create: { envs }, timeoutMs, autoPause })
```

## Run-plan wiring (`services/agent/src/engines/sandbox_agent/run-plan.ts`)

```
E2B_NETWORK_UNSUPPORTED_MESSAGE // new named constant (parallel to LOCAL_)
RunPlan.isE2b: boolean // new field
BuildRunPlanDeps.createE2bCwd? // injectable for tests
defaultE2bCwd() // /root/work/agenta-<hex>

buildRunPlan():
isE2b = sandboxId === "e2b"
if networkRestricted && isE2b -> error E2B_NETWORK_UNSUPPORTED_MESSAGE
if networkRestricted && !isDaytona && !isE2b -> error LOCAL_NETWORK_UNSUPPORTED_MESSAGE
cwd = isDaytona ? createDaytonaCwd() : isE2b ? createE2bCwd() : createLocalCwd()
plan.isE2b = isE2b
Comment on lines +28 to +38
```

## Credential provisioning

opencode reads `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` from env. Both are present in
`plan.secrets` (from the wire request) and injected via `buildE2bCreate(...).envs`. No
auth file, no Zen key, no cookie fetch. `planMode: false` is already set in
`capabilities.ts` and does not change for E2B.

## `api/oss/src/utils/env.py` — E2bConfig

```python
class E2bConfig(BaseModel):
api_key: str | None = os.getenv("E2B_API_KEY")
template: str | None = os.getenv("E2B_TEMPLATE")
model_config = ConfigDict(extra="ignore")

# EnvironSettings:
e2b: E2bConfig = E2bConfig()
```
Comment on lines +48 to +58

## `services/agent/package.json`

Add `"@e2b/code-interpreter": "^1.0.0"` to `dependencies`.

## `services/agent/sandbox-images/e2b/`

Reuse the `e2b.Dockerfile` from the Pi-only sibling template. The opencode binary is
auto-installed at runtime by the daemon (`install-agent opencode`), so no additional
bake step is needed. The README notes both Pi and opencode support.
Comment on lines +60 to +68

## Sandbox teardown / leak parity

The engine `finally` calls `sandbox.destroySandbox()` on every exit path. The
`timeoutMs` backstop self-reaps a leaked sandbox when a process KILL skips the `finally`.
`autoPause: true` pauses (not deletes) after idle, so billing stops. No sandbox outlives
its run.

## Network policy enforcement

E2B exposes no egress-control API. Any restricted `network` policy on E2B is refused
up-front via `E2B_NETWORK_UNSUPPORTED_MESSAGE` — never silently accepted.

## Arch gotcha

E2B runs on real x86-64 cloud. The daemon's `install-agent opencode` fetches the linux-x64
binary, which is correct on E2B. No arch override needed. The SIGTRAP gotcha only applies
to local arm64 dev machines.
39 changes: 39 additions & 0 deletions docs/design/agent-workflows/projects/add-opencode-e2b/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# opencode on E2B — tasks

## T1: TypeScript runner — E2B provider

- [x] `provider.ts`: `import { e2b } from "sandbox-agent/e2b"`
- [x] `provider.ts`: `DEFAULT_E2B_TIMEOUT_MS`, `e2bTimeoutMs()`, `buildE2bCreate()`
- [x] `provider.ts`: `buildSandboxProvider` E2B branch
- [x] `run-plan.ts`: `E2B_NETWORK_UNSUPPORTED_MESSAGE`
- [x] `run-plan.ts`: `RunPlan.isE2b`, `BuildRunPlanDeps.createE2bCwd`, `defaultE2bCwd()`
- [x] `run-plan.ts`: E2B network gate + cwd selection + `plan.isE2b`
Comment on lines +5 to +10
- [x] `package.json`: `@e2b/code-interpreter ^1.0.0`

## T2: Python API — E2bConfig

- [x] `api/oss/src/utils/env.py`: `E2bConfig` class + `EnvironSettings.e2b`

Comment on lines +13 to +16
## T3: Sandbox image

- [x] `services/agent/sandbox-images/e2b/e2b.Dockerfile`: Node 22, sandbox-agent, pi-acp
- [x] `services/agent/sandbox-images/e2b/README.md`: note Pi + opencode (auto-installed)
Comment on lines +19 to +20

## T4: Unit tests

- [x] `sandbox-agent-provider.test.ts`: `buildE2bCreate`, `e2bTimeoutMs`, `DEFAULT_E2B_TIMEOUT_MS`
- [x] `sandbox-agent-run-plan.test.ts`: opencode-on-E2B normalizes; isE2b=true; restricted network refused
- [x] `sandbox-agent-run-plan.test.ts`: opencode-on-E2B with ANTHROPIC_API_KEY carries key via secrets

## T5: Integration test (deferred — requires live E2B account)

- [ ] opencode-on-E2B returns `ok:true` output + trace (needs `E2B_API_KEY` + `ANTHROPIC_API_KEY`)
- [ ] sandbox is torn down after run (verify no leaked sandboxes)

## Decisions already made

- No Zen, no auth file: opencode uses plain provider key only.
- No cookie fetch: `createAcpFetch()` like local (cookie is Daytona-only).
- Arch override not needed on E2B (real x64 hardware, daemon fetches correct binary).
- E2B network policy: refused (not silently accepted) — no egress control API.
- Template default: `agenta-sandbox-agent` (shared with Pi template, opencode auto-installs).
1 change: 1 addition & 0 deletions services/runner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
},
"dependencies": {
"@daytonaio/sdk": "^0.187.0",
"@e2b/code-interpreter": "^1.0.0",
"@earendil-works/pi-coding-agent": "0.79.4",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/exporter-trace-otlp-proto": "0.54.0",
Expand Down
Loading
Loading