Cover all six pathological character classes (single-quote,
double-quote, space, semicolon, newline, backtick) in both
upstream URL and name positions. Each case validates rendered
output via `sh -n` and asserts the original value is preserved
verbatim after shlex.quote encoding. Also add `sh -n` smoke
tests for the static pre-receive and access-hook scripts.
Use shlex.quote() on name and upstream_url in git_gate_render_entrypoint()
so special characters (single quotes, spaces, semicolons) cannot break or
inject into the generated sh script.
Add _GIT_NAME_RE validation in GitEntry.from_repos_entry() to restrict
repo names to [A-Za-z0-9._-]+, making the manifest the first line of
defence and shlex.quote() the belt-and-suspenders backstop.
Closes#155
A new ref made the pre-receive hook scan the full ancestry
(`log_opts="$new"`), so historical test-fixture findings rejected every
new-branch push (#106). Scope it to `$new --not --all` — only commits
new to the gate, which (since the bare repo is populated solely by
upstream mirror-fetch and gitleaks-gated pushes) loses no coverage on
what a push actually brings to the upstream. Also add BatchMode=yes +
ConnectTimeout=10 to both the forward and access-hook ssh so an
unreachable upstream fails fast instead of hanging.
Refs #106
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GitGateUpstream carries each entry's extra_hosts; a new
git_gate_aggregate_extra_hosts() merges them into one map for the
gate container's /etc/hosts. Same host -> same IP is harmless
duplication; same host -> different IPs is a manifest bug
(/etc/hosts is per-container, not per-upstream) and dies with
the conflicting upstream names.
DockerGitGate.start passes one --add-host host:ip per merged
entry on docker create. Empty map (the default) emits no flags
and is a no-op for bottles that don't need DNS overrides.
The gate is now a transparent mirror, not push-only. Per-repo
init now runs `git remote add --mirror=fetch origin <url>` so a
later `git fetch origin` mirrors the upstream's full ref graph at
canonical paths. The pre-receive hook forwards accepted refs via
`git push origin` (renamed from upstream).
New: an access-hook script wired via `git daemon --access-hook`
runs `git fetch origin --prune` against the real upstream before
every upload-pack request (clone, fetch, pull, ls-remote). On
upstream error the hook exits non-zero — the agent's fetch fails
rather than the gate serving stale data.
The pre-existing smoke test (ls-remote against unreachable
upstream returns refs) had to invert: under the bidirectional
design any ls-remote success is necessarily a success against
the upstream, so the unreachable-upstream case now correctly
fails closed.
Mirrors the SSHGate/PipelockProxy shape: a host-side prepare that
lifts bottle.git into a tuple of GitGateUpstreams and renders two
shell scripts under stage_dir — the gate's entrypoint (which
initializes a bare repo per upstream and execs git daemon
--enable=receive-pack) and the shared pre-receive hook
(gitleaks-scan, then forward each accepted ref to the real
upstream using the per-repo credential).
Failure in either hook phase aborts the push so the agent sees a
real rejection, not a silent success. KnownHostKey absence is
fail-closed: the hook refuses to forward without a pinned key
rather than TOFU-trusting the upstream from inside the gate.
PRD: docs/prds/0008-git-gate.md