Skip to content

feat(sandbox): task-scoped cloud credential so secret-rotation can run sandboxed - #30

Merged
isadominguez314 merged 3 commits into
integrationfrom
sandbox/agy-secret-rotation-cloud-cred
Sep 11, 2026
Merged

isadominguez314 merged 3 commits into
integrationfrom
sandbox/agy-secret-rotation-cloud-cred

Conversation

@pradeepvrd

Copy link
Copy Markdown
Owner

Stacked on #28 (sandbox/agy-model-ids): the first commit here is #28's; review only the top commit. Rebase/retarget after #28 lands.

The gap

secret-rotation is exempted from the sandbox because rotating the credential is a Secret Manager write, and a sandboxed agent has no cloud identity to make it with: the boundary strips CLOUDSDK_CONFIG/GOOGLE_APPLICATION_CREDENTIALS by design, the metadata endpoint is blocked on the bastion, and the org's disableServiceAccountKeyCreation constraint means there is no key file to mount. The scoped-ServiceAccount kubeconfig covers kubectl, but not the cloud API call the task is actually about.

What lands

Stack side. The secret-rotation stack provisions a run-unique rot-<ns>-<hex> service account holding exactly two roles — secretmanager.secretVersionManager and secretmanager.secretAccessor — scoped to exactly this run's secret, and names it in a new agent_cloud_identity output. The provisioning identity is granted iam.serviceAccountTokenCreator on that account in the same stack (an SA-level binding, not a shared project-level one, so concurrent runs cannot fight over it at teardown), and the whole loop tears down with the run.

Harness side. A new Provider.sandbox_cloud_credential_env seam (default: nothing crosses). The GCP provider reads the output, impersonates the account host-side via gcloud auth print-access-token, and returns CLOUDSDK_AUTH_ACCESS_TOKEN / GOOGLE_OAUTH_ACCESS_TOKEN plus the project id. The executor injects these spec-owned values after the overlay filter; container-owned names are still skipped and HOME/KUBECONFIG still win any duplicate -e. A mint failure raises — a failed record, never a silent run without the credential the task depends on.

Properties. No key file exists and none is mounted; the operator's ADC never crosses; the token lives ≤1h and is not refreshed, so an agent still calling past that gets a clean 401. Tasks with no agent_cloud_identity output are byte-for-byte unaffected.

What this does not solve (known, separate)

Validation

  • Full unit suite: 1751 passed; ruff check and format clean.
  • tofu validate on the modified cluster module: clean. The root secret-rotation stack has a pre-existing validate-time cycle in the shared vcluster module arm, present on the base commit before this change (verified by validating the untouched base tree).
  • Not yet exercised against a live GKE project — the mint path needs a real tokenCreator grant to prove end to end; suggested smoke: provision the stack, then gcloud auth print-access-token --impersonate-service-account=$(tofu output -raw agent_cloud_identity) as the runner identity.

isadominguez314 and others added 3 commits September 10, 2026 10:21
Every agy run has been dying at startup. The matrix sets one AGENT_MODEL
for the whole run -- gemini-3.1-pro-preview, spelled for Vertex, which
needs that suffix -- and agy rejects the -preview suffix outright and
refuses any selection that does not name a reasoning tier exactly once.
_resolve_model_name only stripped the provider prefix, so the Vertex id
reached the command line verbatim and agy exited 1 in about two seconds
with no conversation DB and no transcript.

Not a sandbox problem, though that is where it surfaced: an ambient run
fails identically, and has since the harness was written.

Normalize inside this harness rather than respelling AGENT_MODEL for the
agy arm. That value labels every arm in the matrix and feeds the judge,
so bending it to suit one CLI would desynchronize that arm from the runs
it is compared against, and Vertex needs exactly the suffix agy refuses.
Emit the bare slug plus --effort: it is the one form that cannot collide,
and no lookup table means nothing to maintain as the catalogue grows --
agy validates the result and still fails loud on anything unknown.

The tier is a scoring variable rather than a formatting detail, since
low and high are materially different agents and agy has no untiered
form. Default it to high, because the other harnesses run their model
with no reasoning throttle and anything lower would hand the agy arm a
handicap that reads as a capability gap. AGENT_MODEL_EFFORT overrides
it, and an unknown tier is rejected on the spot rather than passed
through to fail later as agy's own error mid-arm.

settings.json now carries the same resolved spelling as the flag. agy
does not validate modelConfigs.defaultModel -- an unknown value there is
ignored in silence and the run quietly falls back to another model -- so
the flag is the only guard, and it should not be guarding a value that
settings contradicts.

Drop GEMINI_MODEL from the env overlay. It is a Gemini CLI variable that
agy ignores: a garbage value raises no error and a valid one does not
change the model the run reports. It looked like a lever and was not.

Spellings verified against agy 1.2.0 on the bastion, in the pinned image
and under the executor's real invocation.
…n cloud calls

A sandboxed agent has no ambient cloud identity, by design — so a task
whose work includes cloud API calls beyond kubectl (secret-rotation must
add a Secret Manager secret version) could previously only run
unsandboxed. Close that gap without widening the boundary:

- The task's stack provisions a run-unique service account holding only
  the roles the task needs, scoped to the resources it provisioned, and
  names it in an agent_cloud_identity output. The provisioning identity
  gets serviceAccountTokenCreator on that account in the same stack, so
  the whole loop tears down with the run.
- The GCP provider impersonates that account host-side and mints a
  short-lived access token; a mint failure fails the run loudly rather
  than letting the agent be graded on a missing credential.
- The executor injects the minted env by value (CLOUDSDK_AUTH_ACCESS_TOKEN,
  GOOGLE_OAUTH_ACCESS_TOKEN, project id), after the overlay filter and
  before the container-owned vars, which still win any duplicate.

Tasks that declare no agent_cloud_identity output are unaffected:
nothing extra is minted and nothing extra crosses.
…xplicitly

The ADC userinfo derivation reads a null email on a VM service-account
credential that lacks the userinfo-email scope, which failed the whole
apply. An explicit token_creator_member variable now wins when set; the
derivation stays as the fallback, and with neither available the binding
is skipped so the harness's mint fails loud naming the missing grant.
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.

2 participants