Add Google Cloud Run sandbox integration#5055
Open
htahir1 wants to merge 10 commits into
Open
Conversation
Cloud Run sandboxes (public preview) provide fast, strongly isolated environments for executing untrusted code, but expose no standalone REST API: the sandbox CLI only exists inside a Cloud Run service deployed with --sandbox-launcher. This adds a "cloudrun" sandbox flavor to the GCP integration that talks to a small bridge service deployed on such a service, following the bridge pattern established by the Cloudflare sandbox flavor. The client authenticates with Google-signed ID tokens (Cloud Run IAM invoker flow) derived from the standard GCP credential chain (service connector, SA key file, or ADC). Unlike the Cloudflare flavor it supports per-exec env vars (native `sandbox exec -e`) and filesystem snapshots to GCS via `sandbox tar`, giving create_snapshot()/restore() parity with Modal. The deployable bridge (sync stdlib HTTP server shelling out to the sandbox CLI) ships as examples/cloudrun_sandbox_bridge/. Design notes in CLOUDRUN_SANDBOX_SPEC.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| ) | ||
| with _sandboxes_lock: | ||
| _sandboxes.pop(sandbox_id, None) | ||
| shutil.rmtree(_share_dir(sandbox_id), ignore_errors=True) |
| data = self._read_body() | ||
| staging_name = f"put-{uuid.uuid4().hex}" | ||
| staging_host = os.path.join(_share_dir(sandbox_id), staging_name) | ||
| with open(staging_host, "wb") as f: |
| f"{shlex.quote(dest)}", | ||
| ) | ||
| finally: | ||
| os.unlink(staging_host) |
| f"{shlex.quote(f'{SHARE_MOUNT}/{staging_name}')}", | ||
| ) | ||
| try: | ||
| size = os.path.getsize(staging_host) |
| self.send_header("Content-Type", "application/octet-stream") | ||
| self.send_header("Content-Length", str(size)) | ||
| self.end_headers() | ||
| with open(staging_host, "rb") as f: |
| with open(staging_host, "rb") as f: | ||
| shutil.copyfileobj(f, self.wfile) | ||
| finally: | ||
| os.unlink(staging_host) |
Contributor
Documentation Link Check Results✅ Absolute links check passed |
A linked GCP service connector using the user-account, oauth2-token, or external-account auth methods returns credentials with no ID-token counterpart. Previously these fell through to the ambient-ADC fallback, which would silently authenticate to the bridge as a different identity than the connector the user configured. Raise an actionable error instead, and keep the ADC fallback only for the no-connector path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reuse: retry backoff now uses exponential_backoff_delays (adds jitter, avoiding retry lockstep across concurrent steps), utc_now, and the existing GCP_PATH_PREFIX constant. Efficiency: token refresh no longer holds its lock on the fast path or rebuilds the google transport per refresh; finished processes are pruned from long-lived sessions; newline-free exec output is flushed past 1 MiB instead of growing the remainder unboundedly; plain bridge calls get a real read timeout (only SSE streams disable it); the bridge example reuses one GCS client, streams uploads to disk, and size-checks downloads inside the sandbox before copying. Simplification: single backoff site in the retry loop, snapshot no longer round-trips an echoed URI, exit frames dispatch on event kind, dead TYPE_CHECKING/dashboard-URL/regex code removed, duplicate 32 MiB check collapsed into the upload path, and a Protocol now types the ID-token credential contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The durable content lives in the docs page, module docstrings, and the bridge README. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The /tmp share-root default and 0.0.0.0 bind in the bridge are both deliberate on Cloud Run (per-instance in-memory /tmp; containers must listen on all interfaces behind IAM), so mark them nosec with the reasoning. Test fixtures use /workspace instead of /tmp. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Critical/high: - Bridge file staging now opens host-side descriptors with O_NOFOLLOW (and O_EXCL on upload) and validates a regular file via fstat, so untrusted sandbox code can't redirect a transfer to a host path by planting a symlink at the staged name. - process.kill() now terminates the remote command: exec carries a client-generated exec id the bridge registers, and kill() calls a new DELETE /v1/sandbox/<id>/exec/<exec-id> endpoint instead of only closing the local SSE stream. - SSE buffers are bounded by total bytes, not line count, and the newline-free flush path also respects the cap, so a flood of long or newline-free output can't OOM the caller. - Sandbox ids are client-generated and bridge creation is idempotent, so a create whose response is lost is cleaned up (best-effort delete) instead of orphaning a sleep-infinity process. - Fail loud when an explicit service_account_path (not only a connector) yields credentials that can't mint ID tokens, instead of silently falling back to ambient ADC. Medium: - Rebuild the bridge client (and its ID-token credentials) once the linked service connector has expired. - Snapshot object names include a uuid so two snapshots within one second don't overwrite each other. - Bridge uses the documented --env/--workdir CLI flags; README flags the preview CLI surface for pre-release verification. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The base SandboxSession contract documents, for all flavors, that a
relative `remote_path` in `upload_file`/`download_file` resolves against
the session's working directory — the same directory `exec()` uses as its
default cwd. The Cloud Run flavor diverged: `_exec` honored
`default_cwd`/`settings.cwd`, but file transfer anchored relative paths at
the sandbox root, so `exec("cat foo.txt")` and `upload_file(src, "foo.txt")`
disagreed about where "foo.txt" lived whenever a cwd was configured.
Resolve relative remote paths against the session's default cwd before the
existing sanitization runs, mirroring exec. Absolute paths are unchanged and
the `..`-escape guard still refuses paths that climb above the cwd.
Also tighten the per-exec env docstring (values are hidden from the sandboxed
process's command line but visible in the trusted bridge host's process list)
and document the missing per-task image support in the docs Limitations.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bandit's B108 flags hardcoded /tmp paths; the literals here are example remote sandbox paths, not real temp-file usage, so switch the example to /srv/workspace instead of suppressing the check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Adds a
cloudrunsandbox flavor to the GCP integration, backed by Cloud Run sandboxes (public preview) — Google's new fast, strongly isolated environments for executing untrusted (e.g. LLM-generated) code.Why a bridge?
Cloud Run sandboxes have no standalone REST API: the
sandboxCLI that drives them only exists inside a Cloud Run service deployed with--sandbox-launcher. So this follows the bridge pattern established by the Cloudflare sandbox flavor (#4907): a small HTTP service deployed once to Cloud Run mediates between the ZenML client and the localsandboxCLI.The deployable bridge (sync stdlib HTTP server, ~450 lines, speaks the same wire protocol v1 as the Cloudflare bridge Worker) ships as
examples/cloudrun_sandbox_bridge/with a Dockerfile and one-command deploy instructions.Key implementation decisions
gcpintegration (no new deps —httpxis core, google libs already required). Flavor namecloudrun, mirroring thevertexnaming precedent.GoogleCredentialsMixinchain (GCP service connector → SA key file → ADC), with per-credential-type ID-token minting and refresh. The bridge itself contains no auth code — Cloud Run verifies callers before requests reach it.sandbox exec -e), lifting the Cloudflare flavor'sNotImplementedErrorlimitation.create_snapshot()exports the filesystem overlay viasandbox tarto a configured GCS prefix;restore()boots a new sandbox from the tarball — parity with Modal, which Cloudflare lacks. Gated onsnapshot_uri_prefix.docs/book/component-guide/sandboxes/cloudrun.md), the module docstrings, and the bridge README.Testing
httpx.MockTransport, no real bridge/credentials): flavor metadata, config validation, SSE parsing, retry policy, process demux, session lifecycle, env merging, snapshot gating. All pass; base sandbox suite unaffected (124 passed total).ruff,mypy, andpydoclintclean on the new files.Reviewer attention
CloudflareSandboxProcess— a future refactor could hoist it into a shared bridge-client base once both flavors are in.create_sessionthreads the newerdestroy_on_exitparameter fromdevelop'sBaseSandboxsignature (the Cloudflare branch predates it).🤖 Generated with Claude Code