Skip to content

Check where an agent address redirects to, not just where it starts - #38

Open
beardthelion wants to merge 1 commit into
CopilotKit:mainfrom
beardthelion:fix/agent-endpoint-redirects
Open

Check where an agent address redirects to, not just where it starts#38
beardthelion wants to merge 1 commit into
CopilotKit:mainfrom
beardthelion:fix/agent-endpoint-redirects

Conversation

@beardthelion

Copy link
Copy Markdown

Closes #36.

checkAgentEndpoint decides whether this deployment will dial an address, and the request was then handed to a fetch that follows redirects. The checked address and the dialled address were therefore the same address only while nobody redirected. A registrable agent answering 307 Location: http://169.254.169.254/latest/meta-data/ put the server on its own cloud metadata endpoint.

Both dial sites are affected and the runtime one is worse. The connection test runs once at registration; copilot.ts re-dials the stored endpoint on every run, carrying whatever auth header the registration supplied, so a redirect added after approval is ongoing rather than a one-off.

What it does

createAgentFetch applies the check to each hop, capped at three.

Redirects are followed rather than refused. A deployment that puts its agent behind one has done nothing wrong and http to https is the ordinary case; every destination goes through checkAgentEndpoint first, so following one can only reach somewhere registering it directly would have reached.

The method and body are carried across hops. A browser turns a redirected POST into a GET; doing that here would only ever produce a confusing "that is not an AG-UI endpoint" from an agent that is one, because AG-UI is a POST protocol and this is a server talking to an API.

The stall guard already accepted an inner fetch, so the two compose: a deployment with a timeout configured gets the watch and the redirect check rather than whichever was wired last.

Verification

Three cases in agent-connection-live.test.ts: a redirect to an address the check refuses is refused with the destination never dialled, a redirect to an address the check permits is still followed and still reports the agent's events, and a redirect that never arrives gives up rather than looping. The first fails before the change; the other two pass before and after, which is the point of having them.

Two in copilot.test.ts for the runtime wiring, using the same sentinel-identity trick the stall-guard tests already use, because @ag-ui/client fills fetch in with a wrapper of its own whenever the config does not carry one. One of them is there because resolveRuntimeAgents accepted the fetch and dropped it in an earlier draft of this change: a parameter accepted and not forwarded looks identical from the outside to one that works, and the run goes quietly back to the unguarded fetch.

server suite failure set is identical to main. Typecheck and biome clean.

What this does not close

The check is by hostname, so a name that passes and then resolves to a private address between the check and the connection still gets through. Refusing redirects outright would not close that either; it wants address-level validation at connect time, which is a larger change than this one.

`checkAgentEndpoint` decides whether this deployment is willing to talk to an
address, and then the request was handed to a fetch that follows redirects. The
address that was checked and the address that was dialled were therefore only the
same address while nobody redirected. A registrable agent at
`https://agent.example.com/ag-ui` answering `307 Location:
http://169.254.169.254/latest/meta-data/` put the server on its own cloud metadata
endpoint, which the check refuses under every configuration.

Both places that dial an agent are affected, and the second is the worse one. The
connection test runs once at registration; the runtime dials the stored endpoint on
every single run, carrying whatever auth header the registration supplied, so a
redirect added after approval is an ongoing exposure rather than a one-off.

`createAgentFetch` applies the check to each hop. Redirects are followed rather than
refused, because a deployment that puts its agent behind one has done nothing wrong
and `http` to `https` is the ordinary case; each destination goes through
`checkAgentEndpoint` first, so following one can only reach somewhere registering it
directly would have reached. Three hops, then it gives up.

Method and body are carried across hops. A browser turns a redirected POST into a
GET, and doing that here would only ever produce a confusing "that is not an AG-UI
endpoint" from an agent that is one.

The stall guard already accepted an inner fetch, so the two compose: a deployment
with a timeout configured gets the watch and the redirect check rather than
whichever was wired last.
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.

An agent endpoint check is bypassed by a redirect, at registration and on every run

1 participant