+21

![opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)



![opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)



James Long
Brendan Allan
Kit Langton
opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Affan Ali
affanali2k3
Frank
opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴
Aiden Cline
Jay V
Dax Raad
Aarav Sareen
OpeOginni
Luke Parker
Ben Guthrie
Dax
Filip
Max Anderson
Brendan Allan
Jack
Shoubhit Dash
Dustin Deus
starptech
Aiden Cline
usrnk1
Jay
runvip
opencode
Julian Coy
Vladimir Glafirov
8c94e9005f
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com> Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Jay V <air@live.ca> Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com> Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Ben Guthrie <benjee.012@gmail.com> Co-authored-by: Dax <mail@thdxr.com> Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com> Co-authored-by: Max Anderson <max.a.anderson95@gmail.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: runvip <164729189+runvip@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
320 lines
12 KiB
TypeScript
320 lines
12 KiB
TypeScript
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
|
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|
import { describe, test, expect } from "bun:test"
|
|
import { Effect } from "effect"
|
|
import { Permission } from "../src/permission"
|
|
import { Config } from "@/config/config"
|
|
import { testEffect } from "./lib/effect"
|
|
|
|
const it = testEffect(LayerNode.compile(Config.node))
|
|
|
|
const load = Config.use.get()
|
|
|
|
describe("Permission.evaluate for permission.task", () => {
|
|
const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): PermissionV1.Ruleset =>
|
|
Object.entries(rules).map(([pattern, action]) => ({
|
|
permission: "task",
|
|
pattern,
|
|
action,
|
|
}))
|
|
|
|
test("returns ask when no match (default)", () => {
|
|
expect(Permission.evaluate("task", "code-reviewer", []).action).toBe("ask")
|
|
})
|
|
|
|
test("returns deny for explicit deny", () => {
|
|
const ruleset = createRuleset({ "code-reviewer": "deny" })
|
|
expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("deny")
|
|
})
|
|
|
|
test("returns allow for explicit allow", () => {
|
|
const ruleset = createRuleset({ "code-reviewer": "allow" })
|
|
expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("allow")
|
|
})
|
|
|
|
test("returns ask for explicit ask", () => {
|
|
const ruleset = createRuleset({ "code-reviewer": "ask" })
|
|
expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("ask")
|
|
})
|
|
|
|
test("matches wildcard patterns with deny", () => {
|
|
const ruleset = createRuleset({ "orchestrator-*": "deny" })
|
|
expect(Permission.evaluate("task", "orchestrator-fast", ruleset).action).toBe("deny")
|
|
expect(Permission.evaluate("task", "orchestrator-slow", ruleset).action).toBe("deny")
|
|
expect(Permission.evaluate("task", "general", ruleset).action).toBe("ask")
|
|
})
|
|
|
|
test("matches wildcard patterns with allow", () => {
|
|
const ruleset = createRuleset({ "orchestrator-*": "allow" })
|
|
expect(Permission.evaluate("task", "orchestrator-fast", ruleset).action).toBe("allow")
|
|
expect(Permission.evaluate("task", "orchestrator-slow", ruleset).action).toBe("allow")
|
|
})
|
|
|
|
test("matches wildcard patterns with ask", () => {
|
|
const ruleset = createRuleset({ "orchestrator-*": "ask" })
|
|
expect(Permission.evaluate("task", "orchestrator-fast", ruleset).action).toBe("ask")
|
|
const globalRuleset = createRuleset({ "*": "ask" })
|
|
expect(Permission.evaluate("task", "code-reviewer", globalRuleset).action).toBe("ask")
|
|
})
|
|
|
|
test("later rules take precedence (last match wins)", () => {
|
|
const ruleset = createRuleset({
|
|
"orchestrator-*": "deny",
|
|
"orchestrator-fast": "allow",
|
|
})
|
|
expect(Permission.evaluate("task", "orchestrator-fast", ruleset).action).toBe("allow")
|
|
expect(Permission.evaluate("task", "orchestrator-slow", ruleset).action).toBe("deny")
|
|
})
|
|
|
|
test("matches global wildcard", () => {
|
|
expect(Permission.evaluate("task", "any-agent", createRuleset({ "*": "allow" })).action).toBe("allow")
|
|
expect(Permission.evaluate("task", "any-agent", createRuleset({ "*": "deny" })).action).toBe("deny")
|
|
expect(Permission.evaluate("task", "any-agent", createRuleset({ "*": "ask" })).action).toBe("ask")
|
|
})
|
|
})
|
|
|
|
describe("Permission.disabled for task tool", () => {
|
|
// Note: The `disabled` function checks if a TOOL should be completely removed from the tool list.
|
|
// It only disables a tool when there's a rule with `pattern: "*"` and `action: "deny"`.
|
|
// It does NOT evaluate complex subagent patterns - those are handled at runtime by `evaluate`.
|
|
const createRuleset = (rules: Record<string, "allow" | "deny" | "ask">): PermissionV1.Ruleset =>
|
|
Object.entries(rules).map(([pattern, action]) => ({
|
|
permission: "task",
|
|
pattern,
|
|
action,
|
|
}))
|
|
|
|
test("task tool is disabled when global deny pattern exists (even with specific allows)", () => {
|
|
// When "*": "deny" exists, the task tool is disabled because the disabled() function
|
|
// only checks for wildcard deny patterns - it doesn't consider that specific subagents might be allowed
|
|
const ruleset = createRuleset({
|
|
"orchestrator-*": "allow",
|
|
"*": "deny",
|
|
})
|
|
const disabled = Permission.disabled(["task", "bash", "read"], ruleset)
|
|
// The task tool IS disabled because there's a pattern: "*" with action: "deny"
|
|
expect(disabled.has("task")).toBe(true)
|
|
})
|
|
|
|
test("task tool is disabled when global deny pattern exists (even with ask overrides)", () => {
|
|
const ruleset = createRuleset({
|
|
"orchestrator-*": "ask",
|
|
"*": "deny",
|
|
})
|
|
const disabled = Permission.disabled(["task"], ruleset)
|
|
// The task tool IS disabled because there's a pattern: "*" with action: "deny"
|
|
expect(disabled.has("task")).toBe(true)
|
|
})
|
|
|
|
test("task tool is disabled when global deny pattern exists", () => {
|
|
const ruleset = createRuleset({ "*": "deny" })
|
|
const disabled = Permission.disabled(["task"], ruleset)
|
|
expect(disabled.has("task")).toBe(true)
|
|
})
|
|
|
|
test("task tool is NOT disabled when only specific patterns are denied (no wildcard)", () => {
|
|
// The disabled() function only disables tools when pattern: "*" && action: "deny"
|
|
// Specific subagent denies don't disable the task tool - those are handled at runtime
|
|
const ruleset = createRuleset({
|
|
"orchestrator-*": "deny",
|
|
general: "deny",
|
|
})
|
|
const disabled = Permission.disabled(["task"], ruleset)
|
|
// The task tool is NOT disabled because no rule has pattern: "*" with action: "deny"
|
|
expect(disabled.has("task")).toBe(false)
|
|
})
|
|
|
|
test("task tool is enabled when no task rules exist (default ask)", () => {
|
|
const disabled = Permission.disabled(["task"], [])
|
|
expect(disabled.has("task")).toBe(false)
|
|
})
|
|
|
|
test("task tool is NOT disabled when last wildcard pattern is allow", () => {
|
|
// Last matching rule wins - if wildcard allow comes after wildcard deny, tool is enabled
|
|
const ruleset = createRuleset({
|
|
"*": "deny",
|
|
"orchestrator-coder": "allow",
|
|
})
|
|
const disabled = Permission.disabled(["task"], ruleset)
|
|
// The disabled() function uses findLast and checks if the last matching rule
|
|
// has pattern: "*" and action: "deny". In this case, the last rule matching
|
|
// "task" permission has pattern "orchestrator-coder", not "*", so not disabled
|
|
expect(disabled.has("task")).toBe(false)
|
|
})
|
|
})
|
|
|
|
// Integration tests that load permissions from real config files
|
|
describe("permission.task with real config files", () => {
|
|
it.instance(
|
|
"loads task permissions from opencode.json config",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
const config = yield* load
|
|
const ruleset = Permission.fromConfig(config.permission ?? {})
|
|
// general and orchestrator-fast should be allowed, code-reviewer denied
|
|
expect(Permission.evaluate("task", "general", ruleset).action).toBe("allow")
|
|
expect(Permission.evaluate("task", "orchestrator-fast", ruleset).action).toBe("allow")
|
|
expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("deny")
|
|
}),
|
|
{
|
|
git: true,
|
|
config: {
|
|
permission: {
|
|
task: {
|
|
"*": "allow",
|
|
"code-reviewer": "deny",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
it.instance(
|
|
"loads task permissions with wildcard patterns from config",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
const config = yield* load
|
|
const ruleset = Permission.fromConfig(config.permission ?? {})
|
|
// general and code-reviewer should be ask, orchestrator-* denied
|
|
expect(Permission.evaluate("task", "general", ruleset).action).toBe("ask")
|
|
expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("ask")
|
|
expect(Permission.evaluate("task", "orchestrator-fast", ruleset).action).toBe("deny")
|
|
}),
|
|
{
|
|
git: true,
|
|
config: {
|
|
permission: {
|
|
task: {
|
|
"*": "ask",
|
|
"orchestrator-*": "deny",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
it.instance(
|
|
"evaluate respects task permission from config",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
const config = yield* load
|
|
const ruleset = Permission.fromConfig(config.permission ?? {})
|
|
expect(Permission.evaluate("task", "general", ruleset).action).toBe("allow")
|
|
expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("deny")
|
|
// Unspecified agents default to "ask"
|
|
expect(Permission.evaluate("task", "unknown-agent", ruleset).action).toBe("ask")
|
|
}),
|
|
{
|
|
git: true,
|
|
config: {
|
|
permission: {
|
|
task: {
|
|
general: "allow",
|
|
"code-reviewer": "deny",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
it.instance(
|
|
"mixed permission config with task and other tools",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
const config = yield* load
|
|
const ruleset = Permission.fromConfig(config.permission ?? {})
|
|
|
|
// Verify task permissions
|
|
expect(Permission.evaluate("task", "general", ruleset).action).toBe("allow")
|
|
expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("deny")
|
|
|
|
// Verify other tool permissions
|
|
expect(Permission.evaluate("bash", "*", ruleset).action).toBe("allow")
|
|
expect(Permission.evaluate("edit", "*", ruleset).action).toBe("ask")
|
|
|
|
// Verify disabled tools
|
|
const disabled = Permission.disabled(["bash", "edit", "task"], ruleset)
|
|
expect(disabled.has("bash")).toBe(false)
|
|
expect(disabled.has("edit")).toBe(false)
|
|
// task is NOT disabled because disabled() uses findLast, and the last rule
|
|
// matching "task" permission is {pattern: "general", action: "allow"}, not pattern: "*"
|
|
expect(disabled.has("task")).toBe(false)
|
|
}),
|
|
{
|
|
git: true,
|
|
config: {
|
|
permission: {
|
|
bash: "allow",
|
|
edit: "ask",
|
|
task: {
|
|
"*": "deny",
|
|
general: "allow",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
it.instance(
|
|
"task tool disabled when global deny comes last in config",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
const config = yield* load
|
|
const ruleset = Permission.fromConfig(config.permission ?? {})
|
|
|
|
// Last matching rule wins - "*" deny is last, so all agents are denied
|
|
expect(Permission.evaluate("task", "general", ruleset).action).toBe("deny")
|
|
expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("deny")
|
|
expect(Permission.evaluate("task", "unknown", ruleset).action).toBe("deny")
|
|
|
|
// Since "*": "deny" is the last rule, disabled() finds it with findLast
|
|
// and sees pattern: "*" with action: "deny", so task is disabled
|
|
const disabled = Permission.disabled(["task"], ruleset)
|
|
expect(disabled.has("task")).toBe(true)
|
|
}),
|
|
{
|
|
git: true,
|
|
config: {
|
|
permission: {
|
|
task: {
|
|
general: "allow",
|
|
"code-reviewer": "allow",
|
|
"*": "deny",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
it.instance(
|
|
"task tool NOT disabled when specific allow comes last in config",
|
|
() =>
|
|
Effect.gen(function* () {
|
|
const config = yield* load
|
|
const ruleset = Permission.fromConfig(config.permission ?? {})
|
|
|
|
// Evaluate uses findLast - "general" allow comes after "*" deny
|
|
expect(Permission.evaluate("task", "general", ruleset).action).toBe("allow")
|
|
// Other agents still denied by the earlier "*" deny
|
|
expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("deny")
|
|
|
|
// disabled() uses findLast and checks if the last rule has pattern: "*" with action: "deny"
|
|
// In this case, the last rule is {pattern: "general", action: "allow"}, not pattern: "*"
|
|
// So the task tool is NOT disabled (even though most subagents are denied)
|
|
const disabled = Permission.disabled(["task"], ruleset)
|
|
expect(disabled.has("task")).toBe(false)
|
|
}),
|
|
{
|
|
git: true,
|
|
config: {
|
|
permission: {
|
|
task: {
|
|
"*": "deny",
|
|
general: "allow",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
})
|