refactor: rename egress-proxy → egress everywhere
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m10s

The manifest key is `egress:` now; finish the rename so the rest of
the codebase matches. Files (Dockerfile.egress, claude_bottle/egress.py
etc.), classes (Egress, EgressConfig, EgressRoute, EgressPlan,
DockerEgress), constants (EGRESS_HOSTNAME, EGRESS_ROUTES, ...),
container name prefix (claude-bottle-egress-*), docker network alias
(egress), the introspection host (_egress.local), the MCP tool IDs
(egress-block, list-egress-routes), and the preflight label all drop
the `-proxy` suffix.
This commit is contained in:
2026-05-25 21:59:47 -04:00
parent 14c8a51c16
commit 1e5b0dcfca
30 changed files with 583 additions and 583 deletions
+34 -34
View File
@@ -123,10 +123,10 @@ class GitEntry:
)
# Auth schemes for the egress-proxy route's optional `auth` block.
# Auth schemes for the egress route's optional `auth` block.
# Same values cred-proxy accepts today; `token` sidesteps the Gitea
# token-not-Bearer quirk (go-gitea/gitea#16734).
EGRESS_PROXY_AUTH_SCHEMES = ("Bearer", "token")
EGRESS_AUTH_SCHEMES = ("Bearer", "token")
# Optional per-route role markers. A role signals "this route plays
# a specific named part in the bottle's auth flow"; the launch step
@@ -141,10 +141,10 @@ EGRESS_PROXY_AUTH_SCHEMES = ("Bearer", "token")
# logic — declare the role on whichever route
# injects the OAuth header.
#
# Routes without a `role` are pure proxy entries: egress-proxy
# Routes without a `role` are pure proxy entries: egress
# enforces path_allowlist + injects auth on its own, but nothing
# special happens on the agent side.
EGRESS_PROXY_ROLES = frozenset({
EGRESS_ROLES = frozenset({
"claude_code_oauth",
})
@@ -152,14 +152,14 @@ EGRESS_PROXY_ROLES = frozenset({
# claude_code_oauth drives a single placeholder env var; two routes
# claiming it would leave "which one is the canonical OAuth route?"
# ambiguous for any future role-aware logic.
EGRESS_PROXY_SINGLETON_ROLES = frozenset({
EGRESS_SINGLETON_ROLES = frozenset({
"claude_code_oauth",
})
@dataclass(frozen=True)
class EgressProxyRoute:
"""One route on the per-bottle egress-proxy sidecar (PRD 0017).
class EgressRoute:
"""One route on the per-bottle egress sidecar (PRD 0017).
`Host` matches the request's hostname (case-insensitive). The
optional `PathAllowlist` constrains the URL path to a set of
@@ -171,7 +171,7 @@ class EgressProxyRoute:
no Authorization is written, no token forwarded.
`Role` is an optional tuple of named markers (see
EGRESS_PROXY_ROLES). The launch step reads these and triggers
EGRESS_ROLES). The launch step reads these and triggers
associated side effects (e.g. the `claude_code_oauth` marker
causes prepare.py to set a placeholder OAuth env on the agent).
@@ -183,8 +183,8 @@ class EgressProxyRoute:
error rather than a synonym for "no auth" (omit `auth` for
that case).
- `role` optional. String or list of strings drawn from
EGRESS_PROXY_ROLES. Singleton roles (see
EGRESS_PROXY_SINGLETON_ROLES) may appear on at most one
EGRESS_ROLES. Singleton roles (see
EGRESS_SINGLETON_ROLES) may appear on at most one
route per bottle.
"""
@@ -195,7 +195,7 @@ class EgressProxyRoute:
Role: tuple[str, ...] = ()
@classmethod
def from_dict(cls, bottle_name: str, idx: int, raw: object) -> "EgressProxyRoute":
def from_dict(cls, bottle_name: str, idx: int, raw: object) -> "EgressRoute":
label = f"bottle '{bottle_name}' egress.routes[{idx}]"
d = _as_json_object(raw, label)
host = d.get("host")
@@ -243,10 +243,10 @@ class EgressProxyRoute:
f"{label} auth.scheme is required when 'auth' is set "
f"(non-empty string)"
)
if auth_scheme_raw not in EGRESS_PROXY_AUTH_SCHEMES:
if auth_scheme_raw not in EGRESS_AUTH_SCHEMES:
die(
f"{label} auth.scheme {auth_scheme_raw!r} is not one of "
f"{', '.join(EGRESS_PROXY_AUTH_SCHEMES)}"
f"{', '.join(EGRESS_AUTH_SCHEMES)}"
)
token_ref_raw = auth_d.get("token_ref")
if not isinstance(token_ref_raw, str) or not token_ref_raw:
@@ -283,10 +283,10 @@ class EgressProxyRoute:
f"(was {type(role_raw).__name__})"
)
for r in roles:
if r not in EGRESS_PROXY_ROLES:
if r not in EGRESS_ROLES:
die(
f"{label} role {r!r} is not one of "
f"{', '.join(sorted(EGRESS_PROXY_ROLES))}"
f"{', '.join(sorted(EGRESS_ROLES))}"
)
for k in d:
@@ -306,19 +306,19 @@ class EgressProxyRoute:
@dataclass(frozen=True)
class EgressProxyConfig:
"""Per-bottle egress-proxy configuration. Today this is just the
class EgressConfig:
"""Per-bottle egress configuration. Today this is just the
route table; the nesting under `egress:` leaves room for
per-bottle proxy settings (port override, log level, etc.) in
follow-ups."""
routes: tuple[EgressProxyRoute, ...] = ()
routes: tuple[EgressRoute, ...] = ()
@classmethod
def from_dict(cls, bottle_name: str, raw: object) -> "EgressProxyConfig":
def from_dict(cls, bottle_name: str, raw: object) -> "EgressConfig":
d = _as_json_object(raw, f"bottle '{bottle_name}' egress")
routes_raw = d.get("routes")
routes: tuple[EgressProxyRoute, ...] = ()
routes: tuple[EgressRoute, ...] = ()
if routes_raw is not None:
if not isinstance(routes_raw, list):
die(
@@ -327,10 +327,10 @@ class EgressProxyConfig:
)
routes_list = cast(list[object], routes_raw)
routes = tuple(
EgressProxyRoute.from_dict(bottle_name, i, entry)
EgressRoute.from_dict(bottle_name, i, entry)
for i, entry in enumerate(routes_list)
)
_validate_egress_proxy_routes(bottle_name, routes)
_validate_egress_routes(bottle_name, routes)
for k in d:
if k != "routes":
die(
@@ -344,12 +344,12 @@ class EgressProxyConfig:
class Bottle:
env: Mapping[str, str] = field(default_factory=_empty_str_dict)
git: tuple[GitEntry, ...] = ()
egress: EgressProxyConfig = field(default_factory=EgressProxyConfig)
egress: EgressConfig = field(default_factory=EgressConfig)
# Opt-in per-bottle stuck-recovery sidecar (PRD 0013). When true,
# the launch step brings up a supervise sidecar that exposes three
# MCP tools to the agent (cred-proxy-block, pipelock-block,
# capability-block; the cred-proxy-block tool is renamed and
# retargeted at egress-proxy in PRD 0017 chunk 3) plus mounts the
# retargeted at egress in PRD 0017 chunk 3) plus mounts the
# current-config dir read-only into the agent at /etc/claude-bottle/
# current-config. False (the default) skips the sidecar and mount.
supervise: bool = False
@@ -403,7 +403,7 @@ class Bottle:
die(
f"bottle '{name}' has a 'tokens' field. The shape was reworked: "
f"each route now lives under 'egress.routes' with explicit "
f"host / path_allowlist / auth. See docs/prds/0017-egress-proxy-via-mitmproxy.md."
f"host / path_allowlist / auth. See docs/prds/0017-egress-via-mitmproxy.md."
)
if "cred_proxy" in d:
@@ -414,18 +414,18 @@ class Bottle:
f"'host' (just the upstream hostname)\n"
f" - 'auth_scheme' + 'token_ref' (flat)\n"
f"'auth: {{ scheme, token_ref }}' (nested, optional)\n"
f" - 'role' (provisioner dotfile rewrites): drop — egress-proxy "
f" - 'role' (provisioner dotfile rewrites): drop — egress "
f"is on the agent's HTTP_PROXY path, so dotfile rewrites are no "
f"longer needed.\n"
f" - 'path_allowlist' (new): optional URL prefix gate for the "
f"host.\n"
f"See docs/prds/0017-egress-proxy-via-mitmproxy.md."
f"See docs/prds/0017-egress-via-mitmproxy.md."
)
egress = (
EgressProxyConfig.from_dict(name, d["egress"])
EgressConfig.from_dict(name, d["egress"])
if "egress" in d
else EgressProxyConfig()
else EgressConfig()
)
supervise_raw = d.get("supervise", False)
@@ -711,20 +711,20 @@ def _parse_git_upstream(url: str, label: str) -> tuple[str, str, str, str]:
return (user, host, port, path)
def _validate_egress_proxy_routes(
def _validate_egress_routes(
bottle_name: str,
routes: tuple[EgressProxyRoute, ...],
routes: tuple[EgressRoute, ...],
) -> None:
"""Cross-validation for `bottle.egress.routes`:
- Hosts must be unique within the bottle. The proxy matches by
exact-host (v1, prefix matching is on path_allowlist only);
duplicate hosts leave the route choice ambiguous.
- Singleton roles (see EGRESS_PROXY_SINGLETON_ROLES) may appear
- Singleton roles (see EGRESS_SINGLETON_ROLES) may appear
on at most one route per bottle.
No cross-validation against `bottle.git` is performed. git-gate
(SSH push/fetch) and egress-proxy (HTTPS) broker different
(SSH push/fetch) and egress (HTTPS) broker different
protocols; declaring both for the same host is a legitimate
dev setup."""
seen_hosts: dict[str, None] = {}
@@ -736,7 +736,7 @@ def _validate_egress_proxy_routes(
f"{r.Host!r}; each host must be unique on the proxy."
)
seen_hosts[key] = None
for role in EGRESS_PROXY_SINGLETON_ROLES:
for role in EGRESS_SINGLETON_ROLES:
with_role = [r for r in routes if role in r.Role]
if len(with_role) > 1:
hosts = ", ".join(r.Host for r in with_role)