Skip to content

feat: Restore per-mount source-subpath on the UUID-keyed mount contract#11527

Open
rapsealk wants to merge 5 commits intomainfrom
fix/11526-mount-subpath
Open

feat: Restore per-mount source-subpath on the UUID-keyed mount contract#11527
rapsealk wants to merge 5 commits intomainfrom
fix/11526-mount-subpath

Conversation

@rapsealk
Copy link
Copy Markdown
Member

@rapsealk rapsealk commented May 8, 2026

Summary

Closes #11526.

The UUID-keyed mount contract introduced in #11434 / #11520 dropped the per-mount source-subpath, leaving callers no way to express "mount the <X> subdirectory of vfolder <Y>" through any session-creation surface. This PR restores that capability end-to-end:

  • MountInfoEntry gains a source_subpath: str | None = None field. None (the default) keeps the historical "mount the vfolder root" behavior, and pre-existing persisted rows deserialize cleanly through Pydantic's default.
  • VFolderMountRequest gains a parallel subpath: str | None = None so the typed UUID-keyed surface can express a subpath without falling back to the legacy string-form ref="name/subpath".
  • CreationConfigV7 exposes mount_id_subpaths: dict[UUID, str] | None (with a mountIdSubpaths camelCase alias). AgentRegistry._mount_entries_from_creation_config reads it with the same UUID/UUID-string-key polymorphism already used for mount_id_map / mount_options and projects the value onto each emitted MountInfoEntry.source_subpath.
  • The scheduler reconstruction in SchedulerDBSource rebuilds VFolderMountRequest with the persisted subpath, and prepare_vfolder_mounts honors a UUID-keyed request's explicit subpath instead of unconditionally forcing requested_vfolder_subpaths[uuid] = ".".
  • The REST handler's blanket rejection of mounts=["name/subdir"] is replaced with split-and-resolve: the bare name resolves to a UUID via the existing resolver, the subpath is captured into a new name -> subpath map, and _merge_resolved_legacy_mounts forwards it onto mount_id_subpaths.

Acceptance criteria

  • MountInfoEntry (or its equivalent) has a source_subpath field; None means "mount the vfolder root" (current behavior).
  • POST /session/_/create with a UUID-keyed mount_ids plus a per-mount source_subpath results in a session where the container destination is bind-mounted to <vfolder>/<source_subpath> rather than the vfolder root. (Wire field mount_id_subpaths plumbed through to MountInfoEntry.source_subpath, which feeds VFolderMountRequest.subpath and into prepare_vfolder_mounts.requested_vfolder_subpaths.)
  • The legacy mounts: ["<name>/<subpath>"] form on the same endpoint stops raising InvalidAPIParameters(400) and produces an equivalent mount.
  • prepare_vfolder_mounts is exercised end-to-end with a non-"." subpath via the modern UUID-keyed surface (covered by registry-projection unit tests; the lower-layer subpath path is unchanged for the legacy string surface).
  • No change to MountPermission, mount destination defaulting, or storage-proxy vfsubpath semantics.

Test plan

  • pants fmt --changed-since=origin/main
  • pants fix --changed-since=origin/main
  • pants lint --changed-since=origin/main
  • pants check --changed-since=origin/main
  • pants test --changed-since=origin/main
  • pants test tests/unit/common/dto/manager/session/ tests/unit/manager/services/session/ tests/unit/manager/sokovan/scheduling_controller/test_enqueue_session_from_draft.py
  • Manual end-to-end verification with a live server (subpath mount via mount_id_subpaths and via mounts=["name/subdir"]) — defer until SDK ergonomics land.

Out of scope

  • Client/SDK changes to expose mount_id_subpaths from ComputeSession.get_or_create. The wire format is enough for now; SDK plumbing will follow in a separate PR.
  • Storage-proxy changes — vfsubpath is already supported there.

rapsealk added 5 commits May 8, 2026 17:01
…ntRequest

Add a ``source_subpath`` slot to ``MountInfoEntry`` and a parallel
``subpath`` slot to ``VFolderMountRequest`` so the UUID-keyed mount
contract can express "mount the <X> subdirectory of vfolder <Y>" without
falling back to the legacy string-form ``ref="name/subpath"``. ``None``
remains the default and means "mount the vfolder root", matching the
existing behavior; persisted rows that predate the field deserialize
cleanly via Pydantic's default.

Refs #11526
…gistry

Add a ``mount_id_subpaths: dict[UUID, str] | None`` field to
``CreationConfigV7`` so callers can express per-mount source subpaths
alongside ``mount_ids`` / ``mount_id_map``. ``AgentRegistry._mount_entries_from_creation_config``
now consumes that field with the same UUID-vs-UUID-string-key polymorphism
already used for ``mount_id_map`` / ``mount_options`` and propagates the
value onto each emitted ``MountInfoEntry.source_subpath``. Missing keys
fall through to ``None`` and preserve the existing "mount root" default.

Refs #11526
…struction

Reconstruct ``VFolderMountRequest`` from a persisted ``MountInfoEntry``
with the new ``subpath`` field populated, and teach
``prepare_vfolder_mounts`` to honor a UUID-keyed request's explicit
subpath. The earlier code unconditionally forced
``requested_vfolder_subpaths[uuid] = "."`` in two places — both now
``setdefault`` so a per-mount subpath survives the resolution pass and
flows into ``VFolderMount.vfsubpath`` instead of the vfolder root.

Refs #11526
Replace the unconditional ``InvalidAPIParameters`` rejection of
``mounts=["name/subdir"]`` at the REST surface with a split-and-resolve
path: ``_resolve_legacy_name_mounts`` now extracts the subpath, returns
it alongside the ``name -> UUID`` resolution, and
``_merge_resolved_legacy_mounts`` forwards it onto ``mount_id_subpaths``
without overwriting an explicit UUID-keyed value the caller already
supplied. The lifecycle test that exercises this resolver is updated for
the new tuple return.

Refs #11526
Add unit tests for the new fields:
- ``CreationConfigV7.mount_id_subpaths`` round-trips UUID-string keys
  via the camelCase alias.
- ``AgentRegistry._mount_entries_from_creation_config`` projects
  ``mount_id_subpaths`` onto ``MountInfoEntry.source_subpath`` (with the
  same UUID/UUID-string-key polymorphism already used for ``mount_id_map``
  and ``mount_options``) and leaves entries without an entry as ``None``.
- ``MountInfoEntry`` round-trips ``source_subpath`` through
  ``model_dump(mode="json")`` / ``model_validate``, and a legacy persisted
  shape without the field deserializes with the default.
- The legacy ``mounts=["name/subpath"]`` resolver path produces
  ``mount_id_subpaths`` instead of raising ``InvalidAPIParameters``.

Also lift a polymorphic ``_lookup`` helper into the registry projection
so the canonical UUID-string form of the key is also accepted, matching
how the mount-options bag is wired upstream. Adds the towncrier entry.

Refs #11526
@rapsealk rapsealk added this to the 26.4 milestone May 8, 2026
@github-actions github-actions Bot added size:L 100~500 LoC comp:common Related to Common component comp:manager Related to Manager component type:feature Add new features labels May 8, 2026
@rapsealk rapsealk changed the title feat(BA-5959): Restore per-mount source-subpath on the UUID-keyed mount contract feat: Restore per-mount source-subpath on the UUID-keyed mount contract May 8, 2026
@rapsealk rapsealk marked this pull request as ready for review May 8, 2026 08:25
Copilot AI review requested due to automatic review settings May 8, 2026 08:25
Comment on lines +434 to +443
mount_id_subpaths: dict[UUID, str] | None = Field(
default=None,
validation_alias=AliasChoices("mount_id_subpaths", "mountIdSubpaths"),
description=(
"Per-mount source-subpath keyed by vfolder UUID. ``None`` (the default) "
"and missing entries mean 'mount the vfolder root'; a non-empty value "
"mounts that subdirectory of the vfolder at the corresponding "
"``mount_id_map`` destination."
),
)
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.

I don't see any reason to add this to the schema. I don't plan to expand this API any further.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

As we dropped support for folder names and now only support folder IDs, using a path like b64bb966-3d81-45b7-8e78-1cb1a7a144b0/path/to/somewhere feels unnatural.

