Skip to content

Shared volumes: actors mount an atespace-scoped SharedVolume and hand it off in turn - #2567

Draft
hajiler wants to merge 5 commits into
agent-substrate:mainfrom
hajiler:shared-volumes/milestone-1
Draft

hajiler wants to merge 5 commits into
agent-substrate:mainfrom
hajiler:shared-volumes/milestone-1

Conversation

@hajiler

@hajiler hajiler commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #1637 (sequential hand-off is the first milestone; concurrent READ_WRITE_MANY holders follow). Post-GA follow-up.

Depends on #2653, which carries the breaking change (Volume.external with the ExternalVolumeSource union, and atelet's handle renamed to ExternalVolume). This branch includes that commit until #2653 merges; review commits 2 to 5 here. The per-actor record on ActorStatus.external_volumes keeps its name, ExternalVolume.

It's a good idea to open an issue first for discussion.

  • Tests pass
  • Appropriate changes to documentation are included in the PR

What this adds

A SharedVolume is an atespace-scoped CSI volume whose lifetime is independent of any actor, so several actors can mount the same storage in turn: a producer writes, suspends, and a consumer resumes and reads. Design: go/substrate-shared-volumes. User docs: docs/csi-volumes.md, section "Shared Volumes".

kubectl ate create sharedvolume ctx-42 -a demo --storage-class csi-nfs-sc --capacity 10Gi
kubectl ate create sharedvolume imported -a demo --volume-id <csi-volume-id> --volume-type nfs.csi.k8s.io
kubectl ate create actor producer-42 -a demo --template producer --volume shared=ctx-42
volumes:
- name: shared
  external: {sharedVolume: {name: ctx-42}}   # or {sharedVolume: {}} and bind with --volume
  • Resource and RPCs. Create/Get/List/Update/DeleteSharedVolume (annotated with (resource) and (method).resource, list paging through ListOptions), a Postgres table (migration 000004_shared_volumes.sql), store contract tests, authorization rules and an OpenFGA shared_volume type (atespace editors create, update and delete; viewers get and list), and kubectl ate {create,get,update,delete} sharedvolume.
  • Provision or register. A volume is provisioned from a StorageClass or registers a volume that already exists (volume_id, volume_type, volume_context).
  • Access mode. Fixed at create. Only READ_WRITE_ONCE is accepted today, so at most one resumed actor holds the volume; a resume that would take a held volume fails with FailedPrecondition naming the holder.
  • Delete trigger. MANUAL (default when registering) removes the volume only on an explicit delete; LAST_ACTOR (default when provisioning) removes it when the last actor referencing it is deleted. Deleting the record always deletes the storage. A referenced volume cannot be deleted.
  • References. status.refs lists every actor referencing the volume with active set while the actor is resumed. Create adds an inactive ref, resume activates it, pause and suspend deactivate it, crash leaves it as is, delete removes it.
  • Template binding. Volume.external.sharedVolume either names the volume or leaves the name empty so each actor binds it at creation through Actor.shared_volume_bindings (--volume <slot>=<volume>). Bindings are validated at create and immutable afterwards. The per-actor ExternalVolume record carries shared_volume_name when the volume is borrowed.
  • Golden actors never take a ref; they warm up against an empty directory at the same mount path (new atelet empty_dir volume source). Actors created from such a template cold-boot on their first resume and restore their own snapshot afterwards, because runsc derives the disable_file_handle_sharing mount option from the bind source's filesystem and refuses to restore a golden checkpoint taken over a local directory onto an NFS mount. Details and candidate fixes are in the docs; tracked by TODO(shared-volumes).

Commits

  1. api: nest the external volume template under Volume.external is Volume templates nest the external volume template under external.template #2653.
  2. atelet: add the empty_dir volume source is the golden actor's placeholder mount.
  3. ateapi: add the SharedVolume resource adds the resource, store, RPCs and CLI. Nothing references a volume yet.
  4. ateapi: let actors reference SharedVolumes adds ExternalVolumeSource.shared_volume as the union's second member (inside the existing ExternalVolumes preview gate), refs, bindings, the RWO holder check, and the e2e hand-off test.
  5. ateapi: cold-boot actors whose template references a SharedVolume is the first-resume behaviour described above.

Commits 2 to 5 are additive: no existing field, message or RPC changes shape.

Testing

  • Unit, store (Postgres), controlapi and functional suites pass on every commit of the branch; hack/verify/gofmt.sh, hack/verify/boilerplate.sh and the apitool checks pass.
  • New e2e: TestSharedVolumeHandoff and TestSharedVolumeMissing in internal/e2e/suites/combinedvolumes.
  • Manually validated end to end on a kind cluster with both csi-nfs-sc and csi-hostpath-sc (under the previous names): provisioning and registration, create-time rejections, named and per-actor bindings, the holder check, ref tracking across resume, pause, suspend and crash, delete guards, both delete triggers, and golden actors staying out of refs.

