Skip to content

Store an IPv6 allowed host the way the endpoint check spells it - #383

Merged
davidmckayv merged 2 commits into
CopilotKit:mainfrom
kevin9327:ipv6-allowlist-as-the-parser-spells-it
Sep 5, 2026
Merged

Store an IPv6 allowed host the way the endpoint check spells it#383
davidmckayv merged 2 commits into
CopilotKit:mainfrom
kevin9327:ipv6-allowlist-as-the-parser-spells-it

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

What this changes

AGENT_ENDPOINT_ALLOWED_HOSTS and namedAsAllowed (agents/endpoint.ts) disagree about how an IPv6 address is spelled.

  • The endpoint side reads url.hostname, which the WHATWG parser canonicalises: [0:0:0:0:0:0:0:1] becomes [::1], [FE80::1] becomes [fe80::1]. It then strips the brackets.
  • The config side (normalizeAllowedHost) keeps the address as the operator wrote it, lower-cased, brackets stripped.

So an operator who writes [0:0:0:0:0:0:0:1]:8443 gets a set holding 0:0:0:0:0:0:0:1:8443, the endpoint check looks for ::1:8443, and the entry silently never matches. The comment above agentEndpointAllowedHosts says a mistake in this list is "worth catching here rather than at the first registration that silently never matches", and refuses URLs and wildcards for exactly that reason; this was the same failure for a valid address.

Stripping the brackets on both sides has a second effect: it is not injective. [fd00::1:8443] is an address on the private network and [fd00::1]:8443 is a different address with a port, and both became fd00::1:8443. Naming either admitted the other. ([::1:8443] is the same shape, but the target floor reads it as public, so the test uses ULA addresses.)

Now:

  • normalizeAllowedHost runs a bracketed entry's address through new URL() and stores hostname as the parser spells it, brackets on. The port is kept as written rather than taken from the parser, because the parser drops a scheme's default port and an operator who wrote :80 meant that port. A bracketed entry the parser refuses, or one followed by anything but :port, is refused at boot with the existing "must be a host" message naming the entry, the way a URL or a wildcard already is.
  • namedAsAllowed no longer strips the brackets, so the two sides compare the same string.

Names and IPv4 entries take the early return and are unchanged.

Where it runs

  • New state that outlives a request? None. The list is still built once at boot from the environment.
  • What happens on the second replica? Every replica reads the same environment and builds the same set.
  • Anything serialised? No.
  • Anything fanned out to a browser? No.
  • New listener, port, or schedule? No.

Boundary and audit

  • Every acting call still goes through the gateway: this touches only which private addresses the endpoint floor accepts, and only for entries the operator named.
  • New refusals and new failures each write a row: the new refusal is a boot-time configuration error, which stops the deployment with the entry named, as the existing refusals do.
  • Nothing new is trusted from the client: the list comes from the environment. The never-allowed check runs before the list on both sides and is untouched (the metadata address cannot be named back in still passes).

Changelog

A line under Unreleased.

Proof

config.test.ts: [0:0:0:0:0:0:0:1]:8443, [FE80::1], [::1:8443] is stored as [::1]:8443, [fe80::1], [::1:8443]; [not-an-address] and [::1]junk are refused naming the entry.

agent-endpoint.test.ts: http://[0:0:0:0:0:0:0:1]:8443/ag-ui and http://[::1]:8443/ag-ui are both allowed by a list holding [::1]:8443; a list holding [fd00::1:8443] does not admit http://[fd00::1]:8443/, a list holding [fd00::1]:8443 does not admit http://[fd00::1:8443]/, and each admits its own. Before the change the second test fails on both refusals (each side stripped to fd00::1:8443), and the config test fails on the stored spelling.

bun test server/tests/config.test.ts server/tests/agent-endpoint.test.ts
 136 pass, 0 fail

bunx tsc --noEmit in server and bunx biome check on the four files are clean.

namedAsAllowed compares the list against URL.hostname, which the parser
canonicalises: compressed, lower-case, in brackets. The list kept an
IPv6 entry as written, so [0:0:0:0:0:0:0:1]:8443 silently never matched,
which is the failure the list's URL and wildcard refusals exist to
prevent. Both sides also stripped the brackets, which folded an address
and an address-with-port into one name: [fd00::1:8443] and [fd00::1]:8443
both became fd00::1:8443, so naming either admitted the other.

Canonicalise a bracketed entry through the URL parser at boot, keep the
port as written, refuse an entry the parser does not read as an address
the way a URL is refused, and compare with the brackets on.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@davidmckayv davidmckayv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed diff against current main; resolved CHANGELOG keep-both; validated locally (format + composed test run). CI green.

@davidmckayv
davidmckayv merged commit fa38795 into CopilotKit:main Sep 5, 2026
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