Просмотр исходного кода

fix(task): preserve subagent self permissions (#27201)

Kit Langton 3 месяцев назад
Родитель
Сommit
c4e676b8a0

+ 5 - 4
packages/opencode/src/agent/subagent-permissions.ts

@@ -5,10 +5,10 @@ import type { Agent } from "./agent"
  * Build the `permission` ruleset for a subagent's session when it's spawned
  * via the task tool. Combines:
  *
- * 1. The parent **agent's** deny rules — Plan Mode and other agent-level
- *    restrictions live on the agent ruleset, not on the session, so a
+ * 1. The parent **agent's** edit-class deny rules — Plan Mode's file-edit
+ *    restriction lives on the agent ruleset, not on the session, so a
  *    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 —
  *    same forwarding the original code already did.
  * 3. Default `todowrite` and `task` denies if the subagent's own ruleset
@@ -21,7 +21,8 @@ export function deriveSubagentSessionPermission(input: {
 }): Permission.Ruleset {
   const canTask = input.subagent.permission.some((rule) => rule.permission === "task")
   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 [
     ...parentAgentDenies,
     ...input.parentSessionPermission.filter(

+ 87 - 0
packages/opencode/test/agent/plan-mode-subagent-bypass.test.ts

@@ -27,6 +27,19 @@ import { testEffect } from "../lib/effect"
 
 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
 // exercises the actual helper that task.ts uses to build the subagent's
 // 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")
+  }),
+)