refactor(manifest): rename egress_proxy key to egress
test / unit (pull_request) Successful in 16s
test / integration (pull_request) Successful in 1m4s

Now that `bottle.egress` (the old allowlist/dlp_action block) is
gone, the longer `egress_proxy:` disambiguator isn't needed. The
manifest field reads more naturally as just `egress:` with the
same nested `routes: [...]` shape.

Renamed:
  - Manifest YAML key:    `egress_proxy:` → `egress:`
  - Bottle dataclass attr: `bottle.egress_proxy` → `bottle.egress`
  - `_BOTTLE_KEYS` entry, schema docstring, and all
    user-facing error message labels (`egress.routes[N]`,
    `egress has unknown key …`, etc.).

Kept (these refer to the egress-proxy SIDECAR, not the manifest
field):
  - File names: `egress_proxy.py`, `egress_proxy_apply.py`,
    `egress_proxy_addon.py`, `egress_proxy_addon_core.py`.
  - Class names: `EgressProxyConfig`, `EgressProxyRoute`,
    `EgressProxyPlan`, `EgressProxy`, `DockerEgressProxy`.
  - Helper names: `egress_proxy_manifest_routes`,
    `egress_proxy_routes_for_bottle`,
    `egress_proxy_token_env_map`, etc.
  - Constants: `EGRESS_PROXY_HOSTNAME`, `EGRESS_PROXY_ROLES`,
    `EGRESS_PROXY_AUTH_SCHEMES`, `EGRESS_PROXY_FORWARD_PROXY`,
    `EGRESS_PROXY_INTROSPECT_URL`, `EGRESS_PROXY_PORT`, etc.
  - Container name prefix `claude-bottle-egress-proxy-*`, the
    `egress-proxy` docker network alias, the
    `egress-proxy-block` + `list-egress-proxy-routes` MCP tool
    IDs, the `egress-proxy` audit-log component label.

Local bottle migrated (`~/.claude-bottle/bottles/dev.md` already
updated). The legacy `egress_proxy` key isn't surfaced anywhere
anymore; the generic unknown-key validator catches typos with a
"did you mean: egress, env, git, supervise" hint.

