From e0c273eb775c26f11c11e2b50472151201ea90d6 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Mon, 8 Jun 2026 22:54:14 +0530 Subject: [PATCH] fix(session): avoid sticky prompt tool overrides --- packages/opencode/src/session/prompt.ts | 7 +--- packages/opencode/src/tool/task.ts | 41 ++++++++++++------- packages/opencode/test/session/prompt.test.ts | 27 ++++++++++++ packages/opencode/test/tool/task.test.ts | 10 ++--- 4 files changed, 58 insertions(+), 27 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 701c632345fe..d036c6542d3a 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1198,11 +1198,8 @@ export const layer = Layer.effect( permissions.push({ permission: t, action: enabled ? "allow" : "deny", pattern: "*" }) } if (permissions.length > 0) { - // Merge so per-call tool rules don't clobber inherited session rules - // (e.g. external_directory allows from the parent session). - const merged = Permission.merge(session.permission ?? [], permissions) - session.permission = merged - yield* sessions.setPermission({ sessionID: session.id, permission: merged }) + session.permission = permissions + yield* sessions.setPermission({ sessionID: session.id, permission: permissions }) } if (input.noReply === true) return message diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 5ca1666add7c..dac340184cfc 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -125,6 +125,24 @@ export const TaskTool = Tool.define( const parentAgent = parent.agent ? yield* agent.get(parent.agent).pipe(Effect.catchCause(() => Effect.succeed(undefined))) : undefined + const childPermission = deriveSubagentSessionPermission({ + parentSessionPermission: parent.permission ?? [], + parentAgent, + subagent: next, + }) + const childToolDenies = [ + ...(next.permission.some((rule) => rule.permission === "todowrite") + ? [] + : [{ permission: "todowrite" as const, pattern: "*" as const, action: "deny" as const }]), + ...(next.permission.some((rule) => rule.permission === id) + ? [] + : [{ permission: id, pattern: "*" as const, action: "deny" as const }]), + ...(cfg.experimental?.primary_tools?.map((permission) => ({ + permission, + pattern: "*" as const, + action: "deny" as const, + })) ?? []), + ] const nextSession = session ?? (yield* sessions.create({ @@ -132,16 +150,14 @@ export const TaskTool = Tool.define( title: params.description + ` (@${next.name} subagent)`, agent: next.name, permission: [ - ...deriveSubagentSessionPermission({ - parentSessionPermission: parent.permission ?? [], - parentAgent, - subagent: next, - }), - ...(cfg.experimental?.primary_tools?.map((item) => ({ - pattern: "*", - action: "allow" as const, - permission: item, - })) ?? []), + ...childPermission, + ...childToolDenies.filter( + (deny) => + !childPermission.some( + (rule) => + rule.permission === deny.permission && rule.pattern === deny.pattern && rule.action === deny.action, + ), + ), ], })) @@ -182,11 +198,6 @@ export const TaskTool = Tool.define( }, variant: next.model ? undefined : variant, agent: next.name, - tools: { - ...(next.permission.some((rule) => rule.permission === "todowrite") ? {} : { todowrite: false }), - ...(next.permission.some((rule) => rule.permission === id) ? {} : { task: false }), - ...Object.fromEntries((cfg.experimental?.primary_tools ?? []).map((item) => [item, false])), - }, parts, }) return result.parts.findLast((item) => item.type === "text")?.text ?? "" diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 0cd0802d4671..4f925e402117 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -824,6 +824,33 @@ it.instance("subtask child inherits parent session external_directory allow", () }), ) +noLLMServer.instance("prompt tools replace previous prompt tool rules", () => + Effect.gen(function* () { + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const session = yield* sessions.create({ title: "Prompt tools" }) + + yield* prompt.prompt({ + sessionID: session.id, + agent: "build", + noReply: true, + tools: { bash: false }, + parts: [{ type: "text", text: "first" }], + }) + yield* prompt.prompt({ + sessionID: session.id, + agent: "build", + noReply: true, + tools: { read: true }, + parts: [{ type: "text", text: "second" }], + }) + + const reloaded = yield* sessions.get(session.id) + expect(reloaded.permission).toEqual([{ permission: "read", pattern: "*", action: "allow" }]) + expect(Permission.evaluate("bash", "anything", reloaded.permission ?? []).action).toBe("ask") + }), +) + it.instance( "running subtask preserves metadata after tool-call transition", () => diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index 6a9604a01efa..5a45cfee3e2f 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -421,19 +421,15 @@ describe("tool.task", () => { { permission: "bash", pattern: "*", - action: "allow", + action: "deny", }, { permission: "read", pattern: "*", - action: "allow", + action: "deny", }, ]) - expect(seen?.tools).toEqual({ - todowrite: false, - bash: false, - read: false, - }) + expect(seen?.tools).toBeUndefined() }), { config: {