fix(core): match web permissions against the URL that is requested

Merged
RE
4d ago
+20-14

Problem

webfetch rules were matched against the URL exactly as the model wrote it, but the request goes to the parsed, normalized URL. Rule matching is case-sensitive, so a different spelling of the same URL slipped past a deny rule:

deny rule: https://evil.com/*

webfetch({ url: "HTTPS://EVIL.COM/x" })
  check:   "HTTPS://EVIL.COM/x"  → no match → allowed
  request: https://evil.com/x    → reaches evil.com

Default ports (https://evil.com:443/x) worked the same way. This affected both the webfetch tool and Code Mode fetch.

Change

Both parse the URL once and use the normalized form (URL.href) for the permission check and for the request, so the rule sees exactly what gets fetched.

- permission.assert({ resources: [input.url] })   // as written
- request(input.url)
+ const url = new URL(input.url).href            // lowercase scheme and host, no default port
+ permission.assert({ resources: [url] })
+ request(url)

Paths keep their case, since they're case-sensitive. The webfetch tool's output and error messages still show the URL the model passed. An invalid URL in Code Mode fetch now rejects with a TypeError before the permission check, instead of after.

Visual

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <style>
    .a { animation: fadeA 3s ease-in-out infinite; }
    .b { opacity: 0; animation: fadeB 3s ease-in-out infinite; }
    @keyframes fadeA { 0%, 35% { opacity: 1 } 50%, 100% { opacity: 0 } }
    @keyframes fadeB { 0%, 40% { opacity: 0 } 55%, 95% { opacity: 1 } 100% { opacity: 0 } }
    @media (prefers-reduced-motion: reduce) { .a, .b { animation: none } .a { opacity: 0 } .b { opacity: 1 } }
  </style>
  <text class="a" x="12" y="40" font-family="ui-monospace, monospace" font-size="11" fill="currentColor" stroke="none">HTTPS://EVIL.COM/x</text>
  <text class="b" x="12" y="40" font-family="ui-monospace, monospace" font-size="11" fill="currentColor" stroke="none">https://evil.com/x</text>
  <path d="M150 36 H172" opacity=".35" stroke-dasharray="3 4"/>
  <path d="M196 18 L212 24 V36 C212 46 204 52 196 55 C188 52 180 46 180 36 V24 Z" opacity=".8"/>
  <path class="b" d="M190 31 L202 43 M202 31 L190 43"/>
</svg>

Verification

  • bun test for tool-execute, tool-webfetch and codemode in packages/core pass. bun typecheck and bun run typecheck:tsc are clean.

Added by the opencode bot (AI-generated).

Related: #55232