Skip to content

fix(antigravity): normalize IPv6 proxy bypass entries for Python - #908

Draft
ker2xu wants to merge 1 commit into
spacering-net:mainfrom
ker2xu:fix/portable-loopback-no-proxy
Draft

ker2xu wants to merge 1 commit into
spacering-net:mainfrom
ker2xu:fix/portable-loopback-no-proxy

Conversation

@ker2xu

@ker2xu ker2xu commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Launching Antigravity with a proxy gives its Python ACP server NO_PROXY=localhost,127.0.0.1,::1,[::1]. Python MCP servers inheriting that environment can fail before initialization because HTTPX interprets [::1] as an invalid host/port pattern. The Perplexity MCP server reports Invalid port: ':1]' and exits with EOF.

Normalize standalone bracketed IPv6 bypass entries in Antigravity's launch environment, including launches without a recorded auth method. The common loopback list stays unchanged because Node's environment proxy needs [::1] to bypass IPv6 loopback. Explicit URL entries, domains, wildcards, proxy URLs, and the credential policy retain their existing behavior.

Regression coverage checks both environment-variable spellings, IPv6 alias deduplication, URL/domain preservation, empty and wildcard values, and existing credential behavior. A real Node IPv6 loopback request confirms that the shared bracketed entry remains necessary; the normalized environment allows the installed Perplexity MCP server to initialize and list its tools.

The repository's exact policy functions fail the new IPv6 regression on base and pass all five policy/credential cases with the change. The same five cases also pass in the native crate. Upstream CI has passed the complete Linux server test suite, Clippy, and frontend lint/tests/build at this PR's head. The remaining desktop and cross-platform CI jobs are still running.

@xintaofei

Copy link
Copy Markdown
Collaborator

@ker2xu hi, and thank you for this PR! 🙏

This was a pleasure to pick up. The Invalid port: ':1]' from the Perplexity MCP server pointed straight at the cause, and you had already checked the other side of the trade-off with a real Node request to IPv6 loopback, which is exactly the right instinct. The tests are careful too: both spellings, [::1]/::1 deduplication, URL and domain entries left alone, * and empty values, and the policy with and without a recorded auth method. CI is fully green now as well.

I reproduced the failure, and your diagnosis is right: HTTPX rejects a standalone bracketed IPv6 entry such as [::1] in NO_PROXY, and it does so while building a default client, before any request goes out.

That said, I'd suggest reworking this before it's merged, mainly because of where the fix lives. The [::1] comes from codeg's own loopback list, and that list reaches much more than Antigravity's launch environment, so the same failure can hit HTTPX clients under any agent and in codeg's terminals. Below is everything in one place: what's affected, the trade-off with measurements, a suggested direction, notes on the current diff, and tests. If you think I got something wrong, please push back 🙂

Summary

  • §1 The crash isn't specific to Antigravity. codeg supplies [::1] through its shared proxy environment to agent launches and terminals, and this PR rewrites only Antigravity's launch value.
  • §2 Neither ::1 nor [::1] keeps an IPv6-literal bypass working across every client I tested, so this is one global trade-off. I measured both sides.
  • §3 Suggested direction: drop [::1] where the list is built, and tidy up around it.
  • §4 Smaller notes on the current diff.
  • §5 Tests and manual checks. How I measured everything is in a collapsed section at the end.

1. The crash isn't specific to Antigravity

Where [::1] comes from:

  • It's part of codeg's own loopback list, LOOPBACK_NO_PROXY (proxy.rs L33-L36). It was added in dd13b595b and has shipped since v0.32.2.
  • When a proxy is enabled in Settings, codeg builds the bypass value from that list and apply_system_proxy_settings writes it, in both spellings, into codeg's own process environment (proxy.rs L219-L270).

Who receives it from there:

What breaks, for default clients, which read the environment:

  • httpx 0.28.1: httpx.Client() raises InvalidURL: Invalid port: ':1]'. No *_PROXY variable is needed: NO_PROXY='[::1]' alone is enough.
  • openai 3.26.1, anthropic 1.12.1 and mcp 2.3.0, the versions I tested: their default Python clients use httpx2 2.13.1, which fails the same way.
  • The Hermes agent's bundled Python runtime (openai 2.24.0 on httpx 0.28.1) fails the same way for openai.OpenAI(...).

