# Design: Config Profile Multi-Repo Mounts ## Architecture ### Data Model No database changes. The `git_mounts` JSONB column already stores arbitrary JSON. #### Normalized git mount schema (in memory) After validation/normalization, every git mount entry is converted to the unified form: ```python { "remote_url": str, "branch": str | None, "mappings": [ {"source_path": str, "target_path": str}, ... ] } ``` The normalization step converts legacy `source_path` + `target_path` into a single-entry `mappings` array. ### Backend Changes #### 1. `_resolve_single_git_mount` refactor Split into two functions: **`_clone_git_repo(remote_url, branch, clone_parent) -> repo_path`** - Clones or pulls the repo - Returns the path to `repo-clone` - Same as before, but extracts the clone logic **`_resolve_git_mount_mappings(repo_path, mappings, working_directory) -> list[dict]`** - Takes the already-cloned repo path - For each mapping: 1. Build source path: `os.path.join(repo_path, mapping["source_path"])` 2. Expand globs via `_expand_glob_source` 3. Resolve target path (absolute or relative to working_directory) 4. Build volume mount entries - Returns list of volume mount dicts **`_resolve_single_git_mount` new flow:** 1. Validate entry (remote_url, mappings or source_path+target_path) 2. Normalize legacy format to `mappings` array 3. Compute clone directory (same as before: `git-mounts/{repo_name}-{hash}/`) 4. Clone/pull repo 5. Resolve all mappings from the cloned repo 6. Return flat list of volume mounts #### 2. `_merge_git_mounts` update The merge key changes from `(remote_url, target_path)` to `(remote_url, branch)`. When two entries have the same `remote_url` and `branch`, their `mappings` arrays are concatenated. When different, they are kept as separate entries. ```python def _merge_git_mounts(base, overlay, source_name): result = list(base) seen = {} for i, m in enumerate(result): key = (m["remote_url"], m.get("branch")) seen[key] = i for mount in overlay: key = (mount["remote_url"], mount.get("branch")) if key in seen: # Same repo+branch: concatenate mappings result[seen[key]]["mappings"].extend(mount.get("mappings", [])) else: seen[key] = len(result) result.append(dict(mount)) return result ``` #### 3. Validation on save In the Config Profile API (create/update), validate `git_mounts`: - Each entry must have `remote_url` - Each entry must have either `mappings` OR (`source_path` AND `target_path`) - Each mapping must have `source_path` and `target_path` - `mappings` must be a non-empty array ### Frontend Changes #### GitMountEditor component New or updated component for editing a single git mount entry: ``` Remote URL: [____________________] Branch: [main________________] Mappings: Source Path → Target Path [packages/api ] [/app/api ] [×] [packages/web ] [/app/web ] [×] [ ] [ ] [+ Add] ``` **State shape:** ```typescript interface GitMountMapping { source_path: string; target_path: string; } interface GitMountEntry { remote_url: string; branch?: string; mappings: GitMountMapping[]; // Legacy fields (read-only for old data) source_path?: string; target_path?: string; } ``` **Migration on load:** If an entry has `source_path` and `target_path` but no `mappings`, auto-convert: ```typescript if (!entry.mappings && entry.source_path && entry.target_path) { entry.mappings = [{ source_path: entry.source_path, target_path: entry.target_path }]; } ``` ### File Changes | File | Change | |------|--------| | `apps/api/src/api/tool_instances.py` | Refactor `_resolve_single_git_mount` to support mappings | | `apps/api/src/services/config_profile_resolver.py` | Update `_merge_git_mounts` merge key | | `apps/api/src/api/config_profiles.py` | Add validation for git_mounts schema | | `apps/web/src/components/config-profile-editor.tsx` | Add mappings UI for git mounts | | `apps/web/src/api/config_profiles.ts` | Update types for GitMountEntry | | `apps/api/tests/unit/test_git_mounts.py` | New unit tests for multi-mapping resolution | | `apps/api/tests/unit/test_config_profile_resolver.py` | Update merge tests | ### Testing Strategy #### Backend unit tests 1. `_resolve_single_git_mount` with 3 mappings → single clone, 3 mounts 2. `_resolve_single_git_mount` legacy format → single clone, 1 mount 3. `_merge_git_mounts` same repo+branch → mappings concatenated 4. `_merge_git_mounts` different repos → separate entries 5. Validation: entry with neither mappings nor source_path → error 6. Validation: mapping missing target_path → error #### Integration tests 1. Create profile with 2 mappings from same repo → start instance → verify single clone directory 2. Create profile with legacy format → start instance → verify backward compatibility #### Frontend tests 1. GitMountEditor renders mappings list 2. Adding a mapping updates state correctly 3. Legacy entry auto-converts on load 4. Save sends correct JSON shape ### Migration Plan No database migration. Existing `git_mounts` JSON continues to work because: - The code normalizes legacy `source_path` + `target_path` to `mappings` on read - The frontend auto-converts on load - New saves use the `mappings` format ### Rollback Plan Since there is no schema change, rollback is just reverting the code. Existing profiles with the new `mappings` format will still parse correctly even with old code if we keep the normalization shim.