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
86 changes: 82 additions & 4 deletions .github/workflows/e2b-template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ on:
type: string
required: false

resume_build:
description: Existing unsubmitted templateID:build_UUID after an upload failure; requires ref
type: string
required: false

permissions:
contents: read

Expand Down Expand Up @@ -63,13 +68,18 @@ jobs:
env:
SOURCE_BRANCH: ${{ inputs.source_branch }}
REQUESTED_REF: ${{ inputs.ref }}
RESUME_BUILD: ${{ inputs.resume_build }}
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
if [ -n "$RESUME_BUILD" ]; then
[ -n "$REQUESTED_REF" ] || { echo '::error::Recovery requires the original source SHA.'; exit 1; }
[[ "$RESUME_BUILD" =~ ^[a-zA-Z0-9_-]+:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]] || 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" || {
Expand Down Expand Up @@ -147,6 +157,8 @@ jobs:
printf '%s\n' "$image" > "$output_root/runtime-image.id"
- name: Build one immutable E2B template
env:
RESUME_BUILD: ${{ inputs.resume_build }}
SOURCE_COMMIT: ${{ steps.source.outputs.revision }}
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
E2B_API_URL: ${{ vars.E2B_API_URL }}
run: |
Expand All @@ -157,10 +169,75 @@ jobs:
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 \
"$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"
--api-key-file "$key" --output "$RUNNER_TEMP/e2b-result/template.json" <<'PY'
import hashlib, json, os, pathlib, runpy, sys
from types import SimpleNamespace
from e2b.template.main import TemplateBuilder
import e2b.template_sync.main as sdk

output = pathlib.Path(os.environ['RUNNER_TEMP']) / 'e2b-result'
original_copy = TemplateBuilder.copy
def copy(self, src, dest, *args, **kwargs):
if str(src) != 'runtime.tar.gz':
return original_copy(self, src, dest, *args, **kwargs)
if dest != '/root/runtime.tar.gz' or args or kwargs != {'user': 'root'}:
raise RuntimeError('Runtime archive copy contract changed')
context = pathlib.Path(self._template._file_context_path)
digest = hashlib.sha256()
parts = []
with (context / src).open('rb') as stream:
for index in range(256):
block = stream.read(32 * 1024 * 1024)
if not block:
break
digest.update(block)
name = f'runtime.part{index:04d}'
(context / name).write_bytes(block)
self = original_copy(self, name, '/root/' + name, user='root')
parts.append('/root/' + name)
if stream.read(1):
raise RuntimeError('Runtime archive exceeds 8 GiB')
if not parts:
raise RuntimeError('Runtime archive is empty')
(output / 'upload.json').write_text(json.dumps({
'chunks': len(parts), 'chunk_bytes': 32 * 1024 * 1024,
'runtime_sha256': digest.hexdigest()}, indent=2) + '\n')
command = ('cat ' + ' '.join(parts) + ' > /root/runtime.tar.gz && '
'echo "' + digest.hexdigest() + ' /root/runtime.tar.gz" | sha256sum -c - && '
'rm ' + ' '.join(parts))
return self.run_cmd(command, user='root')
TemplateBuilder.copy = copy

original_request = sdk.request_build
def request(client, **kwargs):
selector = os.environ.get('RESUME_BUILD', '')
if selector:
template_id, build_id = selector.split(':')
# Inspect the raw response before the SDK's typed parser.
response = client.get_httpx_client().get(
f'/templates/{template_id}/builds/{build_id}/status')
response.raise_for_status()
state = response.json()
if (state.get('templateID') != template_id or state.get('buildID') != build_id
or state.get('status') != 'waiting' or state.get('logEntries')
or state.get('logs')):
raise RuntimeError('Recovery requires an unsubmitted waiting build with no logs')
result = SimpleNamespace(template_id=template_id, build_id=build_id, tags=[])
else:
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)}
(output / 'build-request.json').write_text(json.dumps(receipt, indent=2) + '\n')
return result
sdk.request_build = request
builder = sys.argv[1]
sys.argv = sys.argv[1:]
runpy.run_path(builder, run_name='__main__')
PY
- name: Record the build identity
env:
SOURCE_COMMIT: ${{ steps.source.outputs.revision }}
Expand All @@ -180,8 +257,9 @@ jobs:
out.write('One combined Runtime template built. No Core configuration or production deployment was changed.\n')
PY
- uses: actions/upload-artifact@v6
if: always()
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
path: ${{ runner.temp }}/e2b-result/*.json
if-no-files-found: warn
retention-days: 90
8 changes: 7 additions & 1 deletion deploy/e2b/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ Both are required. For the SandBase endpoint, set `E2B_API_URL` to `https://sand

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.

## Bounded archive uploads

The workflow splits the Runtime archive into files of at most 32 MiB and uploads each through the SDK's standard template file-copy API. These are independent files, not a multipart upload extension. The template concatenates them in order, verifies the complete archive's SHA-256, then removes the parts before the maintained extraction step. It still produces one template with unchanged Runtime contents. Archives are bounded to 8 GiB.

For an upload failure before build submission, inspect the original run's logs and use `resume_build` with its `templateID:build_UUID` and the same explicit source SHA in `ref`. Recovery checks that the authenticated build is still `waiting` with no logs before reusing its identifiers. Do not use recovery after build submission or an ambiguous request outcome. All other runs create a new template build.

## 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 successful 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. The artifact also retains `upload.json` (chunk count and archive checksum) and `build-request.json` (cloud identifiers and source revision) when those stages are reached, including on failure. 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