Skip to content
Merged
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
187 changes: 187 additions & 0 deletions .github/workflows/e2b-template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
name: e2b-template-build

on:
workflow_dispatch:
inputs:
source_branch:
description: Source branch for the combined Runtime
type: choice
options: [main, beta]
default: main
required: true
ref:
description: Full commit SHA in that branch; empty selects its current tip
type: string
required: false

permissions:
contents: read

concurrency:
group: e2b-template-build
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-22.04
environment: e2b-build
timeout-minutes: 120
steps:
- name: Check build settings
env:
WORKFLOW_REF: ${{ github.ref }}
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
E2B_API_URL: ${{ vars.E2B_API_URL }}
run: |
set -euo pipefail
[ "$WORKFLOW_REF" = refs/heads/main ] || { echo '::error::Run this workflow from main.'; exit 1; }
[ -n "$E2B_API_KEY" ] || { echo '::error::Set the E2B_API_KEY secret in e2b-build.'; exit 1; }
python3 - <<'PY'
import os, urllib.parse
value = os.environ['E2B_API_URL']
try:
url = urllib.parse.urlsplit(value)
valid = (url.scheme == 'https' and bool(url.hostname)
and url.username is None and url.password is None
and not url.path and not url.query and not url.fragment
and not any(c in value for c in '\\ \t\r\n?#%'))
if url.port is not None:
valid = valid and 1 <= url.port <= 65535
except ValueError:
valid = False
if not valid:
raise SystemExit('Set E2B_API_URL to an HTTPS API origin without path or credentials.')
PY
- uses: actions/checkout@v7
with:
path: build-tools
persist-credentials: false
fetch-depth: 0
- name: Pin source revision
id: source
working-directory: build-tools
env:
SOURCE_BRANCH: ${{ inputs.source_branch }}
REQUESTED_REF: ${{ inputs.ref }}
run: |
set -euo pipefail
case "$SOURCE_BRANCH" in main|beta) ;; *) exit 1 ;; esac
if [ -n "$REQUESTED_REF" ] && [[ ! "$REQUESTED_REF" =~ ^[0-9a-f]{40}$ ]]; then
echo '::error::ref must be a full lowercase commit SHA.'
exit 1
fi
git fetch --no-tags origin "refs/heads/$SOURCE_BRANCH:refs/remotes/origin/$SOURCE_BRANCH"
revision="$(git rev-parse "${REQUESTED_REF:-origin/$SOURCE_BRANCH}^{commit}")"
git merge-base --is-ancestor "$revision" "origin/$SOURCE_BRANCH" || {
echo '::error::ref must be in the selected source branch.'; exit 1;
}
echo "revision=$revision" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v7
with:
path: source
ref: ${{ steps.source.outputs.revision }}
persist-credentials: false
- uses: actions/setup-go@v7
with:
go-version-file: source/go.mod
cache: false
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- uses: ./build-tools/.github/actions/node
with:
lockfiles: source/packages/claude-sdk-adapter/pnpm-lock.yaml
- name: Enable Corepack for the pinned MiniMax source build
run: corepack enable
- name: Select shared Go caches
run: |
echo "GOCACHE=$HOME/.oac/cache/go-build" >> "$GITHUB_ENV"
echo "GOMODCACHE=$HOME/.oac/cache/go-mod" >> "$GITHUB_ENV"
- uses: actions/cache@v6
with:
path: |
~/.oac/cache/go-build
~/.oac/cache/go-mod
key: e2b-runtime-go-${{ runner.os }}-${{ hashFiles('source/**/go.mod', 'source/**/go.sum') }}
- name: Install the selected revision's pinned E2B SDK
run: |
python3 -m venv "$RUNNER_TEMP/e2b-sdk"
"$RUNNER_TEMP/e2b-sdk/bin/pip" install -r source/services/core/deploy/e2b/requirements.txt
- name: Build the combined Runtime image
working-directory: source
run: |
set -euo pipefail
source_root="$GITHUB_WORKSPACE/source"
output_root="$HOME/.oac/build/e2b-runtime"
cd "$source_root"
mkdir -p "$output_root"
bash scripts/prepare-release-runtimes.sh
inputs="$HOME/.oac/build/release-inputs/inputs.json"
AGENTS_RUNTIME_CODEX_PACKAGE="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["codex"])' "$inputs")"
MCODE_HARNESS_BUILD_DIR="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["mcode"])' "$inputs")"
CLAUDE_SDK_BUILD_DIR="$output_root/claude-sdk"
export AGENTS_RUNTIME_CODEX_PACKAGE MCODE_HARNESS_BUILD_DIR CLAUDE_SDK_BUILD_DIR
bash scripts/build-claude-sdk-runtime.sh
revision="$(git rev-parse HEAD)"
for harness in codex claude mcode; do
builder="scripts/build-$harness-runtime.sh"
if [[ "$harness" == codex ]]; then builder=scripts/build-agents-runtime.sh; fi
AGENTS_RUNTIME_BUILD_DIR="$output_root/$harness" bash "$builder"
docker build --platform linux/amd64 --tag "oac-e2b-$harness:$revision" "$output_root/$harness"
image="$(docker image inspect --format '{{.Id}}' "oac-e2b-$harness:$revision")"
python3 scripts/core-distribution-manifest.py verify-runtime \
"$image" "$output_root/$harness/oac-daemon" "$source_root"
done
# The maintained multi-stage Dockerfile advertises and checks all three Harnesses.
mkdir -p "$output_root/combined"
cp deploy/distribution/Runtime.Dockerfile "$output_root/combined/Dockerfile"
docker build --platform linux/amd64 \
--build-arg "CODEX_IMAGE=oac-e2b-codex:$revision" \
--build-arg "CLAUDE_IMAGE=oac-e2b-claude:$revision" \
--build-arg "MCODE_IMAGE=oac-e2b-mcode:$revision" \
--label "org.opencontainers.image.revision=$revision" \
--tag "oac-e2b-runtime:$revision" "$output_root/combined"
image="$(docker image inspect --format '{{.Id}}' "oac-e2b-runtime:$revision")"
python3 scripts/core-distribution-manifest.py verify-runtime \
"$image" "$output_root/mcode/oac-daemon" "$source_root"
printf '%s\n' "$image" > "$output_root/runtime-image.id"
- name: Build one immutable E2B template
env:
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
E2B_API_URL: ${{ vars.E2B_API_URL }}
run: |
set -euo pipefail
umask 077
key="$(mktemp "$RUNNER_TEMP/e2b-key.XXXXXX")"
trap 'rm -f "$key"' EXIT
printf '%s' "$E2B_API_KEY" > "$key"
unset E2B_API_KEY
mkdir -p "$RUNNER_TEMP/e2b-result"
"$RUNNER_TEMP/e2b-sdk/bin/python" source/services/core/deploy/e2b/build-template.py \
--image "$(cat "$HOME/.oac/build/e2b-runtime/runtime-image.id")" \
--name "sandbase-oac-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" \
--api-key-file "$key" --output "$RUNNER_TEMP/e2b-result/template.json"
- name: Record the build identity
env:
SOURCE_COMMIT: ${{ steps.source.outputs.revision }}
SOURCE_BRANCH: ${{ inputs.source_branch }}
E2B_API_URL: ${{ vars.E2B_API_URL }}
run: |
python3 - <<'PY'
import json, os, pathlib
report = pathlib.Path(os.environ['RUNNER_TEMP']) / 'e2b-result/template.json'
value = json.loads(report.read_text())
value.update(source_commit=os.environ['SOURCE_COMMIT'], source_branch=os.environ['SOURCE_BRANCH'],
api_url=os.environ['E2B_API_URL'], harnesses=['codex', 'claude_sdk', 'mcode'],
qualification='image identity and native package checks; no live Session/Turn')
report.write_text(json.dumps(value, indent=2) + '\n')
with open(os.environ['GITHUB_STEP_SUMMARY'], 'a') as out:
out.write(f"Source: `{value['source_commit']}`\n\nTemplate: `{value['template']}`\n\nImage: `{value['image']}`\n\n")
out.write('One combined Runtime template built. No Core configuration or production deployment was changed.\n')
PY
- uses: actions/upload-artifact@v6
with:
name: e2b-template-${{ steps.source.outputs.revision }}-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/e2b-result/template.json
if-no-files-found: error
retention-days: 90
24 changes: 24 additions & 0 deletions deploy/e2b/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Combined E2B template build

