diff --git a/docs/prds/0047-git-gate-manifest-redesign.md b/docs/prds/0047-git-gate-manifest-redesign.md new file mode 100644 index 0000000..3fdd8e0 --- /dev/null +++ b/docs/prds/0047-git-gate-manifest-redesign.md @@ -0,0 +1,168 @@ +# 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).