Skip to content

feat: web_request system tool (v1) + FRD 0005#96

Merged
larohra merged 18 commits into
mainfrom
larohra-web-request-system-tool
Jul 14, 2026
Merged

feat: web_request system tool (v1) + FRD 0005#96
larohra merged 18 commits into
mainfrom
larohra-web-request-system-tool

Conversation

@larohra

@larohra larohra commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Purpose

Adds a built-in web_request system tool so agents can make outbound HTTP(S) calls directly, instead of hand-writing a custom tool per endpoint or spinning up the Dynamic Sessions code interpreter (an ACA sessions resource + managed identity) just to hit a REST API. Calling an HTTP API is the single most common integration need, and making it safe is genuinely hard (SSRF, IMDS token theft, DNS rebinding). Centralizing it as a governed system tool means every agent inherits the security floor for free.

This PR delivers the v1 scope from FRD 0005 and also brings in the FRD document itself (this PR supersedes the FRD-only PR #87, which can be closed).

What v1 gives you:

  • A single tool call web_request(method, url, headers?, query?, body?|json?) that makes ONE outbound request to a public host and returns a structured status + headers + parsed body.
  • Default-on (see note below): agents get the tool automatically; it can be disabled app-wide (system_tools.web_request: false) or per-agent (web_request: false), and configured via a system_tools.web_request object.
  • An always-on SSRF floor that cannot be turned off: rejects non-HTTP(S)/userinfo/bad-port URLs, enforces an optional exact-host allowlist before DNS, resolves and validates every IP as global-unicast-only (blocks loopback, link-local, RFC1918, ULA, CGNAT 100.64/10, IMDS 169.254.169.254, unspecified, reserved, multicast), unwraps IPv4-mapped IPv6, rejects non-canonical numeric IPs, then pins the connection to the validated IP (custom aiohttp resolver/connector, DNS cache off) so DNS rebinding cannot swap the address after validation. HTTPS is required unless require_https: false.

Approach: mirrors the existing dynamic_sessions_code_interpreter (sandbox) system tool across the config -> merge -> impl -> registration -> telemetry pipeline, with two deliberate differences: (1) resolution is default-on rather than opt-in, and (2) because the tool is stateless it is built once per agent at registration and threaded through a dedicated web_request_tools channel (never merged with sandbox_tools), rather than rebuilt per request.

Note on default-on: this is additive and does not break any existing config or API, but it does mean agents gain outbound-HTTP capability by default. The SSRF floor (public hosts only, private/link-local/IMDS blocked) is always enforced, and operators can disable globally or per-agent. Flagging it explicitly so reviewers can weigh the posture.

Validated and works E2E -
image

Does this introduce a breaking change?

[ ] Yes
[x] No

Pull Request Type

What kind of change does this Pull Request introduce?

[ ] Bugfix
[x] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Documentation content changes
[ ] Other... Please describe:

How to Test

  • Get the code
git clone https://github.com/Azure/azure-functions-agents-runtime.git
cd azure-functions-agents-runtime
git checkout larohra-web-request-system-tool
pip install -e .[dev]
  • Test the code

The repo requires Python 3.13+ (PEP 695 syntax). Run the canonical gate:

python -m ruff check src tests
python -m mypy src
python -m pytest tests -q

Expected: ruff clean, mypy strict clean (37 source files), 587 tests passed. The new SSRF/response/telemetry suite lives in tests/test_web_request.py; config resolution in tests/test_config_merge.py + fixture tests/fixtures/config_scenarios/14_web_request/; default-on registration in tests/test_registration_capabilities.py.

What to Check

Verify that the following are valid

  • SSRF floor is fail-closed and validates every resolved IP, pins the connection to it, and preserves the Host header / TLS SNI. Allowlist is enforced before DNS.
  • Default-on resolution (config/merge.py _resolve_web_request): global absent/None/true -> defaults; global false -> disabled app-wide; global object -> that config; per-agent false -> opt-out regardless of global.
  • v1 scope discipline: no auth / per-host credentials, no {env: VAR} secret refs, no redirect following (allow_redirects=False, redirect_count always 0), no per-agent override object (per-agent is bool-only), no wildcard/suffix host matching, no IDNA/punycode. These are documented in the FRD as v2/v3+ and must be absent from product code.
  • Telemetry redaction: the web_request span and counter record success/error without leaking full URL, query string, or secret header values.
  • Caps clamping: timeout and request/response byte sizes are clamped to absolute operational maximums.

Other Information

  • Design: docs/frds/0005-web-request-system-tool.md (Section 4 documents the full target design; only untagged items are v1, v2/v3+ items are tagged). Deferred to later versions: authenticated requests + per-host secrets, redirect following with per-hop re-validation, per-agent override objects, wildcard host matching, IDNA.
  • Refs Azure/azure-functions-bucees-planning#1176 (this delivers v1 of that phased ask; later versions remain open, so this does not auto-close it).
  • Docs updated: front-matter-reference.md (regenerated), front-matter-spec.md, architecture.md, observability.md, README.md.
  • Validation: implemented by a claude-sonnet-5 session, then independently reviewed by a gpt-5.5 rubber-duck pass (no blocking correctness or SSRF defect, no v2 scope leakage) plus a re-run of the full gate. The last commit hardens regression coverage on the security-critical paths (trailing-dot allowlist normalization, non-HTTP(S) scheme rejection, full header denylist/redaction sets with mixed-case, and default-on capability build/suppress).

larohra and others added 13 commits July 8, 2026 22:49
Design doc for a built-in, opt-in http_call system tool that lets agents invoke HTTP endpoints declaratively instead of code-generating requests in the sandbox. Includes SSRF security floor, per-host auth via typed env secret-refs, per-agent override model, and full telemetry parity with the sandbox.

Incorporates two independent architecture reviews (gpt-5.4 + gpt-5.5). Status: In review, pending human sign-off.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Fold the follow-up security discussion into FRD 0004 for the http_call system tool:

- New section 4 subsections: 'Execution surfaces & the in-worker-code boundary'
  (model code runs only in the ACA sandbox, never the worker, with no in-worker
  fallback; tools/*.py is the sole in-worker author-code path, in-trust-domain and
  not model-reachable at runtime; standing invariant + author guideline),
  'Residual threat: reflection / confused-deputy exfiltration' (named, with a
  mitigation ladder), and 'Alternatives considered: out-of-process secret custody'
  (ACA egress-proxy pros/cons table, why v1 is independent of it, compute re-host
  rejection, and a tiered custody roadmap).
- New section 5 Decisions rows I1-I4 (Human-decided, 2026-07-09).
- Section 7 docs impact + section 8 review record updated.

No product-code or scope changes; the sign-off gate is unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
In-progress: reduce v1 to a public, unauthenticated fetch primitive (SSRF floor + caps + exact-host allowlist + minimal telemetry). Summary, motivation, and Goals updated; Non-goals/Phased-delivery table, section 4 v2 tags, and Decisions rows still pending in a follow-up commit.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6c80e793-f72f-4629-ba8e-16e5847dbe53
Resolved docs/frds/README.md index conflict. main added FRD 0004 (Dynamic workflows, Finalized), which collides with this branch's FRD 0004 (http_call). Renumbered the http_call FRD to 0005 (git mv + frontmatter + heading) so main's 0004 owns the slot; README lists both 0004 and 0005.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6c80e793-f72f-4629-ba8e-16e5847dbe53
Narrow FRD 0005 v1 to a public, unauthenticated fetch primitive and make the tool on-by-default (opt out via `http_call: false` or `tools: false`). Restructure the section 4 examples to a v1-minimal surface, moving the full auth/wildcard/override example into a 'once all versions ship' subsection. Tag the section 4 security subsections and section 6 tests with v1/v2 markers, append Decisions J1-J7 (scope reduction + default-on), and refresh the docs-impact and status sections. FRD stays In review; the human sign-off gate is unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6c80e793-f72f-4629-ba8e-16e5847dbe53
Rename the FRD 0005 tool and config key http_call -> web_request, and the
scheme flag allow_http (default false) -> require_https (default true,
inverted polarity). This removes the overloaded "http" (tool name vs URL
scheme) and the misread where allow_http: false looked like it disabled a
now default-on tool.

Also renames the FRD file slug, updates the docs/frds README index, and
appends Decisions row J8. Historical log rows A1-J7 intentionally keep the
original identifiers (append-only log); a naming note points to J8.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6c80e793-f72f-4629-ba8e-16e5847dbe53
Remove deep v2/v3+ why-analysis prose that duplicated the append-only Decisions log: drop the Trust-model subsection, collapse Redirect/Secret-custody/Execution-surface/Reflection/egress-proxy-alternatives (~200 lines) into a compact 'Deferred to v2/v3+' notes block, and cut the review-history retrospective from section 8. Also compress the section 2 motivation and section 3 non-goals prose.

Section 5 Decisions log (A1-J8) is preserved verbatim per the append-only convention; the full rationale remains there and in git history. No design change; FRD stays In review. 865 -> 594 lines.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6c80e793-f72f-4629-ba8e-16e5847dbe53
… and telemetry

Adds WebRequestConfig, SystemToolsConfig.web_request, SystemToolsAgentOverride.web_request,
and ResolvedAgent.web_request_config to config/schema.py; adds default-on _resolve_web_request()
to config/merge.py; adds FaultDomain.WEB_REQUEST and record_web_request() counter plumbing to
_observability.py.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 53035269-5774-46e6-a0bc-a7eba5601967
Default-on, unauthenticated, single-request web_request tool with an always-on SSRF
security floor: exact-host allowlist, global-unicast-only IP validation via an injectable
async resolver, IP pinning through a custom aiohttp resolver/connector (DNS caching
disabled), request-header denylist, response-header redaction, and capped/truncating
response reads. Config is clamped to operator-defined absolute ceilings (timeout 120s max,
request/response body 10MB max; defaults: 30s timeout, 5MB max response, 1MB max request).
Built once per agent at registration (no Azure resource required).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 53035269-5774-46e6-a0bc-a7eba5601967
Adds AgentCapabilities.web_request_tools (built once per agent, lazily importing the
factory), threads a dedicated web_request_tools parameter through runner.run_agent()/
run_agent_stream() parallel to sandbox_tools, passes it at every registration/_handlers.py
and registration/endpoints.py call site, tracks 'web_request' in app.py's system_tools_used
reporting (respecting default-on + global/per-agent disable), and exports
create_web_request_tools from the package __init__.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 53035269-5774-46e6-a0bc-a7eba5601967
New tests/test_web_request.py: param-schema validation, response shaping (JSON/text/
binary/HEAD/truncation), the full SSRF suite (IMDS/loopback/private/link-local/ULA/CGNAT/
unspecified/multicast/reserved, IPv4-mapped IPv6 unwrap, non-canonical numeric IP literal
rejection, embedded-userinfo rejection, exact-host allowlist, https floor), IP pinning
(validated IPs, DNS cache disabled, Host/SNI preserved), header policy, telemetry
(span + counter, no query/secret leakage), config clamping, and default-on registration.
New tests/test_config_merge.py cases for _resolve_web_request's default-on resolution.
New fixtures/config_scenarios/14_web_request scenario plus a test_config_fixtures.py case
exercising global config + per-agent opt-out through compose(). Updates
test_package_imports.py and test_registration_capabilities.py for the new export/field.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 53035269-5774-46e6-a0bc-a7eba5601967
Regenerates docs/front-matter-reference.md (schema-driven) after adding WEB_REQUEST_DESCRIPTIONS
and a system_tools.web_request table to eng/scripts/generate_config_reference.py. Adds a
system_tools.web_request section with default-on/disable/opt-out examples to
front-matter-spec.md (v2 items called out as future work), updates docs/architecture.md's
module map and pipeline narrative for the new default-on, built-once-at-registration tool,
adds a web_request span/metric/fault-domain reference to docs/observability.md, and adds a
one-line quickstart mention to README.md.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 53035269-5774-46e6-a0bc-a7eba5601967
Address gpt-5.5 FIX-THEN-SHIP validation feedback (test-only, no product
code changes):

- tests/test_web_request.py: add explicit regression tests for
  trailing-dot allowlist normalization (single dot stripped ->
  match; double dot -> no match) and non-HTTP(S) scheme rejection
  (ftp://, file://) prior to any DNS/connect. Parameterize the
  request-header-denylist and response-header-redaction tests over
  the actual module-level '_REQUEST_HEADER_DENYLIST' /
  '_RESPONSE_HEADER_REDACT' constants (canonical + mixed-case
  variants) so coverage can't silently drift from the implementation.
- tests/test_registration_capabilities.py: add a direct
  build_capabilities() regression for the web_request_tools channel —
  default WebRequestConfig() builds exactly one tool; None config and
  tools_disabled=True both suppress the tool AND skip the lazy
  'import_module' factory call entirely (asserted via a monkeypatched
  import_module that raises if invoked).

Gate (via uv run, Python 3.13.7): ruff clean, mypy --strict clean
(37 files), pytest 587 passed (+35 vs. 552 baseline).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 53035269-5774-46e6-a0bc-a7eba5601967
Human sign-off received: flip status In review -> Finalized, confirm the Agent-decided rows, add PR #96 to pull_requests, and record the sign-off / PR-consolidation decision as append-only row K1. Update section 8 to reflect completed implementation (587 tests) shipped via #96; PR #87 closed as superseded.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6c80e793-f72f-4629-ba8e-16e5847dbe53

Copilot AI 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.

Pull request overview

Introduces a new built-in web_request system tool (default-on) that lets agents make outbound HTTP(S) requests to public hosts with an always-on SSRF safety floor, and wires it through config resolution, capability building, runner/tool assembly, and observability. The PR also adds FRD 0005 and updates user/docs surfaces to reflect the new tool and configuration model.

Changes:

  • Add web_request tool implementation with SSRF validation + IP pinning + response shaping + telemetry/metrics.
  • Add config schema + merge logic for default-on enablement with global disable and per-agent opt-out.
  • Add comprehensive tests, fixture scenarios, and docs/FRD updates.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/test_web_request.py New unit suite covering params schema, response shaping, SSRF validator, header policy, and telemetry behavior.
tests/test_registration_capabilities.py Verifies default-on tool creation at registration and suppression when disabled.
tests/test_package_imports.py Adds create_web_request_tools to public export surface checks.
tests/test_config_merge.py Tests _resolve_web_request default-on merge/override semantics.
tests/test_config_fixtures.py Adds config scenario fixture assertions for global config + per-agent opt-out.
tests/fixtures/config_scenarios/14_web_request/opted_out.agent.md New per-agent opt-out fixture.
tests/fixtures/config_scenarios/14_web_request/main.agent.md New default agent fixture inheriting global web_request config.
tests/fixtures/config_scenarios/14_web_request/agents.config.yaml New global config fixture for system_tools.web_request.
src/azure_functions_agents/system_tools/web_request.py New tool implementation (SSRF floor, IP pinning via aiohttp resolver, caps, response shaping, telemetry).
src/azure_functions_agents/runner.py Adds web_request_tools channel to tool assembly for run + stream paths.
src/azure_functions_agents/registration/endpoints.py Threads capabilities.web_request_tools into builtin endpoint execution paths.
src/azure_functions_agents/registration/capabilities.py Builds web_request tools once per agent at registration via lazy import.
src/azure_functions_agents/registration/_handlers.py Threads web_request_tools through trigger handlers into runner calls.
src/azure_functions_agents/config/schema.py Adds WebRequestConfig and web_request fields to global/per-agent system tool config + resolved agent.
src/azure_functions_agents/config/merge.py Implements default-on _resolve_web_request resolution semantics.
src/azure_functions_agents/app.py Adds web_request to system tools indexing summary.
src/azure_functions_agents/_observability.py Adds FaultDomain.WEB_REQUEST and new metrics counters for web_request.
src/azure_functions_agents/init.py Exposes create_web_request_tools in the package public API.
README.md Documents the new default-on web_request capability and observability surface.
eng/scripts/generate_config_reference.py Updates config reference generator to include system_tools.web_request.
docs/observability.md Documents web_request span attributes and metrics.
docs/front-matter-spec.md Adds authoring/spec documentation for system_tools.web_request.
docs/front-matter-reference.md Regenerated reference including system_tools.web_request.
docs/frds/README.md Adds FRD 0005 index entry.
docs/frds/0005-web-request-system-tool.md Adds FRD 0005 (design + decisions log + scope).
docs/architecture.md Updates module map/pipeline narrative to include web_request wiring.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/azure_functions_agents/system_tools/web_request.py Outdated
Comment thread src/azure_functions_agents/_observability.py Outdated
Comment thread docs/front-matter-reference.md Outdated
Comment thread docs/front-matter-spec.md Outdated
Comment thread eng/scripts/generate_config_reference.py Outdated

@larohra larohra left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@copilot fix this

Comment thread src/azure_functions_agents/system_tools/web_request.py Outdated

Copilot AI commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

@copilot fix this

Done in commits fix: address PR reviewer feedback and fix: split global statement:

  1. URL leakage (web_request.py:648-652): aiohttp.ClientError handler now uses type(exc).__name__ only — the full exception message (which can embed the request URL + query string) is no longer surfaced in the telemetry span, log, or the JSON error returned to the model.

  2. global backslash continuations (_observability.py:434): replaced with two clean global statements (Python doesn't support parenthesized global).

  3. allowed_hosts default (front-matter-reference.md:40): was [], now null — matching the schema (list[str] | None = None) and the actual semantic (omit = any public host allowed). Fixed in generate_config_reference.py via a custom_defaults entry, then regenerated.

  4. "operator-defined maximum" wording (front-matter-reference.md:42-44, front-matter-spec.md:440, generate_config_reference.py:252-255): replaced with "runtime-defined ceiling (N)" with the concrete values (timeout: 120 s, response/request: 10 MB). front-matter-reference.md regenerated.

@larohra larohra marked this pull request as ready for review July 13, 2026 23:30
@larohra larohra requested a review from a team as a code owner July 13, 2026 23:30
Comment thread docs/front-matter-reference.md
Comment thread docs/front-matter-reference.md
@larohra larohra merged commit 5092ca8 into main Jul 14, 2026
7 checks passed
@larohra larohra deleted the larohra-web-request-system-tool branch July 14, 2026 20:41
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.

4 participants