Skip to content

feat(sandbox): grant a GCP sandbox read on the registry it pulls from - #663

Merged
ItamarZand88 merged 4 commits into
mainfrom
itamar/alien-844-grant-a-customers-vertex-sandbox-service-agent-read-on-the
Sep 20, 2026
Merged

ItamarZand88 merged 4 commits into
mainfrom
itamar/alien-844-grant-a-customers-vertex-sandbox-service-agent-read-on-the

Conversation

@ItamarZand88

Copy link
Copy Markdown
Contributor

Summary

A GCP sandbox runs a container it pulls from the registry Alien hosts, but the
grant machinery counted a sandbox only on AWS, so the image it needs was never
covered. This grants it, and makes the service types the grant and the revoke
name one definition instead of two.

When a deployment reconciles its registry access:

  1. The platform decides whether this deployment's sandbox pulls from Alien's
    registry at all. AWS builds a MicroVM image from a published bundle and GCP
    pulls a container, so both earn a grant; Azure names an image from its own
    catalog and earns nothing.
  2. Each compute service is granted as its own Google-managed service agent.
    A sandbox pulls as a different principal from a worker, so a grant made for
    one reaches neither the other.
  3. On teardown the last project consumer revokes the same list, because a member
    the revoke does not name stays on the repository policy with nothing left to
    remove it.

This changes a hardcoded "only AWS sandboxes pull from us" to a question asked
per platform.

What I did

Replaced the INCLUDE_SANDBOX / EXCLUDE_SANDBOX constant pair with a function
that answers per platform, and added a sandbox compute-service type mapped to the
Vertex sandbox service agent.

Moved the grant off the deployment's extra service-account emails, which was the
wrong place: those are revoked per deployment, so the first deployment deleted in
a project would strip a binding its siblings still need. The service-type list is
refcounted and survives until the last consumer goes.

Fixed a regression the widened guard introduced. That guard is deliberately
prefix-blind, so it now admits a GCP sandbox whose image Alien never hosted. Such
a deployment was never granted, and on a manager that hosts no images there is no
registry binding to load, so its delete reconcile started returning an error every
tick. It now answers that there is nothing to strand.

How I tested

cargo test -p alien-manager -p alien-bindings --lib — 239 and 338 pass, and
cargo check --workspace --all-targets is clean, on a branch rebased onto current
main.

Pinned the new behaviour rather than assuming it: a GCP sandbox-only stack is
covered by the grant, the shared project grant survives while any sibling
deployment is alive, and a never-granted GCP sandbox on a public image reaches
the binding lookup with nothing recorded to clean up.

Checked the attacks this change makes possible:

  • A tenant reading another tenant's images. Pre-existing and unchanged by this
    diff: GCP has one registry repository per environment, so the worker agent
    already holds this. Tracked separately; this adds a second principal inside the
    same customer project, which holds no capability the customer lacks today.
  • A grant outliving its deployment. The revoke names every service type, and
    removal is a retain, so naming one never granted is a no-op while omitting one
    would orphan a member (gar.rs:207).
  • One deployment's teardown stripping a live sibling's access. The project-level
    members are revoked only in the last-consumer branch, and the per-deployment
    identity stays per-deployment (registry_access.rs:92-152).
  • Granting to a principal that does not exist yet. The sandbox service agent is
    created at a project's first session, so the binding necessarily precedes it.
    The binding is accepted; whether it becomes effective when the agent appears is
    measured by the cross-project pull test, not claimed here.

Nothing turned up.

@greptile-apps

greptile-apps Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge; no new actionable failures were established in the changes since the previous review.

Summary

This PR extends cross-account registry access to GCP sandboxes and makes grant state explicitly describe the repositories and compute-service principals it covers.

  • Adds the GCP Vertex sandbox service agent to GAR grant, revoke, and permission-decoding paths.
  • Reconciles existing or incomplete grants when repository or service-type requirements change.
  • Preserves shared project access until the final GCP consumer is removed, then revokes every known compute-service principal.
  • Distinguishes absent registry configuration from temporarily unavailable bindings during deletion cleanup.
  • Adds focused coverage for sandbox image selection, grant migration, partial grants, decoding, and cleanup behavior.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Deployment reconcile] --> B{Uses Alien-hosted image?}
    B -- No --> C[No registry grant]
    B -- Yes --> D[Determine repositories and service types]
    D --> E{Recorded grant complete and current?}
    E -- Yes --> F[Skip policy rewrite]
    E -- No --> G[Grant worker and optional sandbox agents]
    G --> H[Persist repositories, service types, and completion marker]
    I[Deployment deletion] --> J[Load registry binding]
    J --> K{Other active GCP project consumer?}
    K -- Yes --> L[Remove deployment-owned identity only]
    K -- No --> M[Remove worker and sandbox project agents]
Loading

Reviews (6) · Last reviewed commit: "fix(sandbox): tell a registry that was n..."

Comment thread crates/alien-manager/src/registry_access.rs Outdated
Comment thread crates/alien-manager/src/registry_access.rs Outdated
Comment thread crates/alien-bindings/src/providers/artifact_registry/gar.rs Outdated
@ItamarZand88

Copy link
Copy Markdown
Contributor Author

Two fixed in 49a4fb2, one is real and tracked but out of scope here.

Cleanup Skips Live Grants — fixed, and it was my regression. You are right, and the comment six lines above the guard already said so: the marker is written after the cloud grant lands, so a false one still covers a live grant. My early return would have left registry read alive after the deployment was deleted. Reverted to failing loudly.

For context on what I was trying to fix: counting a GCP sandbox widened a prefix-blind guard, so a GCP sandbox whose image Alien never hosted now reaches the binding lookup and errors on a manager that hosts no images. That behaviour is pre-existing on AWS, where sandboxes were always counted, so GCP now simply matches it. Erroring loudly beats silently dropping a revoke.

Sandbox Permissions Decode Incorrectly — fixed. The read path now derives its service agents from the same table the write path uses, so a member decodes as the type that produced it. Added every_service_type_survives_a_write_then_read as the round-trip test you asked for.

Existing Grants Stay Incomplete — real, tracked, not fixed here. registry_access_granted is a one-shot latch with no false assignment anywhere in the tree, so this is broader than the sandbox type: no change to what a grant contains ever reaches an already-granted deployment. The fix is to record what was granted (a member-set fingerprint) rather than that something was, which is a runtime-metadata schema change that wants its own review. It is filed and raised to urgent.

Worth knowing alongside it: this binding only matters cross-project. Google grants the sandbox agent a project-scoped role that already carries artifactregistry.repositories.downloadArtifacts, so a same-project pull needs nothing from us. Whether a binding created before the agent exists becomes effective once it materialises is not yet measured, and that is the gating question for this feature rather than for this diff.

@ItamarZand88

Copy link
Copy Markdown
Contributor Author

@greptile review

Comment thread crates/alien-manager/src/registry_access.rs Outdated
@ItamarZand88

Copy link
Copy Markdown
Contributor Author

Both valid. Neither is fixed here, and here is why.

Issue 1 — the distinction you are asking for cannot be made at this call site. You are right that the path should tell an unpersisted partial grant apart from a deployment that could never have been granted. It cannot, because load_artifact_registry (registry_access.rs:661) reaches None through if let Ok(ar) = ..., so None means both no binding configured and binding failed to load. The marker does not break the tie either: it is written after the cloud grant lands, so a false one still covers a live grant.

That leaves two wrong answers, and I have now written both. Returning Ok on an unset marker was my previous commit, and you correctly called it a dropped revoke. Erroring is what is there now: loud, and wrong only for a deployment that could never have been granted on a manager with no registry binding at all.

I am keeping the loud one. A cleanup that retries is recoverable; a grant that outlives its deployment is not. The real fix is to stop discarding the reason one function up, which changes that signature for every caller, so it is filed separately rather than attempted as a third version of this guard inside a feature PR.

Issue 2 — real, filed, and broader than this diff. registry_access_granted has no false assignment anywhere in the tree, so it is not only the sandbox type that never reaches an already-granted deployment; no change to a grant ever does. The fix is to record what was granted rather than that something was. Filed as urgent.

One thing worth knowing about the blast radius of both: this binding only matters cross-project. Google grants the sandbox agent a project-scoped role that already carries artifactregistry.repositories.downloadArtifacts, so a same-project pull needs nothing from us.

@ItamarZand88

Copy link
Copy Markdown
Contributor Author

Ordering note: this waits on #675, which is at 5/5 and touches the same code in registry_access.rs.

#675 gives collect_image_repositories / repository_ids_for_access a project_id argument, binds a sandbox's grant to {prefix}-{project_id}, returns a refusal flag that keeps an incomplete grant from being marked done, and reworks the cleanup guard this PR's first finding is about.

Once #675 merges I'll rebase this branch onto it, port the per-platform sandbox question onto the new signatures, and fix the two findings here:

  1. a manager with no registry binding failing cleanup forever for a never-granted GCP sandbox on a public image;
  2. existing GCP deployments whose grant is already recorded never picking up the sandbox service agent.

A GCP sandbox runs a container it pulls from the registry Alien hosts, but
the grant counted a sandbox only on AWS, so the image it needs was never
covered.

Which field names that image now follows the platform. AWS builds a MicroVM
image from a published bundle, so `code.image` is the bundle and
`privateBaseImage` is what the build pulls; GCP pulls `code.image` itself;
Azure names an image from its own catalog and reads nothing of ours.

Each compute service is granted as its own Google-managed service agent,
since a sandbox pulls as a different principal from a worker and a grant
made for one reaches neither the other. The same list is revoked, because a
member the revoke does not name stays on the repository policy with nothing
left to remove it.

The grant also moves off the deployment's extra service-account emails,
which are revoked per deployment: the first deployment deleted in a project
would otherwise strip a binding its siblings still need. The service-type
list is refcounted and survives until the last consumer goes.
@ItamarZand88

Copy link
Copy Markdown
Contributor Author

Rebased onto main now that #675 has landed, and squashed to one commit.

The two designs had to merge rather than stack: #675 made a sandbox's registry reference privateBaseImage, which is AWS-only, while this PR needs a GCP sandbox counted through its own code.image. Which field names the image is now a per-platform question (sandbox_image_source): AWS reads privateBaseImage, GCP reads code.image, Azure reads neither. The project-bound grant, the refusal flag and the cleanup fallback from #675 are all kept.

alien: manager 247, bindings 339, cargo check --workspace --all-targets clean.

@greptile review

@ItamarZand88
ItamarZand88 force-pushed the itamar/alien-844-grant-a-customers-vertex-sandbox-service-agent-read-on-the branch from 49a4fb2 to c8801bf Compare September 20, 2026 16:33
@greptile-apps

This comment has been minimized.

Three gaps found in review, all on the AWS grant path.

A China registry answers on `.amazonaws.com.cn`. The parser stripped only
`.amazonaws.com`, leaving `.cn` on the region, so a valid image in a `cn-*`
region was refused and its grant never completed.

Cleanup read the stack to name the repository it must revoke, so a release
that removed the sandbox entirely left the grant with nothing naming it and
the delete failed forever. A sandbox is only ever granted its own project's
repository, so a recorded grant now revokes that repository without reading
the stack.

A deployment that never recorded a grant no longer fails its delete when no
registry binding can be loaded: the grant would have been made through that
same binding, so there is nothing to revoke and erroring only parks the
deployment at `deleting`.
…licy read honest

A cross-account grant is named by the project, not by the deployment that asked
for it, so the last deployment leaving a GCP project has to remove every
principal the project was granted — a worker-only deployment was removing only
Cloud Run's agent and leaving the sandbox agent reading Alien's registry with
nothing left to revoke it.

The grant and revoke paths also read an IAM policy before writing it back.
Reading any failure there as "no policy" wrote a policy holding one deployment's
members, with no etag to lose the race on, over one every deployment shares.
Only a repository that is gone answers that question now.

Alongside those: the delete revokes the union of what the record names and what
the stack derives, so a grant written before its state was saved is still
removed; a GCP grant that names no repository falls back to the registry prefix,
since GAR binds its policy there; and an empty project number no longer completes
a grant that reached no compute agent.
@ItamarZand88

Copy link
Copy Markdown
Contributor Author

@greptile review

… will not answer

A delete that cannot reach the registry has to keep failing, since the IAM write
lands before the state recording it and this is the last chance to revoke. A
manager that never had a registry binding is the opposite case: nothing was ever
granted through one, and failing forever would strand the deployment at
`deleting`.

The probe now reports which it found, reading `BindingNotConfigured` as the
contract for "no such binding". Without that split, a sandbox on a public image
under a manager with no registry could never finish deleting.
@ItamarZand88

Copy link
Copy Markdown
Contributor Author

@greptile review

@ItamarZand88
ItamarZand88 merged commit 23318e3 into main Sep 20, 2026
22 checks passed
@ItamarZand88
ItamarZand88 deleted the itamar/alien-844-grant-a-customers-vertex-sandbox-service-agent-read-on-the branch September 20, 2026 22:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant