# 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.
