# PRD 0047: Git-gate Manifest Redesign - **Status:** Draft - **Author:** didericis - **Created:** 2026-06-03 - **Issue:** #160 ## Summary Replace the `git.remotes` subsection in bottle manifests with a top-level `git-gate` key whose `repos` map uses lowercase snake_case field names and derives the local repo name from the YAML key. The change removes the ambiguity that the current `git` block carries: its fields are not generic git or SSH config — they are specifically the credential and host-trust material the git-gate sidecar needs to mirror each upstream. ## Problem The current bottle manifest uses a `git` top-level key that mixes two concerns: - `git.user` — `git config --global user.name / user.email` identity, which the provisioner injects into the agent's shell and is not gate-specific. - `git.remotes` — upstream URL, identity file, and host key material that the git-gate sidecar consumes; the agent never sees these values. That grouping suggests the `remotes` entries behave like an SSH config or a generic `.gitconfig` remote declaration. They do not. The gate reads the credential material to push upstream after gitleaks passes; the agent's `.gitconfig` receives only the `insteadOf` rewrite that redirects traffic through the gate. Nothing in the current key name or field names signals this. The field names inside each remote entry also use PascalCase (`Name`, `Upstream`, `IdentityFile`, `KnownHostKey`), inconsistent with every other manifest section, which uses snake_case. The current `git.remotes` dict is keyed by upstream host, which works for simple remotes but forces a separate `Name` field to give the gate's bare repo a local label. The host key and `Name` field are often redundant or confusing (e.g., IP-literal upstreams where the key carries no semantic meaning). ## Goals / Success Criteria - `git-gate` is accepted as a top-level bottle key; `git-gate.repos` is a named map where each key is the local repo name exposed by the gate. - Each entry in `git-gate.repos` accepts exactly: `url` (required), `identity` (required), `host_key` (optional). - The `git.remotes` subkey is removed from the `git` block; `git` accepts only `user` (unchanged). - The manifest parser rejects `git.remotes` with an error that points to the new key. - `GitEntry` internal fields are updated to match the new names; all callers (provisioner, git-gate render, plan, tests) compile and pass. - Existing unit tests in `tests/unit/test_manifest_git.py` are rewritten to use the new YAML shape; all other manifest unit tests remain green. - The demo manifest (`bot-bottle.demo.json`) and any examples using the old shape are updated. ## Non-goals - No change to `git.user` semantics or field names. - No change to git-gate runtime behavior (mirroring, gitleaks, access-hook refresh). - No change to the `insteadOf` rewrite the provisioner emits. - No migration shim: the old `git.remotes` shape is rejected immediately with a clear error message. - No change to how agent-level `git.user` overlays the bottle-level value. ## Design ### New manifest shape **Before** (bottle frontmatter): ```yaml git: user: name: implementer-bot email: eric+implementer@dideric.is remotes: gitea.dideric.is: Name: bot-bottle Upstream: ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git IdentityFile: ~/.ssh/gitea-delos-2.pem KnownHostKey: "ssh-rsa AAAA..." ``` **After**: ```yaml git: user: name: implementer-bot email: eric+implementer@dideric.is git-gate: repos: bot-bottle: url: ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git identity: ~/.ssh/gitea-delos-2.pem host_key: "ssh-rsa AAAA..." ``` The `git` block is unchanged and remains optional; `git-gate` is a separate optional top-level key. Bottles that use only `git.user` continue to work without touching `git-gate`. ### Key-name-as-repo-name The YAML key in `git-gate.repos` becomes the local repo name (previously `Name`). The upstream host is no longer the primary key; the provisioner and gate derive it from the `url` field during parse. IP-literal upstreams work without an artificial host-as-key constraint. ### Field renames | Old field | New field | |-----------|-----------| | `Name` (from dict key) | YAML key in `repos` | | `Upstream` | `url` | | `IdentityFile` | `identity` | | `KnownHostKey` | `host_key` | ### Parser changes - `manifest_schema.py`: add `"git-gate"` to `BOTTLE_KEYS`; leave `"git"` in `BOTTLE_KEYS` (it still carries `user`). - `manifest.py`: add `_parse_git_gate_config(bottle_name, raw)` that validates the new shape and returns `tuple[GitEntry, ...]`. Update `Bottle.from_dict` to call it for the `"git-gate"` key. - Remove `from_remote_dict` and update `GitEntry._from_object` to accept the new field names. Internal dataclass field names (`UpstreamUser`, etc.) are unchanged — they are internal plumbing, not user-facing. - `_parse_git_config` narrows to reject `remotes` with a helpful error: ``` bottle 'dev' git.remotes is no longer supported; declare git-gate upstreams under 'git-gate.repos' instead (see PRD 0047). ``` ### Error on rejected key The parser emits the error above whenever `git.remotes` is present, regardless of whether `git-gate` is also present. ## Testing Strategy Run: ``` python3 -m unittest discover -s tests/unit ``` Test files to update: - `tests/unit/test_manifest_git.py` — rewrite fixtures and assertions to use `git-gate.repos` / lowercase fields. Cover: minimal entry, optional `host_key`, missing `url`, missing `identity`, unknown key, IP-literal upstreams, duplicate name rejection, old `git.remotes` shape rejected. ## Open Questions - **`git.user` on agents after `git` narrows.** Today both bottle and agent `git` blocks are validated by the same `_parse_git_config` path. After this change, bottle `git` allows only `user`; agent `git` already only allows `user`. No code change needed — but confirm the agent-side rejection message for `git.remotes` still makes sense once `remotes` is also invalid for bottles (the current message says "remotes is bottle-only"; after this PRD it's invalid everywhere).