The previous sorting fix exposed a deeper bug: ResolvedMount always mounted its staging directory as a single bind mount. When a config profile mount targeted /workspace/x/y and contained a single file z.json, the staging directory (containing only z.json) replaced the ENTIRE /workspace/x/y directory, hiding all sibling files from git repo mounts. - Change apply_resolved_profile to mount each file individually: - source: staging_dir/relative_path - target: expanded_target/relative_path - Sibling files from other mounts are preserved. - Empty mounts produce no volume entries. - Keep volume sorting (parent paths before child paths) which is still necessary for directory mounts and ensures parent dirs exist before file mounts inside them. - Add 4 unit tests for file-level mount behavior. Quality gates: pytest (218 passed, 6 pre-existing), tsc --noEmit (clean)
2.5 KiB
Exploration: File-Level Mount Overlays
Follow-up to Mount Specificity Ordering
The sorting fix (parent paths before child paths) was correct for directory mounts,
but it exposed a deeper bug: ResolvedMount always mounts its staging directory
as a single bind mount. When a config profile mount targets /workspace/x/y and
contains a single file z.json, the staging directory (containing only z.json)
replaces the ENTIRE /workspace/x/y directory, hiding all sibling files from the
git repo.
Root Cause
In apply_resolved_profile:
volume_mounts.append({
"source": str(mount_dir), # staging dir with ONLY z.json
"target": expanded_target, # /workspace/x/y
"type": "bind",
})
This mounts a directory. Docker bind mounts at a directory path completely replace the target directory. There is no merge.
What the User Expects
Git repo mount: /repo/x → /workspace/x (directory with many files)
Config profile mount: z.json → /workspace/x/y/z.json (single file overlay)
Expected: /workspace/x/y/ contains all repo files PLUS the overlaid z.json.
Actual (before sorting): parent mount hides child mount (child never visible).
Actual (after sorting): child directory mount replaces parent subdirectory
(/workspace/x/y now contains ONLY z.json).
Solution
Mount each file individually instead of the staging directory.
for file_path, content in mount.files.items():
full_path = mount_dir / file_path
full_path.write_text(content)
volume_mounts.append({
"source": str(full_path),
"target": os.path.join(expanded_target, file_path),
"type": "bind",
})
This creates file-level bind mounts. Docker mounts a single file without affecting sibling files in the parent directory.
Edge Cases
- Empty
filesdict: skip, no mounts created. - Nested file paths (
a/b/c.txt): mountstaging/a/b/c.txt→target/a/b/c.txt. Docker creates parent directories as needed. - File target already exists in git repo: file mount wins (desired override behavior).
- No git repo mount (directory doesn't exist in image): Docker creates parent dirs for the first file mount.
Sorting Fix Status
KEEP the sorting. It is still correct and necessary for cases where directory
mounts genuinely override parent directories (e.g., a git mount to /workspace/x
and another git mount to /workspace/x/sub). File-level mounts also benefit from
sorting because the parent directory mount must exist before file mounts inside it.