fix(task): preserve subagent self permissions (#27201)
This commit is contained in:
parent
c2c40b5b61
commit
c4e676b8a0
2 changed files with 92 additions and 4 deletions
|
|
@ -5,10 +5,10 @@ import type { Agent } from "./agent"
|
||||||
* Build the `permission` ruleset for a subagent's session when it's spawned
|
* Build the `permission` ruleset for a subagent's session when it's spawned
|
||||||
* via the task tool. Combines:
|
* via the task tool. Combines:
|
||||||
*
|
*
|
||||||
* 1. The parent **agent's** deny rules — Plan Mode and other agent-level
|
* 1. The parent **agent's** edit-class deny rules — Plan Mode's file-edit
|
||||||
* restrictions live on the agent ruleset, not on the session, so a
|
* restriction lives on the agent ruleset, not on the session, so a
|
||||||
* subagent that only inherited the parent SESSION's permission would
|
* subagent that only inherited the parent SESSION's permission would
|
||||||
* silently bypass them. (#26514)
|
* silently bypass it. (#26514)
|
||||||
* 2. The parent **session's** deny rules and external_directory rules —
|
* 2. The parent **session's** deny rules and external_directory rules —
|
||||||
* same forwarding the original code already did.
|
* same forwarding the original code already did.
|
||||||
* 3. Default `todowrite` and `task` denies if the subagent's own ruleset
|
* 3. Default `todowrite` and `task` denies if the subagent's own ruleset
|
||||||
|
|
@ -21,7 +21,8 @@ export function deriveSubagentSessionPermission(input: {
|
||||||
}): Permission.Ruleset {
|
}): Permission.Ruleset {
|
||||||
const canTask = input.subagent.permission.some((rule) => rule.permission === "task")
|
const canTask = input.subagent.permission.some((rule) => rule.permission === "task")
|
||||||
const canTodo = input.subagent.permission.some((rule) => rule.permission === "todowrite")
|
const canTodo = input.subagent.permission.some((rule) => rule.permission === "todowrite")
|
||||||
const parentAgentDenies = input.parentAgent?.permission.filter((rule) => rule.action === "deny") ?? []
|
const parentAgentDenies =
|
||||||
|
input.parentAgent?.permission.filter((rule) => rule.action === "deny" && rule.permission === "edit") ?? []
|
||||||
return [
|
return [
|
||||||
...parentAgentDenies,
|
...parentAgentDenies,
|
||||||
...input.parentSessionPermission.filter(
|
...input.parentSessionPermission.filter(
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,19 @@ import { testEffect } from "../lib/effect"
|
||||||
|
|
||||||
const it = testEffect(Agent.defaultLayer)
|
const it = testEffect(Agent.defaultLayer)
|
||||||
|
|
||||||
|
function testAgent(input: {
|
||||||
|
name: string
|
||||||
|
mode: Agent.Info["mode"]
|
||||||
|
permission: Parameters<typeof Permission.fromConfig>[0]
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
name: input.name,
|
||||||
|
mode: input.mode,
|
||||||
|
permission: Permission.fromConfig(input.permission),
|
||||||
|
options: {},
|
||||||
|
} satisfies Agent.Info
|
||||||
|
}
|
||||||
|
|
||||||
// `deriveSubagentSessionPermission` is imported from production. The test
|
// `deriveSubagentSessionPermission` is imported from production. The test
|
||||||
// exercises the actual helper that task.ts uses to build the subagent's
|
// exercises the actual helper that task.ts uses to build the subagent's
|
||||||
// session permission, so any regression in that helper trips this test.
|
// session permission, so any regression in that helper trips this test.
|
||||||
|
|
@ -123,3 +136,77 @@ it.instance(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("[#26700] controller self-restrictions do not erase executor permissions", () =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
const controller = testAgent({
|
||||||
|
name: "controller",
|
||||||
|
mode: "primary",
|
||||||
|
permission: {
|
||||||
|
"*": "deny",
|
||||||
|
read: "deny",
|
||||||
|
bash: "deny",
|
||||||
|
task: {
|
||||||
|
"*": "deny",
|
||||||
|
executor: "allow",
|
||||||
|
},
|
||||||
|
edit: "deny",
|
||||||
|
write: "deny",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const executor = testAgent({
|
||||||
|
name: "executor",
|
||||||
|
mode: "subagent",
|
||||||
|
permission: {
|
||||||
|
"*": "deny",
|
||||||
|
read: "allow",
|
||||||
|
bash: "allow",
|
||||||
|
task: {
|
||||||
|
"*": "deny",
|
||||||
|
worker: "allow",
|
||||||
|
},
|
||||||
|
edit: "deny",
|
||||||
|
write: "deny",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const effective = Permission.merge(
|
||||||
|
executor.permission,
|
||||||
|
deriveSubagentSessionPermission({
|
||||||
|
parentSessionPermission: [],
|
||||||
|
parentAgent: controller,
|
||||||
|
subagent: executor,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(Permission.evaluate("read", "README.md", effective).action).toBe("allow")
|
||||||
|
expect(Permission.evaluate("bash", "git status", effective).action).toBe("allow")
|
||||||
|
expect(Permission.evaluate("task", "worker", effective).action).toBe("allow")
|
||||||
|
expect(Permission.evaluate("task", "other", effective).action).toBe("deny")
|
||||||
|
expect(Permission.disabled(["edit", "write", "apply_patch"], effective)).toEqual(
|
||||||
|
new Set(["edit", "write", "apply_patch"]),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("subagent inherits parent session deny rules as hard runtime ceilings", () =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
const executor = testAgent({
|
||||||
|
name: "executor",
|
||||||
|
mode: "subagent",
|
||||||
|
permission: {
|
||||||
|
bash: "allow",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const effective = Permission.merge(
|
||||||
|
executor.permission,
|
||||||
|
deriveSubagentSessionPermission({
|
||||||
|
parentSessionPermission: Permission.fromConfig({ bash: "deny" }),
|
||||||
|
parentAgent: undefined,
|
||||||
|
subagent: executor,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(Permission.evaluate("bash", "git status", effective).action).toBe("deny")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue