项目文件夹

文件
Ali Khokhar 258ed3e7ed Keep FCC-local traffic off outbound proxies (#1214)
## 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 -->
2026-07-20 03:28:56 -07:00

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"]