From 0050134d9eca104b7e38b52b6f0aa62b8c0926db Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Mon, 8 Jun 2026 09:32:10 +0200 Subject: [PATCH] fix(session): merge per-call tool rules into session permission (#30529) Co-authored-by: Simon Klee --- packages/opencode/src/session/prompt.ts | 7 +++-- packages/opencode/test/session/prompt.test.ts | 27 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index d036c6542d3..701c632345f 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1198,8 +1198,11 @@ export const layer = Layer.effect( permissions.push({ permission: t, action: enabled ? "allow" : "deny", pattern: "*" }) } if (permissions.length > 0) { - session.permission = permissions - yield* sessions.setPermission({ sessionID: session.id, permission: permissions }) + // 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 }) } if (input.noReply === true) return message diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index d3c69143212..0cd0802d467 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -797,6 +797,33 @@ it.instance("failed subtask preserves metadata on error tool state", () => }), ) +it.instance("subtask child inherits parent session external_directory allow", () => + Effect.gen(function* () { + const { llm } = yield* useServerConfig(providerCfg) + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const chat = yield* sessions.create({ + title: "Parent", + permission: [{ permission: "external_directory", pattern: "/tmp/allowed/*", action: "allow" }], + }) + yield* llm.text("done") + const msg = yield* user(chat.id, "hello") + yield* addSubtask(chat.id, msg.id) + + yield* prompt.loop({ sessionID: chat.id }) + + const kids = yield* sessions.children(chat.id) + expect(kids).toHaveLength(1) + const child = kids[0]! + const rules = child.permission ?? [] + expect(rules).toEqual( + expect.arrayContaining([{ permission: "external_directory", pattern: "/tmp/allowed/*", action: "allow" }]), + ) + expect(Permission.evaluate("external_directory", "/tmp/allowed/file", rules).action).toBe("allow") + expect(Permission.evaluate("task", "anything", rules).action).toBe("deny") + }), +) + it.instance( "running subtask preserves metadata after tool-call transition", () =>