feat(git-gate+orchestrator): slice 10 — source-IP-keyed multi-tenant repo namespace #365

Open
didericis-claude wants to merge 2 commits from orchestrator-gitgate-multitenant into orchestrator-rename-gateway
Collaborator

First slice of git-gate consolidation (the docker "Everything" consolidation program). Makes the smart-HTTP git backend serve every bottle from one process, selecting each request's repo root by the calling bottle's source IP — the same attribution invariant + PolicyResolver the multi-tenant egress addon already uses.

Why HTTP, not git daemon

git daemon can't source-IP-route per connection, so the consolidated gateway serves git-gate over the Python git_http_backend.py (HTTP, port 9420 — the transport firecracker/macOS already use). This slice makes that transport multi-tenant; wiring the docker path onto it (replacing git daemon) lands with the launch-integration slice.

Changes

  • policy_resolver: factor out _post_resolve; add resolve_bottle_id (source IP → bottle id, same fail-closed 403→None contract as resolve). The git-gate has no policy blob to parse — the bottle is the namespace.
  • git_http_backend: resolve_repo_root(resolver, base, source_ip, token) — single-tenant passthrough when no resolver; else <base>/<bottle_id>, fail-closed on unattributed / resolver error / namespace-escape. BOT_BOTTLE_ORCHESTRATOR_URL (same env as egress) flips the shared gateway multi-tenant. The identity header is read for attribution and never forwarded to the CGI. Per-repo creds + hooks already scope by repo directory, so isolating the root per bottle isolates its creds too.

Verification

Single-tenant path unchanged — the existing real-git-push tests stay green. New unit coverage for resolve_bottle_id + the repo-root selection matrix (passthrough / attributed / unattributed / error / escape). Full unit suite green (1690 tests; the 13 test_sidecar_init /bin/sleep errors are pre-existing NixOS-local noise).

Stacked on #364 (the Sidecar→Gateway rename).

🤖 Generated with Claude Code

First slice of **git-gate consolidation** (the docker "Everything" consolidation program). Makes the smart-HTTP git backend serve every bottle from one process, selecting each request's repo root by the calling bottle's **source IP** — the same attribution invariant + `PolicyResolver` the multi-tenant egress addon already uses. ### Why HTTP, not `git daemon` `git daemon` can't source-IP-route per connection, so the consolidated gateway serves git-gate over the Python `git_http_backend.py` (HTTP, port 9420 — the transport firecracker/macOS already use). This slice makes that transport multi-tenant; **wiring the docker path onto it** (replacing `git daemon`) lands with the launch-integration slice. ### Changes - **`policy_resolver`**: factor out `_post_resolve`; add `resolve_bottle_id` (source IP → bottle id, same fail-closed `403→None` contract as `resolve`). The git-gate has no policy blob to parse — the bottle *is* the namespace. - **`git_http_backend`**: `resolve_repo_root(resolver, base, source_ip, token)` — single-tenant passthrough when no resolver; else `<base>/<bottle_id>`, **fail-closed** on unattributed / resolver error / namespace-escape. `BOT_BOTTLE_ORCHESTRATOR_URL` (same env as egress) flips the shared gateway multi-tenant. The identity header is read for attribution and never forwarded to the CGI. Per-repo creds + hooks already scope by repo directory, so isolating the *root* per bottle isolates its creds too. ### Verification Single-tenant path unchanged — the existing real-git-push tests stay green. New unit coverage for `resolve_bottle_id` + the repo-root selection matrix (passthrough / attributed / unattributed / error / escape). Full unit suite green (1690 tests; the 13 `test_sidecar_init` `/bin/sleep` errors are pre-existing NixOS-local noise). Stacked on #364 (the Sidecar→Gateway rename). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
didericis-claude added 1 commit 2026-07-13 18:31:18 -04:00
feat(git-gate): source-IP-keyed multi-tenant repo namespace (PRD 0070)
lint / lint (push) Failing after 2m0s
test / unit (pull_request) Successful in 1m1s
test / integration (pull_request) Successful in 20s
test / coverage (pull_request) Successful in 1m19s
08aef30d87
First slice of git-gate consolidation: make the smart-HTTP git backend
serve every bottle from one process, selecting each request's repo root by
the calling bottle's source IP — the same attribution invariant + resolver
the multi-tenant egress addon uses. `git daemon` can't source-IP-route per
connection, so the consolidated gateway serves git-gate over this HTTP
backend (the transport firecracker/macOS already use); wiring the docker
path onto it lands with the launch-integration slice.

