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
38 changes: 37 additions & 1 deletion .github/workflows/e2b-template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ on:
default: creative
required: true

compute_profile:
description: Template CPU and memory allocation
type: choice
options: [2c2g, 2c8g, 4c8g]
default: 2c2g
required: true

resume_build:
description: Existing unsubmitted templateID:build_UUID after an upload failure; requires ref
type: string
Expand Down Expand Up @@ -181,6 +188,7 @@ jobs:
env:
RESUME_BUILD: ${{ inputs.resume_build }}
SOURCE_COMMIT: ${{ steps.source.outputs.revision }}
COMPUTE_PROFILE: ${{ inputs.compute_profile }}
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
E2B_API_URL: ${{ vars.E2B_API_URL }}
run: |
Expand All @@ -191,9 +199,16 @@ jobs:
printf '%s' "$E2B_API_KEY" > "$key"
unset E2B_API_KEY
mkdir -p "$RUNNER_TEMP/e2b-result"
compute_args=()
case "$COMPUTE_PROFILE" in
2c2g) ;;
2c8g|4c8g) compute_args=(--compute-profile "$COMPUTE_PROFILE") ;;
Comment on lines +203 to +205

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass the selected compute profile explicitly

When 2c2g is selected, this branch deliberately omits the flag to preserve compatibility with historical source parsers. That makes the operator’s selection depend on the checked-out builder’s implicit default; if a selected beta or later source changes that default, the subsequent check still accepts it because it compares E2B’s allocation with that same builder’s kwargs rather than with COMPUTE_PROFILE. Require the selected revision to support the interface and pass --compute-profile 2c2g explicitly instead of retaining this compatibility fallback.

AGENTS.md reference: AGENTS.md:L59-L61

Useful? React with 👍 / 👎.