So with a proxy set in codeg's Settings, the same failure can hit any Python code that inherits this list and builds a default HTTPX or SDK client: a script that an agent runs through its shell tool, a Python MCP server under another agent that passes its environment down, or a script run in the built-in terminal. This PR covers processes under Antigravity's launch environment only (connection.rs L403-L410).

For reference, the mechanism: [::1] isn't a bare IPv6 address, so get_environment_proxies falls through to the domain branch and mounts all://*[::1] (httpx _utils.py L64-L74). When the client turns that into a URL pattern (_client.py L685-L699), the authority regex can only take *[ as the host (_urlparse.py L125-L134). That leaves :1] as the port, and the port check raises (_urlparse.py L408-L411).

2. It's one trade-off, and it's global

Your point about Node holds up, and it isn't only Node. I sent a request to http://[::1]:<port> through a stand-in proxy that answers 502, with both spellings of each variable set:

Client Today: localhost,127.0.0.1,::1,[::1] Without [::1]
httpx 0.28.1 client creation fails, nothing is sent direct
Python urllib direct proxied
Bun fetch direct proxied
Node 24 fetch, NODE_USE_ENV_PROXY=1 direct proxied
Node 24 http, NODE_USE_ENV_PROXY=1 direct direct
Node 24 fetch without the flag direct direct
requests, curl direct direct

Removing [::1] keeps the localhost, 127.0.0.1 and ::1 entries and lets HTTPX build its client again. What no value can do is keep an address-specific bypass for the IPv6 literal working everywhere: urllib only matches an entry equal to [::1] (or [::1]:<port>, ignoring leading dots), and HTTPX rejects those standalone entries. Only * works for both, by bypassing everything.

So the choice is between:

  • Keeping [::1]: default HTTPX clients that inherit the list fail during setup, whatever they were about to connect to.
  • Dropping it: urllib, Bun fetch and env-proxied Node fetch send requests for an IPv6 literal loopback URL to the proxy. Those fail if the proxy can't or won't forward them to this machine's loopback, for example a proxy on another host, or one that refuses loopback destinations.

I'd favor dropping it. Today's value stops HTTPX clients from being created at all, even for unrelated destinations, while the routing change only affects IPv6-literal loopback URLs and can be documented. The loopback traffic behind #804 uses IPv4: for OpenCode builds that support the listen flags, codeg passes --hostname 127.0.0.1 (opencode_launch.rs L47, L139-L153), and older builds run no HTTP server at all. Antigravity's server dials ws://127.0.0.1:<port> (proxy.rs L24-L31). codeg's registry note for Antigravity 1.3.0 also records that its main.py appends localhost,127.0.0.1,::1, without brackets, to both spellings whenever a proxy variable is set (registry.rs L3787-L3794). And the Settings page already lists only localhost,127.0.0.1,::1 as the local addresses that skip the proxy (en.json L246).

The PR already makes this trade, just inside one subtree: a Node or Bun process started under Antigravity, for example an npx MCP server, gets the list without [::1] as well.

3. Suggested direction

Removing the bracketed default where it's built takes the codeg-generated [::1] out of every path in §1 at once, and keeps one source of truth:

  1. Remove "[::1]" from LOOPBACK_NO_PROXY, and update the doc comment above it (proxy.rs L33-L36) to say why the bracketed form is left out and what that costs (§2).
  2. Optionally, run your unbracket_ipv6_no_proxy (proxy.rs L161-L179) inside no_proxy_value (proxy.rs L204-L217). That function builds the Settings-generated process value and the bypass value of every launch that carries a proxy, so a standalone bracketed entry from those lists, such as a user-entered [fd00::1], couldn't bring the crash back. The catch is that it extends the §2 trade-off to those addresses, so a warning on the Settings page would be a fine alternative. I'm happy with either.
  3. Drop the rewrite from apply_antigravity_env_policy, which is otherwise all about credentials. Its two new tests call that function directly (connection.rs L20686-L20724), so move their bypass assertions next to the shared builder and keep the credential tests as they are. If you skip step 2, remove the helper and its unit test as well: with no caller left, Clippy's dead_code lint would fail the build under -D warnings.
  4. Update the tests that pin the old value: the LOOPBACK constant in proxy.rs's tests (L401), four assertions in system_settings.rs (L1324-L1327, L1374-L1382, L1397-L1402), and a_proxied_launch_env_carries_the_loopback_exception in connection.rs (L29725-L29736).
  5. Consider softening the Settings hint. It says localhost,127.0.0.1,::1 "always connect directly", and after this change that no longer holds for IPv6-literal URLs in the clients from §2. The hint is translated in all ten locales, so it's a small change in ten places.

Your helper and its unit test already live in network::proxy. If you take step 2, keep their normalization and preservation cases, and adapt the moved policy cases to the shared builder: its output always starts with the loopback entries, launch environments get it in both spellings, and a list containing * becomes * alone.

This fixes codeg's own default. Normalizing a value that is only inherited, in a launch without a proxy variable (§4), would be a separate follow-up.

4. Smaller notes on the current diff

These mostly go away with §3, but for completeness:

  • The helper recognizes standalone bracketed entries whose contents parse as an IPv6 address. [::1]:8000, ::1/128 and [::1]/128 also break HTTPX, but they can't simply be stripped, because dropping a port or a prefix length widens the bypass. Leaving them as they are and documenting the limit, or warning about them in Settings, seems safer.
  • When a launch carries no proxy variable, add_no_proxy_to_launch_env returns early (proxy.rs L286-L291). An inherited-only NO_PROXY or no_proxy containing [::1], say one exported in the user's shell, is then never copied into the merged launch environment. The child still inherits it straight from codeg, the rewrite never sees it, and HTTPX still fails.
  • The code comment says Node-based agents still need the bracketed spelling. What matters, though, is each child process rather than the agent's own runtime: Node and Bun children of Antigravity lose the match (§2), and Python children of other agents keep the crash (§1).

5. Tests and manual checks

Tests:

  • no_proxy_value: the loopback entries still come first and no longer include [::1], and * still wins alone.
  • If you take step 2: a user-entered [fd00::1] comes out bare, while http://[::1]:8000, .corp.example and [example.com] come out unchanged.

Manual, with a proxy set in Settings. Fully restart codeg first, so that fresh terminals and agent sessions pick up the new environment:

  • With a Python that has httpx installed, python -c 'import httpx; httpx.Client()' works in the built-in terminal, through an agent that runs commands via ACP terminal/create, and as a child process of an agent.
  • The Perplexity MCP server initializes under Antigravity, and under one other agent that passes its environment to MCP servers.
  • The 当代理设置不为本机时,opencode和antigravity会报错无法正常使用 #804 scenario still works: with the proxy on another machine, OpenCode and Antigravity both get through session/new.
How I measured this (macOS 27, Python 3.13, httpx 0.28.1, requests 2.34.2, Node 24.18, Bun 1.3.14, curl 8.7.1)

Client construction with today's list. uv installs the pinned packages with your normal network settings first, and the bypass list is only set inside Python:

uv run -q --no-project --with 'httpx==0.28.1' --with 'openai==3.26.1' python - <<'EOF'
import os
os.environ["NO_PROXY"] = os.environ["no_proxy"] = "localhost,127.0.0.1,::1,[::1]"
import httpx, openai
for name, build in [("httpx.Client()", httpx.Client), ("openai.OpenAI()", lambda: openai.OpenAI(api_key="x"))]:
    try:
        build()
        print(f"{name}: OK")
    except Exception as e:
        print(f"{name}: {type(e).__module__}.{type(e).__name__}: {e}")
EOF
# httpx.Client(): httpx.InvalidURL: Invalid port: ':1]'
# openai.OpenAI(): httpx2.InvalidURL: Invalid port: ':1]'

The table in §2. Save this as matrix.sh and run bash matrix.sh. It needs uv, node 24+, bun and curl on PATH and a working IPv6 loopback, and it stops if the listeners can't start:

#!/usr/bin/env bash
# Needs uv, node (24+), bun and curl on PATH, and a working IPv6 loopback.
cd "$(mktemp -d)" || exit 1
uv venv -q .venv && uv pip install -q --python .venv 'httpx==0.28.1' 'requests==2.34.2' || exit 1

