refactor(de-sidecar): purge the "sidecar" name from live code, tests, and current docs
lint / lint (push) Failing after 2m3s
test / unit (pull_request) Successful in 1m11s
test / integration (pull_request) Successful in 26s
test / coverage (pull_request) Successful in 1m22s

Completes the de-sidecar cleanup: no live code, test, current doc,
script, or nix file mentions or is named "sidecar" any more. Only the
dated PRD/research docs keep the term as historical record (agreed on
the #385 thread).

- Rename `sidecar_init.py`→`gateway_init.py` was done earlier; this pass
  sweeps the remaining descriptive uses: the egress / git-gate / supervise
  components are the gateway's *daemons*, the shared container is the
  *gateway*, the old per-bottle container was the *companion container*.
- Rename `tests/integration/test_sidecar_bundle_image.py`→`test_gateway_image.py`
  and its class; update `docs/ci.md` + `tests/README.md` for the renamed/
  removed integration tests.
- `SIDECAR_PORTS` shell var in `scripts/firecracker-netpool.sh`→`GATEWAY_PORTS`.

Full unit suite green (bar the pre-existing `/bin/sleep`-missing env
errors in test_gateway_init); docker integration — gateway singleton,
broker, real two-bottle multitenant isolation, and the gateway-image
build — all pass.

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-14 17:07:56 -04:00
parent 77948ef56c
commit 09393b354b
71 changed files with 163 additions and 168 deletions
+2 -2
View File
@@ -204,9 +204,9 @@ class AgentProvider(ABC):
bottle: "Bottle",
supervise_url: str,
) -> None:
"""Register the per-bottle supervise sidecar as an MCP server
"""Register the per-bottle supervise daemon as an MCP server
in the provider's in-guest config. Called by the backend after
the supervise sidecar is reachable. No-op when
the supervise daemon is reachable. No-op when
`plan.supervise_plan is None`."""
@abstractmethod
+5 -5
View File
@@ -199,7 +199,7 @@ class ActiveAgent:
bottle is the container, the agent is what runs in it.)
Fields are deliberately backend-neutral. `services` is the set
of sidecar daemons currently up for this bottle (`egress`,
of gateway daemons currently up for this bottle (`egress`,
`git-gate`, `supervise`); the dashboard uses it to
gate edit verbs. `backend_name` is the matching key in
`_BACKENDS` (`docker` / `firecracker` / `macos-container`) — used by the active-
@@ -251,7 +251,7 @@ class Bottle(ABC):
`user` (default `node`, matching the agent image's USER
directive) and return the captured stdout/stderr/returncode.
The bottle's environment (including HTTPS_PROXY pointing at
the egress sidecar) is inherited by the child. Non-zero
the egress daemon) is inherited by the child. Non-zero
exit does not raise — callers inspect `returncode`
themselves.
@@ -454,7 +454,7 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
declarative provision-plan apply, supervise MCP registration)
live on the `AgentProvider` plugin. The backend only owns the
steps that are about backend infrastructure (CA, workspace,
git) and surfaces the supervise sidecar URL its launch step
git) and surfaces the supervise daemon URL its launch step
knows about via `supervise_mcp_url`.
PRD 0017: cred-proxy's agent-side dotfile rewrites (~/.npmrc,
@@ -502,7 +502,7 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
def supervise_mcp_url(self, plan: PlanT) -> str:
"""Return the agent-side URL of the per-bottle supervise
sidecar, or "" when this bottle has no sidecar. The provider
gateway, or "" when this bottle has no gateway. The provider
plugin's `provision_supervise_mcp` uses it to register the
MCP entry inside the guest.
@@ -524,7 +524,7 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
def enumerate_active(self) -> Sequence[ActiveAgent]:
"""Return every currently-running agent on this backend.
Empty when none. Backend-specific: docker queries `docker
compose ls`; firecracker cross-references its running sidecar
compose ls`; firecracker cross-references its running gateway
containers against per-bottle metadata."""
@classmethod
+1 -1
View File
@@ -106,7 +106,7 @@ class DockerBottleBackend(BottleBackend["DockerBottlePlan", "DockerBottleCleanup
yield bottle
def supervise_mcp_url(self, plan: DockerBottlePlan) -> str:
"""Docker bottles reach the supervise sidecar via the
"""Docker bottles reach the supervise daemon via the
compose-network alias `supervise:9100`. No per-bottle URL
plumbing needed; the alias resolves inside the bridge."""
if plan.supervise_plan is None:
@@ -1,8 +1,8 @@
"""Agent-only compose for the consolidated docker backend (PRD 0070).
The per-bottle model rendered a compose project with the agent *and* a
sidecar bundle on two per-bottle networks. In the consolidated model the
sidecars are gone — one shared gateway serves every bottle — so this renders
gateway on two per-bottle networks. In the consolidated model the
per-bottle companion containers are gone — one shared gateway serves every bottle — so this renders
just the agent, attached to the **external shared gateway network** with the
pinned source IP the orchestrator allocated, and pointed at the gateway's
address for egress (and, around the proxy, for git-http / supervise).
@@ -1,7 +1,7 @@
"""Consolidated bottle launch sequence for the docker backend (PRD 0070).
Composes the orchestrator primitives into the register/teardown sequence that
replaces the per-bottle sidecar bundle:
replaces the per-bottle gateway:
1. ensure the orchestrator control plane + shared gateway are up;
2. allocate the bottle a pinned source IP on the gateway network (the
+1 -1
View File
@@ -4,7 +4,7 @@ prepare-time routes-yaml rendering itself lives on the
platform-neutral `Egress` ABC — backends instantiate it directly.
The per-container `.start()` / `.stop()` lifecycle was removed in
PRD 0024 chunk 3; the sidecar bundle (PRD 0024) runs egress
PRD 0024 chunk 3; the gateway (PRD 0024) runs egress
under its python init supervisor."""
from __future__ import annotations
+1 -1
View File
@@ -1,7 +1,7 @@
"""Host-side egress route-apply for the docker backend.
The per-bottle companion container this used to signal (`docker kill
--signal HUP <container>`) was removed in the de-sidecar cleanup (#385).
--signal HUP <container>`) was removed in the companion-container removal (#385).
In the consolidated model the shared gateway resolves egress policy
per-request against the orchestrator rather than reloading a per-bottle
routes file, so the live per-bottle reload is not supported here and
+1 -1
View File
@@ -2,7 +2,7 @@
bind-mounts target + the listening port. The prepare-time entrypoint
/ hook render lives on the platform-neutral `GitGate` ABC — backends
instantiate it directly. The git-gate daemon's container lifecycle
is owned by the sidecar bundle (PRD 0024)."""
is owned by the gateway (PRD 0024)."""
from __future__ import annotations
+1 -1
View File
@@ -5,7 +5,7 @@ PRD 0018 chunk 3: each instance is one `docker compose` project.
The flow is:
1. Build the agent image from the provider Dockerfile (compose
builds the sidecar images via the `build:` directive on first up).
builds the gateway image on first up).
2. Mint the per-bottle egress CA (chunk 2 writes it under
state/<slug>/egress/).
3. Populate the inner plans with launch-time fields so the
+1 -1
View File
@@ -76,7 +76,7 @@ def network_create_internal(slug: str) -> str:
def network_create_egress(slug: str) -> str:
"""Create a per-agent user-defined bridge (NOT the legacy `bridge`)
so the egress sidecar has working DNS for upstream hostnames."""
so the egress daemon has working DNS for upstream hostnames."""
return _network_create_with_prefix(network_egress_name_for_slug(slug), internal=False)
+3 -3
View File
@@ -1,7 +1,7 @@
"""Host setup + status for the Docker backend.
Unlike Firecracker, the Docker backend needs no privileged one-time
host provisioning (no TAP pool / nft table) networks and the sidecar
host provisioning (no TAP pool / nft table) networks and the gateway
bundle are created per-launch. So `setup()` is mostly an install/daemon
pointer, and `status()` reports whether docker is usable.
@@ -45,7 +45,7 @@ def setup() -> int:
return 1
sys.stderr.write(
"Docker backend: no privileged host setup required — networks and "
"the sidecar bundle are created per-launch.\n"
"the gateway are created per-launch.\n"
)
if not _daemon_reachable():
sys.stderr.write(
@@ -64,7 +64,7 @@ def setup() -> int:
def teardown() -> int:
sys.stderr.write(
"Docker backend: nothing to undo — it provisions no privileged host "
"state (networks and the sidecar bundle are per-launch and are "
"state (networks and the gateway are per-launch and are "
"removed by `./cli.py cleanup`). Docker itself is left installed.\n"
)
return 0
+1 -1
View File
@@ -7,7 +7,7 @@ session. `ssh -t` forwards the host terminal's SIGWINCH to the remote
PTY natively, so no separate resize bridge is needed.
Commands run as the image's `node` user via `runuser`, with HOME/USER/
PATH and the bottle env (HTTPS_PROXY at the sidecar, CA paths, ) set
PATH and the bottle env (HTTPS_PROXY at the gateway, CA paths, ) set
per-invocation through `env` (the VM itself just runs the init; it has
no baked-in process env like a `docker run` container would).
"""
@@ -13,7 +13,7 @@ from .. import BottlePlan
class FirecrackerBottlePlan(BottlePlan):
slug: str
forwarded_env: dict[str, str] = field(repr=False)
# Stamped by launch once the sidecar is up and its ports are
# Stamped by launch once the gateway is up and its ports are
# published on the host-side TAP IP (empty at prepare time).
agent_proxy_url: str = ""
agent_git_gate_url: str = ""
@@ -21,7 +21,7 @@ class FirecrackerBottlePlan(BottlePlan):
@property
def container_name(self) -> str:
"""Instance name, reused for the sidecar container + VM run dir.
"""Instance name, reused for the gateway container + VM run dir.
Matches the `bot-bottle-<slug>` convention the other backends
use so cleanup/enumerate discovery-by-prefix keeps working."""
return self.agent_provision.instance_name
+1 -1
View File
@@ -1,6 +1,6 @@
"""Active-agent enumeration for the Firecracker backend.
The backend is disabled during the de-sidecar cleanup (#385) — it can't
The backend is disabled during the companion-container removal (#385) — it can't
launch bottles, so there are none to enumerate. Real enumeration returns
with the backend's consolidated relaunch (#354).
"""
+1 -1
View File
@@ -2,7 +2,7 @@
The firecracker backend launched a per-bottle companion container (the
egress / git-gate / supervise data plane) alongside each microVM. That
per-bottle-companion architecture was removed in the de-sidecar cleanup;
per-bottle-companion architecture was removed in the companion-container removal;
firecracker's replacement — the consolidated per-host gateway — lands in
its own cutover (#354).
+2 -2
View File
@@ -18,7 +18,7 @@ Topology (per slot i):
* a /31 host<->guest link: host = base + 2i (the gateway the VM
routes through), guest = base + 2i + 1 (the VM's address).
* isolation via ``table inet bot_bottle_fc``: a VM reaches only its
own sidecar (DNAT'd from the host TAP IP) and nothing else.
own gateway (DNAT'd from the host TAP IP) and nothing else.
The default IP block is ``10.243.0.0/16`` an intentionally obscure
corner of RFC-1918 private space. RFC-1918 is the range *designated*
@@ -88,7 +88,7 @@ def ip_base() -> str:
return _cfg("BOT_BOTTLE_FC_IP_BASE")
# Sidecar ports the VM reaches at its host-side TAP IP. Kept in sync
# Gateway ports the VM reaches at its host-side TAP IP. Kept in sync
# with the backend constants (egress 9099, supervise 9100, git-http
# 9420); rendered into the setup output for operator visibility.
GATEWAY_PORTS = (9099, 9100, 9420)
@@ -2,7 +2,7 @@
Selectable via `BOT_BOTTLE_BACKEND=macos-container`. This package owns
the Apple `container` CLI integration; launch remains gated until the
sidecar network enforcement shape is implemented.
gateway network enforcement shape is implemented.
"""
from .backend import MacosContainerBottleBackend
@@ -1,7 +1,7 @@
"""Host-side egress route-apply for the macos-container backend.
The per-bottle companion container this used to signal (`container kill
--signal HUP <container>`) was removed in the de-sidecar cleanup (#385),
--signal HUP <container>`) was removed in the companion-container removal (#385),
along with the disabled macOS launch path. Fails closed until the macOS
backend grows the consolidated gateway.
"""
@@ -1,6 +1,6 @@
"""Active-agent enumeration for the macOS Apple Container backend.
The backend is disabled during the de-sidecar cleanup (#385) — it can't
The backend is disabled during the companion-container removal (#385) — it can't
launch bottles, so there are none to enumerate. Enumeration returns when
the backend grows the consolidated gateway.
"""
+1 -1
View File
@@ -3,7 +3,7 @@
This backend launched a per-bottle companion container (the egress /
git-gate / supervise data plane) alongside the agent container, with the
agent's proxy env pointed at the companion's host-only IP. That
per-bottle-companion architecture was removed in the de-sidecar cleanup;
per-bottle-companion architecture was removed in the companion-container removal;
the macOS backend will be re-enabled once it grows the consolidated
per-host gateway the docker backend already uses.
+1 -1
View File
@@ -94,7 +94,7 @@ def prepare_egress(
def prepare_supervise(bottle: ManifestBottle, slug: str) -> SupervisePlan | None:
"""Prepare the supervise sidecar state dir. Returns None when
"""Prepare the supervise daemon state dir. Returns None when
bottle.supervise is falsy."""
if not bottle.supervise:
return None
+9 -9
View File
@@ -44,16 +44,16 @@ _STATE_SUBDIR = "state"
_PER_BOTTLE_DOCKERFILE_NAME = "Dockerfile"
_COMMITTED_IMAGE_NAME = "committed-image"
_TRANSCRIPT_SUBDIR = "transcript"
# Per-sidecar scratch subdirs. PRD 0018 chunk 2: bind-mount sources
# Per-daemon scratch subdirs. PRD 0018 chunk 2: bind-mount sources
# live here so chunk 3's `docker compose up` can find them at stable
# paths. Each sidecar's `prepare()` writes config + CAs into its own
# paths. Each daemon's `prepare()` writes config + CAs into its own
# subdir; the launch step is unchanged today (still `docker cp`).
_EGRESS_SUBDIR = "egress"
_GIT_GATE_SUBDIR = "git-gate"
_SUPERVISE_SUBDIR = "supervise"
_AGENT_SUBDIR = "agent"
_METADATA_NAME = "metadata.json"
# Live-config dir bind-mounted into the supervise sidecar (read-only).
# Live-config dir bind-mounted into the supervise daemon (read-only).
# Host's apply paths keep these files fresh so supervise's
# `list-egress-routes` MCP tool returns the current state —
# not a snapshot from launch time.
@@ -222,7 +222,7 @@ def per_bottle_image_tag(identity: str) -> str:
def live_config_dir(identity: str) -> Path:
"""Per-bottle live-config dir. Bind-mounted read-only into the
supervise sidecar; the host's apply paths refresh the files on
supervise daemon; the host's apply paths refresh the files on
every operator approval so the agent's `list-*` MCP tools always
return current state."""
return bottle_state_dir(identity) / _LIVE_CONFIG_SUBDIR
@@ -260,9 +260,9 @@ def transcript_snapshot_dir(identity: str) -> Path:
return bottle_state_dir(identity) / _TRANSCRIPT_SUBDIR
# --- Per-sidecar scratch subdirs (PRD 0018 chunk 2) ------------------------
# --- Per-daemon scratch subdirs (PRD 0018 chunk 2) ------------------------
#
# Each sidecar gets its own subdir under the bottle's state dir for
# Each daemon gets its own subdir under the bottle's state dir for
# bind-mount sources (config, CAs, hooks, etc.). Prepare-time writes
# land here; the state dir's normal cleanup (`cleanup_state`) reaps
# them along with everything else when the bottle session ends and
@@ -270,20 +270,20 @@ def transcript_snapshot_dir(identity: str) -> Path:
def egress_state_dir(identity: str) -> Path:
"""State subdir for the egress sidecar: routes.yaml + the
"""State subdir for the egress daemon: routes.yaml + the
per-bottle mitmproxy CA. Bind-mount source from chunk 3 onward."""
return bottle_state_dir(identity) / _EGRESS_SUBDIR
def git_gate_state_dir(identity: str) -> Path:
"""State subdir for the git-gate sidecar: entrypoint + hooks +
"""State subdir for the git-gate daemon: entrypoint + hooks +
per-upstream known_hosts. Bind-mount source from chunk 3
onward."""
return bottle_state_dir(identity) / _GIT_GATE_SUBDIR
def supervise_state_dir(identity: str) -> Path:
"""State subdir reserved for supervise sidecar bind-mount sources.
"""State subdir reserved for supervise daemon bind-mount sources.
Runtime queue/audit rows live in the host-level bot-bottle SQLite
database, so they survive state-dir cleanup."""
return bottle_state_dir(identity) / _SUPERVISE_SUBDIR
+2 -2
View File
@@ -2,8 +2,8 @@
Walks every registered backend (docker, firecracker, macos-container)
so a single `./cli.py cleanup` reaps every backend's leftovers — a
firecracker bottle's sidecars won't survive a docker-only cleanup pass
(issue addressed alongside #77).
firecracker bottle's VM processes and run dirs won't survive a
docker-only cleanup pass (issue addressed alongside #77).
Each backend's `prepare_cleanup` enumerates its own resources;
docker's `_list_orphan_state_dirs` consults
+2 -2
View File
@@ -4,7 +4,7 @@ The Claude-specific behavior previously inlined under
`agent_provider.agent_provision_plan` (claude.json trust marker,
api.anthropic.com egress route, OAuth-token placeholder), plus
the `claude mcp add` invocation that registers the supervise
sidecar in claude-code's user config (PRD 0013)."""
gateway in claude-code's user config (PRD 0013)."""
from __future__ import annotations
@@ -293,7 +293,7 @@ class ClaudeAgentProvider(AgentProvider):
supervise_url: str,
) -> None:
"""Run `claude mcp add` inside the agent guest to register the
supervise sidecar in claude-code's user config (~/.claude.json).
supervise daemon in claude-code's user config (~/.claude.json).
Failure is logged but not fatal the bottle still works without
the entry; the operator can register it manually."""
+2 -2
View File
@@ -4,7 +4,7 @@ The Codex-specific behavior previously inlined under
`agent_provider.agent_provision_plan` (config.toml trust marker,
chatgpt.com / api.openai.com egress routes, optional host-credential
forwarding with dummy-auth.json + verify), plus the `codex mcp add`
invocation that registers the supervise sidecar in Codex's
invocation that registers the supervise daemon in Codex's
~/.codex/config.toml (PRD 0050)."""
from __future__ import annotations
@@ -266,7 +266,7 @@ class CodexAgentProvider(AgentProvider):
supervise_url: str,
) -> None:
"""Run `codex mcp add` inside the agent guest to register the
supervise sidecar in Codex's user config (~/.codex/config.toml).
supervise daemon in Codex's user config (~/.codex/config.toml).
Mirrors the Claude provider's `claude mcp add` flow — failure
is logged but not fatal."""
+1 -1
View File
@@ -3,7 +3,7 @@
Pure Python, no mitmproxy dependency. Each detector is a module-level
function returning `ScanResult | None`.
Ships flat into the sidecar bundle image alongside
Ships flat into the gateway image alongside
`egress_addon_core.py` both this file and the package source use
the same try/except import shim pattern.
"""
+3 -3
View File
@@ -2,7 +2,7 @@
This module defines the abstract proxy (`Egress`), its plan
dataclass (`EgressPlan`), and the resolved per-route shape
(`EgressRoute`). The sidecar's start/stop lifecycle is backend-
(`EgressRoute`). The gateway's start/stop lifecycle is backend-
specific and lives on concrete subclasses (see
`bot_bottle/backend/docker/egress.py`).
"""
@@ -87,7 +87,7 @@ class EgressRoute(Route):
Inherits `host`, `matches`, `auth_scheme`, and `token_env`
from `egress_addon_core.Route` those are the fields that cross the
YAML wire into the sidecar. The fields below are host-only and
YAML wire into the gateway. The fields below are host-only and
are never serialised to the addon.
`token_ref` is the host env var the CLI reads at launch and forwards
@@ -386,7 +386,7 @@ class Egress(ABC):
routes_path.write_text(egress_render_routes(routes, log=log))
routes_path.chmod(0o600)
# Generate a per-session fake secret under a plausible random env name.
# The sidecar marks that exact env name as sensitive for known-secret
# The gateway marks that exact env name as sensitive for known-secret
# scanning; the agent receives the same name/value as exfil bait.
canary = secrets.token_urlsafe(32)
return EgressPlan(
+1 -1
View File
@@ -240,7 +240,7 @@ class EgressAddon:
tokens, resolved by source IP in one round-trip (fail-closed to deny-all
+ empty slug if unattributed); `env` is the process env overlaid with
the bottle's tokens, so upstream-auth injection (and DLP) use *this*
bottle's credentials — exactly what the per-bottle sidecar's env did.
bottle's credentials — exactly what the per-bottle gateway daemon's env did.
The identity token, if the agent injected one, is read then stripped so
it never leaks upstream."""
if self._resolver is None:
+1 -1
View File
@@ -7,7 +7,7 @@ exercise the parse + decision functions without depending on the
container.
Imports: stdlib + `yaml_subset` (which is itself stdlib-only and
ships flat into the sidecar bundle image alongside this file
ships flat into the gateway image alongside this file
see `Dockerfile.gateway`)."""
from __future__ import annotations
+1 -1
View File
@@ -6,7 +6,7 @@ and what the proxy does when an outbound detector matches a token
kept apart from the request-time scan/decision flow in `egress_addon_core`
so each half reads top-to-bottom without scrolling past the other.
Stdlib-only; ships flat into the sidecar bundle image alongside
Stdlib-only; ships flat into the gateway image alongside
`egress_addon_core.py` see `Dockerfile.gateway`."""
from __future__ import annotations
+3 -3
View File
@@ -1,8 +1,8 @@
#!/bin/sh
# Egress daemon entrypoint inside the sidecar bundle (PRD 0024).
# Egress daemon entrypoint inside the gateway (PRD 0024).
#
# Extracted verbatim from Dockerfile.egress's prior inline `sh -c`
# ENTRYPOINT so the supervisor in bot_bottle/sidecar_init.py can
# ENTRYPOINT so the supervisor in bot_bottle/gateway_init.py can
# call it as a normal child. Behavior is unchanged:
#
# * Upstream proxy: when EGRESS_UPSTREAM_PROXY is set, switch
@@ -22,7 +22,7 @@ set -e
# Pin mitmproxy's config dir to the bind-mount location of its CA
# regardless of which user mitmdump runs as. In the legacy
# four-sidecar setup (Dockerfile.egress, USER mitmproxy) this
# four-daemon setup (Dockerfile.egress, USER mitmproxy) this
# resolved naturally to `~mitmproxy/.mitmproxy`. In the PRD 0024
# bundle (USER root) `~root/.mitmproxy` is empty, so without this
# flag mitmdump would generate a fresh CA on the wrong path and
+4 -4
View File
@@ -1,6 +1,6 @@
"""Per-agent git-gate (PRD 0008).
A third per-agent sidecar that fronts the bottle's declared git
A third per-agent daemon that fronts the bottle's declared git
upstreams as a transparent mirror. Each `bottle.git` entry maps to
a bare repo on the gate; `git daemon` serves the bare repos over
`git://<gate>/<name>.git`. Two hooks make the mirror bidirectional:
@@ -15,7 +15,7 @@ a bare repo on the gate; `git daemon` serves the bare repos over
The agent never sees the upstream credential under either path.
Why a separate sidecar (not folded into egress or ssh-gate): the
Why a separate daemon (not folded into egress or ssh-gate): the
gate is the only one of the three that holds upstream push
credentials. Mixing it with egress would put push creds in the
same blast radius as internet-facing TLS interception; mixing it
@@ -23,7 +23,7 @@ with ssh-gate would force ssh-gate above L4 and into git-protocol
land. See `docs/prds/0008-git-gate.md`.
This module defines the abstract gate (`GitGate`) and its plan
dataclass (`GitGatePlan`). The sidecar's start/stop lifecycle is
dataclass (`GitGatePlan`). The gateway's start/stop lifecycle is
backend-specific and lives on concrete subclasses (see
`bot_bottle/backend/docker/git_gate.py`)."""
@@ -86,7 +86,7 @@ class GitGatePlan:
class GitGate(ABC):
"""The per-agent git-gate. Encapsulates the host-side prepare
(upstream lift + entrypoint/hook render); the sidecar's
(upstream lift + entrypoint/hook render); the gateway's
start/stop lifecycle is backend-specific and lives on concrete
subclasses."""
+3 -3
View File
@@ -1,7 +1,7 @@
"""Pure host-side rendering for the per-agent git-gate (PRD 0008).
Builds the agent's `.gitconfig` insteadOf rewrites, the known_hosts
line, and the entrypoint / pre-receive / access-hook scripts the sidecar
line, and the entrypoint / pre-receive / access-hook scripts the gateway
runs. No docker or forge calls exposed for tests and reuse across
backends. Split out of `git_gate.py` so the control surface (`GitGate`)
and the deploy-key lifecycle (`git_gate_provision`) each read on their
@@ -16,7 +16,7 @@ from pathlib import Path
from .manifest import ManifestBottle, ManifestGitEntry
# Short network alias for git-gate inside the sidecar bundle. The
# Short network alias for git-gate inside the gateway. The
# agent's `.gitconfig` insteadOf rewrites resolve through this name.
GIT_GATE_HOSTNAME = "git-gate"
# Shared timeout (seconds) for all git-gate subprocess and CGI calls:
@@ -38,7 +38,7 @@ class GitGateUpstream:
KnownHostKey string from the manifest; the gate's start step
materialises it into a known_hosts file if non-empty.
the gate credential paths inside the running sidecar."""
the gate credential paths inside the running gateway."""
name: str
upstream_url: str
+3 -3
View File
@@ -2,7 +2,7 @@
Used where `git://` push traffic over a host-published Docker port can
hang before receive-pack reaches hooks (e.g. the firecracker backend,
where the guest reaches the sidecar over the point-to-point TAP). The
where the guest reaches the gateway over the point-to-point TAP). The
wrapper serves the same `/git/*.git` bare repos through
`git http-backend`, so pre-receive and upstream forwarding remain the
git-gate enforcement point.
@@ -27,7 +27,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import urlsplit
# policy_resolver ships flat alongside this file in the sidecar bundle
# policy_resolver ships flat alongside this file in the gateway
# image (see Dockerfile.gateway); the bot_bottle.* fallback is the
# host-side / test path. Mirrors egress_addon's import shape.
try:
@@ -103,7 +103,7 @@ def resolve_sandbox_root(
return namespace
# Mirrors git_gate_render.GIT_GATE_TIMEOUT_SECS. Duplicated rather than
# imported: this module ships as a flat top-level sibling in the sidecar
# imported: this module ships as a flat top-level sibling in the gateway
# bundle image (see Dockerfile.gateway), not as part of the bot_bottle
# package, so `bot_bottle.git_gate` and its dependency chain aren't
# available at runtime.
+2 -2
View File
@@ -17,7 +17,7 @@ class ManifestAgentProvider:
`template` selects a built-in launch/runtime contract. `dockerfile`
optionally points at a custom agent-image Dockerfile while leaving
bot-bottle's sidecar infrastructure intact.
bot-bottle's gateway infrastructure intact.
`auth_token` names the host env var that holds the provider's OAuth
token (Claude only). The provisioner injects a provider-owned egress
@@ -26,7 +26,7 @@ class ManifestAgentProvider:
so the Claude Code CLI starts.
`forward_host_credentials` forwards the host Codex auth token into
the egress sidecar (Codex only).
the egress daemon (Codex only).
"""
template: str = "claude"
+4 -4
View File
@@ -39,10 +39,10 @@ class ManifestBottle:
# identity without any git-gate.repos upstreams, and vice versa.
git_user: ManifestGitUser = field(default_factory=ManifestGitUser)
egress: ManifestEgressConfig = field(default_factory=ManifestEgressConfig)
# Per-bottle stuck-recovery sidecar (PRD 0013). When true (the
# Per-bottle stuck-recovery daemon (PRD 0013). When true (the
# default, issue #249), the launch step brings up a supervise
# sidecar that exposes egress MCP tools to the agent. Set
# `supervise: false` to skip the sidecar.
# daemon that exposes egress MCP tools to the agent. Set
# `supervise: false` to skip the gateway.
supervise: bool = True
@classmethod
@@ -61,7 +61,7 @@ class ManifestBottle:
raise ManifestError(
f"bottle '{name}' has an 'ssh' field, which has been removed "
f"(PRD 0009). Declare upstreams under 'git-gate.repos' with "
f"url + identity + host_key; the git-gate sidecar (PRD 0008) "
f"url + identity + host_key; the git-gate daemon (PRD 0008) "
f"holds the credential and gitleaks-scans pushes."
)
+1 -1
View File
@@ -39,7 +39,7 @@ def main(argv: list[str] | None = None) -> int:
)
parser.add_argument(
"--gateway", action="store_true",
help="run one consolidated per-host sidecar bundle (build-if-missing)",
help="run one consolidated per-host gateway (build-if-missing)",
)
args = parser.parse_args(argv)
+2 -2
View File
@@ -2,12 +2,12 @@
On a verified launch request it starts a Docker container; on teardown it
removes it. This proves the orchestrator -> backend seam on the cheapest
backend (the sidecar bundle is already containers). Only the request's
backend (the gateway is already containers). Only the request's
static ids/flags reach `docker`, so nothing free-form crosses the boundary.
Slice 3 launches a single container from the request's `image_ref`, named
after the bottle id and labelled for cleanup. Wiring the full agent +
sidecar bundle (networks, mounts, the consolidated sidecar) is a later
gateway (networks, mounts, the consolidated gateway) is a later
slice this is the seam, not the finished launcher.
"""
+2 -2
View File
@@ -1,7 +1,7 @@
"""The consolidated per-host gateway (PRD 0070).
The core consolidation win: **one** persistent gateway per host, shared by
every bottle, instead of a sidecar bundle per bottle. It's safe to share
every bottle, instead of a gateway per bottle. It's safe to share
because the attribution invariant (source IP + identity token, see
`registry`) lets the gateway attribute each request to the right bottle
so per-bottle policy lives in one long-lived process keyed on who's calling.
@@ -46,7 +46,7 @@ GATEWAY_CA_VOLUME = "bot-bottle-gateway-mitmproxy"
GATEWAY_CA_CERT = f"{MITMPROXY_HOME}/mitmproxy-ca-cert.pem"
# The gateway data-plane image + its Dockerfile. Kept as a local constant
# rather than imported from backend.docker.sidecar_bundle, which would drag
# rather than imported from the backend layer, which would drag
# the whole backend layer into the lean orchestrator (see #359); unify when
# that lands. Env override matches the backend's BOT_BOTTLE_GATEWAY_IMAGE.
GATEWAY_IMAGE = os.environ.get("BOT_BOTTLE_GATEWAY_IMAGE", "bot-bottle-gateway:latest")
+1 -1
View File
@@ -5,7 +5,7 @@ registry: turns a prepared bottle's egress plan into the backend-neutral
inputs `Orchestrator.launch_bottle` takes the egress **policy** blob and
launch **metadata**.
The policy blob is the exact routes YAML the per-bottle egress sidecar used
The policy blob is the exact routes YAML the per-bottle egress daemon used
to read from a file; in the consolidated model the multi-tenant gateway's
`PolicyResolver` fetches it from the registry per request (keyed by source
IP) instead. Same render, so consolidated and single-tenant egress apply
+2 -2
View File
@@ -10,7 +10,7 @@ suite points it at a throwaway dir instead of monkey-patching the function
override covers them all), and operators can relocate the root if needed.
This module has no bot-bottle imports, so it is safe to import from any
layer (and to COPY flat into the sidecar bundle).
layer (and to COPY flat into the gateway).
"""
from __future__ import annotations
@@ -35,7 +35,7 @@ def host_db_path() -> Path:
Kept in its own `db/` subdirectory (not directly under the root) so a
backend that can only bind-mount *directories* can share this one file
with a sidecar without exposing the root's other contents (git-gate
with a gateway without exposing the root's other contents (git-gate
keys, per-bottle state, ...)."""
return bot_bottle_root() / "db" / HOST_DB_FILENAME
+1 -1
View File
@@ -23,7 +23,7 @@ closed too rather than silently serving stale or empty policy.
The resolved value is the policy blob the orchestrator stores verbatim; the
consumer parses it (e.g. the egress addon's `load_config`). This module is
stdlib-only and free of bot-bottle imports so it can be COPYed flat into
the sidecar bundle.
the gateway.
"""
from __future__ import annotations
+1 -1
View File
@@ -26,7 +26,7 @@ class QueueStore(DbStore):
if db_path is not None:
resolved = db_path
else:
# In the sidecar container SUPERVISE_DB_PATH points at the
# In the gateway container SUPERVISE_DB_PATH points at the
# bind-mounted host DB. On the host this env var is never set,
# so we always fall through to host_db_path().
env_path = os.environ.get("SUPERVISE_DB_PATH", "").strip()
+14 -14
View File
@@ -1,25 +1,25 @@
"""Per-bottle supervise plane (PRD 0013).
The supervise plane is the per-bottle MCP sidecar plus its host-side
queue/audit support. The sidecar (bot_bottle.supervise_server)
The supervise plane is the per-bottle MCP daemon plus its host-side
queue/audit support. The daemon (bot_bottle.supervise_server)
sits on the bottle's internal network and exposes MCP tools the agent
calls when it needs an operator-reviewed egress change:
* egress-block / allow agent proposes a new routes.yaml
Each tool call: the agent passes the full proposed file plus a
justification text. The sidecar validates the proposal syntactically,
justification text. The gateway validates the proposal syntactically,
writes it to the host SQLite queue table, and holds the tool-call
connection open. The operator's supervise TUI
(bot_bottle.cli.supervise) sees the proposal, accepts
approve / modify / reject, and writes a response row. The sidecar sees
approve / modify / reject, and writes a response row. The gateway sees
the response and returns `{status, notes}` to the agent.
This module defines the host-side library: dataclasses for the queue
record shapes, queue read/write helpers, the audit log writer, and the
diff renderer. The in-container sidecar lives in
diff renderer. The in-gateway daemon lives in
bot_bottle/supervise_server.py; the supervise daemon's container
lifecycle is owned by the sidecar bundle (PRD 0024).
lifecycle is owned by the gateway (PRD 0024).
For 0013 the supervisor's approval handlers are deliberately no-ops:
on approval the audit log is written and the response file is
@@ -75,18 +75,18 @@ except ImportError:
try:
from .paths import bot_bottle_root
except ImportError: # flat imports inside the sidecar bundle
except ImportError: # flat imports inside the gateway
from paths import bot_bottle_root # type: ignore[import-not-found,no-redef] # pylint: disable=import-error,no-name-in-module
SUPERVISE_HOSTNAME = "supervise"
SUPERVISE_PORT = 9100
# The supervise sidecar uses these to query egress's
# The supervise daemon uses these to query egress's
# introspection endpoint for the `list-egress-routes` MCP
# tool. The hostname + port match egress's docker network
# listen port (see backend.docker.egress.EGRESS_PORT). The supervise
# daemon runs inside the sidecar bundle alongside egress, so loopback
# daemon runs inside the gateway alongside egress, so loopback
# is the stable address across docker, firecracker, and Apple
# Container backends.
EGRESS_FORWARD_PROXY = "http://127.0.0.1:9099"
@@ -117,7 +117,7 @@ try:
from .audit_store import AuditStore
from .store_manager import StoreManager
except ImportError:
# Sidecar bundle: files are flat-copied under /app, not a package.
# Gateway: files are flat-copied under /app, not a package.
from queue_store import QueueStore # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
from audit_store import AuditStore # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
from store_manager import StoreManager # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
@@ -222,14 +222,14 @@ def sha256_hex(content: str) -> str:
return hashlib.sha256(content.encode("utf-8")).hexdigest()
# --- Sidecar plan + abstract lifecycle -------------------------------------
# --- Gateway plan + abstract lifecycle -------------------------------------
@dataclass(frozen=True)
class SupervisePlan:
"""Output of Supervise.prepare; consumed by .start.
`db_path` is the host database bind-mounted into the sidecar at
`db_path` is the host database bind-mounted into the gateway at
/run/supervise/bot-bottle.db. `internal_network` is empty at
prepare time; the backend's launch step fills it via
dataclasses.replace before calling .start."""
@@ -240,8 +240,8 @@ class SupervisePlan:
class Supervise(ABC):
"""Per-bottle supervise sidecar. Encapsulates host-side database
staging; the sidecar's start/stop lifecycle is backend-specific."""
"""Per-bottle supervise daemon. Encapsulates host-side database
staging; the gateway's start/stop lifecycle is backend-specific."""
def prepare(
self,
+1 -1
View File
@@ -1,4 +1,4 @@
"""Supervise sidecar HTTP server (PRD 0013).
"""Supervise daemon HTTP server (PRD 0013).
Per-bottle MCP server exposing tools the agent calls to propose egress
config changes when stuck. The tools are `egress-allow`,
+1 -1
View File
@@ -71,7 +71,7 @@ class YamlSubsetError(ValueError):
that want fatal-exit semantics (manifest loader, egress-apply,
etc.) catch this at their own boundary and forward to `die`;
callers running outside the bot-bottle CLI process (the
egress sidecar's addon) handle it as a normal exception."""
egress daemon's addon) handle it as a normal exception."""