feat(aws-strands): expose the URL fetch policy through the TypeScript adapter config - #2618
Merged
Merged
Conversation
… adapter config The TypeScript bridge fetched every URL content source under a hard-coded DEFAULT_URL_FETCH_POLICY, so a deployment whose attachments live on a private CDN or behind split DNS had no supported way to reach them, and a deployment that wanted the policy tighter had no way to say so. Python has carried `url_fetch_policy` since #2517's Python half; this is the TypeScript side of the same option. - `StrandsAgentConfig.urlFetchPolicy`, optional, `undefined` meaning the safe default, mirroring Python's `Optional[UrlFetchPolicy] = None`. - `DEFAULT_URL_FETCH_POLICY` and `UrlFetchPolicyError` exported as values, `UrlFetchPolicy` and `SchemeAllowlist` as types, so an override can actually be written (the interface has no constructor, so it is a spread over the default). `UrlFetchUnavailableError` stays internal, as in Python. - Threaded through `MediaConversionOptions`, set once where the run builds its fetch options, so the construction seed, the replayed history and the live turn all fetch under one policy. Every URL-backed path (image, audio, video, document, the deprecated binary form) resolves through that one call. - The per-request fetch cache now keys on the URL together with the policy's values rather than the URL alone, so a refusal memoised under a strict policy is never served to a caller who opted in. Two conversions under one policy still share a single download. - An unusable policy ends the run with URL_FETCH_POLICY_INVALID before any fetch is attempted. Without that check it would have thrown per attachment inside the history-replay try/catch and been reported as a conversion that fell back to text: closed, but silent, per message, and easy to mistake for a dead URL. `UrlFetchPolicyError` loses its internal marker because `stripInternal` drops a marked declaration from the emitted types, which would make it unexportable from the package entry. Docs: the docs truth pass in #2611 documented this exact gap, so its statements that the TypeScript bridge cannot configure the policy are what this change falsifies and what it rewrites. In the TypeScript README, the "This bridge uses the default policy and offers no way to change it" paragraph becomes the opt-in, a worked example of the spread idiom, and the failure behaviour; that section's account of the defaults and of the missing per-run budget is upstream's and is still true, so it stands. In ARCHITECTURE.md, the parity bullet and the config-primitives row that both said TypeScript has no equivalent field are corrected, and the one-sided error-code bullet gains a clause for URL_FETCH_POLICY_INVALID. The Python README needs no change: it makes no claim about the TypeScript side, and the shape divergence between the two policies is already stated in ARCHITECTURE.md. No per-run budget is added here, so that divergence remains real and documented.
Contributor
Python Preview PackagesVersion
Install with uvAdd the TestPyPI index to your [[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = trueThen install the packages you need: # Core SDK
uv add 'ag-ui-protocol==0.0.0.dev1788423809' --index testpypi
# Integrations (each already depends on the matching ag-ui-protocol preview)
uv add 'ag-ui-langgraph==0.0.0.dev1788423809' --index testpypi
uv add 'ag-ui-crewai==0.0.0.dev1788423809' --index testpypi
# NOTE: ag-ui-agent-spec depends on pyagentspec (git-only, not on PyPI).
# You will need to install pyagentspec separately from its git repo.
uv add 'ag-ui-agent-spec==0.0.0.dev1788423809' --index testpypi
uv add 'ag_ui_adk==0.0.0.dev1788423809' --index testpypi
uv add 'ag_ui_strands==0.0.0.dev1788423809' --index testpypiInstall with pippip install \
--index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
ag-ui-protocol==0.0.0.dev1788423809
Commit: 5ec3b4b |
@ag-ui/a2a-middleware
@ag-ui/a2ui-middleware
@ag-ui/event-throttle-middleware
@ag-ui/mcp-apps-middleware
@ag-ui/mcp-middleware
@ag-ui/a2a
@ag-ui/adk
@ag-ui/ag2
@ag-ui/agno
@ag-ui/aws-strands
@ag-ui/claude-agent-sdk
@ag-ui/claude-managed-agents
@ag-ui/crewai
@ag-ui/langchain
@ag-ui/langgraph
@ag-ui/llamaindex
@ag-ui/mastra
@ag-ui/pydantic-ai
@ag-ui/vercel-ai-sdk
@ag-ui/watsonx
@ag-ui/a2ui-toolkit
create-ag-ui-app
@ag-ui/client
@ag-ui/core
@ag-ui/encoder
@ag-ui/proto
commit: |
AlemTuzlak
approved these changes
Sep 3, 2026
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 #2517.
The Python half of this issue landed already. The TypeScript bridge defined
UrlFetchPolicyand enforced it, but nothing let a host supply one: every URL content source was fetched under a hard-codedDEFAULT_URL_FETCH_POLICY. A deployment whose attachments live on a private CDN or behind split-horizon DNS had no supported way to reach them, and one that wanted the policy tighter had no way to say so. This is the TypeScript side of the same option.What changed
StrandsAgentConfig.urlFetchPolicy, optional,undefinedmeaning the safe default, mirroring Python'sOptional[UrlFetchPolicy] = None.DEFAULT_URL_FETCH_POLICYandUrlFetchPolicyErroras values,UrlFetchPolicyandSchemeAllowlistas types. The last one matters: the policy is an interface with no constructor, so an override is a spread over the exported default, and narrowingallowedSchemesneeds a name for the field's type.UrlFetchUnavailableErrorstays internal, as it does in Python.MediaConversionOptions, set once where a run builds its fetch options, so the construction seed, the replayed history and the live turn all fetch under one policy. Every URL-backed path (image, audio, video, document, and the deprecated binary form) resolves through that one call.maxBytes,timeoutMs,maxRedirects,allowPrivateNetworks,allowedSchemesandnat64Prefixesall change what a fetch returns for the same URL, and a refusal memoised under a strict policy must never be served to a caller who opted in. The key is now aJSON.stringifyof one array holding the URL and the policy's values, with both lists sorted, so the parts cannot run together the way a delimiter-joined string can and two structurally equal policies still share a download.RUN_ERROR { code: "URL_FETCH_POLICY_INVALID" }before any fetch is attempted. Checking it per fetch alone would have been swallowed: the history-replay conversion catches a throw and falls back to text, which would have turned a configuration mistake into attachments quietly stripped per message, closed but silent and easy to mistake for a dead URL.Per-hop redirect validation needed no production change. The loop in
fetchUrlContentalready re-validates each hop against the policy it was handed and pins the transport to that hop's approved addresses, so threading the caller's policy in is what makes the existing behaviour reachable. Two new tests hold it in both directions.Safe defaults are untouched. Non-HTTP(S), loopback, private, reserved, multicast and unspecified destinations are still refused with no configuration. Private-network access is the host's explicit opt-in and never anything a client can put in a
RunAgentInput; link-local addresses and the cloud metadata endpoints stay blocked even under it, andallowedSchemescan only be narrowed, because any scheme outsidehttp/httpswould resolve the host again at connection time and reopen the rebinding window the pinned transport closes.Tests
url-fetch-policy-configuration.test.tsis new (23 tests) andurl-fetch-policy.test.tsgains 6. Between them: the safe defaults with nothing configured, a private CDN refused with no policy and reached with one (over a real loopback server, throughagent.run, as Python does it), the metadata endpoint still refused under the opt-in, a redirect chain where an opt-in policy passes a later hop the default refuses and a narrowed policy refuses one the default allows, nine unusable-policy cases each failing the run with no fetch attempted, and the cache keyed so one policy's refusal is not reused under another while equal policies still share a single download, including a case where a policy field could be confused with the URL.All four of the Python criterion tests in
test_url_fetch_ssrf.pyhave TypeScript counterparts, so no parity exceptions were needed.Docs
The docs truth pass in #2611 had documented this exact gap, so its statements are what this change falsifies and rewrites rather than adds beside. In the TypeScript README, "This bridge uses the default policy and offers no way to change it" becomes the opt-in, a worked example and the failure behaviour; that section's account of the defaults and of the absent per-run budget is upstream's and still true, so it stands. In
ARCHITECTURE.md, the parity bullet and the config-primitives row that both said TypeScript has no equivalent field are corrected, and the one-sided error-code bullet gains a clause for the new code. The Python README needs no change: it makes no claim about the TypeScript side.docs-contract.test.tsgains two assertions over the new section, both mutation-checked so they are not vacuous.Deliberately not in scope
No request-level cumulative budget. Python's policy carries
max_attachments,max_total_bytesandmax_total_secondsand TypeScript's does not, while TypeScript carriesmaxRedirectsandnat64Prefixesand Python does not. That divergence predates this change, and the issue lists cumulative budgets as a non-goal tracked separately. It is stated in the docs rather than left implicit.One incidental change:
UrlFetchPolicyErrorloses its@internalmarker, becausestripInternaldrops a marked declaration from the emitted types and the package entry could not otherwise re-export it. A comment warns against adding one back.Verification
From
integrations/aws-strands/typescript:pnpm test: 77 files, 1647 tests passedpnpm typecheck: cleanpnpm build: clean, and all four new names are present indist/index.d.tswithUrlFetchUnavailableErrorcorrectly absentFrom
integrations/aws-strands/python:uv run pytest -q: 1274 passedtests/test_url_fetch_ssrf.py: 97 passed