cat > servers.py <<'EOF'
import socket, threading, time

def serve(sock, tag, status):
    while True:
        conn, _ = sock.accept()
        conn.recv(4096)
        with open("hits.log", "a") as log:
            log.write(tag + "\n")
        conn.sendall(f"HTTP/1.1 {status}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n".encode())
        conn.close()

proxy = socket.create_server(("127.0.0.1", 18999))  # stands in for a proxy on another machine
target = socket.create_server(("::1", 18998), family=socket.AF_INET6)
threading.Thread(target=serve, args=(proxy, "proxy", "502 Bad Gateway"), daemon=True).start()
threading.Thread(target=serve, args=(target, "direct", "200 OK"), daemon=True).start()
open("ready", "w").close()
time.sleep(600)
EOF
.venv/bin/python servers.py 2>servers.err & SERVERS=$!
trap 'kill $SERVERS 2>/dev/null' EXIT
for _ in 1 2 3 4 5 6 7 8 9 10; do [ -e ready ] && break; sleep 0.5; done
[ -e ready ] || { echo "The listeners did not start:"; cat servers.err; exit 1; }

URL='http://[::1]:18998/'
PX='http://127.0.0.1:18999'
PY="$PWD/.venv/bin/python"
JS_FETCH="fetch('$URL', { signal: AbortSignal.timeout(3000) }).then(r => r.text()).catch(e => console.error(String(e)))"
JS_HTTP="require('http').get('$URL', r => r.resume()).setTimeout(3000, function () { this.destroy() }).on('error', e => console.error(String(e)))"

probe() {  # probe <label> <command...>, run with the NO_PROXY in $NP
  local label=$1; shift
  : > hits.log
  env -i PATH="$PATH" HOME="$HOME" HTTP_PROXY=$PX http_proxy=$PX HTTPS_PROXY=$PX https_proxy=$PX \
    NO_PROXY="$NP" no_proxy="$NP" "$@" >/dev/null 2>"$label.err"
  sleep 0.2
  local hits; hits=$(cat hits.log)
  printf '  %-11s %s\n' "$label" "${hits:-no request sent: $(tail -n 1 "$label.err")}"
}

for NP in 'localhost,127.0.0.1,::1,[::1]' 'localhost,127.0.0.1,::1'; do
  echo "NO_PROXY=$NP"
  probe urllib      "$PY" -c "import urllib.request as u; u.urlopen('$URL', timeout=3)"
  probe requests    "$PY" -c "import requests; requests.get('$URL', timeout=3)"
  probe httpx       "$PY" -c "import httpx; httpx.get('$URL', timeout=3)"
  probe node-fetch  env NODE_USE_ENV_PROXY=1 node -e "$JS_FETCH"
  probe node-http   env NODE_USE_ENV_PROXY=1 node -e "$JS_HTTP"
  probe node-noflag node -e "$JS_FETCH"
  probe bun-fetch   bun -e "$JS_FETCH"
  probe curl        curl -sS -m 3 "$URL"
done

echo "No proxy variable at all, NO_PROXY='[::1]':"
env -i NO_PROXY='[::1]' "$PY" -c 'import httpx; httpx.Client()' 2>&1 | tail -n 1

Output:

NO_PROXY=localhost,127.0.0.1,::1,[::1]
  urllib      direct
  requests    direct
  httpx       no request sent: httpx.InvalidURL: Invalid port: ':1]'
  node-fetch  direct
  node-http   direct
  node-noflag direct
  bun-fetch   direct
  curl        direct
NO_PROXY=localhost,127.0.0.1,::1
  urllib      proxy
  requests    direct
  httpx       direct
  node-fetch  proxy
  node-http   direct
  node-noflag direct
  bun-fetch   proxy
  curl        direct
No proxy variable at all, NO_PROXY='[::1]':
httpx.InvalidURL: Invalid port: ':1]'

Thanks again! Your diagnosis is spot on and the tests are careful, and most of this carries straight over to the shared list. If anything here is unclear, or you'd rather keep the fix narrower for a reason I've missed, just reply here. I'm happy to look at the next round.

@xintaofei

Copy link
Copy Markdown
Collaborator

codeg work task 300 is done — it made no changes, so nothing was pushed to this pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants