Check where an agent address redirects to, not just where it starts - #38
Open
beardthelion wants to merge 1 commit into
Open
Check where an agent address redirects to, not just where it starts#38beardthelion wants to merge 1 commit into
beardthelion wants to merge 1 commit into
Conversation
`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.
beardthelion
requested review from
MikeRyanDev,
davidmckayv,
guidovizoso and
tylerslaton
as code owners
August 20, 2026 04:27
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #36.
checkAgentEndpointdecides 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 answering307 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.tsre-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
createAgentFetchapplies 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
httptohttpsis the ordinary case; every destination goes throughcheckAgentEndpointfirst, 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.tsfor the runtime wiring, using the same sentinel-identity trick the stall-guard tests already use, because@ag-ui/clientfillsfetchin with a wrapper of its own whenever the config does not carry one. One of them is there becauseresolveRuntimeAgentsaccepted 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.serversuite failure set is identical tomain. 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.