Run **Actions → e2b-template-build → Run workflow** from `main`. Select `main` or `beta` as `source_branch`; optionally enter a full commit SHA belonging to that branch. An empty `ref` pins the branch tip once at the start of the run.

This fork-owned workflow builds one Linux amd64 Runtime image containing `oac-daemon`, Codex, Claude SDK and MiniMax Code, then packages it as one E2B template using the selected revision's maintained builders and pinned dependencies. The three intermediate images are local build stages, not three templates. The build uses the maintained `deploy/distribution/Runtime.Dockerfile` and does not build Core, Web or a full offline distribution.

## GitHub configuration

Create the `e2b-build` GitHub Environment and restrict its deployment branches to `main`. Configure:

| Kind | Name | Value |
| --- | --- | --- |
| Secret | `E2B_API_KEY` | The key for the E2B-compatible account that owns the template |
| Variable | `E2B_API_URL` | The exact HTTPS API origin, without a path or trailing slash; use `https://api.e2b.app` for official E2B or your compatible service's endpoint |

Both are required. For the SandBase endpoint, set `E2B_API_URL` to `https://sandbox.sandbase.ai` (HTTPS, without the trailing slash). The workflow supplies the endpoint through the pinned SDK's `E2B_API_URL` setting. Template creation/upload/build APIs must be implemented by the endpoint; Sandbox Create compatibility alone does not establish template-build support. Returned upload destinations must be reachable from GitHub's hosted runner. The builder currently requests 2 vCPUs and 2048 MiB, as defined by the upstream [template builder](../../services/core/deploy/e2b/README.md#build-a-template).

The E2B key is available only to the settings check and template-build steps. It is temporarily written to a private file, removed on exit, excluded from Runtime images and build reports, and never sent to a model provider. No model credentials are needed to build the template. A run creates a cloud template build and may incur the provider's build charges; do not trigger a run merely to validate workflow syntax.

## Output and qualification

The run summary and the `e2b-template-*` artifact contain `template.json`: the immutable `templateID:build_UUID`, source commit and branch, image ID, packaged Runtime checksum, base image, endpoint and advertised Harnesses. Template names include the run ID and attempt so another run does not replace this build's name. There is no automatic retry of an uncertain cloud build; inspect the provider before starting another run after a failure.

The image checks verify committed daemon/adapter identity and native package loading/version checks. Template build completion establishes packaging readiness, not Core enrollment, real model execution or pause/resume qualification. The workflow changes no active Core selection, Kubernetes resource or production Session. [Sandbox deployment](../../contracts/agents-api/sandbox-deployment.md) owns later template selection and generation behavior.
Loading