Repository navigation
Let operators turn off or host-limit the agent's http_fetch (agent.httpFetch) - #3011
Merged
Merged
Conversation
…tpFetch)
agent.httpFetch: false removes http_fetch from the composed toolset; an
{ allow: [...] } list (host, host:port, *.domain, [ipv6]) refuses any
request whose host does not match. The policy is read once at boot:
set_agent_config rejects an httpFetch patch, and the tool is built once
rather than from live config.
Redirects are now followed one hop at a time so every target is checked
before it is sent, in every mode, which also closes a redirect into the
metadata blocklist. The blocklist compares canonical addresses, so the
AWS IPv6 IMDS address and IPv4-mapped spellings no longer slip past.
Refs #2974
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tching does An IPv6 entry carrying a zone ID passed isIP but threw from URL outside the entry check, so the boot log said "Invalid URL" without naming it. The design note now says IPv4-mapped spellings are equated only by the metadata blocklist, and that the policy bounds http_fetch alone. Refs #2974 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h-policy Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Release cherry-pick
|
Matches the file's existing `err instanceof Error` idiom in the malformed-policy log line, and lets the request-handling suite's after hook survive a before hook that failed partway (gemini review). Refs #2974 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…et it Without an entry, AGENT_HTTPFETCH=false and --AGENT_HTTPFETCH were read by nothing and left the tool fully enabled, silently. Leaving the key out to keep set_configuration from writing it bought little: that operation is destructive, outside the agent's default toolset, needs a restart to take effect, and can already write every other agent_* key. Refs #2974 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DavidCockerill
added a commit
to HarperFast/documentation
that referenced
this pull request
Oct 5, 2026
…uration Follows HarperFast/harper#3011 registering agent_httpFetch and agent_httpFetch_allow in CONFIG_PARAMS. Refs HarperFast/harper#2974 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DavidCockerill
marked this pull request as ready for review
October 5, 2026 15:15
kriszyp
approved these changes
Oct 5, 2026
|
|
||
| const ALLOW_ENTRY = /^(?:\[([^\]]+)\]|([^:[\]]+))(?::(\d{1,5}))?$/; | ||
|
|
||
| export interface HostRule { |
Member
There was a problem hiding this comment.
HostRule is used by functions in this module, but nothing in this checkout imports the interface itself. Could we remove export? That keeps this tool-internal type out of the module API while retaining its local type checks.
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.
Operators can now remove the built-in agent's
http_fetchtool or limit it to named hosts with a new boot-timeagent.httpFetchsetting:falsedrops the tool from the toolset, and{ allow: [...] }refuses any request, including any redirect hop, whose host is not listed. Redirects are now checked hop by hop in every mode, which also stops a redirect into the cloud-metadata blocklist, and the blocklist now catches the AWS IPv6 IMDS address and IPv4-mapped spellings it used to miss.Closes #2974 · Docs: HarperFast/documentation#708
For the human reviewer
better-alternative-exists) proposed leaving nativefetchredirect-following in place forhttpFetch: trueand following hops manually only under an allow-list, so the default path changes nothing. I kept one path. Today a single redirect defeats the tool's own metadata blocklist (from169.254.169.254to the AWS IPv6 IMDS address), and the split would leave that open in the mode every enabled deployment runs, Fabric's default render included. The only default-mode behavior change is that a redirect into the blocklist now fails. The cost is about 0.17 ms over three loopback redirects (median 5.18 ms native vs 5.35 ms, 500 runs). Reversible: the default path could go back toredirect: 'follow'in one line, re-opening the bypass. The hop step re-implements fetch's redirect rules (method rewrite, body-header removal, credential headers dropped across origins, 20-hop cap). The tests pin it to nativefetch's observed behavior, but it is now code we own.mcp.operations.allowis widened (for exampledeploy_component's package source). That is a separate decision, and the design note says so.set_agent_configrejects anhttpFetchpatch with a 400 instead of ignoring it, so an operator can't tighten it at runtime either; an emergency stop is a restart. Allowing runtime narrowing only would be easy to add later. The key is registered inCONFIG_PARAMSlike every otheragent_*key, soAGENT_HTTPFETCH=false,--AGENT_HTTPFETCH false,AGENT_HTTPFETCH_ALLOW='["…"]'andset_configurationall work, each taking effect on restart. My first draft left it out, so thatset_configurationcouldn't rewrite the policy. That leftAGENT_HTTPFETCH=falsesilently ignored, with the tool fully on, and the protection it bought was thin.set_configurationis destructive and outside the agent's default toolset, needs a restart (also destructive) to take effect, and can already writeagent_allowDestructiveandagent_user. The residual risk the delta review named is an operator who optsset_configurationandrestartintomcp.operations.allowwithautoApprove: true. That agent could widen its own egress across one restart, but that configuration already hands it the whole config.http_fetch. An empty allow-list also removes the tool, since a tool that refuses everything only wastes model turns.localhostalso admits the operations API port.*.example.comcovers any depth but not the apex. Matching is by host name: an allowed name that resolves to an internal address is reached, and the docs say to list names whose DNS you control. Resolving and pinning addresses would need a custom dispatcher, which the planning note rejected because it sees the proxy peer, not the target.Product and architecture tour
What an operator can set
What can the agent reach, and who can change that?
http_fetchis always in the toolset. It refuses only cloud-metadata hosts by literal name, and follows redirects without checking them.agent.httpFetch: truekeeps that reach, but every redirect hop is now checked.falseremoves the tool.{ allow: [...] }refuses any host not listed, and the tool description names the listed hosts.Example: an investigation role with no egress
An operator enables the agent to read logs and tables, and sets
httpFetch: false. The boot log reports29 tools (http_fetch: disabled). The model's tool list has nohttp_fetch, and the system prompt no longer tells it to verify work over HTTP.Outcome: A prompt-injected instruction to send data somewhere has no tool to do it with.
Every hop is checked before it is sent
Why does the tool follow redirects itself?
fetchwithredirect: 'follow'sends the next request before any caller code can see where it goes, so a host check on the first URL says nothing about the second. The tool now requests each hop withredirect: 'manual', cancels the redirect body, resolvesLocation, and runs the same target check before the next request. Method, body and credential-header handling copy fetch's own redirect step, so allowed redirects behave as they did before.A redirect toward a host outside the policy
The refused host is never contacted; the model gets a policy error naming it.
sequenceDiagram participant Model participant Tool as http_fetch participant Allowed as allowed host participant Other as other host Model->>Tool: fetch allowed host Tool->>Tool: check target Tool->>Allowed: request, redirect manual Allowed-->>Tool: 302 Location other host Tool->>Tool: cancel body, check Location Tool-->>Model: refused by policy Note over Other: never contactedHow a host matches
What does an allow-list entry admit?
Example: a wildcard entry
With
allow: ['*.example.org', 'api.example.com:8443'],https://a.b.example.org/is allowed.http://example.org/is refused, as ishttps://api.example.com/, whose effective port is 443.Outcome: An apex domain and every port pin are explicit decisions in the list.
Changes
agent/tools/httpFetchTool.ts:resolveHttpFetchConfigvalidates the raw setting.parseAllowEntrynormalizes each entry and rejects a bracketed host that is not a plain IPv6 address.checkHttpFetchTargetis the per-target gate, ending in the refused-by-policy error. The blocklist now includes the AWS IPv6 IMDS address.buildHttpFetchToolreplaces the module constant and lists the allowed hosts in the description.agent/toolset.ts:composeToolsettakes the policy-built tool and reserves thehttp_fetchname.agent/agent.ts:mergeConfigresolves the settingagent/operations.ts:set_agent_configrejectshttpFetch.agent/types.ts:HttpFetchConfigandAgentConfig.httpFetch.utility/hdbTerms.ts:AGENT_HTTPFETCHandAGENT_HTTPFETCH_ALLOWinCONFIG_PARAMS, so per-key env vars, CLI flags andset_configurationreach the setting.config-root.schema.json:agent.httpFetchas boolean or{ allow: string[] }with an entry pattern.agent/DESIGN.md,DESIGN.md: the invariant note and its index line.Verification
Route: live smoke (c). The agent can't run a tool without a model, so no integration test reaches it. I booted the built
distfrom a scratch root against a local fake OpenAI-compatible model that answers the first prompt with threehttp_fetchcalls: an allowed host, a denied host, and an allowed host that 302s to the denied one. Each row is a fresh boot:agent.httpFetch30 tools (http_fetch: enabled){ allow: ['127.0.0.1:18081'] }30 tools (http_fetch: allow-list 127.0.0.1:18081)refused by policy: 127.0.0.1:18082 is not in agent.httpFetch.allowfalse29 tools (http_fetch: disabled)unknown_tool; model's tool list has nohttp_fetch, system prompt has no fetch mention{ allow: [] }29 tools (http_fetch: disabled)unknown_tool"nope"agent.httpFetch must be true, false, or { allow: [...] }; got "nope"; http_fetch is disabled…, then29 toolsunknown_toolThe tool description in the model request listed the allowed host, and
set_agent_config {httpFetch: true}returnedagent.httpFetch is fixed at startup…in every row. The smoke ran at 8e24c38; later commits only fix an IPv6 zone-ID error message, tighten the design note, and mergemain.The config routes, each on a fresh root at fad656c:
AGENT_HTTPFETCH=false29 tools (http_fetch: disabled)AGENT_HTTPFETCH_ALLOW='["127.0.0.1:18081"]'30 tools (http_fetch: allow-list 127.0.0.1:18081); denied and redirected calls refused, denied server 0 hitsharper run --AGENT_HTTPFETCH false29 tools (http_fetch: disabled)set_configuration {agent_httpFetch_allow: ["127.0.0.1:18081"]}allow-list 127.0.0.1:18081, enforced end to endset_configuration {agent_httpFetch: false}, restart, thenset_configuration {agent_httpFetch_allow: [...]}disabled, thenallow-list 127.0.0.1:18081. The boolean parent is replaced, which the delta review had doubted.AGENT_HTTPFETCH_ALLOW='127.0.0.1:18081,*.example.com'agent.httpFetch.allow must be a list of hosts; got "…", then29 tools (http_fetch: disabled)Unit tests:
unitTests/agent/httpFetchTool.test.js:fetch, credential headers, the 20-hop cap, redirect-body release on refusal, one timeout across hopsunitTests/agent/registryTools.test.js— the reserved name.unitTests/agent/operations.test.js— theset_agent_config400.The redirect-body test fails when the cancel is removed. On
origin/main,http://[fd00:ec2::254]/and a redirect to169.254.169.254both passed the policy and attempted the connection; both are refused here.Gates, run locally on macOS:
npx mocha "unitTests/agent/*.test.js": 158 passing, re-run after the merge frommain.npx mocha "unitTests/config/**/*.test.js" "unitTests/agent/*.test.js": 528 passing at fad656c, after theCONFIG_PARAMSchange.test:unit:main: 6410 passing, 18 failing. I ran it withunitTests/components/applicationSpawn.test.jsexcluded, because that file hangs on macOS onorigin/maintoo (harper#2538).test:unit:resources: 3915 passing, 3 failing.origin/mainbuild of the same files, and none are in agent code. Causes:/varvs/private/vartmpdir paths, agit tagfixture under a signing config, native addons without install scripts, process-group harnesses, and RocksDB flush-premise tests. The Linux CI run is the real gate for these.test:integration:all: 2248 passing, 11 failing, 18 skipped. All 11 are worker-thread restart and isolation tests (isolated-application,shutdown-drain-e2e,record-lock-concurrency,log-rotation-write-path,rolling-restart), and they fail the same way on a cleanorigin/mainrun of those five files: a UDS mirror path over the macOS socket-path limit, and a single HTTP worker. None of these configs loads the agent.npm run lintreports 13 warnings, all in files this PR doesn't touch;prettier --checkandcheck:design-docspass on the changed files. The schema was checked with ajv against 12 valid and invalidhttpFetchdocuments.Complexity: medium
🤖 Generated with Claude Code
Review-Coverage: authored=claude; ran=gemini,codex; adjudicated=domain; blocked=cursor-grok(not-installed); declined=cursor-composer,cursor-kimi,cursor-muse; rounds=2; full=1 @ fad656c
Human-Review-Need: 4 (decisions: restart-effective-config-writes, boot-only-policy, default-unrestricted, manual-redirects-all-modes, malformed-disables-tool, portless-entry-any-port, name-only-matching, scope-http-fetch-only) @ fad656c