Skip to content

the execute tool should inherit webfetch permissions #54595

Closed

Description

@ferdinandyb

Description

The execute tool currently has fetch available every time. This means an agent that has webfetch disabled can bypass this by running a fetch query in the execute tool.

Plugins

No response

OpenCode version

No response

Steps to reproduce

No response

Screenshot and/or share link

No response

Operating System

No response

Terminal

No response

Activity

  1. opencode-agent commented on Oct 11, 2026

    @opencode-agent
    Contributor

    Thanks for the report. I could reproduce this on 2.0.26 (the latest release, since no version was given), and it still happens on the latest dev (38454c6).

    What I did:

    1. Set "permission": { "webfetch": "deny" } in the config.
    2. Had the model call the execute tool with await fetch("http://127.0.0.1:<port>/probe") against a local server that logs requests.
    3. As a control, had it call webfetch directly on the same server.

    The fetch inside execute reached the server with no permission prompt or error, while the direct webfetch call was blocked as expected. The recording and screenshot below show it, and the script below reproduces it.

    Related: #50771 (with open PR #54510) covers file:// URLs in the same Code Mode fetch getting past the read permission. That fix doesn't cover HTTP requests, so this is a separate issue.

    recording.mp4

    Screenshot

    Reproduction script (drive.ts)
    // Repro for #54595: Code Mode `execute` fetch ignores a denied `webfetch` permission.
    import { Effect } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    // Local HTTP server that records every request it receives.
    const hits: string[] = []
    const server = Bun.serve({
      port: 0,
      hostname: "127.0.0.1",
      fetch(req) {
        hits.push(req.url)
        return new Response("SECRET-FROM-NETWORK")
      },
    })
    const url = `http://127.0.0.1:${server.port}/probe`
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true },
        config: {
          // webfetch is denied for every agent
          permission: { webfetch: "deny" },
        },
      },
      ({ ui, llm }) =>
        Effect.gen(function* () {
          yield* ui.screenshot("start")
          yield* llm.queue(
            Llm.toolCall({
              index: 0,
              id: "call_exec",
              name: "execute",
              input: { code: `const r = await fetch(${JSON.stringify(url)}); return { status: r.status, body: await r.text() }` },
            }),
            Llm.finish("tool-calls"),
          )
          yield* llm.queue(
            // Control: the direct webfetch tool is blocked by the same config.
            Llm.toolCall({ index: 0, id: "call_wf", name: "webfetch", input: { url: url + "?direct" } }),
            Llm.finish("tool-calls"),
          )
          yield* llm.queue(Llm.text("Done: fetch went through despite webfetch deny."))
          yield* ui.submit("Fetch the probe URL")
          yield* ui.waitFor("Done: fetch went through", { timeout: 60_000 })
          yield* Effect.sleep(1500)
          yield* ui.screenshot("after")
          yield* Effect.log(`local server received ${hits.length} request(s): ${JSON.stringify(hits)}`)
          if (hits.filter((h) => !h.includes("?direct")).length === 0) yield* Effect.log("NOT REPRODUCED: no request reached the server")
          else yield* Effect.log("REPRODUCED: execute's fetch reached the network although webfetch is denied")
          const direct = hits.filter((h) => h.includes("?direct")).length
          yield* Effect.log(`direct webfetch requests: ${direct} (expected 0, tool is denied)`)
          server.stop(true)
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

  2. argszero commented on Oct 12, 2026

    @argszero
    Contributor

    I'd like to work on this.

    Reproduced on dev: a Code Mode fetch reaches the network while "webfetch": "deny" is set, and no approval request is created. The check belongs inside the fetch global, immediately before the request is dispatched, next to the file:// guard from #54510 — a Code Mode extension's extension.before hook runs after the global's body, so gating there would deny only the result, not the request itself.

  3. rekram1-node commented on Nov 5, 2026

    @rekram1-node
    Contributor

    Fixed by #59954: Code Mode fetch now goes through the webfetch permission rules.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions