fix(supervise): stage current-config routes file as routes.yaml
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m6s

The supervise sidecar mounted a snapshot named routes.json into
the agent at /etc/claude-bottle/current-config/routes.json, but
the egress-proxy-block tool description (and the live proxy file
the apply step writes) say routes.yaml. The agent couldn't find
the file at the documented path, composed proposals against stale
or empty current state, and reported "routes wasn't updated on
disk" because it was looking at the wrong filename.

Rename the staged file to routes.yaml so the tool description,
the staged snapshot, and the live proxy file all agree on the
name. Content stays JSON-in-a-yaml-extension (per PRD 0017
chunk 1's decision: every JSON document is valid YAML, stdlib
parsers handle it on both ends).

Note: the staged file is still a one-shot snapshot taken at
bottle prep time. It does NOT auto-update when the operator
approves an egress-proxy-block. Agents that want to verify
their proposal took effect should retry the request that
triggered the block — a successful upstream response is the
real signal. Fixing the snapshot-staleness UX is a separate
follow-up.

Tests migrated from routes.json → routes.yaml. 364 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 17:01:12 -04:00
parent c4cf2453e2
commit fad76d3364
4 changed files with 15 additions and 9 deletions
+1 -1
View File
@@ -278,7 +278,7 @@ def _run_agent_container(plan: DockerBottlePlan, internal_network: str) -> str:
docker_args.extend(["-e", name])
# PRD 0013: read-only current-config mount so the agent can read
# routes.json / allowlist / Dockerfile before composing a
# routes.yaml / allowlist / Dockerfile before composing a
# supervise tool-call proposal. Mounted from the per-bottle
# stage_dir/current-config/ populated at prepare time.
if plan.supervise_plan is not None:
+8 -2
View File
@@ -425,7 +425,13 @@ def sha256_hex(content: str) -> str:
# Filenames inside the per-bottle current-config dir. The agent reads
# these (read-only) from CURRENT_CONFIG_DIR_IN_AGENT and proposes
# modified versions back via the three MCP tools.
CURRENT_CONFIG_ROUTES = "routes.json"
# Filename of the staged egress-proxy routes file inside the agent's
# read-only current-config mount. JSON content under a `.yaml`
# extension to match the live file the egress-proxy sidecar reads
# (`/etc/egress-proxy/routes.yaml`) — the egress-proxy-block tool
# description points at this exact path, and the apply step writes
# the new content to the matching live path.
CURRENT_CONFIG_ROUTES = "routes.yaml"
CURRENT_CONFIG_ALLOWLIST = "allowlist"
CURRENT_CONFIG_DOCKERFILE = "Dockerfile"
@@ -437,7 +443,7 @@ class SupervisePlan:
`queue_dir` is the host directory bind-mounted into the sidecar
at /run/supervise/queue. `current_config_dir` is the host
directory bind-mounted (read-only) into the *agent* container at
/etc/claude-bottle/current-config, holding routes.json + allowlist
/etc/claude-bottle/current-config, holding routes.yaml + allowlist
+ Dockerfile so the agent can read them before composing a
proposal. `internal_network` is empty at prepare time; the
backend's launch step fills it via dataclasses.replace before
+1 -1
View File
@@ -38,7 +38,7 @@ FIXED = datetime(2026, 5, 25, 12, 0, 0, tzinfo=timezone.utc)
def _proposal(slug: str = "dev", tool: str = TOOL_EGRESS_PROXY_BLOCK) -> Proposal:
# Per-tool payload shape: cred-proxy gets routes.json, pipelock
# Per-tool payload shape: cred-proxy gets routes.yaml, pipelock
# gets a failed URL (PR #25 follow-up), capability gets a
# Dockerfile-ish blob. Match the production dispatch in
# PROPOSED_FILE_FIELD.
+5 -5
View File
@@ -297,9 +297,9 @@ class TestDiffAndHash(unittest.TestCase):
self.assertEqual("", render_diff("a\nb\n", "a\nb\n"))
def test_render_diff_shows_changes(self):
diff = render_diff("a\nb\nc\n", "a\nB\nc\n", label="routes.json")
self.assertIn("routes.json (current)", diff)
self.assertIn("routes.json (proposed)", diff)
diff = render_diff("a\nb\nc\n", "a\nB\nc\n", label="routes.yaml")
self.assertIn("routes.yaml (current)", diff)
self.assertIn("routes.yaml (proposed)", diff)
self.assertIn("-b", diff)
self.assertIn("+B", diff)
@@ -365,7 +365,7 @@ class TestSupervisePrepare(unittest.TestCase):
self.assertTrue(plan.current_config_dir.is_dir())
self.assertEqual(
'{"routes": [{"path": "/x/"}]}\n',
(plan.current_config_dir / "routes.json").read_text(),
(plan.current_config_dir / "routes.yaml").read_text(),
)
self.assertEqual(
"example.com\n",
@@ -382,7 +382,7 @@ class TestSupervisePrepare(unittest.TestCase):
plan = _StubSupervise().prepare("dev", self.stage_dir)
self.assertEqual(
'{"routes": []}\n',
(plan.current_config_dir / "routes.json").read_text(),
(plan.current_config_dir / "routes.yaml").read_text(),
)