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
6.1 KiB
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.emailidentity, 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-gateis accepted as a top-level bottle key;git-gate.reposis a named map where each key is the local repo name exposed by the gate.- Each entry in
git-gate.reposaccepts exactly:url(required),identity(required),host_key(optional). - The
git.remotessubkey is removed from thegitblock;gitaccepts onlyuser(unchanged). - The manifest parser rejects
git.remoteswith an error that points to the new key. GitEntryinternal 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.pyare 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.usersemantics or field names. - No change to git-gate runtime behavior (mirroring, gitleaks, access-hook refresh).
- No change to the
insteadOfrewrite the provisioner emits. - No migration shim: the old
git.remotesshape is rejected immediately with a clear error message. - No change to how agent-level
git.useroverlays the bottle-level value.
Design
New manifest shape
Before (bottle frontmatter):
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:
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"toBOTTLE_KEYS; leave"git"inBOTTLE_KEYS(it still carriesuser). -
manifest.py: add_parse_git_gate_config(bottle_name, raw)that validates the new shape and returnstuple[GitEntry, ...]. UpdateBottle.from_dictto call it for the"git-gate"key. -
Remove
from_remote_dictand updateGitEntry._from_objectto accept the new field names. Internal dataclass field names (UpstreamUser, etc.) are unchanged — they are internal plumbing, not user-facing. -
_parse_git_confignarrows to rejectremoteswith 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 usegit-gate.repos/ lowercase fields. Cover: minimal entry, optionalhost_key, missingurl, missingidentity, unknown key, IP-literal upstreams, duplicate name rejection, oldgit.remotesshape rejected.
Open Questions
git.useron agents aftergitnarrows. Today both bottle and agentgitblocks are validated by the same_parse_git_configpath. After this change, bottlegitallows onlyuser; agentgitalready only allowsuser. No code change needed — but confirm the agent-side rejection message forgit.remotesstill makes sense onceremotesis also invalid for bottles (the current message says "remotes is bottle-only"; after this PRD it's invalid everywhere).