When git repo mounts and regular file mounts have overlapping target paths, broader parent mounts hide deeper child mounts because Docker Compose applies volumes in array order. - Add sort_volumes_by_specificity() to docker.py: - Sorts by target path depth (parent paths first, child paths last) - Logs warnings for duplicate targets - Handles :bind and :ro suffixes correctly - Integrate into manifest flow (compile_compose): - Sorts manifest mounts + EXTRA_VOLUMES before writing compose - Integrate into legacy flow (_modify_compose_file): - Sorts after appending extra_volumes to existing template volumes - Add 6 unit tests covering parent/child ordering, stable sort, type suffixes, empty list, single volume, and duplicate warnings. Quality gates: pytest (214 passed, 6 pre-existing), tsc --noEmit (clean)
4.4 KiB
Exploration: Mount Specificity Ordering
Problem Statement
When a tool instance uses both git repo mounts and regular file mounts, overlapping target paths can cause the broader mount to hide the more specific one.
Example:
- Git repo mount:
repo/x/→/workspace/x(directory) - Regular file mount:
config.json→/workspace/x/y/config.json(single file)
Expected: /workspace/x/y/config.json contains the file mount contents.
Actual: The git mount overwrites /workspace/x, hiding /workspace/x/y/config.json.
Root Cause
Docker Compose mounts volumes in the order they appear in the volumes array.
In Linux, a later mount at a parent path hides earlier mounts at child paths.
Current code ordering:
compile_composeadds manifest-defined mounts firstEXTRA_VOLUMES(profile + git mounts) appended after
Within EXTRA_VOLUMES in start_instance:
profile_mountsfromapply_resolved_profilegit_mount_volumesfrom_resolve_git_mounts
Since git mounts are appended after regular mounts, a broad git mount
(e.g. /workspace/x) overwrites a specific regular mount
(e.g. /workspace/x/y/config.json).
Affected Code Paths
-
Manifest flow:
compile_compose()inmanifest_compiler.py- Manifest mounts →
EXTRA_VOLUMES - All appended to compose
volumeslist in that order
- Manifest mounts →
-
Legacy flow:
_modify_compose_file()intool_instances.py- Existing template volumes →
extra_volumesappended extra_volumes= profile_mounts + git_mount_volumes
- Existing template volumes →
-
Both flows: Volume entries are strings like
source:targetorsource:target:bind- No structured sorting happens before write
Options
Option A: Sort by path depth (recommended)
Sort all volume entries by target path specificity before writing compose.
- Shorter / parent paths first
- Deeper / child paths last
- Deeper mounts "win" by being layered on top
Pros:
- Simple, predictable rule
- Works for all mount types (manifest, git, profile, template)
- Minimal code change
Cons:
- Sorting by string length is naive (edge cases with similar paths)
- Need proper path-segment counting
- Doesn't handle exact same target conflicts
Option B: Detect and warn on overlaps
Before writing compose, detect when any two mounts have overlapping target paths. Log a warning and optionally fail fast.
Pros:
- Surfaces conflicts to user early
- No silent data loss
Cons:
- Doesn't actually fix the problem; user has to redesign mounts
- False positives for legitimate use cases (mounting different files into same tree)
Option C: Merge overlapping mounts into a single staging directory
Instead of mounting multiple sources, stage all files into a single merged directory on disk, then mount that single directory.
Pros:
- Eliminates Docker mount ordering entirely
- Natural specificity: later file writes overwrite earlier ones
Cons:
- Complex to implement correctly
- Git mounts would need to be cloned into staging area
- Breaks live file editing (bind mounts from host)
- Large refactor
Option D: Annotate mount specificity and merge in compiler
Add a priority or specificity field to mount definitions.
Compiler sorts by this field.
Pros:
- Explicit control
Cons:
- Adds schema complexity
- Users must understand mount ordering
- Overkill for this use case
Recommendation
Option A — sort by path depth.
Rationale:
- Mount specificity should "just work" without user intervention
- Path depth is a natural proxy for specificity
- A parent directory mount is almost always less specific than a child file mount
- Implementation is ~20 lines in the compose write path
- Can be combined with Option B (warn on exact conflicts) for safety
Acceptance Criteria
- A git repo mount to
/workspace/xand a regular mount to/workspace/x/y/config.jsonboth work: the config.json file contains the regular mount contents. - Multiple overlapping mounts sort consistently (deterministic).
- Exact same-target conflicts are logged as warnings.
- Both manifest and legacy flows behave correctly.
- Unit tests cover overlap scenarios.
Files to Modify
apps/api/src/services/manifest_compiler.py—compile_compose()sortingapps/api/src/api/tool_instances.py—_modify_compose_file()sortingapps/api/tests/unit/test_manifest_compiler.py— new testsapps/api/tests/unit/test_tool_instances.py— new tests (ortest_tool_instances_legacy.py)