alishahryar1--free-claude-code
258ed3e7ed
## Problem FCC-local health and model-catalog requests inherited machine proxy settings, so an outbound proxy could return HTTP 502 even while `fcc-server` was healthy. Spawned agents inherited the same missing loopback bypass. Fixes #1199. ## Changes | Before | After | | --- | --- | | Launcher probes used the process-wide urllib proxy policy. | FCC-local probes use one proxy-disabled transport owner. | | Codex catalog loading could leave through an outbound proxy. | Codex catalog loading uses the same direct local transport. | | Claude, Codex, Pi, and managed messaging inherited incomplete bypass lists. | Spawned clients preserve outbound proxies while merging FCC and loopback hosts into `NO_PROXY` and `no_proxy`. | | Local and upstream proxy responsibilities overlapped. | CLI-local transport and provider-specific upstream proxies have explicit separate owners. | | Tests mocked launcher reachability without exercising proxy interception. | A real fake-proxy regression proves loopback traffic never reaches the proxy, and an ownership contract prevents raw local transports from returning. | <!-- greptile_comment --> <details open><summary><h3>Greptile Summary</h3></summary> This PR keeps FCC-local traffic separate from configured outbound proxies. The main changes are: - Adds one direct HTTP transport for FCC health and model-catalog requests. - Adds FCC and loopback hosts to spawned clients' proxy-bypass environments. - Updates Claude, Codex, and Pi launcher integration. - Adds proxy-interception and transport-ownership tests. - Documents the boundary and bumps the package version. </details> <h3>Confidence Score: 5/5</h3> This looks safe to merge. No blocking issues found in the changed code. Updated callers supply the new Codex environment argument. The direct opener changes proxy handling while retaining urllib's standard request handlers. Tests cover real proxy interception and preservation of existing bypass policy. No changed files need additional attention. <details><summary><h3><a href="https://www.greptile.com/trex"><img alt="T-Rex" src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg" height="20" align="absmiddle"></a> T-Rex Logs</h3></summary> **What T-Rex did** - T-Rex validated the contract by comparing preflight and forward-proxy state before and after HEAD, noting the preflight result moved from HTTP 502 and the forward-proxy hits to a clean health-check state. - T-Rex confirmed all three tests in tests/cli/test\_local\_http.py passed. <a href="https://app.greptile.com/trex/runs/15066178/artifacts"><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifactsDark.svg?v=4"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"><img alt="View all artifacts" src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"></picture></a> <sub><a href="https://www.greptile.com/trex"><img alt="T-Rex" src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg" height="14" align="absmiddle"></a> Ran code and verified through T-Rex</sub> </details> <details open><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | src/free_claude_code/cli/local_http.py | Adds the shared direct opener and proxy-bypass environment policy. | | src/free_claude_code/cli/launchers/common.py | Routes FCC health checks through the direct local transport. | | src/free_claude_code/cli/launchers/codex.py | Routes catalog requests directly and adds local bypass entries to the Codex environment. | | src/free_claude_code/cli/claude_env.py | Adds FCC and loopback proxy bypasses to Claude environments. | | src/free_claude_code/cli/launchers/pi.py | Adds FCC and loopback proxy bypasses to Pi environments. | | tests/cli/test_local_http.py | Tests direct local access, proxy interception, environment merging, and deduplication. | | tests/contracts/test_import_boundaries.py | Enforces one owner for direct local transports and proxy-bypass variables. | </details> <details open><summary><h3>Flowchart</h3></summary> <a href="#gh-light-mode-only"> ```mermaid %%{init: {'theme': 'neutral'}}%% flowchart LR L[Launcher] --> R[FCC health or catalog request] R --> D[Direct local opener] D --> F[FCC server] L --> E[Build child environment] E --> N[Merge FCC and loopback hosts into NO_PROXY] N --> C[Claude, Codex, or Pi] C -->|FCC-local traffic| F C -->|Other outbound traffic| P[Configured outbound proxy] ``` </a> <a href="#gh-dark-mode-only"> ```mermaid %%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% flowchart LR L[Launcher] --> R[FCC health or catalog request] R --> D[Direct local opener] D --> F[FCC server] L --> E[Build child environment] E --> N[Merge FCC and loopback hosts into NO_PROXY] N --> C[Claude, Codex, or Pi] C -->|FCC-local traffic| F C -->|Other outbound traffic| P[Configured outbound proxy] ``` </a> </details> <sub>Reviews (1): Last reviewed commit: ["Keep FCC-local traffic off outbound prox..."](https://github.com/alishahryar1/free-claude-code/commit/072cc4b7474c1b33bfc368d148da5c1aa24914b6) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=45589926)</sub> <!-- /greptile_comment -->
85 行
2.7 KiB
Python
85 行
2.7 KiB
Python
"""Local FCC transport must never escape through an outbound proxy."""
|
|
|
|
import threading
|
|
from collections.abc import Iterator
|
|
from contextlib import contextmanager
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
|
|
import pytest
|
|
|
|
from free_claude_code.cli.launchers.common import preflight_proxy
|
|
from free_claude_code.cli.local_http import with_local_proxy_bypass
|
|
|
|
|
|
@contextmanager
|
|
def _status_server(status_code: int) -> Iterator[tuple[str, list[str]]]:
|
|
hits: list[str] = []
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def do_GET(self) -> None:
|
|
hits.append(self.path)
|
|
self.send_response(status_code)
|
|
self.end_headers()
|
|
|
|
def log_message(self, format: str, *args: object) -> None:
|
|
del format, args
|
|
|
|
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
yield f"http://127.0.0.1:{server.server_port}", hits
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join()
|
|
|
|
|
|
def test_proxy_preflight_connects_directly_when_http_proxy_is_configured(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
with _status_server(502) as (forward_proxy_url, forward_proxy_hits):
|
|
monkeypatch.setenv("HTTP_PROXY", forward_proxy_url)
|
|
monkeypatch.setenv("http_proxy", forward_proxy_url)
|
|
monkeypatch.delenv("NO_PROXY", raising=False)
|
|
monkeypatch.delenv("no_proxy", raising=False)
|
|
|
|
with _status_server(200) as (fcc_url, fcc_hits):
|
|
assert preflight_proxy(fcc_url) is None
|
|
|
|
assert fcc_hits == ["/health"]
|
|
assert forward_proxy_hits == []
|
|
|
|
|
|
def test_child_proxy_bypass_preserves_existing_proxy_policy() -> None:
|
|
base_env = {
|
|
"HTTP_PROXY": "http://proxy.example:3128",
|
|
"NO_PROXY": "example.com, localhost",
|
|
"no_proxy": "10.0.0.0/8,EXAMPLE.COM",
|
|
"KEEP_ME": "yes",
|
|
}
|
|
|
|
env = with_local_proxy_bypass(
|
|
base_env,
|
|
proxy_root_url="http://fcc.internal:8082",
|
|
)
|
|
|
|
assert env["HTTP_PROXY"] == "http://proxy.example:3128"
|
|
assert env["KEEP_ME"] == "yes"
|
|
assert env["NO_PROXY"] == (
|
|
"example.com,localhost,10.0.0.0/8,127.0.0.1,::1,fcc.internal"
|
|
)
|
|
assert env["no_proxy"] == env["NO_PROXY"]
|
|
assert base_env["NO_PROXY"] == "example.com, localhost"
|
|
assert base_env["no_proxy"] == "10.0.0.0/8,EXAMPLE.COM"
|
|
|
|
|
|
def test_child_proxy_bypass_uses_all_loopback_spellings_without_duplicates() -> None:
|
|
env = with_local_proxy_bypass(
|
|
{"NO_PROXY": "127.0.0.1"},
|
|
proxy_root_url="http://127.0.0.1:8082",
|
|
)
|
|
|
|
assert env["NO_PROXY"] == "127.0.0.1,localhost,::1"
|
|
assert env["no_proxy"] == env["NO_PROXY"]
|