Skip to content

Commit f43209b

Browse files
authored
fix(session): avoid sticky prompt tool overrides (anomalyco#31394)
1 parent b34d924 commit f43209b

4 files changed

Lines changed: 58 additions & 27 deletions

File tree

packages/opencode/src/session/prompt.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,11 +1198,8 @@ export const layer = Layer.effect(
11981198
permissions.push({ permission: t, action: enabled ? "allow" : "deny", pattern: "*" })
11991199
}
12001200
if (permissions.length > 0) {
1201-
// Merge so per-call tool rules don't clobber inherited session rules
1202-
// (e.g. external_directory allows from the parent session).
1203-
const merged = Permission.merge(session.permission ?? [], permissions)
1204-
session.permission = merged
1205-
yield* sessions.setPermission({ sessionID: session.id, permission: merged })
1201+
session.permission = permissions
1202+
yield* sessions.setPermission({ sessionID: session.id, permission: permissions })
12061203
}
12071204

12081205
if (input.noReply === true) return message

packages/opencode/src/tool/task.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -125,23 +125,39 @@ export const TaskTool = Tool.define(
125125
const parentAgent = parent.agent
126126
? yield* agent.get(parent.agent).pipe(Effect.catchCause(() => Effect.succeed(undefined)))
127127
: undefined
128+
const childPermission = deriveSubagentSessionPermission({
129+
parentSessionPermission: parent.permission ?? [],
130+
parentAgent,
131+
subagent: next,
132+
})
133+
const childToolDenies = [
134+
...(next.permission.some((rule) => rule.permission === "todowrite")
135+
? []
136+
: [{ permission: "todowrite" as const, pattern: "*" as const, action: "deny" as const }]),
137+
...(next.permission.some((rule) => rule.permission === id)
138+
? []
139+
: [{ permission: id, pattern: "*" as const, action: "deny" as const }]),
140+
...(cfg.experimental?.primary_tools?.map((permission) => ({
141+
permission,
142+
pattern: "*" as const,
143+
action: "deny" as const,
144+
})) ?? []),
145+
]
128146
const nextSession =
129147
session ??
130148
(yield* sessions.create({
131149
parentID: ctx.sessionID,
132150
title: params.description + ` (@${next.name} subagent)`,
133151
agent: next.name,
134152
permission: [
135-
...deriveSubagentSessionPermission({
136-
parentSessionPermission: parent.permission ?? [],
137-
parentAgent,
138-
subagent: next,
139-
}),
140-
...(cfg.experimental?.primary_tools?.map((item) => ({
141-
pattern: "*",
142-
action: "allow" as const,
143-
permission: item,
144-
})) ?? []),
153+
...childPermission,
154+
...childToolDenies.filter(
155+
(deny) =>
156+
!childPermission.some(
157+
(rule) =>
158+
rule.permission === deny.permission && rule.pattern === deny.pattern && rule.action === deny.action,
159+
),
160+
),
145161
],
146162
}))
147163

@@ -182,11 +198,6 @@ export const TaskTool = Tool.define(
182198
},
183199
variant: next.model ? undefined : variant,
184200
agent: next.name,
185-
tools: {
186-
...(next.permission.some((rule) => rule.permission === "todowrite") ? {} : { todowrite: false }),
187-
...(next.permission.some((rule) => rule.permission === id) ? {} : { task: false }),
188-
...Object.fromEntries((cfg.experimental?.primary_tools ?? []).map((item) => [item, false])),
189-
},
190201
parts,
191202
})
192203
return result.parts.findLast((item) => item.type === "text")?.text ?? ""

packages/opencode/test/session/prompt.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,33 @@ it.instance("subtask child inherits parent session external_directory allow", ()
824824
}),
825825
)
826826

827+
noLLMServer.instance("prompt tools replace previous prompt tool rules", () =>
828+
Effect.gen(function* () {
829+
const prompt = yield* SessionPrompt.Service
830+
const sessions = yield* Session.Service
831+
const session = yield* sessions.create({ title: "Prompt tools" })
832+
833+
yield* prompt.prompt({
834+
sessionID: session.id,
835+
agent: "build",
836+
noReply: true,
837+
tools: { bash: false },
838+
parts: [{ type: "text", text: "first" }],
839+
})
840+
yield* prompt.prompt({
841+
sessionID: session.id,
842+
agent: "build",
843+
noReply: true,
844+
tools: { read: true },
845+
parts: [{ type: "text", text: "second" }],
846+
})
847+
848+
const reloaded = yield* sessions.get(session.id)
849+
expect(reloaded.permission).toEqual([{ permission: "read", pattern: "*", action: "allow" }])
850+
expect(Permission.evaluate("bash", "anything", reloaded.permission ?? []).action).toBe("ask")
851+
}),
852+
)
853+
827854
it.instance(
828855
"running subtask preserves metadata after tool-call transition",
829856
() =>

packages/opencode/test/tool/task.test.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -421,19 +421,15 @@ describe("tool.task", () => {
421421
{
422422
permission: "bash",
423423
pattern: "*",
424-
action: "allow",
424+
action: "deny",
425425
},
426426
{
427427
permission: "read",
428428
pattern: "*",
429-
action: "allow",
429+
action: "deny",
430430
},
431431
])
432-
expect(seen?.tools).toEqual({
433-
todowrite: false,
434-
bash: false,
435-
read: false,
436-
})
432+
expect(seen?.tools).toBeUndefined()
437433
}),
438434
{
439435
config: {

0 commit comments

Comments
 (0)