feat(gateway): mandatory identity-token attribution on every data plane (PR #354 review)
lint / lint (push) Successful in 2m12s
test / unit (pull_request) Successful in 1m12s
test / integration (pull_request) Successful in 27s
test / coverage (pull_request) Successful in 1m21s

Codex review: the /31 TAP doesn't make source IP unspoofable, and the
app-layer token was returned by launch but never delivered or enforced, so
a spoofed source could select a victim bottle's policy/tokens. Make the
token mandatory and deliver it on each attributed plane (anti-spoof landed
separately as the network boundary).

Enforcement (control plane):
- `Orchestrator.resolve` now requires a matching (source_ip, identity_token)
  pair (constant-time) — no source-IP-only fallback. `/resolve` fail-closes
  (403) on a missing/empty/mismatched token.

Delivery, per plane (the token is `token_urlsafe`, safe in a URL):
- egress: proxy credentials (`HTTPS_PROXY=http://bottle:<token>@gw`). The
  addon reads `Proxy-Authorization` — from the request (HTTP) or captured at
  the CONNECT for HTTPS tunnels (keyed by client conn, cleared on disconnect)
  — validates, and strips it (+ the legacy header) before upstream.
- git-http: a URL-scoped `http.<gate>/.extraHeader: x-bot-bottle-identity`
  in the agent's git config (only over the http transport).
- supervise: `mcp add --header x-bot-bottle-identity: <token>` (claude +
  codex); the server reads the header and passes it to resolve.

Wiring: thread `ctx.identity_token` onto the firecracker + docker plans and
into the agent env/config at launch.

Verified on a KVM host: egress with the correct proxy-cred token returns
200 (HTTP and HTTPS/CONNECT), and no-token / wrong-token return 403; a real
`cli.py start --backend=firecracker` launch provisions git config + the
supervise MCP header and reaches the agent session, all under mandatory
enforcement. Fixed a `claude mcp add` arg-order bug (--header must follow
the positional name/url) found by that launch.

Transparent proxy for tools that ignore proxy env is deferred to a
follow-up (see thread); anti-spoof + host firewall remain the fail-closed
boundary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
This commit is contained in:
2026-07-16 17:02:41 -04:00
parent 914f01fa8f
commit d4b27ebf1f
16 changed files with 177 additions and 27 deletions
+13
View File
@@ -19,6 +19,9 @@ from .manifest import ManifestBottle, ManifestGitEntry
# Short network alias for git-gate inside the gateway. The
# agent's `.gitconfig` insteadOf rewrites resolve through this name.
GIT_GATE_HOSTNAME = "git-gate"
# App-layer identity token header the agent's git sends to git-http and the
# gateway validates (mirrors egress_addon / git_http_backend IDENTITY_HEADER).
IDENTITY_HEADER = "x-bot-bottle-identity"
# Shared timeout (seconds) for all git-gate subprocess and CGI calls:
# git daemon (--timeout/--init-timeout), the access-hook subprocess in
# git_http_backend, and the git http-backend CGI subprocess.
@@ -75,6 +78,7 @@ def _gitconfig_validate_value(field: str, value: str) -> None:
def git_gate_render_gitconfig(
entries: tuple[ManifestGitEntry, ...], gate_host: str, *, scheme: str = "git",
identity_token: str = "",
) -> str:
"""Render the agent's ~/.gitconfig content for git-gate
`insteadOf` rewrites. Pure host-side, no docker / VM;
@@ -96,6 +100,15 @@ def git_gate_render_gitconfig(
"# the upstream bidirectionally (gitleaks-scanned push;\n",
"# fetch-from-upstream-before-every-upload-pack via access-hook).\n",
]
# Over the smart-HTTP transport (VM backends), attach the per-bottle
# identity token as a request header on requests to the gate, scoped to
# its URL so it never goes to any other remote. git-http requires it (the
# gateway's mandatory (source_ip, token) attribution). git:// (single-tenant
# docker) carries no header — attribution there is the network alias.
if identity_token and scheme == "http":
_gitconfig_validate_value("identity_token", identity_token)
out.append(f'[http "http://{gate_host}/"]\n')
out.append(f"\textraHeader = {IDENTITY_HEADER}: {identity_token}\n")
for entry in entries:
_gitconfig_validate_value(f"repos[{entry.Name!r}].url", entry.Upstream)
out.append(f'[url "{scheme}://{gate_host}/{entry.Name}.git"]\n')