Known follow-ups

  • A crashed or failed-restore actor never unmounts its external volume on the node, which can make the next holder's unpublish fail (Actor crash leaves external volumes attached to the node; delete then skips detach #1715 covers the same failure for owned volumes).
  • Golden-snapshot restores for templates that reference a SharedVolume (see docs for the two candidate approaches).
  • READ_WRITE_MANY claims, a dedicated refs table, and safe unstaging (Milestone 2).

🤖 Generated with Claude Code

@google-cla

google-cla Bot commented Oct 13, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@hajiler
hajiler force-pushed the shared-volumes/milestone-1 branch from dbb7290 to 9503c93 Compare October 14, 2026 04:54
@hajiler hajiler changed the title Shared external volumes: actors mount an atespace-scoped ExternalVolume and hand it off in turn Shared volumes: actors mount an atespace-scoped SharedVolume and hand it off in turn Oct 14, 2026
@spichen

Copy link
Copy Markdown

Thanks for moving this forward!

I noticed the Golden Snapshot limitation mentioned in this PR, particularly the mount-option mismatch when restoring an actor with an external NFS volume.

We're building a multi-tenant agent execution platform where each session needs actor-specific external mounts (read-only skills/knowledgebase files and writable persistent memory), with a target of sub-second sandbox startup using microVMs.

We'd be interested in contributing Golden Snapshot compatibility for shared volumes, including the necessary runtime changes and end-to-end benchmarks.

I see this is already tracked as TODO(shared-volumes). Would you be comfortable with us taking on that follow-up as a separate PR?

We'd initially focus on Cloud Hypervisor/virtio-fs, while ensuring the design doesn't prevent subsequent gVisor support.

We're also interested in contributing per-actor subPath and readOnly mount support, potentially adapting the implementation from Giant Swarm PR #234 to the new SharedVolume abstraction.

Would you prefer these as two separate follow-up PRs?

Comment thread pkg/proto/ateapipb/ateapi.proto Outdated
@@ -1341,6 +1535,25 @@ message Volume {
// +k8s:optional
// +k8s:unionMember
ImageVolumeSource image = 6;

reserved 3;

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.

No need to reserve ids or field names for now.

hajiler and others added 5 commits October 15, 2026 11:00
The ActorTemplate volume union now carries one CSI member, Volume.external
(ExternalVolumeSource), whose template member replaces
external_volume_template. The field name matches its siblings and the message
follows the *VolumeSource convention; a member for shared volumes follows
after GA. The ExternalVolumes preview gate moves with it. atelet's resolved
handle ExternalVolumeSource becomes ExternalVolume, matching its sibling
volume messages and no longer sharing a name with the new ateapi message.

Templates that still carry external_volume_template must be recreated.

Fixes agent-substrate#2407, agent-substrate#2436.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An EmptyDirVolume is an empty host directory at the same path an external
volume would be published to, mounted into the sandbox the same way. It lets a
golden actor warm up against a placeholder for an external volume it must not
mount, so the snapshot's mount table matches the real actor's on both gVisor
and microVM.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A SharedVolume is an atespace-scoped CSI volume whose lifetime is
independent of any actor. It is provisioned from a StorageClass or registers a
volume that already exists, carries an immutable access mode, and is removed
either manually or when the last actor referencing it is deleted, according to
its delete trigger. Deleting the record also deletes the storage.

This adds the resource, its Postgres table and store methods, the authorization
rules and OpenFGA type, the
create/get/list/update/delete RPCs, and kubectl-ate commands. Nothing
references a volume yet; actor references arrive separately.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A template volume whose external source names a shared_volume mounts a
SharedVolume instead of provisioning one per actor. The reference names the
volume, or leaves the name empty so each actor binds it at creation through
Actor.shared_volume_bindings.

Every actor referencing a volume is recorded in the volume's refs, with active
set while the actor is resumed. Resume refuses a READ_WRITE_ONCE volume another
actor holds active, pause and suspend mark the reference inactive, crash leaves
it as is, and delete removes it and deletes a LAST_ACTOR volume that no actor
references any more. Golden actors never reference a volume; they warm up
against an empty directory at the same mount path.

Depends on the empty_dir volume source and the SharedVolume resource.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The golden actor warms up against a local empty directory in place of a
shared volume, but runsc derives the disable_file_handle_sharing mount option
from the bind source's filesystem and refuses a restore whose mount options
differ from the checkpoint's. On NFS the first resume of every such actor
therefore failed with "Mounts" does not match across checkpoint restore.

Actors created from a template that references a SharedVolume no longer seed
from the golden tag: the first resume boots from the image and later resumes
restore the actor's own snapshot, which was taken over the real volume. An
explicit source tag is refused for these templates. Golden restores for
shared volumes remain follow-up work, tracked by TODO(shared-volumes).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@hajiler
hajiler force-pushed the shared-volumes/milestone-1 branch from 21adc9a to a987b68 Compare October 15, 2026 18:49
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.

3 participants