*) echo '::error::Unknown compute profile.'; exit 1 ;;
esac
"$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" \
"${compute_args[@]}" \
--api-key-file "$key" --output "$RUNNER_TEMP/e2b-result/template.json" <<'PY'
import hashlib, json, os, pathlib, runpy, sys
from types import SimpleNamespace
Expand Down Expand Up @@ -252,7 +267,22 @@ jobs:
result = original_request(client, **kwargs)
receipt = {'template': result.template_id + ':' + result.build_id,
'source_commit': os.environ['SOURCE_COMMIT'],
'api_url': os.environ['E2B_API_URL'], 'resumed': bool(selector)}
'api_url': os.environ['E2B_API_URL'], 'resumed': bool(selector),
'resources_verified': False}
(output / 'build-request.json').write_text(json.dumps(receipt, indent=2) + '\n')
response = client.get_httpx_client().get(f'/templates/{result.template_id}')
response.raise_for_status()
detail = response.json()
builds = [build for build in detail.get('builds', [])
if build.get('buildID') == result.build_id]
if (detail.get('templateID') != result.template_id or len(builds) != 1
or type(builds[0].get('cpuCount')) is not int
or type(builds[0].get('memoryMB')) is not int
or builds[0]['cpuCount'] != kwargs['cpu_count']
or builds[0]['memoryMB'] != kwargs['memory_mb']):
raise RuntimeError('Template build CPU/memory does not match the requested compute profile')
receipt.update(cpu_count=builds[0]['cpuCount'], memory_mb=builds[0]['memoryMB'],
resources_verified=True)
(output / 'build-request.json').write_text(json.dumps(receipt, indent=2) + '\n')
return result
sdk.request_build = request
Expand All @@ -271,12 +301,18 @@ jobs:
import json, os, pathlib
report = pathlib.Path(os.environ['RUNNER_TEMP']) / 'e2b-result/template.json'
value = json.loads(report.read_text())
receipt = json.loads(report.with_name('build-request.json').read_text())
if (receipt.get('resources_verified') is not True
or receipt.get('template') != value['template']):
raise RuntimeError('Build report does not match a verified allocation receipt')
value.update(cpu_count=receipt['cpu_count'], memory_mb=receipt['memory_mb'])
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(f"Compute: `{value['cpu_count']} vCPU / {value['memory_mb']} MiB`\n\n")
out.write('One combined Runtime template built. No Core configuration or production deployment was changed.\n')
PY
- uses: actions/upload-artifact@v6
Expand Down
6 changes: 4 additions & 2 deletions services/core/deploy/e2b/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ python -m venv "$HOME/.oac/build/e2b-sdk"
--output "$HOME/.oac/build/e2b-template.json"
```

[`build-template.py`](build-template.py) requires a `sha256:` image ID, a Linux amd64 image and the Runtime layout (`OAC_RUNTIME_WORKSPACE=/environment/workspace`). It copies the image's `/usr/local/bin`, `/opt` and, when present, `/usr/local/codex-resources` and `/etc/codex`, together with its `HOME` and `OAC_*` environment, onto a digest-pinned `node:22.23.1-bookworm-slim` base with `ca-certificates`, `bash`, `git`, `python3`, `python3-pip`, `ripgrep` and `util-linux`. It installs [`init.py`](init.py) and [`managed_init.py`](managed_init.py) read-only under `/opt/oac-e2b`, runs the daemon as UID/GID 1000 (`runtime`, home `/home/runtime`) and builds with 2 vCPUs and 2048 MiB. Only the `usr`, `usr/local` and `etc` archive ancestors it creates get traversable modes; Runtime file modes, private build contexts, key inputs and the output's umask stay unchanged.
[`build-template.py`](build-template.py) requires a `sha256:` image ID, a Linux amd64 image and the Runtime layout (`OAC_RUNTIME_WORKSPACE=/environment/workspace`). It copies the image's `/usr/local/bin`, `/opt` and, when present, `/usr/local/codex-resources` and `/etc/codex`, together with its `HOME` and `OAC_*` environment, onto a digest-pinned `node:22.23.1-bookworm-slim` base with `ca-certificates`, `bash`, `git`, `python3`, `python3-pip`, `ripgrep` and `util-linux`. It installs [`init.py`](init.py) and [`managed_init.py`](managed_init.py) read-only under `/opt/oac-e2b`, runs the daemon as UID/GID 1000 (`runtime`, home `/home/runtime`) and defaults to 2 vCPUs and 2048 MiB. Select `--compute-profile 2c8g` for 2 vCPUs and 8192 MiB or `--compute-profile 4c8g` for 4 vCPUs and 8192 MiB; only these profiles and the default `2c2g` are accepted. Only the `usr`, `usr/local` and `etc` archive ancestors it creates get traversable modes; Runtime file modes, private build contexts, key inputs and the output's umask stay unchanged.

The output file records `template` (the immutable `templateID:build_UUID`), the source `image`, the packaged `runtime_sha256` and the `base`. Use that exact `template` value. The template must qualify every Harness its image advertises. Install system dependencies into the image; the daemon runs as UID/GID 1000. No E2B account key, executor key or model credential belongs in a build, template environment, metadata, command argument or log.
The output file records `template` (the immutable `templateID:build_UUID`), the source `image`, the packaged `runtime_sha256`, the `base`, `compute_profile`, `cpu_count` and `memory_mb`. Use that exact `template` value. The template must qualify every Harness its image advertises. Install system dependencies into the image; the daemon runs as UID/GID 1000. No E2B account key, executor key or model credential belongs in a build, template environment, metadata, command argument or log.

The `e2b-template-build` workflow exposes the same `compute_profile` choices and keeps `2c2g` as its default. The default omits the optional builder flag, allowing an exact historical source revision to retain its default allocation; 8 GiB selections require a source revision supporting `--compute-profile` and never fall back. It verifies the registered build’s CPU and memory before uploading, including when `resume_build` is supplied; missing or mismatched resource metadata stops the build. The build receipt records the verified allocation, and the final report takes its CPU and memory from that receipt after checking the immutable template identity. Building a separate 8 GiB template does not change the deployment’s default template.

## Launch an application-managed Runtime

Expand Down
8 changes: 6 additions & 2 deletions services/core/deploy/e2b/build-template.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@
from e2b import Template

BASE = 'node:22.23.1-bookworm-slim@sha256:8607a9064d4a571140998ae9e52a3b3fcf9cff361d04642d5971e6cd76d39e27'
COMPUTE_PROFILES = {'2c2g': (2, 2048), '2c8g': (2, 8192), '4c8g': (4, 8192)}
parser = argparse.ArgumentParser()
parser.add_argument('--image', required=True, help='Qualified linux/amd64 Runtime image digest')
parser.add_argument('--name', required=True)
parser.add_argument('--api-key-file', type=Path, required=True)
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--compute-profile', choices=COMPUTE_PROFILES, default='2c2g')
args = parser.parse_args()
cpu_count, memory_mb = COMPUTE_PROFILES[args.compute_profile]
if not args.image.startswith('sha256:') or not args.output.is_absolute() or not args.api_key_file.is_absolute():
parser.error('Use an image digest and absolute private key/output paths')
image = json.loads(subprocess.check_output(['docker', 'image', 'inspect', args.image]))[0]
Expand Down Expand Up @@ -88,12 +91,13 @@
# Reinstall their system dependencies in E2B using the image's own installer.
if creative_tools:
template = template.run_cmd('sh /opt/oac-creative/install.sh --system-only', user='root')
result = Template.build(template, name=args.name, cpu_count=2, memory_mb=2048,
result = Template.build(template, name=args.name, cpu_count=cpu_count, memory_mb=memory_mb,
on_build_logs=lambda entry: print(entry.message, flush=True),
api_key=args.api_key_file.read_text().strip())
report = {'template': result.template_id + ':' + result.build_id, 'image': image['Id'],
'runtime_sha256': hashlib.sha256(bundle.read_bytes()).hexdigest(), 'base': BASE,
'creative_tools': creative_tools}
'creative_tools': creative_tools, 'compute_profile': args.compute_profile,
'cpu_count': cpu_count, 'memory_mb': memory_mb}
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(report, indent=2) + '\n')
print(json.dumps(report))
Loading
Loading