fix(macos): supply proxy env only at exec, so Codex keeps its identity token #442
@@ -68,9 +68,14 @@ class MacosContainerBottle(Bottle):
|
|||||||
# reaches the agent (PRD 0070): registration mints it *after* the
|
# reaches the agent (PRD 0070): registration mints it *after* the
|
||||||
# container exists — its source IP is the registration key and Apple
|
# container exists — its source IP is the registration key and Apple
|
||||||
# Container assigns that by DHCP — so it cannot be in the run-time env
|
# Container assigns that by DHCP — so it cannot be in the run-time env
|
||||||
# the way docker's compose spec does it. `container exec --env` wins
|
# the way docker's compose spec does it.
|
||||||
# over the run-time value, so the token-bearing proxy URL set here
|
#
|
||||||
# supersedes the token-less one baked in at launch.
|
# `container exec --env` does NOT override a run-time value — it
|
||||||
|
# appends, leaving duplicate entries in the agent's `environ` whose
|
||||||
|
# resolution is runtime-specific (Node last-wins, Rust first-wins). So
|
||||||
|
# nothing here may rely on superseding: the proxy vars are supplied
|
||||||
|
# *only* at exec time and are deliberately absent from the run-time
|
||||||
|
# env. See `launch._agent_env_entries`.
|
||||||
self._exec_env = dict(exec_env or {})
|
self._exec_env = dict(exec_env or {})
|
||||||
self._closed = False
|
self._closed = False
|
||||||
|
|
||||||
|
|||||||
@@ -265,9 +265,15 @@ def _no_proxy(gateway_ip: str) -> str:
|
|||||||
def _identity_proxy_env(
|
def _identity_proxy_env(
|
||||||
endpoint: GatewayEndpoint, identity_token: str,
|
endpoint: GatewayEndpoint, identity_token: str,
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""The token-bearing proxy env applied at `container exec`. It supersedes
|
"""The token-bearing proxy env applied at `container exec` — the only way
|
||||||
the token-less run-time value (exec `--env` wins), which is the only way to
|
to get the token in, since it does not exist until after the container
|
||||||
get the token in: it does not exist until after the container runs."""
|
runs (registration keys on the DHCP-assigned address).
|
||||||
|
|
||||||
|
This is the *sole* source of `*_PROXY` for the agent. It deliberately does
|
||||||
|
not rely on overriding a run-time value: `container exec --env` appends
|
||||||
|
rather than replaces, so a run-time `HTTPS_PROXY` would survive alongside
|
||||||
|
this one and first-wins runtimes would read the wrong entry. See
|
||||||
|
`_agent_env_entries`."""
|
||||||
if not identity_token:
|
if not identity_token:
|
||||||
return {}
|
return {}
|
||||||
url = _proxy_url(endpoint.gateway_ip, identity_token)
|
url = _proxy_url(endpoint.gateway_ip, identity_token)
|
||||||
@@ -317,16 +323,23 @@ def _agent_run_argv(
|
|||||||
def _agent_env_entries(
|
def _agent_env_entries(
|
||||||
plan: MacosContainerBottlePlan, endpoint: GatewayEndpoint,
|
plan: MacosContainerBottlePlan, endpoint: GatewayEndpoint,
|
||||||
) -> tuple[str, ...]:
|
) -> tuple[str, ...]:
|
||||||
# Token-less at run time — the token does not exist yet (see
|
# No `*_PROXY` here on purpose. The token-bearing URL is applied at
|
||||||
# `_identity_proxy_env`). Anything egressing before the exec-time override
|
# `container exec` (`_identity_proxy_env`), and Apple's `container exec
|
||||||
# is denied by `/resolve`, which is the safe direction.
|
# --env` **appends** to the run-time environment rather than replacing it:
|
||||||
proxy_url = _proxy_url(endpoint.gateway_ip)
|
# setting a token-less value here leaves two `HTTPS_PROXY` entries in the
|
||||||
|
# agent's `environ`, token-less first. Which one a runtime reads is then
|
||||||
|
# pure luck — Node takes the last (and worked), Rust's `std::env::var`
|
||||||
|
# takes the first, so Codex proxied without its identity token and
|
||||||
|
# `/resolve` fail-closed on every request.
|
||||||
|
#
|
||||||
|
# A token-less proxy URL has no legitimate consumer anyway: the init
|
||||||
|
# process is `sleep` and everything that egresses arrives via exec. Its
|
||||||
|
# only value was a tidy 403 for unattributed callers, which is not worth
|
||||||
|
# silently dropping attribution for. Without it a process that egresses
|
||||||
|
# before the exec-time env still fails closed — the agent network is
|
||||||
|
# host-only, so there is no route off it except the gateway.
|
||||||
no_proxy = _no_proxy(endpoint.gateway_ip)
|
no_proxy = _no_proxy(endpoint.gateway_ip)
|
||||||
env = [
|
env = [
|
||||||
f"HTTPS_PROXY={proxy_url}",
|
|
||||||
f"HTTP_PROXY={proxy_url}",
|
|
||||||
f"https_proxy={proxy_url}",
|
|
||||||
f"http_proxy={proxy_url}",
|
|
||||||
f"NO_PROXY={no_proxy}",
|
f"NO_PROXY={no_proxy}",
|
||||||
f"no_proxy={no_proxy}",
|
f"no_proxy={no_proxy}",
|
||||||
f"NODE_EXTRA_CA_CERTS={AGENT_CA_PATH}",
|
f"NODE_EXTRA_CA_CERTS={AGENT_CA_PATH}",
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from bot_bottle.backend.macos_container.consolidated_launch import GatewayEndpoi
|
|||||||
from bot_bottle.backend.macos_container.launch import (
|
from bot_bottle.backend.macos_container.launch import (
|
||||||
_agent_run_argv,
|
_agent_run_argv,
|
||||||
_identity_proxy_env,
|
_identity_proxy_env,
|
||||||
_proxy_url,
|
|
||||||
)
|
)
|
||||||
from bot_bottle.manifest import ManifestIndex
|
from bot_bottle.manifest import ManifestIndex
|
||||||
|
|
||||||
@@ -104,13 +103,26 @@ class TestAgentRunArgv(unittest.TestCase):
|
|||||||
read back after start."""
|
read back after start."""
|
||||||
self.assertNotIn("--ip", self.argv)
|
self.assertNotIn("--ip", self.argv)
|
||||||
|
|
||||||
def test_run_time_proxy_carries_no_identity_token(self) -> None:
|
def test_run_time_env_sets_no_proxy_vars_at_all(self) -> None:
|
||||||
"""The token is minted by registration, which happens after this run —
|
"""Regression: the proxy vars must exist *only* in the exec-time env.
|
||||||
so it cannot be here. `/resolve` denies the token-less pair (#366),
|
|
||||||
which is the safe direction; the real value arrives at exec time."""
|
`container exec --env` appends rather than replaces, so a token-less
|
||||||
joined = " ".join(self.argv)
|
`HTTPS_PROXY` here would survive next to the token-bearing one and
|
||||||
self.assertIn(f"HTTP_PROXY={_proxy_url('192.168.128.3')}", joined)
|
leave two entries in the agent's `environ`. Resolution is then
|
||||||
self.assertNotIn("bottle:", joined)
|
runtime-specific — Node reads the last, Rust's `std::env::var` reads
|
||||||
|
the first — so Codex proxied without its identity token and every
|
||||||
|
request fail-closed at `/resolve`.
|
||||||
|
"""
|
||||||
|
for entry in self.argv:
|
||||||
|
self.assertFalse(
|
||||||
|
entry.upper().startswith(("HTTP_PROXY=", "HTTPS_PROXY=")),
|
||||||
|
f"run-time env must not set a proxy var, got {entry!r}",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_run_time_env_carries_no_identity_token(self) -> None:
|
||||||
|
"""The token is minted by registration, which happens after this run,
|
||||||
|
so it cannot be here under any name."""
|
||||||
|
self.assertNotIn("bottle:", " ".join(self.argv))
|
||||||
|
|
||||||
def test_gateway_bypasses_the_proxy(self) -> None:
|
def test_gateway_bypasses_the_proxy(self) -> None:
|
||||||
"""git-http + supervise live on the gateway and must be reached
|
"""git-http + supervise live on the gateway and must be reached
|
||||||
|
|||||||
Reference in New Issue
Block a user