Skip to content

fix(agent-endpoint): correctly handle bracketed IPv6 hosts with port in allowed list - #330

Open
Ayush7614 wants to merge 1 commit into
CopilotKit:mainfrom
Ayush7614:fix/agent-endpoint-ipv6-allowed-hosts
Open

fix(agent-endpoint): correctly handle bracketed IPv6 hosts with port in allowed list#330
Ayush7614 wants to merge 1 commit into
CopilotKit:mainfrom
Ayush7614:fix/agent-endpoint-ipv6-allowed-hosts

Conversation

@Ayush7614

Copy link
Copy Markdown
Contributor

What this changes

AGENT_ENDPOINT_ALLOWED_HOSTS is the narrow allow-list that lets a deployment reach a private agent address without opening the whole network (AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS). For IPv6 the host is written bracketed as [::1]:8080 or [::1].

The previous normalization did:

host.replace(/^\[/, "").replace(/\]$/, "")

For [::1]:8080 that stripped the leading [ to ::1]:8080 and left the ] because the string ends with 0, not ]. The stored entry became ::1]:8080.

endpoint.ts:52 did a different strip:

url.host.replace(/^\[/, "").replace(/\]/, "")

which for [::1]:8080 produced ::1:8080. The two sides never matched, so a named IPv6 endpoint was refused with "inside this deployment's own network" even though the deployment had explicitly named it. A deployment reaching its own LangGraph or BYO agent over IPv6 loopback or ULA could not be configured at all.

This normalizes both sides consistently:

  • Config (server/src/config.ts:619): parse [host]:port explicitly — find the closing ], take the IPv6 inside and the optional :port after it. [::1]:8080 stores as ::1:8080, [::1] as ::1.
  • Endpoint check (server/src/agents/endpoint.ts:51): derive hostname as url.hostname stripped of brackets, and host as hostname + (port ? :port) rather than string-replacing url.host. This matches the stored form on both Bun (hostname [::1]) and Node.

Behaviour after:

  • AGENT_ENDPOINT_ALLOWED_HOSTS=[::1]:8080 allows http://[::1]:8080/ag-ui and refuses http://[::1]:9090/ag-ui (port pinned).
  • AGENT_ENDPOINT_ALLOWED_HOSTS=[::1] allows any port on ::1 — the existing "host without port covers any port" rule.
  • IPv4/hostname paths unchanged: 10.0.0.42:9000 pins that port, 10.0.0.42 and agents.internal cover any port, exactly as before.

Why it matters

Private IPv6 loopback (::1) and ULAs (fc00::/7) are the IPv6 answers to 10/8/192.168/16. A deployment whose BYO agent or LangGraph runs in a container reached over http://[::1]:4201 had no way to name that address — the one host it legitimately needs is the one the check rejected. The failure is silent at config time and surfaces as a refused agent at runtime, with nothing in the trail naming the mis-normalization.

Proof

Before:

AGENT_ENDPOINT_ALLOWED_HOSTS=[::1]:8080, check http://[::1]:8080/ag-ui => refused (inside network)
stored entry was "::1]:8080" vs derived "::1:8080" => never equal

After (verified inline against loadConfig + checkAgentEndpoint):

[::1]:8080 => stored ["::1:8080"], check http://[::1]:8080/ag-ui => allowed: true
[::1]:8080 => check http://[::1]:9090/ag-ui => allowed: false (pinned)
[::1]      => stored ["::1"], check http://[::1]:9090/ag-ui => allowed: true (covers any port)
10.0.0.42:9000 => stored, check :9000 => true, :9001 => false (unchanged)
agents.internal (non-private) => always allowed (unchanged)

bun test server/tests/config.test.ts — 72 pass, 0 fail. bun run typecheck · bun run lint · bun run format:check clean. No other caller uses the stripped form.

Checklist

  • No new state, route, or schedule
  • Private-host floor still checked first via checkNavigationTarget
  • AGENT_ENDPOINT_ALLOWED_HOSTS exact-match semantics preserved (host with port pins, without covers any port)
  • IPv4 and hostname paths untouched

…in allowed list

AGENT_ENDPOINT_ALLOWED_HOSTS is a list of hosts optionally with a
port. For IPv6 the host is bracketed as [::1]:8080. The previous
normalization did host.replace(/^\[/, "").replace(/\]$/, ""),
which stripped the opening bracket but left the closing bracket when
a port followed: "[::1]:8080" became "::1]:8080". The check in
endpoint.ts did a different strip, producing "::1:8080", so the two
sides never matched and a named IPv6 endpoint was always refused.

Similarly, endpoint.ts derived host as url.host with a one-sided
replace, which for Bun's URL.hostname="[::1]" produced a mismatched
form versus the stored entry.

Normalize both sides consistently: store the host as hostname (without
brackets) plus optional :port, and derive the same from URL.hostname/
URL.port. This makes [::1]:8080 pin that port (and [::1] cover any
port, per "host without port covers any port"), and fixes the
bracket stripping for the allowed-host path.
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.

1 participant