Skip to content

Project auto-accept Off is ignored for permission requests from unloaded sessions #55176

Open

Description

@dakjdakd

Description

The app can automatically approve a permission request for a project whose auto-accept setting is explicitly Off when the app-wide default is On. This happens when the request belongs to a session that the app has not loaded yet. The same project setting works correctly once the session's information is available in the local session store, so the effective approval policy depends on the client's cache state.

Project overrides allow a user to enable automatic approval generally while retaining manual approval for a particular project. For example, one project can follow the global On setting while another project stays Off because its tool requests should be reviewed individually. An incoming request from that second project should remain pending regardless of whether its session already has an open tab, was created by another client, or is a child session that has not been visited.

The problem is in packages/app/src/session/requests/auto-approve.ts. The consider() function takes a shortcut when covers(undefined) is true. That check evaluates the app-wide default without a project ID. With the default On, it immediately calls approve(permission) instead of loading the unknown session's information. The default argument in approve() also reads from the local session store. Because that session is absent, the project ID is still undefined, and the eventual settings lookup again resolves to the global default. The app sends a permission reply with decision: "once" even though the request's actual project has an explicit Off override.

For a cached session, approve() can obtain the project ID and apply the override normally. This explains why opening a session first can make the behavior look correct, and why a test that covers only already-loaded sessions does not catch the problem. The important trigger is a permission event arriving before the client has session metadata, rather than a failure to save or display the project preference.

The expected behavior is to resolve the request's project before deciding whether it is covered by automatic approval. A cached session can continue using its existing information. An unknown session should use the existing session-get operation, then pass the returned project ID through the same server-scoped settings lookup. If that lookup returns Off, the client should leave the request pending for manual review. Projects set to On, and projects that follow an On default, should continue receiving automatic replies.

Plugins

No third-party plugin is required. The reproduction uses the app's existing permission auto-approver and the shared multi-server E2E fixture server.

OpenCode version

Source version 2.0.28. The affected implementation is present on dev at 23e3493ab.

Steps to reproduce

  1. Connect the app to a server with two projects, such as api and web, and open an existing session so that the connection and settings are ready.
  2. While the app-wide default is Off, set api to On and web to Off in project settings. Then turn the app-wide default On. The explicit Off setting for web should remain in effect.
  3. Allow the initial permission sweep to finish. Create additional sessions on the server after that sweep, without opening them or sending the app session-created or session-updated events that would populate its cache. The keeper reproduction uses one child session in each project.
  4. Deliver a permission.asked event for each new session. Both requests can ask for the same action, such as running git status; their project settings are the relevant difference.
  5. Observe permission replies. The api request should receive one automatic reply, while the web request should receive none. With the affected implementation, both requests receive an automatic reply.

The regression is reproducible in the existing multi-server.spec.ts keeper. The extended project-override case confirms that the sessions were not previously fetched, checks the exact reply identities, opens the blocked session to verify that its permission remains visible, and verifies that returning the project to the global default releases the pending requests. With the proposed fix, all five cases in this keeper pass, and the app package typecheck passes.

Screenshot and/or share link

No screenshot or share link is needed to reproduce this event-handling issue. The deterministic keeper observes the outgoing permission replies and the pending permission shown in the session UI.

Operating System

Verified locally on Windows using Bun 1.4.2. Browser verification used the installed Chromium headless-shell build 1200 with Playwright's shared fixture harness; the project normally selects build 1217.

Terminal

PowerShell was used to run the local checks. The bug is in the app's permission handling and does not depend on the terminal used to launch it.

Linked fix

A focused fix is available in #55178.

Activity

  1. opencode-agent commented on Oct 13, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I reproduced this on the latest dev (23e3493ab). The auto-approve code there is unchanged from v2.0.28.

    The script below runs createPermissionAutoApprover with a mocked settings module and SDK:

    • The app-wide default is On, project api is set to On, and project web is set to Off.
    • Neither session is in the local session store.
    • One permission.asked event is sent per session.

    Both requests received an automatic once reply, including the one from the web project. Only the api request should have been answered.

    The cause is what you described: the covers(undefined) shortcut in consider() approves without fetching the session's project first. The open PR #55178 looks like it targets this.

    Reproduction script (auto-approve-repro.test.ts)
    // Repro for anomalyco/opencode#55176: project auto-accept Off is ignored for
    // permission requests from sessions the client has not loaded yet.
    // Copy to packages/app/src/session/requests/, then run from packages/app: bun test --conditions=solid --preload ./happydom.ts src/session/requests/auto-approve-repro.test.ts
    import { expect, mock, test } from "bun:test"
    import { createRoot } from "solid-js"
    
    // Settings: app-wide default ON, project "web" explicitly OFF, project "api" ON.
    const overrides: Record<string, boolean> = { "srv:web": false, "srv:api": true }
    mock.module("@/settings/model", () => ({
      projectSettingsKey: (scope: string, projectID: string | undefined) => (projectID ? `${scope}:${projectID}` : undefined),
      useSettings: () => ({
        permissions: {
          autoApprovesOnServer: () => true,
          autoApproves: (key: string | undefined) => (key && key in overrides ? overrides[key] : true),
        },
      }),
    }))
    
    const { createPermissionAutoApprover } = await import("./auto-approve")
    
    test("project Off override is honored for unloaded sessions", async () => {
      const replies: string[] = []
      const handlers: Array<(e: any) => void> = []
      // Sessions known to the server but NOT in the local store.
      const serverSessions: Record<string, { projectID: string }> = {
        ses_api: { projectID: "api" },
        ses_web: { projectID: "web" },
      }
      const sdk: any = {
        scope: "srv",
        connection: { status: () => "disconnected" }, // skip the initial sweep
        event: { on: (_: string, fn: any) => (handlers.push(fn), () => {}) },
        api: {
          session: { get: async ({ sessionID }: any) => serverSessions[sessionID], active: async () => ({}) },
          permission: {
            reply: async (r: any) => void replies.push(r.requestID),
            request: { list: async () => ({ data: [] }) },
          },
        },
      }
      const data: any = {
        session: { get: () => undefined, list: () => [], permission: { list: () => [] } },
      }
    
      createRoot(() => createPermissionAutoApprover({ sdk, data }))
      for (const sessionID of ["ses_api", "ses_web"])
        handlers.forEach((h) => h({ data: { id: `per_${sessionID}`, sessionID } }))
      await new Promise((r) => setTimeout(r, 50))
    
      console.log("auto-replied:", replies)
      expect(replies).toContain("per_ses_api")
      expect(replies).not.toContain("per_ses_web") // FAILS: web (Off) is auto-approved
    })

    Run it: bun auto-approve-repro.test.ts

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