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.
This commit is contained in:
Mohamed Boudra
2026-11-01 19:49:12 +01:00
parent 43f7dc4ad1
commit e640fe4281
2 changed files with 50 additions and 5 deletions
@@ -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({
@@ -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();