- policy_resolver: factor out `_post_resolve`; add `resolve_bottle_id`
  (source IP -> bottle id, same fail-closed 403->None contract as
  `resolve`) — the git-gate has no policy blob to parse, the bottle *is*
  the namespace.
- git_http_backend: `resolve_repo_root(resolver, base, source_ip, token)`
  — single-tenant passthrough when no resolver; else `<base>/<bottle_id>`,
  fail-closed on unattributed / resolver error / namespace escape.
  `BOT_BOTTLE_ORCHESTRATOR_URL` (same env as egress) flips the shared
  gateway multi-tenant; the identity header is read for attribution and
  never forwarded to the CGI. Per-repo creds + hooks scope by repo dir, so
  isolating the root per bottle isolates its creds too.

Single-tenant path unchanged (existing real-git-push tests green). New unit
coverage for the resolver + repo-root selection matrix. Full unit suite
green (1690 tests; the 13 test_sidecar_init /bin/sleep errors are the
pre-existing NixOS-local noise).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
didericis reviewed 2026-07-13 18:36:00 -04:00
@@ -9,0 +13,4 @@
the unspoofable source IP via the orchestrator. Per-repo credentials +
hooks scope by repo directory, so isolating the *root* per bottle isolates
its creds too. Unattributed clients fail closed (404). Unset the legacy
per-bottle single-tenant flat root, unchanged.
Owner

Should note that we're going to remove this/legacy behavior will be stripped out once we've got all the different backends working.

Should note that we're going to remove this/legacy behavior will be stripped out once we've got all the different backends working.
didericis reviewed 2026-07-13 18:38:11 -04:00
@@ -40,10 +106,24 @@ class GitHttpHandler(BaseHTTPRequestHandler):
def do_POST(self) -> None:
self._run_backend()
def _project_root(self) -> Path | None:
Owner

nit, but should change this to _sandbox_root or something that better indicates this is scoped per sandbox, and not a "project" like a specific repo included in git-gate (project is too amorphous/non specific)

nit, but should change this to `_sandbox_root` or something that better indicates this is scoped per sandbox, and not a "project" like a specific repo included in git-gate (project is too amorphous/non specific)
didericis reviewed 2026-07-13 18:41:50 -04:00
@@ -73,0 +75,4 @@
fetches from the orchestrator so revocations / changes / teardowns
are honored immediately. `identity_token` is optional omit it to
resolve by source IP alone (the network-layer attribution).
Raises `PolicyResolveError` if the orchestrator can't be reached."""
Owner

think we should always require the identity token... is there a reason not to?

think we should always require the identity token... is there a reason not to?
didericis added 1 commit 2026-07-13 18:48:00 -04:00
fix(git-gate): review — sandbox naming, ResolverLike Protocol, token-required note
test / unit (pull_request) Successful in 1m8s
test / integration (pull_request) Successful in 22s
test / coverage (pull_request) Successful in 1m20s
lint / lint (push) Successful in 2m7s
115a64c161
Addresses PR #365 review:
- Type `resolve_sandbox_root`'s resolver param as a `ResolverLike` Protocol
  (structural, `resolve_bottle_id` only) — fixes the pyright errors from
  passing a duck-typed fake resolver in tests; mirrors
  egress_addon_core.PolicyResolverLike.
- Rename `_project_root`/`project_root`/`resolve_repo_root`/
  `DEFAULT_PROJECT_ROOT` -> `_sandbox_root`/`sandbox_root`/
  `resolve_sandbox_root`/`DEFAULT_REPO_ROOT`: "sandbox" names the per-tenant
  scope; "project" was too amorphous. `GIT_PROJECT_ROOT` keeps git's own
  env-var name.
- Note the single-tenant path is transitional (stripped once every backend
  runs the consolidated gateway).
- policy_resolver: document that the optional identity token is
  transitional — the consolidated end state requires it (flips at the
  /resolve boundary once token *delivery* lands, a PRD 0070 open question).
- Split the long line in main() (was 105 cols).

pyright 0 errors; pylint 9.95/10; unit suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
Author
Collaborator

Addressed in 115a64c.

