refactor(de-sidecar): purge the "sidecar" name from live code, tests, and current docs
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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).
|
||||
"""
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
|
||||
@@ -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,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.
|
||||
"""
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,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`,
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user