feat(sandbox): GCP Agent Platform sandbox backend, replacing Cloud Run - #478
Open
ItamarZand88 wants to merge 21 commits into
Open
feat(sandbox): GCP Agent Platform sandbox backend, replacing Cloud Run#478ItamarZand88 wants to merge 21 commits into
ItamarZand88 wants to merge 21 commits into
Conversation
Add a gcp block to each of the four sandbox permission sets. The split keeps the one content-reaching verb, sandboxEnvironments.execute, in execute alone; heartbeat, management and provision never reach a live session. A test pins that execute appears in sandbox/execute and no other set, and the sensitive-content invariant covers GCP alongside AWS and Azure. The preview sandboxEnvironment(s) permissions are absent from the upstream IAM dataset, so the validation test allowlists them exactly while leaving the published reasoningEngines permissions gated.
The GCP Agent Platform proxies its :execute call to POST / with the request body verbatim and can set neither path nor method, so the per-operation /v1/* routes are unreachable there. Add a single POST / endpoint whose required `op` selects exec, readFile, writeFile, mkdir or health, and whose `v` is reconciled before dispatch. Each arm hands off to the matching /v1/* handler, so the response — the exec NDJSON stream included — is byte-identical and authorization stays that handler's job. An absent or unknown op and an unsupported version are refused with a typed error rather than defaulted. Additive: /v1/* is unchanged, so AWS, Kubernetes and Local are unaffected.
Two backends that report the same supervisorPidNamespace can still differ on whether the process supervising a command is a separate identity from it, and that difference is a security property. Publish it as its own bit so a caller comparing capability sets is not told they are equivalent.
Client for the Vertex AI Agent Platform sandbox REST API: engines, templates (immutable config), sandboxes, execute, and the pause/resume/ snapshot transitions, over the regional aiplatform host under a parent reasoning engine. Retry classification is the load-bearing part. create/execute/pause/ resume/snapshot deliver exactly once through a new single-attempt transport path, so a network hiccup surfaces to the caller instead of minting an orphan or repeating a state transition; get/list retry; delete retries and treats a not-found as done. Execute request bodies are redacted out of any error chain, and long-running operations are polled within a bounded budget that reports the last error rather than a bare timeout.
A single proxied call to POST / stays open only ~30s regardless of the deadline asked for, so a command that runs longer cannot answer in one call. Add jobStart/jobPoll/jobCancel — as /v1 routes and as envelope ops — so a command runs detached under the agent's own deadline while its output is polled across as many short calls as it takes. jobPoll returns frames strictly after a caller-supplied sequence so a retried poll neither duplicates nor drops output. jobCancel reuses the existing process-group kill path by dropping the collector's receiver. Retention is bounded: finished jobs are kept for replay until evicted under a per-session cap or the session ends. The synchronous exec path is untouched; choosing job vs exec is the provider's call, not the agent's.
Introduce the GcpAgentPlatform sandbox binding — engine, template and region reach a durable Agent Engine and its regional endpoint, with an optional session ttl. The three required fields carry no serde default: the template is egress-bearing, so a binding missing one must fail to load rather than deserialize into a session with no image, no limits and open egress. A test pins that, and that an absent ttl still parses. Publish the Agent Platform capability row as gcp_agent_platform(), asserted value by value against measured behaviour. It is deliberately not returned by for_platform(Platform::Gcp) yet, which still reports the registered Cloud Run backend; it becomes that arm's body once the backend is swapped in. reconnect stays false until a stable session generation is wired, and the test that asserts it is the tripwire that forces both to flip together.
Implement the Sandbox trait against Agent Platform sessions over the :execute envelope: exec under the proxy window and detached jobs above it, files and health as envelope ops, lifecycle as polled operations. Compiled and unit-tested against a mocked client but left out of the provider factory, so no declaration can select it until the cutover.
…t id A GCP sandbox can report STATE_RUNNING while its container has been silently replaced under a stable session name, so resource state is not health and a session id alone cannot tell a caller's container from a blank one wearing the same name. The agent's health op now returns the container's kernel boot id, read from /proc/sys/kernel/random/boot_id, which is stable across calls and processes on one kernel and changes when the container is replaced. The GCP provider derives a numeric generation from it with a deterministic FNV-1a hash, so a caller comparing generations across two get()s detects a replacement even across processes. A health reply that omits or empties the boot id is refused rather than reconnected to a container of unknown identity, and the probe is bounded by a named budget so a wedged agent cannot hang get() or create(). With identity wired, the Agent Platform capability row flips reconnect to true, and its tripwire test asserts the new value against the wiring.
…oller The Agent Engine is an empty Frozen parent; the SandboxEnvironmentTemplate carries the image digest, ceilings and egress and warms the session pool, so it is Live and release-owned. Template config is immutable — there is no update verb — so reconciliation is replace-not-update: create the new template, wait for it to reach ACTIVE, and only then reap the old one, so a release never leaves a session pointing at a template that has already been deleted. Add list_templates to the client so the reaper can find superseded templates, and give SandboxEgress::internet_access_switch a single home for the mode->bool mapping so the provider, the controller and the emitter cannot disagree on what a mode means. AllowDomains has no switch position and is refused, naming the sandbox and both accepted modes, at plan time in the emitter and at build time in the controller. The emitter and controller stay unregistered; the registered GCP sandbox backend is still Cloud Run until the cutover moves the registration.
A mocked test can only confirm the request we chose to send. These live tests drive the client and provider directly against a real project — the emitter and controller are unregistered, so a deployed stack is not yet a path — covering create, cross-process reconnect, the detached path past the ~30s execute cap, suspend/resume, egress-deny, and snapshot restore, each with teardown that leaves nothing behind and a sweep for orphans a failed run left. They are #[ignore]d and need credentials, so CI does not run them. Two unit gaps are closed alongside: a provider test pinning a failing mutating :execute as delivered once while a read still retries, and a job-poll test proving overlapping sinceSeq windows rebuild the stream with no duplication and no gap.
…kend Where a backend runs a command under the agent's own user, the command can read the supervisor's environment and signal it, so the container — not the supervisor — is the isolation boundary. Say that in the customer-facing sandbox doc rather than leaving a caller to infer it from `supervisorIsolation: false`, and record at the template's container spec why no env is set there: a secret placed in it would be readable by the very code the agent supervises.
…uid-split The agent refused to start whenever the command's uid equalled its own, which is right where the agent can drop to a separate uid but impossible where the platform runs everything as one user — a container that allows no other uid, or a pod that has dropped every capability. Such a backend could never boot the agent at all. Take the model as a declared value, ALIEN_SANDBOX_ISOLATION = uid-split | platform, required with no default because it selects a security model. uid-split keeps today's refusal; platform accepts the shared uid because the container is then the only boundary. Root and the root group are refused in both — the concession is the shared uid, never root. The AWS image declares uid-split, the model it already runs.
…missed The live sweep could only delete engines a run had already recorded, so an engine killed between creation and its record was invisible to it. Add a paginating list of reasoning engines and back the sweep with it, reaping every engine that carries the suite's display-name prefix and nothing else.
Without one, a single stalled request hangs the whole live test indefinitely — the poll budget bounds the number of attempts, not the wait inside one call, so a create or execute that never returns is never abandoned. Set a 90s per-request timeout, above the ~30s :execute proxy window, so a stall fails the test rather than wedging it.
A tokio TcpListener stops accepting after the GCP sandbox platform detaches and reattaches the data plane on pause/resume: the process stays alive and the socket stays in LISTEN, but the reactor's epoll registration for the listen socket goes stale across the reattach and nothing is ever served again. A plain blocking accept() re-wakes on the next connection regardless, which is why a blocking server survives the same transition. Serve behind a custom axum Listener whose accept blocks in the syscall on a worker thread; per-connection IO stays a fresh tokio stream, so only the long-lived listen socket needs this. Measured against the live backend: without it a resumed sandbox is unreachable indefinitely; with it, reachable at once.
Two AgentState literals behind `#[cfg(target_os = "linux")]` never got the `jobs` field, so the crate's test build did not compile on Linux — invisible on a macOS dev machine, where those tests are gated out. Add it so the Linux test build works.
open_beneath resolved a caller's path with openat2 (RESOLVE_BENEATH, RESOLVE_NO_SYMLINKS), which the gVisor runtime under GCP Agent Platform does not implement: every file operation returned ENOSYS, so readFile, writeFile and mkdir all failed there. Walk the path one component at a time instead, each openat taken relative to the previous component's descriptor and every step O_NOFOLLOW. The inode a step opens is the inode the previous step checked, a symlink or magic link at any component fails the step, and `.`/`..` are refused — so nothing can leave the root and there is no name left to re-resolve. That is what openat2 gave in one syscall, done in userspace so it holds on every runtime. Measured on GCP Agent Platform: mkdir, writeFile and readFile round-trip and a symlink escape is refused; the confine unit tests pass on a real kernel; the AWS and Local paths keep the same behaviour.
The live tests wrote markers with a command (`> /tmp/x`, the real /tmp) but read them back through the agent file API, which confines to the session root (/sandbox) — so the read looked at /sandbox/tmp/x and never found them. Write and read the same session-root path instead. Suspend/resume: assert the load-bearing property — the filesystem survives — and observe the generation rather than requiring it unchanged, since resume may return onto a reissued container whose new boot id the generation exists to surface. Teardown reaps a session's child sandboxes before its engine and retries, because an engine will not delete while a sandbox remains and a panicked test leaves one behind.
…andbox The GCP Agent Platform sandbox hangs every template and session under a reasoning engine that nothing created — Vertex exposes no Terraform resource for it, so a controller must create it through the API. Add the engine as a Live, Alien-owned resource: a preflight mutation synthesizes one engine per sandbox and makes the sandbox depend on it (so the engine deploys first and tears down last), and a controller creates it, records the server-assigned id, and deletes it at teardown. The template controller reads that id from the engine dependency instead of fabricating one. Additive: the mutation and the template controller stay unregistered until the Cloud Run backend is removed, so nothing reaches this path yet.
…oud Run Register the Gemini Agent Platform provider, emitter, sandbox controller and engine mutation as the GCP sandbox backend, and delete the Cloud Run implementation. The runtime loader builds an Agent Platform client against the binding's own region and creates sessions under the reasoning engine; the capability table for GCP now reports the Agent Platform row (reconnect, suspend/resume, snapshot, enforced limits); and a sandbox stack enables aiplatform at deploy. Removed: the Cloud Run sandbox provider and binding, its Terraform emitter, the launcher preflight mutation and sandbox-host-required check, the Worker.sandboxLauncher field, and the GcpSandbox import type. The shared Cloud Run worker client stays. No compatibility path: the sandbox feature has no users yet, so the binding changes from sandbox-gcp to sandbox-gcp-agent-platform with no migration.
Greptile SummaryThe PR replaces the GCP Cloud Run sandbox launcher with a Vertex AI Agent Platform backend and adds the associated provisioning, lifecycle, execution, permission, and binding support.
Confidence Score: 5/5The PR appears safe to merge because the previously reported job-result eviction and failed-resume-rollback failures no longer remain. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| crates/alien-sandbox-agent/src/jobs.rs | Adds bounded detached-job execution and now preserves unread terminal outcomes until a poll has served them. |
| crates/alien-bindings/src/providers/sandbox/gcp_agent_platform.rs | Implements Agent Platform sandbox lifecycle and execution, including a corrected rollback path that refuses replacement creation when a resumed session cannot be re-suspended. |
| crates/alien-gcp-clients/src/gcp/agent_platform.rs | Adds the typed GCP client operations used for engines, templates, sessions, proxy execution, and lifecycle polling. |
| crates/alien-infra/src/sandbox/gcp_agent_platform_engine.rs | Adds reconciliation for durable reasoning-engine creation, persisted identity, and teardown. |
| crates/alien-infra/src/sandbox/gcp_agent_platform_template.rs | Adds reconciliation of immutable Agent Platform sandbox templates and their readiness lifecycle. |
Sequence Diagram
sequenceDiagram
participant W as Worker binding
participant P as GCP sandbox provider
participant V as Vertex Agent Platform
participant A as Sandbox agent
W->>P: Create session
P->>V: Create sandboxEnvironment
V-->>P: Long-running operation
P->>V: Poll until ready
P->>A: Health via :execute
A-->>P: Protocol version and boot ID
P-->>W: Running session
W->>P: Run command
alt Short deadline
P->>A: exec via :execute
A-->>P: Buffered output frames and outcome
else Long deadline
P->>A: jobStart via :execute
loop Until terminal outcome
P->>A: jobPoll via :execute
A-->>P: New frames and job state
end
end
P-->>W: Command output stream
Reviews (2): Last reviewed commit: "fix(sandbox): retain unread job results ..." | Re-trigger Greptile
…back Two review findings on the GCP Agent Platform sandbox: The job cap evicted the oldest finished job whether or not its terminal result had been read, so a poll racing a new command's start could get JobNotFound and lose the real exit code. Only evict a finished job whose outcome a poll has already returned; refuse a new start otherwise. A get_or_create that woke a suspended session, found it unhealthy, and could not re-suspend it only logged and fell through to create a replacement — leaving the woken session live with no id handed back. Fail instead, naming the session so it stays identifiable.
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.
Summary
Adds the GCP sandbox backend on Gemini Agent Platform — sessions are
sandboxEnvironmentscreated under a durable reasoning engine and reached through the
:executeproxy — and cutsGCP over to it, removing the Cloud Run launcher implementation entirely. The backend carries
files, egress-deny, suspend/resume, snapshot, and enforced-limits; the Cloud Run subprocess
model (and its binding, launcher preflight, and
Worker.sandboxLauncher) is gone.When a GCP sandbox runs, this is what happens:
creates the engine through the Vertex API and records the server-assigned id.
SandboxEnvironmentTemplateunder that engine(image, ceilings, egress switch) and waits for it to become
ACTIVE.Platform client against the binding's own region.
sandboxEnvironmentunder the engine, and every command, fileoperation, and health check flows as one envelope over the
:executeproxy. ← the heartthen the template, then the engine, in dependency order.
This moves the GCP sandbox backend from a Cloud Run subprocess launcher to a durable Agent
Platform resource.
What I added
POST /, detached pollable jobs for longcommands, and a declared isolation model.
gcp-clientsAgent Platform client (engines, templates, sessions,:execute, lifecycle).GcpAgentPlatformEngineresource: a preflight mutation + a state-machine controller thatcreates the reasoning engine, persists its id, and deletes it at teardown; the template
controller reads that id as a dependency.
aiplatformpermission grants (provision vs management split).What I removed (the cutover)
The Cloud Run sandbox provider, binding (
sandbox-gcp→sandbox-gcp-agent-platform), Terraformemitter, launcher preflight mutation, host-required check,
Worker.sandboxLauncher, and theGcpSandboxImportDatatype. The shared Cloud Run worker client stays. No compatibility path —the sandbox feature has no users yet.
How I tested
alien-bindings, alien-gcp-clients — green (the live-GCP Firestore/KV
case_4_gcptests failonly on the shared-project database quota, which is environmental).
serialize↔deserialize round-trip guard test for both new controllers (the executor reloads
controller state between reconciles; a missing by-tag arm fails above the handler layer).
snapshot, egress-deny), and the shared agent changes validated on a real AWS Firecracker MicroVM.
Security review for this diff:
denysets the template's internet-access switch off;AllowDomainsis refused at plan time and again in the controller, never coerced to a boolean.
reasoningEnginescreate/delete +sandboxEnvironmentTemplatesCRUD with no
sandboxEnvironmentsverb, so provisioning cannot reach a live session; theper-session verbs live in the management set. Validated against the GCP IAM permission dataset.
deployment's, so a session is signed against the correct regional endpoint.
the command shares the agent's uid and could read anything placed in the container env.
create_engine/create_templateare single-attempt; a lost responsefails to
CreateFailedrather than creating a duplicate.Breaking changes
sandbox-gcp→sandbox-gcp-agent-platform); analready-deployed GCP sandbox stack must re-provision. No users yet, so no migration.
Worker.sandboxLauncheris removed from the public@alienplatform/coreWorkertype.🤖 Generated with Claude Code