Skip to content

fix(core): apply webfetch permissions to Code Mode fetch - #59952

Closed
rekram1-node wants to merge 1 commit into
effect-extensionsfrom
codemode-fetch-permission
Closed

rekram1-node wants to merge 1 commit into
effect-extensionsfrom
codemode-fetch-permission

Conversation

@rekram1-node

Copy link
Copy Markdown
Contributor

Stacked on #59948. Merge that first.

Problem

fetch inside Code Mode never checked permissions. A URL someone denied for webfetch could still be fetched from a Code Mode program, and an "ask" rule never asked.

Change

fetch is now an Effect global (from the PR this is stacked on). Before sending, it asks the same webfetch permission as the tool, for its URL, on behalf of the execute call it runs under. Code Mode itself still knows nothing about permissions. Core owns the check, as it does for tools.

 execute tool (core/codemode/tool.ts)
   runtime.execute(code)
+    provide CodeModeExecution  (the execute call: session, agent, message, call id)
+    provide Permission
     fetch(url, init)                (core/codemode/web.ts)
+      permission.assert({ action: "webfetch", resources: [url], ... })
       globalThis.fetch(...)

CodeModeExecution is a small new service holding the execute call's context, so future globals (like an fs module) can use the same check as their tools. The Tool layer now depends on Permission so it can hand it to the execute tool.

A denied URL is never requested. The program catches the permission error like any other rejected fetch.

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>
    .dot { animation: go 2.4s ease-in-out infinite; }
    .x { opacity: .9; animation: deny 2.4s ease-in-out infinite; }
    @keyframes go { 0% { transform: translateX(0); opacity: 1 } 45% { transform: translateX(78px); opacity: 1 } 55%, 100% { transform: translateX(78px); opacity: 0 } }
    @keyframes deny { 0%, 40% { opacity: .25 } 50%, 85% { opacity: 1 } 100% { opacity: .25 } }
    @media (prefers-reduced-motion: reduce) { .dot, .x { animation: none } }
  </style>
  <rect x="10" y="22" width="70" height="28" rx="6" opacity=".6"/>
  <text x="45" y="40" font-family="ui-monospace, monospace" font-size="11" text-anchor="middle" fill="currentColor" stroke="none">fetch()</text>
  <path d="M84 36 H150" opacity=".35" stroke-dasharray="3 4"/>
  <circle class="dot" cx="86" cy="36" r="4" fill="currentColor" stroke="none"/>
  <path d="M168 18 L184 24 V36 C184 46 176 52 168 55 C160 52 152 46 152 36 V24 Z" opacity=".8"/>
  <path class="x" d="M162 31 L174 43 M174 31 L162 43"/>
  <circle cx="218" cy="36" r="13" opacity=".3"/>
  <path d="M205 36 H231 M218 23 C212 30 212 42 218 49 C224 42 224 30 218 23" opacity=".3"/>
</svg>

Out of scope

  • Redirects. Code Mode fetch still follows redirects without checking each hop, while the webfetch tool checks every hop. I'll do that in a follow-up PR.
  • Applying file permissions to a future fs extension.

Verification

  • bun test test/tool-execute.test.ts in packages/core: new tests check that fetch asks webfetch for its URL with the execute call's session and call id, and that a denied URL never reaches a local server.
  • bun run test in packages/core: 6900 pass. The 7 failures are shell tests that also fail on dev locally.
  • bun run check from the root: clean.

The execute (Code Mode) tool exposes a `fetch` global that went straight to
the network. The webfetch tool asks the `webfetch` permission for every URL,
but Code Mode did not, so a `webfetch: deny` rule or a Console "Can't fetch
the web" policy did nothing against a model that used `execute` to send data
to any server.

- core: the tool registry now asserts the `webfetch` permission (resource =
  the URL, same shape as the webfetch tool) in the extension.before hook of
  every fetch a Code Mode program makes. A denied URL throws inside the
  program and its progress row settles as an error. Tool.node now depends on
  Permission.node.
- codemode: extension globals were called before their `extension.before`
  hook ran, so a failing hook only hid the result while the request had
  already been sent. The hook now runs first and a denied call never reaches
  the host function. A denied async global still rejects its promise; any
  other denied global throws.

Verified: new cases in codemode/test/codemode.test.ts (denied host function
is never called) and core/test/tool-execute.test.ts (webfetch deny rule fails
the script's fetch, allow rule lets it through, each URL is asserted) both go
red without the fix. bun typecheck in core and codemode, codemode suite
(1415 pass), core suite (only pre-existing local failures: ripgrep download
and macOS bash 3.2 shell tests, also failing on dev), bun run check.
@rekram1-node

Copy link
Copy Markdown
Contributor Author

Opened against the wrong branch by mistake, closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants