fix(session): avoid sticky prompt tool overrides (#31394)

This commit is contained in:
Shoubhit Dash 2026-06-08 23:03:27 +05:30 committed by GitHub
commit f43209bb8c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 58 additions and 27 deletions

View file

@ -1198,11 +1198,8 @@ export const layer = Layer.effect(
permissions.push({ permission: t, action: enabled ? "allow" : "deny", pattern: "*" }) permissions.push({ permission: t, action: enabled ? "allow" : "deny", pattern: "*" })
} }
if (permissions.length > 0) { if (permissions.length > 0) {
// Merge so per-call tool rules don't clobber inherited session rules session.permission = permissions
// (e.g. external_directory allows from the parent session). yield* sessions.setPermission({ sessionID: session.id, permission: permissions })
const merged = Permission.merge(session.permission ?? [], permissions)
session.permission = merged
yield* sessions.setPermission({ sessionID: session.id, permission: merged })
} }
if (input.noReply === true) return message if (input.noReply === true) return message

View file

@ -125,6 +125,24 @@ export const TaskTool = Tool.define(
const parentAgent = parent.agent const parentAgent = parent.agent
? yield* agent.get(parent.agent).pipe(Effect.catchCause(() => Effect.succeed(undefined))) ? yield* agent.get(parent.agent).pipe(Effect.catchCause(() => Effect.succeed(undefined)))
: 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 = const nextSession =
session ?? session ??
(yield* sessions.create({ (yield* sessions.create({
@ -132,16 +150,14 @@ export const TaskTool = Tool.define(
title: params.description + ` (@${next.name} subagent)`, title: params.description + ` (@${next.name} subagent)`,
agent: next.name, agent: next.name,
permission: [ permission: [
...deriveSubagentSessionPermission({ ...childPermission,
parentSessionPermission: parent.permission ?? [], ...childToolDenies.filter(
parentAgent, (deny) =>
subagent: next, !childPermission.some(
}), (rule) =>
...(cfg.experimental?.primary_tools?.map((item) => ({ rule.permission === deny.permission && rule.pattern === deny.pattern && rule.action === deny.action,
pattern: "*", ),
action: "allow" as const, ),
permission: item,
})) ?? []),
], ],
})) }))
@ -182,11 +198,6 @@ export const TaskTool = Tool.define(
}, },
variant: next.model ? undefined : variant, variant: next.model ? undefined : variant,
agent: next.name, 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, parts,
}) })
return result.parts.findLast((item) => item.type === "text")?.text ?? "" return result.parts.findLast((item) => item.type === "text")?.text ?? ""

View file

@ -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( it.instance(
"running subtask preserves metadata after tool-call transition", "running subtask preserves metadata after tool-call transition",
() => () =>

View file

@ -421,19 +421,15 @@ describe("tool.task", () => {
{ {
permission: "bash", permission: "bash",
pattern: "*", pattern: "*",
action: "allow", action: "deny",
}, },
{ {
permission: "read", permission: "read",
pattern: "*", pattern: "*",
action: "allow", action: "deny",
}, },
]) ])
expect(seen?.tools).toEqual({ expect(seen?.tools).toBeUndefined()
todowrite: false,
bash: false,
read: false,
})
}), }),
{ {
config: { config: {