Remove the supervise flag; supervise every bottle
lint / lint (push) Successful in 2m2s
test / unit (pull_request) Successful in 46s
test / integration (pull_request) Successful in 22s

Issue #249: in practice the per-bottle `supervise` flag was never
turned off — all bottles should be supervised. Remove the manifest
flag and make the supervise sidecar unconditional, mirroring egress.

- Reject `supervise:` as a removed bottle key with a migration hint.
- Drop the `supervise` field from ManifestBottle and the extends merge.
- prepare_supervise always returns a SupervisePlan; the plan type is
  now non-optional and the per-backend `is None` guards are gone, so
  the supervise daemon, current-config mount, aliases, and MCP
  registration always render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YcU7nerbg8cVj9R4EkpfLJ
This commit is contained in:
2026-06-23 18:18:29 -04:00
parent 31cde11b0d
commit bdca1c8bea
32 changed files with 170 additions and 239 deletions
+27 -33
View File
@@ -40,13 +40,11 @@ STAGE = Path("/tmp/cb-stage")
STATE = Path("/tmp/cb-state")
def _manifest(*, supervise: bool, with_git: bool, with_egress: bool) -> ManifestIndex:
def _manifest(*, with_git: bool, with_egress: bool) -> ManifestIndex:
"""Minimal manifest with the toggles the chunk-1 matrix needs.
The renderer only reads from the plan, not the manifest, so this
is just here to back BottleSpec."""
bottle: dict[str, object] = {}
if supervise:
bottle["supervise"] = True
if with_git:
bottle["git-gate"] = {"repos": {
"upstream": {
@@ -111,10 +109,11 @@ def _plan(
*,
with_git: bool = False,
with_egress: bool = False,
supervise: bool = False,
) -> DockerBottlePlan:
"""Build a fully-resolved DockerBottlePlan. Toggles cover the
matrix the renderer's conditional-service logic branches on."""
matrix the renderer's conditional-service logic branches on.
Every bottle is supervised (issue #249), so the supervise plan
is always present."""
upstreams: tuple[GitGateUpstream, ...] = ()
if with_git:
upstreams = (GitGateUpstream(
@@ -136,7 +135,7 @@ def _plan(
roles=(),
),)
index = _manifest(supervise=supervise, with_git=with_git, with_egress=with_egress)
index = _manifest(with_git=with_git, with_egress=with_egress)
spec = BottleSpec(
manifest=index,
agent_name="demo",
@@ -151,7 +150,7 @@ def _plan(
forwarded_env={"CLAUDE_CODE_OAUTH_TOKEN": "x"},
git_gate_plan=_git_gate_plan(upstreams),
egress_plan=_egress_plan(routes),
supervise_plan=_supervise_plan() if supervise else None,
supervise_plan=_supervise_plan(),
use_runsc=False,
agent_provision=AgentProvisionPlan(
template="claude",
@@ -220,10 +219,8 @@ class TestAgentAlwaysPresent(unittest.TestCase):
proxy = [e for e in s["environment"] if e.startswith("HTTPS_PROXY=")][0]
self.assertEqual("HTTPS_PROXY=http://egress:9099", proxy)
def test_agent_no_proxy_adds_supervise_when_enabled(self):
s = bottle_plan_to_compose(
_plan(supervise=True)
)["services"]["agent"]
def test_agent_no_proxy_includes_supervise(self):
s = bottle_plan_to_compose(_plan())["services"]["agent"]
no_proxy = [e for e in s["environment"] if e.startswith("NO_PROXY=")][0]
self.assertIn("supervise", no_proxy)
@@ -259,22 +256,18 @@ class TestAgentAlwaysPresent(unittest.TestCase):
def test_agent_depends_only_on_sidecars(self):
# Bundle shape: the init supervisor owns intra-bundle daemon
# ordering, so the agent waits on the bundle container alone.
for kwargs in [{}, {"with_git": True, "with_egress": True, "supervise": True}]:
for kwargs in [{}, {"with_git": True, "with_egress": True}]:
with self.subTest(**kwargs):
s = bottle_plan_to_compose(_plan(**kwargs))["services"]["agent"]
self.assertEqual(["sidecars"], s["depends_on"])
def test_agent_current_config_mount_only_with_supervise(self):
with_sv = bottle_plan_to_compose(_plan(supervise=True))["services"]["agent"]
def test_agent_current_config_always_mounted(self):
# Every bottle is supervised (issue #249), so the read-only
# current-config mount is always present in the agent.
agent = bottle_plan_to_compose(_plan())["services"]["agent"]
self.assertTrue(any(
v["target"] == "/etc/bot-bottle/current-config"
for v in with_sv.get("volumes", [])
))
without_sv = bottle_plan_to_compose(_plan(supervise=False))["services"]["agent"]
# Either no volumes key at all, or no current-config target.
self.assertFalse(any(
v["target"] == "/etc/bot-bottle/current-config"
for v in without_sv.get("volumes", [])
for v in agent.get("volumes", [])
))
@@ -292,7 +285,7 @@ class TestSidecarBundleShape(unittest.TestCase):
self.assertEqual({"sidecars", "agent"}, set(spec["services"].keys()))
def test_emits_two_services_full_matrix(self):
spec = self._render(with_git=True, with_egress=True, supervise=True)
spec = self._render(with_git=True, with_egress=True)
# Still two services — the bundle absorbs git-gate/egress/supervise.
self.assertEqual({"sidecars", "agent"}, set(spec["services"].keys()))
@@ -315,16 +308,16 @@ class TestSidecarBundleShape(unittest.TestCase):
self.assertIn("egress", aliases)
def test_internal_aliases_omit_inactive_sidecars(self):
# With no git-gate / supervise, those names are NOT aliased
# — keeps the alias list honest about what's actually
# listening inside the bundle.
# With no git-gate, that name is NOT aliased — keeps the alias
# list honest about what's actually listening inside the bundle.
# supervise is always present (issue #249).
sc = self._render()["services"]["sidecars"]
aliases = set(sc["networks"]["internal"]["aliases"])
self.assertNotIn("git-gate", aliases)
self.assertNotIn("supervise", aliases)
self.assertIn("supervise", aliases)
def test_internal_aliases_include_active_sidecars(self):
sc = self._render(with_git=True, supervise=True)["services"]["sidecars"]
sc = self._render(with_git=True)["services"]["sidecars"]
aliases = set(sc["networks"]["internal"]["aliases"])
self.assertIn("git-gate", aliases)
self.assertIn("supervise", aliases)
@@ -336,10 +329,11 @@ class TestSidecarBundleShape(unittest.TestCase):
for line in sc["environment"]
if line.startswith("BOT_BOTTLE_SIDECAR_DAEMONS=")
}
self.assertEqual({"egress"}, daemons)
# egress + supervise are always present (issue #249).
self.assertEqual({"egress,supervise"}, daemons)
def test_daemons_csv_expands_with_optional_sidecars(self):
sc = self._render(with_git=True, supervise=True)["services"]["sidecars"]
sc = self._render(with_git=True)["services"]["sidecars"]
for line in sc["environment"]:
if line.startswith("BOT_BOTTLE_SIDECAR_DAEMONS="):
csv = line.split("=", 1)[1]
@@ -347,7 +341,7 @@ class TestSidecarBundleShape(unittest.TestCase):
else:
self.fail("BOT_BOTTLE_SIDECAR_DAEMONS not in env")
self.assertEqual(
["egress", "git-gate", "supervise"],
["egress", "supervise", "git-gate"],
csv.split(","),
)
@@ -376,7 +370,7 @@ class TestSidecarBundleShape(unittest.TestCase):
self.assertNotIn("EGRESS_TOKEN_0", env_strings)
def test_supervise_env_present_when_active(self):
sc = self._render(supervise=True)["services"]["sidecars"]
sc = self._render()["services"]["sidecars"]
env_strings = sc["environment"]
self.assertIn(f"SUPERVISE_BOTTLE_SLUG={SLUG}", env_strings)
self.assertTrue(any(e.startswith("SUPERVISE_QUEUE_DIR=") for e in env_strings))
@@ -388,7 +382,7 @@ class TestSidecarBundleShape(unittest.TestCase):
self.assertIn("/home/mitmproxy/.mitmproxy/mitmproxy-ca.pem", targets)
def test_volumes_union_full_matrix(self):
sc = self._render(with_git=True, with_egress=True, supervise=True)[
sc = self._render(with_git=True, with_egress=True)[
"services"]["sidecars"]
targets = {v["target"] for v in sc["volumes"]}
self.assertIn("/home/mitmproxy/.mitmproxy/mitmproxy-ca.pem", targets)
@@ -403,7 +397,7 @@ class TestSidecarBundleShape(unittest.TestCase):
self.assertNotIn("extra_hosts", sc)
def test_agent_depends_on_bundle_only(self):
sc = self._render(with_git=True, with_egress=True, supervise=True)[
sc = self._render(with_git=True, with_egress=True)[
"services"]["agent"]
self.assertEqual(["sidecars"], sc["depends_on"])