# PRD 0047: Git-gate Manifest Redesign

- **Status:** Active
- **Author:** didericis
- **Created:** 2026-06-03
- **Issue:** #160

## Summary

Replace the `git` top-level key in bottle and agent manifests with `git-gate`,
consolidating git-identity configuration (`user`) and git-gate sidecar
configuration (`repos`) under a single section. Within `repos`, field names
move to lowercase snake_case and the local repo name is promoted to the YAML
key. The change removes the ambiguity in the current `git` block: its fields
are not generic git or SSH config — they are specifically the credential,
host-trust, and identity material that is managed in relation to git-gate.

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

Splitting `git.user` into a separate section from `git.remotes` also doesn't
help: both concepts exist because of git-gate, and keeping them under a single
`git-gate` key makes their relationship and purpose explicit.

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 and agent key; `git` is removed
  from both allowed-key sets.
- `git-gate.repos` is a named map where each key is the local repo name
  exposed by the gate (bottle-only; rejected at the agent level).
- Each entry in `git-gate.repos` accepts exactly: `url` (required), `identity`
  (required), `host_key` (optional).
- `git-gate.user` replaces `git.user` on both bottles and agents, with the
  same `name` / `email` fields and overlay semantics.
- The manifest parser rejects `git.remotes` and `git.user` with errors that
  point to the new keys.
- `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` and
  `tests/unit/test_manifest_git_user.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` / `git-gate.user` semantics or field names (`name`,
  `email`).
- 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.*` shape is rejected immediately with clear
  error messages pointing to the new keys.
- No change to how agent-level user config 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-gate:
  user:
    name: implementer-bot
    email: eric+implementer@dideric.is
  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..."
```

`git-gate` is the single optional top-level key for all git configuration.
Bottles that previously used only `git.user` now use only `git-gate.user`;
those that used only `git.remotes` now use only `git-gate.repos`.

### 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`: replace `"git"` with `"git-gate"` in `BOTTLE_KEYS`
  and `AGENT_KEYS_OPTIONAL`.
- `manifest.py`: replace `_parse_git_config` with `_parse_git_gate_config`
  that validates both `user` and `repos` subkeys. Update `Bottle.from_dict`
  and `Agent.from_dict` to call it for the `"git-gate"` key.
- `Agent.from_dict` continues to reject `repos` at the agent level with a
  clear error.
- 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.
- Any existing `"git"` key raises a targeted error:

  ```
  bottle 'dev' uses 'git' which has been replaced by 'git-gate' (PRD 0047).
  Move git.user → git-gate.user and git.remotes → git-gate.repos.
  ```

## 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` and bare `git` key
  both rejected.
- `tests/unit/test_manifest_git_user.py` and
  `tests/unit/test_manifest_agent_git_user.py` — update fixtures to use
  `git-gate.user` at both bottle and agent level.

## Open Questions

None.
