From e640fe42817b2a2d18bc315b33b162a1fd45f672 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 1 Nov 2026 19:49:12 +0100 Subject: [PATCH] fix(server): a rejected OpenCode auto_accept leaves the session asking for permission applyOptions committed the new flag before approving pending requests, so a failed reply left the session auto-approving while the stored option stayed off. The flag is restored when the apply throws. --- .../agent/providers/opencode/v2/agent.test.ts | 38 +++++++++++++++++++ .../agent/providers/opencode/v2/session.ts | 17 ++++++--- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/packages/server/src/server/agent/providers/opencode/v2/agent.test.ts b/packages/server/src/server/agent/providers/opencode/v2/agent.test.ts index 9cf1b34cb3..8435dccd90 100644 --- a/packages/server/src/server/agent/providers/opencode/v2/agent.test.ts +++ b/packages/server/src/server/agent/providers/opencode/v2/agent.test.ts @@ -50,6 +50,44 @@ describe("OpenCode v2 session lifecycle", () => { await session.close(); }); + test("a rejected auto_accept leaves the running session asking for permission", async () => { + const harness = new V2Harness(); + const permissions = [ + { id: "approval", sessionID: "session", action: "shell", resources: ["pwd"] }, + ]; + harness.api.permission.list = async () => permissions; + const replies: string[] = []; + harness.api.permission.reply = async (input) => { + replies.push(input.requestID); + throw new Error("upstream refused"); + }; + const client = new OpenCodeV2AgentClient({ + logger: createTestLogger(), + runtime: harness.runtime, + }); + const session = await client.createSession({ provider: "opencode", cwd: "/tmp/project" }); + try { + await expect(session.applyOptions?.({ auto_accept: true })).rejects.toThrow( + "upstream refused", + ); + + const next = { + id: "next", + sessionID: "session", + action: "shell", + resources: ["ls"], + }; + permissions.push(next); + harness.push({ id: "asked", created: 2, type: "permission.asked", data: next }); + + const pendingIds = () => session.getPendingPermissions().map(({ id }) => id); + await expect.poll(pendingIds).toContain("next"); + expect(replies).not.toContain("next"); + } finally { + await session.close(); + } + }); + test("rejects a live permission change the running session cannot apply", async () => { const harness = new V2Harness(); const client = new OpenCodeV2AgentClient({ diff --git a/packages/server/src/server/agent/providers/opencode/v2/session.ts b/packages/server/src/server/agent/providers/opencode/v2/session.ts index b442bcf523..a86f0c3619 100644 --- a/packages/server/src/server/agent/providers/opencode/v2/session.ts +++ b/packages/server/src/server/agent/providers/opencode/v2/session.ts @@ -376,11 +376,18 @@ export class OpenCodeV2Session implements AgentSession { ); if (!("auto_accept" in options)) return; const value = next?.auto_accept === true; - this.config.providerOptions = { ...this.config.providerOptions, auto_accept: value }; - if (value && !this.config.toolPolicy) - for (const request of this.permissions.list()) - if (request.kind !== "question") - await this.respondToPermission(request.id, { behavior: "allow" }); + const previous = this.config.providerOptions; + this.config.providerOptions = { ...previous, auto_accept: value }; + try { + if (value && !this.config.toolPolicy) + for (const request of this.permissions.list()) + if (request.kind !== "question") + await this.respondToPermission(request.id, { behavior: "allow" }); + } catch (error) { + // A rejected apply stores nothing, so the running session keeps asking as it did. + this.config.providerOptions = previous; + throw error; + } } async listCommands() { await this.reconnectIfExited();