mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-13 17:48:29 +00:00
fix(core): preserve agent permission precedence
This commit is contained in:
parent
629b304c48
commit
e4ef2acb0b
3 changed files with 88 additions and 28 deletions
|
|
@ -15,6 +15,7 @@ import { PermissionV2 } from "../../permission"
|
|||
import type { LocationMutation } from "../../location-mutation"
|
||||
import type { ReadTool } from "../../tool/read"
|
||||
import type { EditTool } from "../../tool/edit"
|
||||
import { AgentPlugin } from "../../plugin/agent"
|
||||
|
||||
const legacySources = [
|
||||
{ pattern: "{agent,agents}/**/*.md", primary: false },
|
||||
|
|
@ -76,7 +77,22 @@ export const Plugin = define({
|
|||
const configuredDefault = Config.latest(loaded.documents, "default_agent")
|
||||
if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault))
|
||||
for (const current of draft.list()) {
|
||||
draft.update(current.id, (agent) => agent.permissions.push(...permissions))
|
||||
draft.update(current.id, (agent) => {
|
||||
const initial = AgentV2.Info.empty(AgentV2.ID.make(current.id)).permissions.length
|
||||
const hasBuiltInDefaults = AgentPlugin.defaultPermissions.every((rule, index) => {
|
||||
const existing = agent.permissions[initial + index]
|
||||
return (
|
||||
existing?.action === rule.action &&
|
||||
existing.resource === rule.resource &&
|
||||
existing.effect === rule.effect
|
||||
)
|
||||
})
|
||||
agent.permissions.splice(
|
||||
hasBuiltInDefaults ? initial + AgentPlugin.defaultPermissions.length : initial,
|
||||
0,
|
||||
...permissions,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
for (const document of loaded.documents) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,21 @@ import { PermissionV2 } from "../permission"
|
|||
const SHELL_OUTPUT_GLOB = path.join(Global.Path.data, "shell", "*", "*")
|
||||
const BUILD_SYSTEM =
|
||||
"You are an AI coding agent. Help the user accomplish software engineering tasks by inspecting the workspace, making targeted changes, and using tools according to the configured permissions."
|
||||
const readonlyExternalDirectory: PermissionV2.Ruleset = [
|
||||
{ action: "external_directory", resource: "*", effect: "ask" },
|
||||
{ action: "external_directory", resource: SHELL_OUTPUT_GLOB, effect: "allow" },
|
||||
{ action: "external_directory", resource: path.join(Global.Path.tmp, "*"), effect: "allow" },
|
||||
]
|
||||
export const defaultPermissions: PermissionV2.Ruleset = [
|
||||
...readonlyExternalDirectory.slice(1),
|
||||
{ action: "question", resource: "*", effect: "deny" },
|
||||
{ action: "plan_enter", resource: "*", effect: "deny" },
|
||||
{ action: "plan_exit", resource: "*", effect: "deny" },
|
||||
{ action: "read", resource: "*", effect: "allow" },
|
||||
{ action: "read", resource: "*.env", effect: "ask" },
|
||||
{ action: "read", resource: "*.env.*", effect: "ask" },
|
||||
{ action: "read", resource: "*.env.example", effect: "allow" },
|
||||
]
|
||||
|
||||
const PROMPT_EXPLORE = `You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
|
||||
|
||||
|
|
@ -104,24 +119,6 @@ export const Plugin = define({
|
|||
effect: Effect.fn(function* (ctx) {
|
||||
const location = yield* Location.Service
|
||||
const worktree = location.directory
|
||||
const whitelistedDirs = [SHELL_OUTPUT_GLOB, path.join(Global.Path.tmp, "*")]
|
||||
const readonlyExternalDirectory: PermissionV2.Ruleset = [
|
||||
{ action: "external_directory", resource: "*", effect: "ask" },
|
||||
...whitelistedDirs.map(
|
||||
(resource): PermissionV2.Rule => ({ action: "external_directory", resource, effect: "allow" }),
|
||||
),
|
||||
]
|
||||
const defaults: PermissionV2.Ruleset = [
|
||||
{ action: "*", resource: "*", effect: "allow" },
|
||||
...readonlyExternalDirectory,
|
||||
{ action: "question", resource: "*", effect: "deny" },
|
||||
{ action: "plan_enter", resource: "*", effect: "deny" },
|
||||
{ action: "plan_exit", resource: "*", effect: "deny" },
|
||||
{ action: "read", resource: "*", effect: "allow" },
|
||||
{ action: "read", resource: "*.env", effect: "ask" },
|
||||
{ action: "read", resource: "*.env.*", effect: "ask" },
|
||||
{ action: "read", resource: "*.env.example", effect: "allow" },
|
||||
]
|
||||
|
||||
yield* ctx.agent.transform((draft) => {
|
||||
draft.update(AgentV2.defaultID, (item) => {
|
||||
|
|
@ -129,7 +126,7 @@ export const Plugin = define({
|
|||
item.description = "The default agent. Executes tools based on configured permissions."
|
||||
item.mode = "primary"
|
||||
item.permissions.push(
|
||||
...PermissionV2.merge(defaults, [
|
||||
...PermissionV2.merge(defaultPermissions, [
|
||||
{ action: "question", resource: "*", effect: "allow" },
|
||||
{ action: "plan_enter", resource: "*", effect: "allow" },
|
||||
]),
|
||||
|
|
@ -141,7 +138,7 @@ export const Plugin = define({
|
|||
item.description = "Plan mode. Disallows all edit tools."
|
||||
item.mode = "primary"
|
||||
item.permissions.push(
|
||||
...PermissionV2.merge(defaults, [
|
||||
...PermissionV2.merge(defaultPermissions, [
|
||||
{ action: "question", resource: "*", effect: "allow" },
|
||||
{ action: "plan_exit", resource: "*", effect: "allow" },
|
||||
{ action: "external_directory", resource: path.join(Global.Path.data, "plans", "*"), effect: "allow" },
|
||||
|
|
@ -161,7 +158,9 @@ export const Plugin = define({
|
|||
item.description =
|
||||
"General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel."
|
||||
item.mode = "subagent"
|
||||
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "subagent", resource: "*", effect: "deny" }]))
|
||||
item.permissions.push(
|
||||
...PermissionV2.merge(defaultPermissions, [{ action: "subagent", resource: "*", effect: "deny" }]),
|
||||
)
|
||||
})
|
||||
|
||||
draft.update(AgentV2.ID.make("explore"), (item) => {
|
||||
|
|
@ -172,7 +171,7 @@ export const Plugin = define({
|
|||
item.mode = "subagent"
|
||||
item.permissions.push(
|
||||
...PermissionV2.merge(
|
||||
defaults,
|
||||
defaultPermissions,
|
||||
[
|
||||
{ action: "*", resource: "*", effect: "deny" },
|
||||
{ action: "grep", resource: "*", effect: "allow" },
|
||||
|
|
@ -192,7 +191,9 @@ export const Plugin = define({
|
|||
item.mode = "primary"
|
||||
item.hidden = true
|
||||
item.system = PROMPT_COMPACTION
|
||||
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
|
||||
item.permissions.push(
|
||||
...PermissionV2.merge(defaultPermissions, [{ action: "*", resource: "*", effect: "deny" }]),
|
||||
)
|
||||
})
|
||||
|
||||
draft.update(AgentV2.ID.make("title"), (item) => {
|
||||
|
|
@ -200,7 +201,9 @@ export const Plugin = define({
|
|||
item.mode = "primary"
|
||||
item.hidden = true
|
||||
item.system = PROMPT_TITLE
|
||||
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
|
||||
item.permissions.push(
|
||||
...PermissionV2.merge(defaultPermissions, [{ action: "*", resource: "*", effect: "deny" }]),
|
||||
)
|
||||
})
|
||||
|
||||
draft.update(AgentV2.ID.make("summary"), (item) => {
|
||||
|
|
@ -208,7 +211,9 @@ export const Plugin = define({
|
|||
item.mode = "primary"
|
||||
item.hidden = true
|
||||
item.system = PROMPT_SUMMARY
|
||||
item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
|
||||
item.permissions.push(
|
||||
...PermissionV2.merge(defaultPermissions, [{ action: "*", resource: "*", effect: "deny" }]),
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -9,9 +9,12 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { AgentPlugin } from "@opencode-ai/core/plugin/agent"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { ConfigMigrateV1 } from "@opencode-ai/core/v1/config/migrate"
|
||||
import { location } from "../fixture/location"
|
||||
import { tmpdir } from "../fixture/tmpdir"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { agentHost, host } from "../plugin/host"
|
||||
|
|
@ -51,6 +54,42 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("applies global permissions before built-in and configured agent rules", () =>
|
||||
Effect.gen(function* () {
|
||||
const agents = yield* AgentV2.Service
|
||||
yield* AgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
|
||||
Effect.provideService(
|
||||
Location.Service,
|
||||
Location.Service.of(location({ directory: AbsolutePath.make("/project") })),
|
||||
),
|
||||
)
|
||||
const config = Config.Service.of({
|
||||
entries: () =>
|
||||
Effect.succeed([
|
||||
new Config.Document({
|
||||
type: "document",
|
||||
info: decode(
|
||||
ConfigMigrateV1.migrate({
|
||||
permission: { bash: { "rm*": "ask", "git reset*": "ask" } },
|
||||
}),
|
||||
),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
|
||||
yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
|
||||
Effect.provideService(Config.Service, config),
|
||||
)
|
||||
|
||||
const build = yield* agents.get(AgentV2.defaultID)
|
||||
const explore = yield* agents.get(AgentV2.ID.make("explore"))
|
||||
if (!build || !explore) throw new Error("expected built-in agents")
|
||||
expect(PermissionV2.evaluate("shell", "rm -rf tmp", build.permissions).effect).toBe("ask")
|
||||
expect(PermissionV2.evaluate("shell", "rm -rf tmp", explore.permissions).effect).toBe("deny")
|
||||
expect(PermissionV2.evaluate("shell", "ls", explore.permissions).effect).toBe("deny")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("applies all global permissions before agent-specific permissions", () =>
|
||||
Effect.gen(function* () {
|
||||
const agents = yield* AgentV2.Service
|
||||
|
|
@ -110,13 +149,13 @@ describe("ConfigAgentPlugin.Plugin", () => {
|
|||
if (!buildAgent) throw new Error("expected configured build agent")
|
||||
expect(buildAgent.permissions).toEqual([
|
||||
...defaultPermissions,
|
||||
{ action: "bash", resource: "*", effect: "allow" },
|
||||
{ action: "bash", resource: "*", effect: "ask" },
|
||||
{ action: "read", resource: "*", effect: "allow" },
|
||||
{ action: "bash", resource: "*", effect: "allow" },
|
||||
{ action: "bash", resource: "git *", effect: "allow" },
|
||||
])
|
||||
expect(PermissionV2.evaluate("bash", "git status", buildAgent.permissions).effect).toBe("allow")
|
||||
expect(PermissionV2.evaluate("bash", "bun test", buildAgent.permissions).effect).toBe("ask")
|
||||
expect(PermissionV2.evaluate("bash", "bun test", buildAgent.permissions).effect).toBe("allow")
|
||||
|
||||
const reviewer = yield* agents.get(AgentV2.ID.make("reviewer"))
|
||||
if (!reviewer) throw new Error("expected configured reviewer agent")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue