docs(prd): add git-gate manifest redesign plan
PRD 0047 proposes replacing git.remotes with a top-level git-gate.repos section and snake_case field names to make clear the config is specifically for git-gate routing, not generic git or SSH config. Closes #160
This commit is contained in:
@@ -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).
|
||||
Reference in New Issue
Block a user