409 unit + integration tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 21:25:51 -04:00
parent 6456904763
commit 14c8a51c16
11 changed files with 55 additions and 59 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ from claude_bottle.manifest import Manifest
def _bottle(routes):
return Manifest.from_json_obj({
"bottles": {"dev": {"egress_proxy": {"routes": routes}}},
"bottles": {"dev": {"egress": {"routes": routes}}},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
}).bottles["dev"]
+15 -15
View File
@@ -1,4 +1,4 @@
"""Unit: manifest parsing for `bottle.egress_proxy.routes[]` (PRD 0017).
"""Unit: manifest parsing for `bottle.egress.routes[]` (PRD 0017).
The route shape is new: `host` (required), optional `path_allowlist`,
optional nested `auth: { scheme, token_ref }`. Validation rules per
@@ -13,7 +13,7 @@ from claude_bottle.manifest import EgressProxyRoute, Manifest
def _bottle(routes):
return Manifest.from_json_obj({
"bottles": {"dev": {"egress_proxy": {"routes": routes}}},
"bottles": {"dev": {"egress": {"routes": routes}}},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
}).bottles["dev"]
@@ -21,8 +21,8 @@ def _bottle(routes):
class TestMinimalRoute(unittest.TestCase):
def test_host_only(self):
b = _bottle([{"host": "api.example.com"}])
self.assertEqual(1, len(b.egress_proxy.routes))
r = b.egress_proxy.routes[0]
self.assertEqual(1, len(b.egress.routes))
r = b.egress.routes[0]
self.assertEqual("api.example.com", r.Host)
self.assertEqual((), r.PathAllowlist)
self.assertEqual("", r.AuthScheme)
@@ -44,7 +44,7 @@ class TestMinimalRoute(unittest.TestCase):
class TestPathAllowlist(unittest.TestCase):
def test_optional(self):
b = _bottle([{"host": "x.example"}])
self.assertEqual((), b.egress_proxy.routes[0].PathAllowlist)
self.assertEqual((), b.egress.routes[0].PathAllowlist)
def test_must_be_array(self):
with self.assertRaises(Die):
@@ -65,14 +65,14 @@ class TestPathAllowlist(unittest.TestCase):
}])
self.assertEqual(
("/didericis/", "/users/didericis"),
b.egress_proxy.routes[0].PathAllowlist,
b.egress.routes[0].PathAllowlist,
)
class TestAuth(unittest.TestCase):
def test_omitted_means_no_auth(self):
b = _bottle([{"host": "github.com"}])
r = b.egress_proxy.routes[0]
r = b.egress.routes[0]
self.assertEqual("", r.AuthScheme)
self.assertEqual("", r.TokenRef)
@@ -81,7 +81,7 @@ class TestAuth(unittest.TestCase):
"host": "api.github.com",
"auth": {"scheme": "Bearer", "token_ref": "GH_PAT"},
}])
r = b.egress_proxy.routes[0]
r = b.egress.routes[0]
self.assertEqual("Bearer", r.AuthScheme)
self.assertEqual("GH_PAT", r.TokenRef)
@@ -118,7 +118,7 @@ class TestAuth(unittest.TestCase):
"host": "git.example",
"auth": {"scheme": "token", "token_ref": "GITEA_PAT"},
}])
self.assertEqual("token", b.egress_proxy.routes[0].AuthScheme)
self.assertEqual("token", b.egress.routes[0].AuthScheme)
def test_unknown_auth_key_rejected(self):
with self.assertRaises(Die):
@@ -131,7 +131,7 @@ class TestAuth(unittest.TestCase):
class TestRole(unittest.TestCase):
def test_omitted_means_no_roles(self):
b = _bottle([{"host": "x.example"}])
self.assertEqual((), b.egress_proxy.routes[0].Role)
self.assertEqual((), b.egress.routes[0].Role)
def test_string_normalizes_to_tuple(self):
b = _bottle([{
@@ -140,7 +140,7 @@ class TestRole(unittest.TestCase):
"auth": {"scheme": "Bearer", "token_ref": "T"},
}])
self.assertEqual(("claude_code_oauth",),
b.egress_proxy.routes[0].Role)
b.egress.routes[0].Role)
def test_list_supported(self):
b = _bottle([{
@@ -149,7 +149,7 @@ class TestRole(unittest.TestCase):
"auth": {"scheme": "Bearer", "token_ref": "T"},
}])
self.assertEqual(("claude_code_oauth",),
b.egress_proxy.routes[0].Role)
b.egress.routes[0].Role)
def test_unknown_role_rejected(self):
# The role enum is locked down — typos shouldn't silently
@@ -199,7 +199,7 @@ class TestRouteValidation(unittest.TestCase):
def test_empty_routes_allowed(self):
b = _bottle([])
self.assertEqual((), b.egress_proxy.routes)
self.assertEqual((), b.egress.routes)
def test_no_egress_proxy_block_means_empty(self):
# The bottle dataclass defaults to an empty EgressProxyConfig.
@@ -207,14 +207,14 @@ class TestRouteValidation(unittest.TestCase):
"bottles": {"dev": {}},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
}).bottles["dev"]
self.assertEqual((), b.egress_proxy.routes)
self.assertEqual((), b.egress.routes)
class TestConfigShape(unittest.TestCase):
def test_unknown_egress_proxy_key_rejected(self):
with self.assertRaises(Die):
Manifest.from_json_obj({
"bottles": {"dev": {"egress_proxy": {"wat": []}}},
"bottles": {"dev": {"egress": {"wat": []}}},
"agents": {"demo": {"skills": [], "prompt": "",
"bottle": "dev"}},
})
+5 -5
View File
@@ -22,7 +22,7 @@ def _write(p: Path, text: str) -> None:
_BOTTLE_DEV = """
---
egress_proxy:
egress:
routes:
- host: api.anthropic.com
auth:
@@ -85,7 +85,7 @@ class TestBottleFileParses(_ResolveCase):
_write(self.home_cb / "agents" / "implementer.md", _AGENT_IMPL)
m = self.resolve()
self.assertIn("dev", m.bottles)
routes = m.bottles["dev"].egress_proxy.routes
routes = m.bottles["dev"].egress.routes
self.assertEqual(2, len(routes))
self.assertEqual("api.anthropic.com", routes[0].Host)
self.assertEqual("Bearer", routes[0].AuthScheme)
@@ -132,7 +132,7 @@ class TestCwdAgentOverridesHome(_ResolveCase):
m = self.resolve()
self.assertIn("CWD-OVERRIDE-PROMPT", m.agents["implementer"].prompt)
# Home bottle still present
self.assertEqual(2, len(m.bottles["dev"].egress_proxy.routes))
self.assertEqual(2, len(m.bottles["dev"].egress.routes))
class TestCwdBottlesIgnored(_ResolveCase):
@@ -147,7 +147,7 @@ class TestCwdBottlesIgnored(_ResolveCase):
self.cwd_cb / "bottles" / "dev.md",
"""
---
egress_proxy:
egress:
routes:
- host: attacker.example.com
auth:
@@ -160,7 +160,7 @@ class TestCwdBottlesIgnored(_ResolveCase):
# Home value wins because cwd bottles are ignored entirely.
self.assertEqual(
"api.anthropic.com",
m.bottles["dev"].egress_proxy.routes[0].Host,
m.bottles["dev"].egress.routes[0].Host,
)
+1 -1
View File
@@ -20,7 +20,7 @@ def _bottle(spec):
def _routes(routes):
return {"egress_proxy": {"routes": routes}}
return {"egress": {"routes": routes}}
class TestEffectiveAllowlist(unittest.TestCase):
+3 -3
View File
@@ -109,7 +109,7 @@ class TestBuildConfig(unittest.TestCase):
# up to route claude through pipelock.
from claude_bottle.manifest import Manifest
bottle = Manifest.from_json_obj({
"bottles": {"dev": {"egress_proxy": {"routes": [
"bottles": {"dev": {"egress": {"routes": [
{"host": "api.anthropic.com",
"auth": {"scheme": "Bearer", "token_ref": "T"}},
]}}},
@@ -158,7 +158,7 @@ class TestRenderAndWrite(unittest.TestCase):
"MY_SECRET": "literal-value-should-not-appear",
"ANOTHER": "?prompt-message",
},
"egress_proxy": {"routes": [{"host": "github.com"}]},
"egress": {"routes": [{"host": "github.com"}]},
}
},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
@@ -204,7 +204,7 @@ class TestRenderAndWrite(unittest.TestCase):
def test_render_emits_seed_phrase_off_for_anthropic_route(self):
from claude_bottle.manifest import Manifest
bottle = Manifest.from_json_obj({
"bottles": {"dev": {"egress_proxy": {"routes": [
"bottles": {"dev": {"egress": {"routes": [
{"host": "api.anthropic.com",
"auth": {"scheme": "Bearer", "token_ref": "T"}},
]}}},
+4 -8
View File
@@ -252,7 +252,7 @@ class TestRealisticBottleFile(unittest.TestCase):
def test_dev_bottle(self):
out = _y("""
egress_proxy:
egress:
routes:
- host: api.anthropic.com
auth:
@@ -270,25 +270,21 @@ class TestRealisticBottleFile(unittest.TestCase):
IdentityFile: ~/.ssh/gitea.pem
ExtraHosts:
gitea.dideric.is: 100.78.141.42
egress:
allowlist:
- example.com
""")
# Spot-check the deep parts; the structure is large.
self.assertEqual(2, len(out["egress_proxy"]["routes"]))
self.assertEqual(2, len(out["egress"]["routes"]))
self.assertEqual(
["/didericis/"],
out["egress_proxy"]["routes"][1]["path_allowlist"],
out["egress"]["routes"][1]["path_allowlist"],
)
self.assertEqual(
"Bearer",
out["egress_proxy"]["routes"][0]["auth"]["scheme"],
out["egress"]["routes"][0]["auth"]["scheme"],
)
self.assertEqual(
"100.78.141.42",
out["git"][0]["ExtraHosts"]["gitea.dideric.is"],
)
self.assertEqual(["example.com"], out["egress"]["allowlist"])
class TestFrontmatter(unittest.TestCase):