Skip to content

feat(aws-strands): expose the URL fetch policy through the TypeScript adapter config - #2618

Merged
ranst91 merged 1 commit into
mainfrom
claude/objective-sammet-f3df76
Sep 3, 2026
Merged

feat(aws-strands): expose the URL fetch policy through the TypeScript adapter config#2618
ranst91 merged 1 commit into
mainfrom
claude/objective-sammet-f3df76

Conversation

@ranst91

@ranst91 ranst91 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Closes #2517.

The Python half of this issue landed already. The TypeScript bridge defined UrlFetchPolicy and enforced it, but nothing let a host supply one: every URL content source was fetched under a hard-coded DEFAULT_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, undefined meaning the safe default, mirroring Python's Optional[UrlFetchPolicy] = None.
  • Public exports. DEFAULT_URL_FETCH_POLICY and UrlFetchPolicyError as values, UrlFetchPolicy and SchemeAllowlist as types. The last one matters: the policy is an interface with no constructor, so an override is a spread over the exported default, and narrowing allowedSchemes needs a name for the field's type. UrlFetchUnavailableError stays internal, as it does in Python.
  • Threaded through 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.
  • Policy-aware fetch cache. Once a policy can vary, a URL alone is no longer a sufficient key: maxBytes, timeoutMs, maxRedirects, allowPrivateNetworks, allowedSchemes and nat64Prefixes all 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 a JSON.stringify of 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.
  • Invalid configuration fails closed at run level. An unusable policy ends the run with 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 fetchUrlContent already 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, and allowedSchemes can only be narrowed, because any scheme outside http/https would resolve the host again at connection time and reopen the rebinding window the pinned transport closes.

Tests

url-fetch-policy-configuration.test.ts is new (23 tests) and url-fetch-policy.test.ts gains 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, through agent.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.py have 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.ts gains 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_bytes and max_total_seconds and TypeScript's does not, while TypeScript carries maxRedirects and nat64Prefixes and 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: UrlFetchPolicyError loses its @internal marker, because stripInternal drops 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 passed
  • pnpm typecheck: clean
  • pnpm build: clean, and all four new names are present in dist/index.d.ts with UrlFetchUnavailableError correctly absent

From integrations/aws-strands/python:

  • uv run pytest -q: 1274 passed
  • tests/test_url_fetch_ssrf.py: 97 passed

… 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.
@ranst91
ranst91 requested a review from a team as a code owner September 3, 2026 08:23
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Python Preview Packages

Version 0.0.0.dev1788423809 published to TestPyPI.

Warning: These packages are built from contributor code that may not yet have been vetted for correctness or security. Install at your own risk and do not use in production.

Install with uv

Add the TestPyPI index to your pyproject.toml:

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = true

Then 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 testpypi

Install with pip

pip install \
  --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple/ \
  ag-ui-protocol==0.0.0.dev1788423809

Use --extra-index-url https://pypi.org/simple/ so pip can resolve
transitive dependencies (pydantic, fastapi, etc.) from real PyPI.


Commit: 5ec3b4b

@pkg-pr-new

pkg-pr-new Bot commented Sep 3, 2026

Copy link
Copy Markdown

Open in StackBlitz

@ag-ui/a2a-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a-middleware@2618

@ag-ui/a2ui-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-middleware@2618

@ag-ui/event-throttle-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/event-throttle-middleware@2618

@ag-ui/mcp-apps-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-apps-middleware@2618

@ag-ui/mcp-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-middleware@2618

@ag-ui/a2a

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a@2618

@ag-ui/adk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/adk@2618

@ag-ui/ag2

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/ag2@2618

@ag-ui/agno

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/agno@2618

@ag-ui/aws-strands

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/aws-strands@2618

@ag-ui/claude-agent-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-agent-sdk@2618

@ag-ui/claude-managed-agents

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-managed-agents@2618

@ag-ui/crewai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/crewai@2618

@ag-ui/langchain

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langchain@2618

@ag-ui/langgraph

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langgraph@2618

@ag-ui/llamaindex

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/llamaindex@2618

@ag-ui/mastra

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mastra@2618

@ag-ui/pydantic-ai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/pydantic-ai@2618

@ag-ui/vercel-ai-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/vercel-ai-sdk@2618

@ag-ui/watsonx

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/watsonx@2618

@ag-ui/a2ui-toolkit

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-toolkit@2618

create-ag-ui-app

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/create-ag-ui-app@2618

@ag-ui/client

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/client@2618

@ag-ui/core

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/core@2618

@ag-ui/encoder

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/encoder@2618

@ag-ui/proto

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/proto@2618

commit: 7d0c439

@ranst91
ranst91 merged commit 778386f into main Sep 3, 2026
55 checks passed
@ranst91
ranst91 deleted the claude/objective-sammet-f3df76 branch September 3, 2026 13:16
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.

[Feature]: expose AWS Strands URL fetch policy through public adapter configuration

2 participants