Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
31153a1
docs: add E2B sandbox provider design
skrishnan22 Oct 1, 2026
2b09218
docs: add E2B sandbox provider implementation plan
skrishnan22 Oct 1, 2026
6277cda
refactor(sandbox): route sandbox access through a provider interface
skrishnan22 Oct 1, 2026
7fbc01c
fix(sandbox): gate preview sandbox access behind auth and tighten pro…
skrishnan22 Oct 1, 2026
fb5bc61
feat(sandbox): add idle-pause session fields and paused sandbox state
skrishnan22 Oct 1, 2026
cbd9d74
test(sandbox): cover idle-pause init defaults and create-session forw…
skrishnan22 Oct 1, 2026
9599526
feat(sandbox-image): reread sandbox WebSocket token file on reconnect
skrishnan22 Oct 1, 2026
12028ac
fix(sandbox-image): prefer newer in-memory ws token over an unchanged…
skrishnan22 Oct 1, 2026
ecb446d
feat(sandbox): add idle-pause and lease scheduling helpers
skrishnan22 Oct 1, 2026
8d13135
fix(sandbox): retry past-due pause and lease deadlines and guard inva…
skrishnan22 Oct 1, 2026
ee3bd1a
test(sandbox): pin past-due clamp against competing alarm deadlines
skrishnan22 Oct 1, 2026
737b5b2
docs(sandbox): record E2B feasibility findings
skrishnan22 Oct 1, 2026
c8fea76
chore(worker): minimize lockfile churn for e2b dependency
skrishnan22 Oct 1, 2026
c59c69a
docs(sandbox): align E2B spec body with feasibility findings
skrishnan22 Oct 1, 2026
581fe5f
feat(sandbox): add E2B sandbox provider and make it the default
skrishnan22 Oct 1, 2026
766ec04
fix(sandbox): disconnect E2B command streams and harden E2B adapter e…
skrishnan22 Oct 2, 2026
83e11eb
fix(sandbox): tolerate E2B stream disconnect errors and clean staged …
skrishnan22 Oct 2, 2026
ba9d3f3
feat(sandbox): pause idle E2B sandboxes and resume on demand
skrishnan22 Oct 2, 2026
098b9fc
fix(sandbox): count only live sandbox sockets and harden pause/resume…
skrishnan22 Oct 2, 2026
65ff66b
fix(sandbox): flush deferred preview after resume and cover the recon…
skrishnan22 Oct 2, 2026
99787bc
fix(sandbox): replay the latest preview intent after resume
skrishnan22 Oct 2, 2026
1b5799b
feat(sandbox-image): share Dockerfile across providers and publish E2…
skrishnan22 Oct 2, 2026
610467b
fix(sandbox-image): test the real template publish path and tighten i…
skrishnan22 Oct 2, 2026
4892a9f
fix(sandbox-image): make skipped template publish unmistakable
skrishnan22 Oct 2, 2026
46de0ae
docs(sandbox): record E2B provider verification results
skrishnan22 Oct 2, 2026
654dc5e
fix(sandbox): unbounded E2B agent process, create retries, static des…
skrishnan22 Oct 2, 2026
7d95e37
test(worker): assert no E2B_API_KEY assignment in wrangler.toml
skrishnan22 Oct 2, 2026
79a9100
feat(sandbox): allow the E2B preview host, adopt resumed tokens immed…
skrishnan22 Oct 2, 2026
d4624d8
fix(sandbox): fail fast on a missing provider key, surface provisioni…
skrishnan22 Oct 2, 2026
36fc799
fix(sandbox): cache provider handles, kill paused sandboxes by refere…
skrishnan22 Oct 2, 2026
a5b74ad
fix(sandbox): never cache Cloudflare sandbox stubs
skrishnan22 Oct 2, 2026
6bcddf6
Merge remote-tracking branch 'orgin/main' into HEAD
skrishnan22 Oct 2, 2026
21c8103
ci(sandbox): publish the E2B template on deploy and pin the Worker to it
skrishnan22 Oct 2, 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
64 changes: 64 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,71 @@ jobs:

- run: node packages/sandbox-image/test/sandbox-image-smoke.mjs

# The E2B template is built from the same Dockerfile on a plain Node base.
- run: docker build -f Dockerfile.sandbox --build-arg SANDBOX_BASE=node:22-slim -t codevil-sandbox-e2b:ci .

- run: node packages/sandbox-image/test/sandbox-image-smoke.mjs
env:
CODEVIL_SANDBOX_IMAGE: codevil-sandbox-e2b:ci

# Like wrangler's container build for Cloudflare: when wrangler.toml selects
# E2B, build this commit's sandbox image and publish it as the E2B template
# the deployed Worker is pinned to. A failed publish blocks the deploy. This is
# its own job so the GITHUB_TOKEN that E2B receives as registry credentials
# (it can also push packages) expires as soon as the publish finishes, and the
# Cloudflare deploy job never holds packages: write.
e2b-template:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs:
- verify
- sandbox-image
runs-on: ubuntu-latest
timeout-minutes: 45
environment: production
permissions:
contents: read
packages: write
concurrency:
group: production-e2b-template
cancel-in-progress: false
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false

- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
with:
version: 10.28.1

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22.19.0
cache: pnpm

- run: pnpm install --frozen-lockfile

- name: Read sandbox provider
id: sandbox
run: pnpm --filter @codevil/worker exec node scripts/sandbox-deploy-settings.mjs

- name: Publish E2B sandbox template
if: steps.sandbox.outputs.sandbox_provider == 'e2b'
run: |
export CODEVIL_SANDBOX_IMAGE="ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/codevil-sandbox:${GITHUB_SHA}"
pnpm --filter @codevil/sandbox-image e2b:template
env:
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
E2B_TEMPLATE_ID: ${{ steps.sandbox.outputs.e2b_template_id }}
E2B_TEMPLATE_TAG: ${{ github.sha }}
CODEVIL_REGISTRY_USERNAME: ${{ github.actor }}
CODEVIL_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}

deploy:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs:
- verify
- sandbox-image
- e2b-template
runs-on: ubuntu-latest
timeout-minutes: 60
environment: production
Expand Down Expand Up @@ -79,8 +139,12 @@ jobs:

- run: pnpm install --frozen-lockfile

# With SANDBOX_PROVIDER = "e2b" this pins E2B_TEMPLATE_ID to
# "<template>:<commit sha>", the build the e2b-template job published.
- name: Generate production Wrangler config
run: pnpm --filter @codevil/worker exec node scripts/write-deployment-config.mjs
env:
E2B_TEMPLATE_TAG: ${{ github.sha }}

- name: Check for pending D1 migrations
run: pnpm --filter @codevil/worker run check:migrations
Expand Down
6 changes: 4 additions & 2 deletions Dockerfile.sandbox
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
ARG CODEVIL_SANDBOX_PLATFORM=linux/amd64
ARG SANDBOX_BASE=docker.io/cloudflare/sandbox:0.12.7

FROM --platform=$CODEVIL_SANDBOX_PLATFORM node:22-slim AS builder
RUN npm install -g pnpm@10.28.1
Expand All @@ -16,7 +17,7 @@ RUN pnpm install --filter @codevil/sandbox-image... --prod=false \
RUN rm -rf node_modules \
&& pnpm install --filter @codevil/sandbox-image... --prod

FROM --platform=$CODEVIL_SANDBOX_PLATFORM docker.io/cloudflare/sandbox:0.12.7
FROM --platform=$CODEVIL_SANDBOX_PLATFORM ${SANDBOX_BASE}

USER root

Expand All @@ -37,6 +38,7 @@ RUN apt-get update \
&& npm install -g pnpm@10.28.1 \
&& node -e 'const [major, minor] = process.versions.node.split(".").map(Number); if (major < 22 || (major === 22 && minor < 19)) throw new Error(`Node ${process.versions.node} does not satisfy >=22.19`); console.log(process.version)' \
&& git --version \
&& (command -v bun >/dev/null || npm install -g bun@1) \
&& bun --version \
&& setpriv --version

Expand All @@ -46,7 +48,7 @@ COPY packages/sandbox-image/skills /opt/codevil/pi-agent/skills

RUN groupadd --gid 10001 codevil \
&& useradd --uid 10001 --gid codevil --home-dir /home/codevil --create-home codevil \
&& mkdir -p /workspace \
&& mkdir -p /workspace /var/log/codevil /run/codevil \
&& chown codevil:codevil /workspace

WORKDIR /workspace
Expand Down
44 changes: 41 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

[![CI](https://github.com/skrishnan22/codevil/actions/workflows/ci.yml/badge.svg)](https://github.com/skrishnan22/codevil/actions/workflows/ci.yml)

Codevil is a self-hosted AI coding agent platform. Each coding session runs in an isolated Cloudflare Sandbox and streams its progress to a collaborative web UI.
Codevil is a self-hosted AI coding agent platform. Each coding session runs in an isolated sandbox (an E2B sandbox or a Cloudflare Sandbox container, selectable per deployment) and streams its progress to a collaborative web UI.

## Self-hosting

Prerequisites: Node.js 20+, pnpm 10, a Cloudflare account with Workers Containers access, a Google OAuth client, at least one supported provider API key, and a fine-grained GitHub token.
Prerequisites: Node.js 20+, pnpm 10, a Cloudflare account (with Workers Containers access for the Cloudflare sandbox provider), an E2B account and API key if you use the E2B provider, a Google OAuth client, at least one supported provider API key, and a fine-grained GitHub token.

Install dependencies, authenticate Wrangler, and prepare the auth, GitHub, and bootstrap secrets:

Expand All @@ -22,7 +22,7 @@ Set `GITHUB_PAT`, `GOOGLE_CLIENT_ID`, and `GOOGLE_CLIENT_SECRET` in `packages/wo
openssl rand -hex 32
```

Replace every `REPLACE_ME` placeholder before upload. Never upload the example placeholders unchanged. Once all seven values are set, upload the file through Wrangler's existing bootstrap path:
Replace every `REPLACE_ME` placeholder before upload. Never upload the example placeholders unchanged. Add `E2B_API_KEY` to the file as well if you will run sessions on E2B (see [Sandbox provider](#sandbox-provider)); leave it out for Cloudflare-only deployments. Once all values are set, upload the file through Wrangler's existing bootstrap path:

```sh
cd packages/worker
Expand Down Expand Up @@ -54,6 +54,44 @@ In the Google OAuth client, add the deployed Worker origin as an authorized Java

Open the Worker URL, sign in with Google, claim the first owner account using `CODEVIL_SETUP_TOKEN`, and invite the rest of the team.

## Sandbox provider

Codevil runs each session in either an E2B sandbox or a Cloudflare Sandbox container. The shipped `wrangler.toml` pins `SANDBOX_PROVIDER = "cloudflare"` so a deploy never switches to E2B before it is ready. To move to E2B: (1) add `E2B_API_KEY` both as a Worker secret and as a GitHub Actions secret in the `production` environment, (2) pass the manual end-to-end check, (3) set `SANDBOX_PROVIDER = "e2b"` and merge to `main`. CI then publishes the template on every deploy (see below). The Worker's code default (no var set) is `e2b`. Configure the provider with Worker vars in `packages/worker/wrangler.toml` (or an untracked `wrangler.operator.toml` overlay):

| Name | Kind | Default | Purpose |
| --- | --- | --- | --- |
| `SANDBOX_PROVIDER` | var | `e2b` in code; `cloudflare` in the shipped `wrangler.toml` | `e2b` or `cloudflare`. |
| `E2B_API_KEY` | secret | none | Required when `SANDBOX_PROVIDER=e2b`. Upload with `pnpm exec wrangler secret put E2B_API_KEY`; never put it in `wrangler.toml`. |
| `E2B_TEMPLATE_ID` | var | `codevil-sandbox` | E2B template the sandboxes start from, without a tag. CI deploys pin it to `<template>:<commit sha>`. |
| `E2B_MAX_SANDBOX_SECONDS` | var | `3600` | Maximum continuous sandbox runtime; the default matches the E2B Hobby limit. |

### Publishing the E2B template

On a CI deploy this is automatic, the same way `wrangler deploy` builds the Cloudflare container image. When `wrangler.toml` sets `SANDBOX_PROVIDER = "e2b"`, the `e2b-template` job builds this commit's sandbox image, pushes it to `ghcr.io/<owner, lowercased>/codevil-sandbox:<commit sha>` with the workflow's `GITHUB_TOKEN`, and publishes it as the E2B template tagged with the commit SHA (and `default`, so the untagged name follows the latest publish). Only then does the `deploy` job ship the Worker pinned to `E2B_TEMPLATE_ID = "<template>:<commit sha>"`; a failed publish stops the deploy. With `SANDBOX_PROVIDER = "cloudflare"` the publish is skipped and E2B is never called. The deploy config must set `SANDBOX_PROVIDER` explicitly in `[vars]`.

The first CI push creates the GHCR package and links it to the repository. If you already created `codevil-sandbox` by hand, CI's push is refused until you grant access: in the package settings, under **Manage Actions access**, add this repository with the **Write** role.

To publish by hand (an operator deploy without CI, or a first manual test), run the script below. Both providers share `Dockerfile.sandbox`. The Cloudflare build uses its default `cloudflare/sandbox` base; the E2B template is built from the same file on a plain `node:22-slim` base. E2B's Template SDK cannot read multi-stage Dockerfiles, so the publish script builds the image with local Docker (the Dockerfile pins `linux/amd64`, which E2B runs, also on Apple Silicon), pushes it to a registry, and then registers the pushed image as an E2B template with 2 vCPU / 4096 MiB:

```sh
export E2B_API_KEY=... # E2B account key
export CODEVIL_SANDBOX_IMAGE=ghcr.io/<owner>/codevil-sandbox:<tag>
export CODEVIL_REGISTRY_USERNAME=... # private registries only
export CODEVIL_REGISTRY_PASSWORD=... # private registries only
pnpm --filter @codevil/sandbox-image e2b:template -- --dry-run # print the plan without running it
pnpm --filter @codevil/sandbox-image e2b:template # build -> push -> template
```

| Name | Required | Purpose |
| --- | --- | --- |
| `E2B_API_KEY` | yes | Authenticates the template build. |
| `CODEVIL_SANDBOX_IMAGE` | yes | Full registry reference that is built, pushed, and used as the template base. |
| `E2B_TEMPLATE_ID` | no | Template name to publish (default `codevil-sandbox`); keep it equal to the Worker's `E2B_TEMPLATE_ID` without the tag. |
| `E2B_TEMPLATE_TAG` | no | Extra tag for this build, so a Worker can pin `<template>:<tag>`; the `default` tag moves with it. CI passes the commit SHA. |
| `CODEVIL_REGISTRY_USERNAME`, `CODEVIL_REGISTRY_PASSWORD` | no | Registry credentials, needed together when the image is private. |

Registry credentials come only from the environment of the person publishing the template. The password is passed to `docker login` over stdin (never as an argument), is never printed, and is never committed or stored in wrangler config. The machine running the script needs Docker, access to push to the registry, and the built image must be pullable by E2B (public, or private with the credentials above).

## Slack integration

The first Slack integration supports one statically configured Slack workspace per Codevil deployment. Any non-bot member of that workspace can configure a channel repository and invoke Codevil, but an Agent Request is created only when `@codevil` is explicitly mentioned.
Expand Down
Loading
Loading