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
Fourth per-module ratchet under ADR 0004. Cover the pure
`git_gate_render_gitconfig` renderer (empty entries, insteadOf URL,
scheme override, RemoteKey ssh alias with/without non-default port,
newline-injection rejection) and the dynamic gitea deploy-key
lifecycle with the forge provisioner mocked:
- `_provision_dynamic_key`: writes key + key-id files, strips `.git`
from owner/repo, builds the proposal title; missing token raises.
- `revoke_git_gate_provisioned_keys`: revokes a gitea key when the
id-file is present, skips static-provider entries and missing
id-files, raises on a missing token.
bot_bottle/git_gate.py: 70% -> 99% (unit only). Two remaining partial
branches are inner conditionals on the alias/owner-repo paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NkwFXLFff9PYPy4wgVBJp9