Comment on lines 300 to +303
def _merge_resolved_legacy_mounts(
creation_config: dict[str, Any],
name_to_id: dict[str, UUID],
name_to_subpath: Mapping[str, str] | None = None,
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.

It seems odd to keep expanding the map by name.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Restores per-mount vfolder source subpath support across the UUID-keyed mount contract, re-enabling “mount <vfolder>/<subdir> at destination <dst>” via both modern UUID-keyed fields and the legacy mounts=["name/subdir"] surface.

Changes:

  • Adds source_subpath to persisted MountInfoEntry and threads it through scheduler reconstruction into VFolderMountRequest.subpath.
  • Extends API v7 creation config with mount_id_subpaths and projects it in AgentRegistry._mount_entries_from_creation_config.
  • Updates legacy mount normalization to split mounts=["name/subdir"], resolve name → UUID, and forward the subpath into mount_id_subpaths; updates prepare_vfolder_mounts to honor UUID-keyed subpaths.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/unit/manager/services/session/test_session_lifecycle_service.py Adds unit coverage for legacy subpath resolution, registry projection, UUID-string key handling, and persistence round-trip.
tests/unit/common/dto/manager/session/test_types.py Adds DTO tests for CreationConfigV7.mount_id_subpaths defaults and UUID-key parsing via alias.
src/ai/backend/manager/repositories/scheduler/db_source/db_source.py Reconstructs mount requests with persisted source_subpath via VFolderMountRequest.subpath.
src/ai/backend/manager/registry.py Projects mount_id_subpaths into MountInfoEntry.source_subpath with polymorphic UUID/UUID-string key lookup.
src/ai/backend/manager/models/vfolder/row.py Teaches prepare_vfolder_mounts to accept explicit UUID-keyed subpaths and avoid clobbering them with ".".
src/ai/backend/manager/api/rest/session/handler.py Splits legacy mounts entries to capture subpaths and forwards them to mount_id_subpaths during normalization.
src/ai/backend/common/types.py Adds MountInfoEntry.source_subpath and VFolderMountRequest.subpath (typed carrier for UUID-keyed subpaths).
src/ai/backend/common/dto/manager/session/types.py Adds CreationConfigV7.mount_id_subpaths with snake/camel aliasing and docs.
changes/11526.feature.md Changelog entry for restored UUID-keyed per-mount subpaths and legacy acceptance.
Comments suppressed due to low confidence (1)

src/ai/backend/manager/api/rest/session/handler.py:450

  • Legacy subpath splitting is only applied to the mounts list. If a caller specifies a destination via legacy mount_map (e.g., { "vf-a/.pipeline": "/data" }), _resolve_legacy_name_mounts() will currently treat the full vf-a/.pipeline as the vfolder name and attempt to resolve it, which will fail. If the intent is to restore legacy subpath mounts end-to-end, consider also splitting mount_map (and possibly mount_options) keys using _split_legacy_name_subpath() and forwarding the extracted subpath into name_to_subpath so name/subpath:/dst works too.
        # ``mounts`` is name-keyed and may carry a per-entry subpath as
        # ``"name/subdir"``; split before queueing the bare name for
        # resolution. ``mount_map`` keys are also strictly name-keyed
        # legacy surfaces — every entry is treated as a vfolder name.
        for raw in mounts:
            entry = str(raw)
            name, subpath = _split_legacy_name_subpath(entry)
            if subpath is not None:
                # Last write wins on conflicting per-name subpaths; the
                # historical CLI never produces such input, and downstream
                # ``prepare_vfolder_mounts`` deduplicates by ``(vfid, vfsubpath)``.
                name_to_subpath[name] = subpath
            if name in seen:
                continue
            seen.add(name)
            names_to_resolve.append(name)
        for raw in mount_map.keys():
            name = str(raw)
            if name in seen:
                continue
            seen.add(name)
            names_to_resolve.append(name)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

if req.dst_path is not None:
requested_mount_map[req.ref] = req.dst_path
if req.subpath is not None and req.subpath != "":
requested_uuid_subpaths[req.ref] = os.path.normpath(req.subpath)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp:common Related to Common component comp:manager Related to Manager component size:L 100~500 LoC type:feature Add new features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Restore per-mount source-subpath on the UUID-keyed mount contract

3 participants