feat: web_request system tool (v1) + FRD 0005#96
Conversation
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
There was a problem hiding this comment.
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_requesttool 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.
Done in commits
|
Purpose
Adds a built-in
web_requestsystem 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:
web_request(method, url, headers?, query?, body?|json?)that makes ONE outbound request to a public host and returns a structuredstatus+headers+ parsedbody.system_tools.web_request: false) or per-agent (web_request: false), and configured via asystem_tools.web_requestobject.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 dedicatedweb_request_toolschannel (never merged withsandbox_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 -

Does this introduce a breaking change?
Pull Request Type
What kind of change does this Pull Request introduce?
How to Test
The repo requires Python 3.13+ (PEP 695 syntax). Run the canonical gate:
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 intests/test_config_merge.py+ fixturetests/fixtures/config_scenarios/14_web_request/; default-on registration intests/test_registration_capabilities.py.What to Check
Verify that the following are valid
config/merge.py_resolve_web_request): global absent/None/true -> defaults; globalfalse-> disabled app-wide; global object -> that config; per-agentfalse-> opt-out regardless of global.{env: VAR}secret refs, no redirect following (allow_redirects=False,redirect_countalways 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.web_requestspan and counter record success/error without leaking full URL, query string, or secret header values.Other Information
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.front-matter-reference.md(regenerated),front-matter-spec.md,architecture.md,observability.md,README.md.