mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
sync
This commit is contained in:
parent
16381bb20e
commit
598c6171cd
7 changed files with 228 additions and 251 deletions
|
|
@ -1,10 +0,0 @@
|
|||
---
|
||||
description: Use this agent when you are asked to commit and push code changes to a git repository.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You commit and push to git
|
||||
|
||||
Commit messages should be brief since they are used to generate release notes.
|
||||
|
||||
Messages should say WHY the change was made and not WHAT was changed.
|
||||
|
|
@ -5,9 +5,7 @@
|
|||
// "url": "https://enterprise.dev.opencode.ai",
|
||||
// },
|
||||
"instructions": ["STYLE_GUIDE.md"],
|
||||
"permission": {
|
||||
"*": "ask",
|
||||
},
|
||||
// "permission": "deny",
|
||||
"provider": {
|
||||
"opencode": {
|
||||
"options": {},
|
||||
|
|
|
|||
|
|
@ -241,7 +241,8 @@ const AgentListCommand = cmd({
|
|||
})
|
||||
|
||||
for (const agent of sortedAgents) {
|
||||
process.stdout.write(`${agent.name} (${agent.mode})${EOL}`)
|
||||
process.stdout.write(`${agent.name} (${agent.mode})` + EOL)
|
||||
process.stdout.write(` ${JSON.stringify(agent.permission, null, 2)}` + EOL)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -410,6 +410,8 @@ export namespace Config {
|
|||
doom_loop: PermissionAction.optional(),
|
||||
})
|
||||
.catchall(PermissionRule)
|
||||
.or(PermissionAction)
|
||||
.transform((x) => (typeof x === "string" ? { "*": x } : x))
|
||||
.meta({
|
||||
ref: "PermissionConfig",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { Instance } from "@/project/instance"
|
|||
import { fn } from "@/util/fn"
|
||||
import { Log } from "@/util/log"
|
||||
import { Wildcard } from "@/util/wildcard"
|
||||
import { sortBy } from "remeda"
|
||||
import z from "zod"
|
||||
|
||||
export namespace PermissionNext {
|
||||
|
|
@ -19,6 +18,7 @@ export namespace PermissionNext {
|
|||
|
||||
export const Rule = z
|
||||
.object({
|
||||
permission: z.string(),
|
||||
pattern: z.string(),
|
||||
action: Action,
|
||||
})
|
||||
|
|
@ -27,40 +27,29 @@ export namespace PermissionNext {
|
|||
})
|
||||
export type Rule = z.infer<typeof Rule>
|
||||
|
||||
export const Ruleset = z.record(z.string(), Rule.array()).meta({
|
||||
export const Ruleset = Rule.array().meta({
|
||||
ref: "PermissionRuleset",
|
||||
})
|
||||
export type Ruleset = z.infer<typeof Ruleset>
|
||||
|
||||
export function fromConfig(permission: Config.Permission) {
|
||||
const ruleset: Ruleset = {}
|
||||
const ruleset: Ruleset = []
|
||||
for (const [key, value] of Object.entries(permission)) {
|
||||
if (typeof value === "string") {
|
||||
ruleset[key] = [
|
||||
{
|
||||
action: value,
|
||||
pattern: "*",
|
||||
},
|
||||
]
|
||||
ruleset.push({
|
||||
permission: key,
|
||||
action: value,
|
||||
pattern: "*",
|
||||
})
|
||||
continue
|
||||
}
|
||||
ruleset[key] = Object.entries(value).map(([pattern, action]) => ({ pattern, action }))
|
||||
ruleset.push(...Object.entries(value).map(([pattern, action]) => ({ permission: key, pattern, action })))
|
||||
}
|
||||
return ruleset
|
||||
}
|
||||
|
||||
export function merge(...rulesets: Ruleset[]): Ruleset {
|
||||
const result: Ruleset = {}
|
||||
for (const ruleset of rulesets) {
|
||||
for (const [permission, rules] of Object.entries(ruleset)) {
|
||||
if (!result[permission]) {
|
||||
result[permission] = rules
|
||||
continue
|
||||
}
|
||||
result[permission] = result[permission].concat(rules)
|
||||
}
|
||||
}
|
||||
return result as Ruleset
|
||||
return rulesets.flat()
|
||||
}
|
||||
|
||||
export const Request = z
|
||||
|
|
@ -205,20 +194,15 @@ export namespace PermissionNext {
|
|||
|
||||
export function evaluate(permission: string, pattern: string, ruleset: Ruleset): Action {
|
||||
log.info("evaluate", { permission, pattern, ruleset })
|
||||
const rules: Rule[] = []
|
||||
const entries = sortBy(Object.entries(ruleset), ([k]) => k.length)
|
||||
for (const [permPattern, permRules] of entries) {
|
||||
if (Wildcard.match(permission, permPattern)) {
|
||||
rules.push(...permRules)
|
||||
}
|
||||
}
|
||||
const match = rules.findLast((rule) => Wildcard.match(pattern, rule.pattern))
|
||||
const match = ruleset.findLast(
|
||||
(rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern),
|
||||
)
|
||||
return match?.action ?? "ask"
|
||||
}
|
||||
|
||||
const EDIT_TOOLS = ["edit", "write", "patch", "multiedit"]
|
||||
|
||||
export function disabledTools(tools: string[], ruleset: Ruleset): Set<string> {
|
||||
export function disabled(tools: string[], ruleset: Ruleset): Set<string> {
|
||||
const disabled = new Set<string>()
|
||||
for (const tool of tools) {
|
||||
const permission = EDIT_TOOLS.includes(tool) ? "edit" : tool
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ export namespace LLM {
|
|||
}
|
||||
|
||||
async function resolveTools(input: Pick<StreamInput, "tools" | "agent" | "user">) {
|
||||
const disabled = PermissionNext.disabledTools(Object.keys(input.tools), input.agent.permission)
|
||||
const disabled = PermissionNext.disabled(Object.keys(input.tools), input.agent.permission)
|
||||
for (const tool of Object.keys(input.tools)) {
|
||||
if (input.user.tools?.[tool] === false || disabled.has(tool)) {
|
||||
delete input.tools[tool]
|
||||
|
|
|
|||
|
|
@ -5,17 +5,15 @@ import { PermissionNext } from "../../src/permission/next"
|
|||
|
||||
test("fromConfig - string value becomes wildcard rule", () => {
|
||||
const result = PermissionNext.fromConfig({ bash: "allow" })
|
||||
expect(result).toEqual({ bash: [{ pattern: "*", action: "allow" }] })
|
||||
expect(result).toEqual([{ permission: "bash", pattern: "*", action: "allow" }])
|
||||
})
|
||||
|
||||
test("fromConfig - object value converts to rules array", () => {
|
||||
const result = PermissionNext.fromConfig({ bash: { "*": "allow", rm: "deny" } })
|
||||
expect(result).toEqual({
|
||||
bash: [
|
||||
{ pattern: "*", action: "allow" },
|
||||
{ pattern: "rm", action: "deny" },
|
||||
],
|
||||
})
|
||||
expect(result).toEqual([
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
{ permission: "bash", pattern: "rm", action: "deny" },
|
||||
])
|
||||
})
|
||||
|
||||
test("fromConfig - mixed string and object values", () => {
|
||||
|
|
@ -24,310 +22,309 @@ test("fromConfig - mixed string and object values", () => {
|
|||
edit: "allow",
|
||||
webfetch: "ask",
|
||||
})
|
||||
expect(result).toEqual({
|
||||
bash: [
|
||||
{ pattern: "*", action: "allow" },
|
||||
{ pattern: "rm", action: "deny" },
|
||||
],
|
||||
edit: [{ pattern: "*", action: "allow" }],
|
||||
webfetch: [{ pattern: "*", action: "ask" }],
|
||||
})
|
||||
expect(result).toEqual([
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
{ permission: "bash", pattern: "rm", action: "deny" },
|
||||
{ permission: "edit", pattern: "*", action: "allow" },
|
||||
{ permission: "webfetch", pattern: "*", action: "ask" },
|
||||
])
|
||||
})
|
||||
|
||||
test("fromConfig - empty object", () => {
|
||||
const result = PermissionNext.fromConfig({})
|
||||
expect(result).toEqual({})
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
// merge tests
|
||||
|
||||
test("merge - simple concatenation", () => {
|
||||
const result = PermissionNext.merge(
|
||||
{ bash: [{ pattern: "*", action: "allow" }] },
|
||||
{ bash: [{ pattern: "*", action: "deny" }] },
|
||||
[{ permission: "bash", pattern: "*", action: "allow" }],
|
||||
[{ permission: "bash", pattern: "*", action: "deny" }],
|
||||
)
|
||||
expect(result).toEqual({
|
||||
bash: [
|
||||
{ pattern: "*", action: "allow" },
|
||||
{ pattern: "*", action: "deny" },
|
||||
],
|
||||
})
|
||||
expect(result).toEqual([
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
{ permission: "bash", pattern: "*", action: "deny" },
|
||||
])
|
||||
})
|
||||
|
||||
test("merge - adds new permission", () => {
|
||||
const result = PermissionNext.merge(
|
||||
{ bash: [{ pattern: "*", action: "allow" }] },
|
||||
{ edit: [{ pattern: "*", action: "deny" }] },
|
||||
[{ permission: "bash", pattern: "*", action: "allow" }],
|
||||
[{ permission: "edit", pattern: "*", action: "deny" }],
|
||||
)
|
||||
expect(result).toEqual({
|
||||
bash: [{ pattern: "*", action: "allow" }],
|
||||
edit: [{ pattern: "*", action: "deny" }],
|
||||
})
|
||||
expect(result).toEqual([
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
{ permission: "edit", pattern: "*", action: "deny" },
|
||||
])
|
||||
})
|
||||
|
||||
test("merge - concatenates rules for same permission", () => {
|
||||
const result = PermissionNext.merge(
|
||||
{ bash: [{ pattern: "foo", action: "ask" }] },
|
||||
{ bash: [{ pattern: "*", action: "deny" }] },
|
||||
[{ permission: "bash", pattern: "foo", action: "ask" }],
|
||||
[{ permission: "bash", pattern: "*", action: "deny" }],
|
||||
)
|
||||
expect(result).toEqual({
|
||||
bash: [
|
||||
{ pattern: "foo", action: "ask" },
|
||||
{ pattern: "*", action: "deny" },
|
||||
],
|
||||
})
|
||||
expect(result).toEqual([
|
||||
{ permission: "bash", pattern: "foo", action: "ask" },
|
||||
{ permission: "bash", pattern: "*", action: "deny" },
|
||||
])
|
||||
})
|
||||
|
||||
test("merge - multiple rulesets", () => {
|
||||
const result = PermissionNext.merge(
|
||||
{ bash: [{ pattern: "*", action: "allow" }] },
|
||||
{ bash: [{ pattern: "rm", action: "ask" }] },
|
||||
{ edit: [{ pattern: "*", action: "allow" }] },
|
||||
[{ permission: "bash", pattern: "*", action: "allow" }],
|
||||
[{ permission: "bash", pattern: "rm", action: "ask" }],
|
||||
[{ permission: "edit", pattern: "*", action: "allow" }],
|
||||
)
|
||||
expect(result).toEqual({
|
||||
bash: [
|
||||
{ pattern: "*", action: "allow" },
|
||||
{ pattern: "rm", action: "ask" },
|
||||
],
|
||||
edit: [{ pattern: "*", action: "allow" }],
|
||||
})
|
||||
expect(result).toEqual([
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
{ permission: "bash", pattern: "rm", action: "ask" },
|
||||
{ permission: "edit", pattern: "*", action: "allow" },
|
||||
])
|
||||
})
|
||||
|
||||
test("merge - empty ruleset does nothing", () => {
|
||||
const result = PermissionNext.merge({ bash: [{ pattern: "*", action: "allow" }] }, {})
|
||||
expect(result).toEqual({ bash: [{ pattern: "*", action: "allow" }] })
|
||||
const result = PermissionNext.merge([{ permission: "bash", pattern: "*", action: "allow" }], [])
|
||||
expect(result).toEqual([{ permission: "bash", pattern: "*", action: "allow" }])
|
||||
})
|
||||
|
||||
test("merge - preserves rule order", () => {
|
||||
const result = PermissionNext.merge(
|
||||
{
|
||||
edit: [
|
||||
{ pattern: "src/*", action: "allow" },
|
||||
{ pattern: "src/secret/*", action: "deny" },
|
||||
],
|
||||
},
|
||||
{ edit: [{ pattern: "src/secret/ok.ts", action: "allow" }] },
|
||||
)
|
||||
expect(result).toEqual({
|
||||
edit: [
|
||||
{ pattern: "src/*", action: "allow" },
|
||||
{ pattern: "src/secret/*", action: "deny" },
|
||||
{ pattern: "src/secret/ok.ts", action: "allow" },
|
||||
[
|
||||
{ permission: "edit", pattern: "src/*", action: "allow" },
|
||||
{ permission: "edit", pattern: "src/secret/*", action: "deny" },
|
||||
],
|
||||
})
|
||||
[{ permission: "edit", pattern: "src/secret/ok.ts", action: "allow" }],
|
||||
)
|
||||
expect(result).toEqual([
|
||||
{ permission: "edit", pattern: "src/*", action: "allow" },
|
||||
{ permission: "edit", pattern: "src/secret/*", action: "deny" },
|
||||
{ permission: "edit", pattern: "src/secret/ok.ts", action: "allow" },
|
||||
])
|
||||
})
|
||||
|
||||
test("merge - config permission overrides default ask", () => {
|
||||
// Simulates: defaults have "*": "ask", config sets bash: "allow"
|
||||
const defaults: PermissionNext.Ruleset = [{ permission: "*", pattern: "*", action: "ask" }]
|
||||
const config: PermissionNext.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
|
||||
const merged = PermissionNext.merge(defaults, config)
|
||||
|
||||
// Config's bash allow should override default ask
|
||||
expect(PermissionNext.evaluate("bash", "ls", merged)).toBe("allow")
|
||||
// Other permissions should still be ask (from defaults)
|
||||
expect(PermissionNext.evaluate("edit", "foo.ts", merged)).toBe("ask")
|
||||
})
|
||||
|
||||
test("merge - config ask overrides default allow", () => {
|
||||
// Simulates: defaults have bash: "allow", config sets bash: "ask"
|
||||
const defaults: PermissionNext.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }]
|
||||
const config: PermissionNext.Ruleset = [{ permission: "bash", pattern: "*", action: "ask" }]
|
||||
const merged = PermissionNext.merge(defaults, config)
|
||||
|
||||
// Config's ask should override default allow
|
||||
expect(PermissionNext.evaluate("bash", "ls", merged)).toBe("ask")
|
||||
})
|
||||
|
||||
// evaluate tests
|
||||
|
||||
test("evaluate - exact pattern match", () => {
|
||||
const result = PermissionNext.evaluate("bash", "rm", { bash: [{ pattern: "rm", action: "deny" }] })
|
||||
const result = PermissionNext.evaluate("bash", "rm", [{ permission: "bash", pattern: "rm", action: "deny" }])
|
||||
expect(result).toBe("deny")
|
||||
})
|
||||
|
||||
test("evaluate - wildcard pattern match", () => {
|
||||
const result = PermissionNext.evaluate("bash", "rm", { bash: [{ pattern: "*", action: "allow" }] })
|
||||
const result = PermissionNext.evaluate("bash", "rm", [{ permission: "bash", pattern: "*", action: "allow" }])
|
||||
expect(result).toBe("allow")
|
||||
})
|
||||
|
||||
test("evaluate - last matching rule wins", () => {
|
||||
const result = PermissionNext.evaluate("bash", "rm", {
|
||||
bash: [
|
||||
{ pattern: "*", action: "allow" },
|
||||
{ pattern: "rm", action: "deny" },
|
||||
],
|
||||
})
|
||||
const result = PermissionNext.evaluate("bash", "rm", [
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
{ permission: "bash", pattern: "rm", action: "deny" },
|
||||
])
|
||||
expect(result).toBe("deny")
|
||||
})
|
||||
|
||||
test("evaluate - last matching rule wins (wildcard after specific)", () => {
|
||||
const result = PermissionNext.evaluate("bash", "rm", {
|
||||
bash: [
|
||||
{ pattern: "rm", action: "deny" },
|
||||
{ pattern: "*", action: "allow" },
|
||||
],
|
||||
})
|
||||
const result = PermissionNext.evaluate("bash", "rm", [
|
||||
{ permission: "bash", pattern: "rm", action: "deny" },
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
])
|
||||
expect(result).toBe("allow")
|
||||
})
|
||||
|
||||
test("evaluate - glob pattern match", () => {
|
||||
const result = PermissionNext.evaluate("edit", "src/foo.ts", { edit: [{ pattern: "src/*", action: "allow" }] })
|
||||
const result = PermissionNext.evaluate("edit", "src/foo.ts", [
|
||||
{ permission: "edit", pattern: "src/*", action: "allow" },
|
||||
])
|
||||
expect(result).toBe("allow")
|
||||
})
|
||||
|
||||
test("evaluate - last matching glob wins", () => {
|
||||
const result = PermissionNext.evaluate("edit", "src/components/Button.tsx", {
|
||||
edit: [
|
||||
{ pattern: "src/*", action: "deny" },
|
||||
{ pattern: "src/components/*", action: "allow" },
|
||||
],
|
||||
})
|
||||
const result = PermissionNext.evaluate("edit", "src/components/Button.tsx", [
|
||||
{ permission: "edit", pattern: "src/*", action: "deny" },
|
||||
{ permission: "edit", pattern: "src/components/*", action: "allow" },
|
||||
])
|
||||
expect(result).toBe("allow")
|
||||
})
|
||||
|
||||
test("evaluate - order matters for specificity", () => {
|
||||
// If more specific rule comes first, later wildcard overrides it
|
||||
const result = PermissionNext.evaluate("edit", "src/components/Button.tsx", {
|
||||
edit: [
|
||||
{ pattern: "src/components/*", action: "allow" },
|
||||
{ pattern: "src/*", action: "deny" },
|
||||
],
|
||||
})
|
||||
const result = PermissionNext.evaluate("edit", "src/components/Button.tsx", [
|
||||
{ permission: "edit", pattern: "src/components/*", action: "allow" },
|
||||
{ permission: "edit", pattern: "src/*", action: "deny" },
|
||||
])
|
||||
expect(result).toBe("deny")
|
||||
})
|
||||
|
||||
test("evaluate - unknown permission returns ask", () => {
|
||||
const result = PermissionNext.evaluate("unknown_tool", "anything", {
|
||||
bash: [{ pattern: "*", action: "allow" }],
|
||||
})
|
||||
const result = PermissionNext.evaluate("unknown_tool", "anything", [
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
])
|
||||
expect(result).toBe("ask")
|
||||
})
|
||||
|
||||
test("evaluate - empty ruleset returns ask", () => {
|
||||
const result = PermissionNext.evaluate("bash", "rm", {})
|
||||
const result = PermissionNext.evaluate("bash", "rm", [])
|
||||
expect(result).toBe("ask")
|
||||
})
|
||||
|
||||
test("evaluate - no matching pattern returns ask", () => {
|
||||
const result = PermissionNext.evaluate("edit", "etc/passwd", { edit: [{ pattern: "src/*", action: "allow" }] })
|
||||
const result = PermissionNext.evaluate("edit", "etc/passwd", [
|
||||
{ permission: "edit", pattern: "src/*", action: "allow" },
|
||||
])
|
||||
expect(result).toBe("ask")
|
||||
})
|
||||
|
||||
test("evaluate - empty rules array returns ask", () => {
|
||||
const result = PermissionNext.evaluate("bash", "rm", { bash: [] })
|
||||
const result = PermissionNext.evaluate("bash", "rm", [])
|
||||
expect(result).toBe("ask")
|
||||
})
|
||||
|
||||
test("evaluate - multiple matching patterns, last wins", () => {
|
||||
const result = PermissionNext.evaluate("edit", "src/secret.ts", {
|
||||
edit: [
|
||||
{ pattern: "*", action: "ask" },
|
||||
{ pattern: "src/*", action: "allow" },
|
||||
{ pattern: "src/secret.ts", action: "deny" },
|
||||
],
|
||||
})
|
||||
const result = PermissionNext.evaluate("edit", "src/secret.ts", [
|
||||
{ permission: "edit", pattern: "*", action: "ask" },
|
||||
{ permission: "edit", pattern: "src/*", action: "allow" },
|
||||
{ permission: "edit", pattern: "src/secret.ts", action: "deny" },
|
||||
])
|
||||
expect(result).toBe("deny")
|
||||
})
|
||||
|
||||
test("evaluate - non-matching patterns are skipped", () => {
|
||||
const result = PermissionNext.evaluate("edit", "src/foo.ts", {
|
||||
edit: [
|
||||
{ pattern: "*", action: "ask" },
|
||||
{ pattern: "test/*", action: "deny" },
|
||||
{ pattern: "src/*", action: "allow" },
|
||||
],
|
||||
})
|
||||
const result = PermissionNext.evaluate("edit", "src/foo.ts", [
|
||||
{ permission: "edit", pattern: "*", action: "ask" },
|
||||
{ permission: "edit", pattern: "test/*", action: "deny" },
|
||||
{ permission: "edit", pattern: "src/*", action: "allow" },
|
||||
])
|
||||
expect(result).toBe("allow")
|
||||
})
|
||||
|
||||
test("evaluate - exact match at end wins over earlier wildcard", () => {
|
||||
const result = PermissionNext.evaluate("bash", "/bin/rm", {
|
||||
bash: [
|
||||
{ pattern: "*", action: "allow" },
|
||||
{ pattern: "/bin/rm", action: "deny" },
|
||||
],
|
||||
})
|
||||
const result = PermissionNext.evaluate("bash", "/bin/rm", [
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
{ permission: "bash", pattern: "/bin/rm", action: "deny" },
|
||||
])
|
||||
expect(result).toBe("deny")
|
||||
})
|
||||
|
||||
test("evaluate - wildcard at end overrides earlier exact match", () => {
|
||||
const result = PermissionNext.evaluate("bash", "/bin/rm", {
|
||||
bash: [
|
||||
{ pattern: "/bin/rm", action: "deny" },
|
||||
{ pattern: "*", action: "allow" },
|
||||
],
|
||||
})
|
||||
const result = PermissionNext.evaluate("bash", "/bin/rm", [
|
||||
{ permission: "bash", pattern: "/bin/rm", action: "deny" },
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
])
|
||||
expect(result).toBe("allow")
|
||||
})
|
||||
|
||||
// wildcard permission tests
|
||||
|
||||
test("evaluate - wildcard permission matches any permission", () => {
|
||||
const result = PermissionNext.evaluate("bash", "rm", {
|
||||
"*": [{ pattern: "*", action: "deny" }],
|
||||
})
|
||||
const result = PermissionNext.evaluate("bash", "rm", [{ permission: "*", pattern: "*", action: "deny" }])
|
||||
expect(result).toBe("deny")
|
||||
})
|
||||
|
||||
test("evaluate - wildcard permission with specific pattern", () => {
|
||||
const result = PermissionNext.evaluate("bash", "rm", {
|
||||
"*": [{ pattern: "rm", action: "deny" }],
|
||||
})
|
||||
const result = PermissionNext.evaluate("bash", "rm", [{ permission: "*", pattern: "rm", action: "deny" }])
|
||||
expect(result).toBe("deny")
|
||||
})
|
||||
|
||||
test("evaluate - glob permission pattern", () => {
|
||||
const result = PermissionNext.evaluate("mcp_server_tool", "anything", {
|
||||
"mcp_*": [{ pattern: "*", action: "allow" }],
|
||||
})
|
||||
const result = PermissionNext.evaluate("mcp_server_tool", "anything", [
|
||||
{ permission: "mcp_*", pattern: "*", action: "allow" },
|
||||
])
|
||||
expect(result).toBe("allow")
|
||||
})
|
||||
|
||||
test("evaluate - specific permission and wildcard permission combined", () => {
|
||||
const result = PermissionNext.evaluate("bash", "rm", {
|
||||
"*": [{ pattern: "*", action: "deny" }],
|
||||
bash: [{ pattern: "*", action: "allow" }],
|
||||
})
|
||||
const result = PermissionNext.evaluate("bash", "rm", [
|
||||
{ permission: "*", pattern: "*", action: "deny" },
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
])
|
||||
expect(result).toBe("allow")
|
||||
})
|
||||
|
||||
test("evaluate - wildcard permission does not match when specific exists", () => {
|
||||
const result = PermissionNext.evaluate("edit", "src/foo.ts", {
|
||||
"*": [{ pattern: "*", action: "deny" }],
|
||||
edit: [{ pattern: "src/*", action: "allow" }],
|
||||
})
|
||||
const result = PermissionNext.evaluate("edit", "src/foo.ts", [
|
||||
{ permission: "*", pattern: "*", action: "deny" },
|
||||
{ permission: "edit", pattern: "src/*", action: "allow" },
|
||||
])
|
||||
expect(result).toBe("allow")
|
||||
})
|
||||
|
||||
test("evaluate - multiple matching permission patterns combine rules", () => {
|
||||
const result = PermissionNext.evaluate("mcp_dangerous", "anything", {
|
||||
"*": [{ pattern: "*", action: "ask" }],
|
||||
"mcp_*": [{ pattern: "*", action: "allow" }],
|
||||
mcp_dangerous: [{ pattern: "*", action: "deny" }],
|
||||
})
|
||||
const result = PermissionNext.evaluate("mcp_dangerous", "anything", [
|
||||
{ permission: "*", pattern: "*", action: "ask" },
|
||||
{ permission: "mcp_*", pattern: "*", action: "allow" },
|
||||
{ permission: "mcp_dangerous", pattern: "*", action: "deny" },
|
||||
])
|
||||
expect(result).toBe("deny")
|
||||
})
|
||||
|
||||
test("evaluate - wildcard permission fallback for unknown tool", () => {
|
||||
const result = PermissionNext.evaluate("unknown_tool", "anything", {
|
||||
"*": [{ pattern: "*", action: "ask" }],
|
||||
bash: [{ pattern: "*", action: "allow" }],
|
||||
})
|
||||
const result = PermissionNext.evaluate("unknown_tool", "anything", [
|
||||
{ permission: "*", pattern: "*", action: "ask" },
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
])
|
||||
expect(result).toBe("ask")
|
||||
})
|
||||
|
||||
test("evaluate - permission patterns sorted by length regardless of object order", () => {
|
||||
// specific permission listed before wildcard, but specific should still win
|
||||
const result = PermissionNext.evaluate("bash", "rm", {
|
||||
bash: [{ pattern: "*", action: "allow" }],
|
||||
"*": [{ pattern: "*", action: "deny" }],
|
||||
})
|
||||
expect(result).toBe("allow")
|
||||
const result = PermissionNext.evaluate("bash", "rm", [
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
{ permission: "*", pattern: "*", action: "deny" },
|
||||
])
|
||||
// With flat list, last matching rule wins - so "*" matches bash and wins
|
||||
expect(result).toBe("deny")
|
||||
})
|
||||
|
||||
// disabledTools tests
|
||||
// disabled tests
|
||||
|
||||
test("disabledTools - returns empty set when all tools allowed", () => {
|
||||
const result = PermissionNext.disabledTools(["bash", "edit", "read"], {
|
||||
"*": [{ pattern: "*", action: "allow" }],
|
||||
})
|
||||
test("disabled - returns empty set when all tools allowed", () => {
|
||||
const result = PermissionNext.disabled(["bash", "edit", "read"], [{ permission: "*", pattern: "*", action: "allow" }])
|
||||
expect(result.size).toBe(0)
|
||||
})
|
||||
|
||||
test("disabledTools - disables tool when denied", () => {
|
||||
const result = PermissionNext.disabledTools(["bash", "edit", "read"], {
|
||||
bash: [{ pattern: "*", action: "deny" }],
|
||||
"*": [{ pattern: "*", action: "allow" }],
|
||||
})
|
||||
test("disabled - disables tool when denied", () => {
|
||||
const result = PermissionNext.disabled(
|
||||
["bash", "edit", "read"],
|
||||
[
|
||||
{ permission: "*", pattern: "*", action: "allow" },
|
||||
{ permission: "bash", pattern: "*", action: "deny" },
|
||||
],
|
||||
)
|
||||
expect(result.has("bash")).toBe(true)
|
||||
expect(result.has("edit")).toBe(false)
|
||||
expect(result.has("read")).toBe(false)
|
||||
})
|
||||
|
||||
test("disabledTools - disables edit/write/patch/multiedit when edit denied", () => {
|
||||
const result = PermissionNext.disabledTools(["edit", "write", "patch", "multiedit", "bash"], {
|
||||
edit: [{ pattern: "*", action: "deny" }],
|
||||
"*": [{ pattern: "*", action: "allow" }],
|
||||
})
|
||||
test("disabled - disables edit/write/patch/multiedit when edit denied", () => {
|
||||
const result = PermissionNext.disabled(
|
||||
["edit", "write", "patch", "multiedit", "bash"],
|
||||
[
|
||||
{ permission: "*", pattern: "*", action: "allow" },
|
||||
{ permission: "edit", pattern: "*", action: "deny" },
|
||||
],
|
||||
)
|
||||
expect(result.has("edit")).toBe(true)
|
||||
expect(result.has("write")).toBe(true)
|
||||
expect(result.has("patch")).toBe(true)
|
||||
|
|
@ -335,70 +332,75 @@ test("disabledTools - disables edit/write/patch/multiedit when edit denied", ()
|
|||
expect(result.has("bash")).toBe(false)
|
||||
})
|
||||
|
||||
test("disabledTools - does not disable when partially denied", () => {
|
||||
const result = PermissionNext.disabledTools(["bash"], {
|
||||
bash: [
|
||||
{ pattern: "*", action: "allow" },
|
||||
{ pattern: "rm *", action: "deny" },
|
||||
test("disabled - does not disable when partially denied", () => {
|
||||
const result = PermissionNext.disabled(
|
||||
["bash"],
|
||||
[
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
{ permission: "bash", pattern: "rm *", action: "deny" },
|
||||
],
|
||||
})
|
||||
)
|
||||
expect(result.has("bash")).toBe(false)
|
||||
})
|
||||
|
||||
test("disabledTools - does not disable when action is ask", () => {
|
||||
const result = PermissionNext.disabledTools(["bash", "edit"], {
|
||||
"*": [{ pattern: "*", action: "ask" }],
|
||||
})
|
||||
test("disabled - does not disable when action is ask", () => {
|
||||
const result = PermissionNext.disabled(["bash", "edit"], [{ permission: "*", pattern: "*", action: "ask" }])
|
||||
expect(result.size).toBe(0)
|
||||
})
|
||||
|
||||
test("disabledTools - disables when wildcard deny even with specific allow", () => {
|
||||
test("disabled - disables when wildcard deny even with specific allow", () => {
|
||||
// Tool is disabled because evaluate("bash", "*", ...) returns "deny"
|
||||
// The "echo *" allow rule doesn't match the "*" pattern we're checking
|
||||
const result = PermissionNext.disabledTools(["bash"], {
|
||||
bash: [
|
||||
{ pattern: "*", action: "deny" },
|
||||
{ pattern: "echo *", action: "allow" },
|
||||
const result = PermissionNext.disabled(
|
||||
["bash"],
|
||||
[
|
||||
{ permission: "bash", pattern: "*", action: "deny" },
|
||||
{ permission: "bash", pattern: "echo *", action: "allow" },
|
||||
],
|
||||
})
|
||||
)
|
||||
expect(result.has("bash")).toBe(true)
|
||||
})
|
||||
|
||||
test("disabledTools - does not disable when wildcard allow after deny", () => {
|
||||
const result = PermissionNext.disabledTools(["bash"], {
|
||||
bash: [
|
||||
{ pattern: "rm *", action: "deny" },
|
||||
{ pattern: "*", action: "allow" },
|
||||
test("disabled - does not disable when wildcard allow after deny", () => {
|
||||
const result = PermissionNext.disabled(
|
||||
["bash"],
|
||||
[
|
||||
{ permission: "bash", pattern: "rm *", action: "deny" },
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
],
|
||||
})
|
||||
)
|
||||
expect(result.has("bash")).toBe(false)
|
||||
})
|
||||
|
||||
test("disabledTools - disables multiple tools", () => {
|
||||
const result = PermissionNext.disabledTools(["bash", "edit", "webfetch"], {
|
||||
bash: [{ pattern: "*", action: "deny" }],
|
||||
edit: [{ pattern: "*", action: "deny" }],
|
||||
webfetch: [{ pattern: "*", action: "deny" }],
|
||||
})
|
||||
test("disabled - disables multiple tools", () => {
|
||||
const result = PermissionNext.disabled(
|
||||
["bash", "edit", "webfetch"],
|
||||
[
|
||||
{ permission: "bash", pattern: "*", action: "deny" },
|
||||
{ permission: "edit", pattern: "*", action: "deny" },
|
||||
{ permission: "webfetch", pattern: "*", action: "deny" },
|
||||
],
|
||||
)
|
||||
expect(result.has("bash")).toBe(true)
|
||||
expect(result.has("edit")).toBe(true)
|
||||
expect(result.has("webfetch")).toBe(true)
|
||||
})
|
||||
|
||||
test("disabledTools - wildcard permission denies all tools", () => {
|
||||
const result = PermissionNext.disabledTools(["bash", "edit", "read"], {
|
||||
"*": [{ pattern: "*", action: "deny" }],
|
||||
})
|
||||
test("disabled - wildcard permission denies all tools", () => {
|
||||
const result = PermissionNext.disabled(["bash", "edit", "read"], [{ permission: "*", pattern: "*", action: "deny" }])
|
||||
expect(result.has("bash")).toBe(true)
|
||||
expect(result.has("edit")).toBe(true)
|
||||
expect(result.has("read")).toBe(true)
|
||||
})
|
||||
|
||||
test("disabledTools - specific allow overrides wildcard deny", () => {
|
||||
const result = PermissionNext.disabledTools(["bash", "edit", "read"], {
|
||||
"*": [{ pattern: "*", action: "deny" }],
|
||||
bash: [{ pattern: "*", action: "allow" }],
|
||||
})
|
||||
test("disabled - specific allow overrides wildcard deny", () => {
|
||||
const result = PermissionNext.disabled(
|
||||
["bash", "edit", "read"],
|
||||
[
|
||||
{ permission: "*", pattern: "*", action: "deny" },
|
||||
{ permission: "bash", pattern: "*", action: "allow" },
|
||||
],
|
||||
)
|
||||
expect(result.has("bash")).toBe(false)
|
||||
expect(result.has("edit")).toBe(true)
|
||||
expect(result.has("read")).toBe(true)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue