Skip to content

fix(atelet): fall back to anonymous GCS read with objectstore-plugin Internal - #2614

Merged
Benjamin Elder (BenTheElder) merged 4 commits into
agent-substrate:mainfrom
annapendleton:fix-gcs-anonymous-fallback
Oct 14, 2026
Merged

Benjamin Elder (BenTheElder) merged 4 commits into
agent-substrate:mainfrom
annapendleton:fix-gcs-anonymous-fallback

Conversation

@annapendleton

@annapendleton Anna Pendleton (annapendleton) commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2613

On GKE with a GCS backend, the object-store plugin's Workload Identity token gets 412 PreconditionFailed ("The operation requires that Uniform Bucket Level Access be enabled.") from the public gs://gvisor. toStatus maps that, like any error it doesn't recognize, to Internal, and atelet didn't fall back on Internal, so sandbox creation failed after #2439.

Changes

  • atelet falls back to an anonymous read on Internal. In practice that's any unexpected plugin failure. That's acceptable because anonymous reads only reach public objects, and atelet still checks the sha256.
  • So a broken plugin isn't hidden: the plugin logs a scrubbed warning when it returns Internal, and atelet warns when it falls back.
  • Updated the AssetProvider contract, api-guide.md and threat model T-08.

Testing

  • Unit tests: go test ./cmd/atelet/... ./internal/objectstoreplugin/... ./pkg/objectstorage/...
  • GKE 1.37 (Envoy dataplane): reproduced the 412 on main. With this PR, gVisor prewarm falls back and succeeds, and the counter demo passes create, persistence across requests, suspend, and resume from snapshot.

A follow-up issue will discuss the long-term design for plugin fallback.

…rns Internal

Fixes an issue introduced in agent-substrate#2439 where running on GKE with a GCS backend causes gs://gvisor asset downloads to fail.

When querying gs://gvisor using the cluster's ambient GCP credentials, GCS returns 403 Forbidden, which status.go maps to codes.Internal. Because codes.Internal was omitted from anonymousFallbackCodes, atelet aborted rather than falling back to an anonymous GCS read.

This adds codes.Internal to anonymousFallbackCodes and adds unit tests covering the GCS Internal fallback path.

- **Unit tests**: `go test ./cmd/atelet/...` passes cleanly.
- **E2E validation on GKE**: Verified on Rapid GKE 1.37 (`DATAPLANE=envoy`) with Counter demo:
  - Actor creation, memory persistence across requests 1 and 2, suspension to snapshot, and resumption from snapshot on request 3 all pass 100%.

Fixes agent-substrate#2613

- [ ] Tests pass
- [ ] Appropriate changes to documentation are included in the PR
@ILLUM1N0X

Copy link
Copy Markdown
Collaborator

One thing the description should say: toStatus maps every unrecognized plugin error to Internal. So in practice this change means "fall back on any unexpected plugin failure," not just "fall back on GCS 403." I think that's fine. The main cost is that a broken plugin is now easier to miss.

Could you also paste the body of the 403 from the plugin logs? It'd be good to record the actual cause in the PR.

@annapendleton

Copy link
Copy Markdown
Collaborator Author

Neither component logged the actual error: the plugin doesn't log failures, and atelet deliberately drops the plugin's status text so credentials can't leak into crash messages, so all we have is code=Internal. I'm updating this PR so the plugin logs any error it maps to Internal and atelet warns whenever it falls back after a plugin failure, and I'll paste the error from a fresh GKE run once it's redeployed.

Comment thread cmd/atelet/sandbox_assets.go Resolved
Comment thread cmd/atelet/sandbox_assets.go Resolved
Comment thread cmd/atelet/sandbox_assets.go Resolved

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for working on a fix, this is blocking all manual testing on GKE

note: you can workaround it in the short term by running your own authenticated mirror ... but it's not great.

Comment thread cmd/atelet/sandbox_assets.go Resolved
@annapendleton

Copy link
Copy Markdown
Collaborator Author

After getting local logging into the plugin and atelet for the issue, it looks like I'm seeing a 412 error instead (still an unrecognized error which is why it gives Internal)

objectstore-plugin:
{"time":"2026-10-14T03:37:22.496348953Z","level":"WARN","msg":"Object-store plugin failed with an unrecognized error; returning Internal","http_status":412,"reasons":[],"service_code":"PreconditionFailed","message":"The operation requires that Uniform Bucket Level Access be enabled."}

atelet:
{"time":"2026-10-14T03:37:22.496702505Z","level":"WARN","msg":"Object-store plugin failed to fetch sandbox asset","url":"gs://gvisor/releases/nightly/2026-10-10/x86_64/gvisor.tar.zstd","code":"Internal"} {"time":"2026-10-14T03:37:22.496790374Z","level":"WARN","msg":"Sandbox asset prewarm failed; will retry","config":"gvisor-default","retries":0,"err":"while fetching sandbox asset \"gvisor\": sandbox asset gs://gvisor/releases/nightly/2026-10-10/x86_64/gvisor.tar.zstd: the object-store plugin failed (Internal)"}

Jetski analysis of this failure on GKE is that the objectstore-plugin authenticates with a Workload Identity federation token, which GCS rejects on buckets that use object ACLs instead of uniform access, like the public gs://gvisor. Anonymous reads of the same object succeed.

Thanks for all the feedback, reading through the comments rn

- objectstore-plugin logs a scrubbed warning when it returns Internal:
  HTTP status, plus the service's code and message where the storage
  backend deems them safe. Never the raw error text.
- atelet warns when it falls back after Internal or Unavailable.
- Update the AssetProvider contract, api-guide.md and threat model T-08.
- Fix the fallback comments and drop the duplicate test.
@annapendleton

Copy link
Copy Markdown
Collaborator Author

Thanks all for the thorough review! I've pushed an update addressing the feedback:

  • Root cause: GCS returns 412 PreconditionFailed ("The operation requires that Uniform Bucket Level Access be enabled.") to the plugin's Workload Identity token for gs://gvisor, not a 403. toStatus maps it to Internal, like any error it doesn't recognize.
  • Visibility: the plugin now logs a scrubbed warning when it returns Internal, and atelet warns when it falls back after Internal/Unavailable.
  • Docs: comments, the AssetProvider contract, api-guide.md and T-08 now reflect the new fallback behavior.

I reran validation of this version on GKE 1.37 (Envoy dataplane): gVisor prewarm falls back and succeeds, and the counter demo passes create, persistence across requests, suspend, and resume from snapshot. I'll also open an issue to discuss the longer-term design for plugin fallback.

@annapendleton

Copy link
Copy Markdown
Collaborator Author

ILLUM1N0X Updated the description to say exactly that. The real error (posted above) is a 412 PreconditionFailed ("Uniform Bucket Level Access be enabled"), not a 403, and the plugin now logs it so a broken plugin isn't silent.

@annapendleton

Copy link
Copy Markdown
Collaborator Author

Benjamin Elder (@BenTheElder) Thanks! Aiming to land this quickly to unblock GKE testing, and I'll follow up on the long-term fallback design separately.

@annapendleton

Copy link
Copy Markdown
Collaborator Author

Opened #2630 to discuss the longer-term design for plugin fallback to anonymous reads of public assets, including Unknown from third-party plugins. Please add thoughts there so this PR can stay focused on unblocking GKE.

DescribeServiceError no longer names GCS: each backend appends its describer from an init function in its own file.
@annapendleton

Copy link
Copy Markdown
Collaborator Author

Something I don't have an immediately good idea on how to handle - the error message proliferation - error messages need to be parsed in a different way across our existing in-tree providers, and I figured each provider would want control over what info is surfaced per error (in case there is sensitive info in an error message).

For now I have the init adding optional per provider processing in but it's not my favorite long-term option as it won't play nicely with 3rd party plugins and it loops through each option regardless which in-tree provider is being used - I think long-term I'd prefer something more extensible.

Curious if anyone has any thoughts here and if we need to solve this cleanly in this PR or if it could be a followup cleanup issue.

@aramase

Copy link
Copy Markdown
Collaborator

Anna Pendleton (@annapendleton) I wouldn't solve it here. Since every backend lives in pkg/objectstorage, DescribeServiceError could just call describeGCSError before the HTTPStatusCode fallback and skip the init registry, and third-party plugins log their own errors anyway, so the extensible version can be a follow-up.

@LiorLieberman

Copy link
Copy Markdown
Collaborator

We are cutting 0.5 today and would like this to be included. Can we make sure this is approved and merged in the next few hours?

@annapendleton

Copy link
Copy Markdown
Collaborator Author

I'm working on quick fix to address Anish Ramasekar (@aramase) 's feedback to remove the init registry, then I'd say this is ready for submission if there's no other outstanding blockers. I'm creating an issue for the extensible error messaging to track that for future PR.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one nit for follow-up.

slog.InfoContext(ctx, "Object-store plugin did not serve sandbox asset; downloading it anonymously",
// Internal and Unavailable mean the plugin failed rather than declined
// the URL, so falling back must not hide them: warn. The plugin logs
// why it failed; atelet logs only the code (see fetchFromPlugin).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit 🟢 – "The plugin logs why it failed" only holds for Internal. toStatus logs its catch-all, but the plugin's own Unavailable statuses aren't logged, and an unreachable plugin can't log anything. Maybe scope that sentence to Internal?

@BenTheElder
Benjamin Elder (BenTheElder) added this pull request to the merge queue Oct 14, 2026
Merged via the queue into agent-substrate:main with commit 05b69e6 Oct 14, 2026
9 checks passed
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.

[Bug]: GCS backend fails to fall back to anonymous read for gs://gvisor sandbox assets

5 participants