git_http_backend.py — "note that the legacy single-tenant behavior gets stripped out" (#3411): Agreed. Added the caveat in both the module docstring and resolve_sandbox_root's docstring — the resolver is None flat-root path is transitional and comes out once every backend runs the consolidated gateway.

git_http_backend.py_project_root naming (#3413): Renamed to sandbox: _project_root_sandbox_root, the local project_rootsandbox_root, resolve_repo_rootresolve_sandbox_root, DEFAULT_PROJECT_ROOTDEFAULT_REPO_ROOT. Kept GIT_PROJECT_ROOT as-is (that's git's own env-var name, not ours).

policy_resolver.py — always require the identity token (#3415): Good instinct, and I agree it's the right end state — defense-in-depth only counts if it's enforced, so a misconfigured /31+nft shouldn't silently degrade to "source IP is the sole attributor." The one real reason it's optional today: identity-token delivery is still a PRD 0070 open question ("exactly how the per-bottle token is placed where the agent can present it but not swap in another's"). Until that exists, the agent has no token to send, so requiring it would make consolidated mode dead-on-arrival. Source-IP-only is the transitional bridge — sound by construction on Firecracker (/31+nft make source IP unforgeable), genuinely weaker on docker.

So the plan: flip it to mandatory at the /resolve boundary (the endpoint rejects an empty token) as part of the launch-integration slice that also wires token delivery — not as a standalone change here, since enforcing it before delivery exists just breaks the path. I documented that trajectory in resolve's docstring so it's not lost. If you'd rather I enforce it now and let that pull token-delivery forward into this arc, say the word.

pyright 0 errors, pylint 9.95/10, unit suite green after these changes.

Addressed in `115a64c`. **`git_http_backend.py` — "note that the legacy single-tenant behavior gets stripped out" (#3411):** Agreed. Added the caveat in both the module docstring and `resolve_sandbox_root`'s docstring — the `resolver is None` flat-root path is transitional and comes out once every backend runs the consolidated gateway. **`git_http_backend.py` — `_project_root` naming (#3413):** Renamed to `sandbox`: `_project_root`→`_sandbox_root`, the local `project_root`→`sandbox_root`, `resolve_repo_root`→`resolve_sandbox_root`, `DEFAULT_PROJECT_ROOT`→`DEFAULT_REPO_ROOT`. Kept `GIT_PROJECT_ROOT` as-is (that's git's own env-var name, not ours). **`policy_resolver.py` — always require the identity token (#3415):** Good instinct, and I agree it's the right *end state* — defense-in-depth only counts if it's enforced, so a misconfigured `/31`+nft shouldn't silently degrade to "source IP is the sole attributor." The one real reason it's optional today: identity-token **delivery** is still a PRD 0070 open question (*"exactly how the per-bottle token is placed where the agent can present it but not swap in another's"*). Until that exists, the agent has no token to send, so requiring it would make consolidated mode dead-on-arrival. Source-IP-only is the transitional bridge — sound *by construction* on Firecracker (`/31`+nft make source IP unforgeable), genuinely weaker on docker. So the plan: flip it to mandatory at the `/resolve` boundary (the endpoint rejects an empty token) as part of the launch-integration slice that also wires token delivery — not as a standalone change here, since enforcing it before delivery exists just breaks the path. I documented that trajectory in `resolve`'s docstring so it's not lost. If you'd rather I enforce it now and let that pull token-delivery forward into this arc, say the word. pyright 0 errors, pylint 9.95/10, unit suite green after these changes.
Owner

@didericis-claude file an issue RE the token

@didericis-claude file an issue RE the token
Author
Collaborator

Filed as #366Require the per-bottle identity token at /resolve (retire source-IP-only attribution).

Captures the why (delivery is the blocker), the end-state change (/resolve rejects an empty token, drop the resolver default + the source-IP-only branch), and that it's sequenced with the launch-integration slice that wires token delivery. The transitional caveats already in the code point at this direction.

Filed as #366 — *Require the per-bottle identity token at /resolve (retire source-IP-only attribution)*. Captures the why (delivery is the blocker), the end-state change (`/resolve` rejects an empty token, drop the resolver default + the source-IP-only branch), and that it's sequenced **with the launch-integration slice** that wires token delivery. The transitional caveats already in the code point at this direction.
didericis-claude changed title from feat(git-gate): source-IP-keyed multi-tenant repo namespace to feat(git-gate): slice 10 — source-IP-keyed multi-tenant repo namespace 2026-07-13 19:33:37 -04:00
didericis-claude changed title from feat(git-gate): slice 10 — source-IP-keyed multi-tenant repo namespace to feat(git-gate+orchestrator): slice 10 — source-IP-keyed multi-tenant repo namespace 2026-07-13 19:37:15 -04:00
Some checks are pending
test / unit (pull_request) Successful in 1m8s
test / integration (pull_request) Successful in 22s
test / coverage (pull_request) Successful in 1m20s
lint / lint (push) Successful in 2m7s
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin orchestrator-gitgate-multitenant:orchestrator-gitgate-multitenant
git checkout orchestrator-gitgate-multitenant
Sign in to join this conversation.