From 26254246a3529d468e420c488a87760ffda10175 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 4 Oct 2026 13:02:20 +0800 Subject: [PATCH] Fix E2B template chunk upload --- .github/workflows/e2b-template.yml | 86 ++++++++++++++++++++++++++++-- deploy/e2b/README.md | 8 ++- 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/.github/workflows/e2b-template.yml b/.github/workflows/e2b-template.yml index 44fce7615..dbf15c0d2 100644 --- a/.github/workflows/e2b-template.yml +++ b/.github/workflows/e2b-template.yml @@ -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 @@ -63,6 +68,7 @@ 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 @@ -70,6 +76,10 @@ jobs: 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" || { @@ -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: | @@ -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 }} @@ -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 diff --git a/deploy/e2b/README.md b/deploy/e2b/README.md index 0da9f2e1f..8e675dc1e 100644 --- a/deploy/e2b/README.md +++ b/deploy/e2b/README.md @@ -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.