feat: Restore per-mount source-subpath on the UUID-keyed mount contract#11527
feat: Restore per-mount source-subpath on the UUID-keyed mount contract#11527
Conversation
…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
| 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." | ||
| ), | ||
| ) |
There was a problem hiding this comment.
I don't see any reason to add this to the schema. I don't plan to expand this API any further.
There was a problem hiding this comment.
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.
| def _merge_resolved_legacy_mounts( | ||
| creation_config: dict[str, Any], | ||
| name_to_id: dict[str, UUID], | ||
| name_to_subpath: Mapping[str, str] | None = None, |
There was a problem hiding this comment.
It seems odd to keep expanding the map by name.
There was a problem hiding this comment.
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_subpathto persistedMountInfoEntryand threads it through scheduler reconstruction intoVFolderMountRequest.subpath. - Extends API v7 creation config with
mount_id_subpathsand projects it inAgentRegistry._mount_entries_from_creation_config. - Updates legacy mount normalization to split
mounts=["name/subdir"], resolvename → UUID, and forward the subpath intomount_id_subpaths; updatesprepare_vfolder_mountsto 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
mountslist. If a caller specifies a destination via legacymount_map(e.g.,{ "vf-a/.pipeline": "/data" }),_resolve_legacy_name_mounts()will currently treat the fullvf-a/.pipelineas 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 splittingmount_map(and possiblymount_options) keys using_split_legacy_name_subpath()and forwarding the extracted subpath intoname_to_subpathsoname/subpath:/dstworks 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) |
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:MountInfoEntrygains asource_subpath: str | None = Nonefield.None(the default) keeps the historical "mount the vfolder root" behavior, and pre-existing persisted rows deserialize cleanly through Pydantic's default.VFolderMountRequestgains a parallelsubpath: str | None = Noneso the typed UUID-keyed surface can express a subpath without falling back to the legacy string-formref="name/subpath".CreationConfigV7exposesmount_id_subpaths: dict[UUID, str] | None(with amountIdSubpathscamelCase alias).AgentRegistry._mount_entries_from_creation_configreads it with the same UUID/UUID-string-key polymorphism already used formount_id_map/mount_optionsand projects the value onto each emittedMountInfoEntry.source_subpath.SchedulerDBSourcerebuildsVFolderMountRequestwith the persisted subpath, andprepare_vfolder_mountshonors a UUID-keyed request's explicit subpath instead of unconditionally forcingrequested_vfolder_subpaths[uuid] = ".".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 newname -> subpathmap, and_merge_resolved_legacy_mountsforwards it ontomount_id_subpaths.Acceptance criteria
MountInfoEntry(or its equivalent) has asource_subpathfield;Nonemeans "mount the vfolder root" (current behavior).POST /session/_/createwith a UUID-keyedmount_idsplus a per-mountsource_subpathresults in a session where the container destination is bind-mounted to<vfolder>/<source_subpath>rather than the vfolder root. (Wire fieldmount_id_subpathsplumbed through toMountInfoEntry.source_subpath, which feedsVFolderMountRequest.subpathand intoprepare_vfolder_mounts.requested_vfolder_subpaths.)mounts: ["<name>/<subpath>"]form on the same endpoint stops raisingInvalidAPIParameters(400)and produces an equivalent mount.prepare_vfolder_mountsis 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).MountPermission, mount destination defaulting, or storage-proxyvfsubpathsemantics.Test plan
pants fmt --changed-since=origin/mainpants fix --changed-since=origin/mainpants lint --changed-since=origin/mainpants check --changed-since=origin/mainpants test --changed-since=origin/mainpants test tests/unit/common/dto/manager/session/ tests/unit/manager/services/session/ tests/unit/manager/sokovan/scheduling_controller/test_enqueue_session_from_draft.pymount_id_subpathsand viamounts=["name/subdir"]) — defer until SDK ergonomics land.Out of scope
mount_id_subpathsfromComputeSession.get_or_create. The wire format is enough for now; SDK plumbing will follow in a separate PR.vfsubpathis already supported there.