fix(core): ensure user permission overrides apply correctly (#9975)

This commit is contained in:
Josh Holmer
2026-05-07 09:29:26 +02:00
committed by GitHub
parent c4fa72adc7
commit c1ea8100e1
4 changed files with 117 additions and 12 deletions
@@ -0,0 +1,75 @@
import { afterEach, expect, test } from "bun:test"
import { Effect } from "effect"
import { Agent } from "../../src/agent/agent"
import { Permission } from "../../src/permission"
import { Instance } from "../../src/project/instance"
import { provideInstance, tmpdir } from "../fixture/fixture"
function load<A>(dir: string, fn: (svc: Agent.Interface) => Effect.Effect<A>) {
return Effect.runPromise(provideInstance(dir)(Agent.Service.use(fn)).pipe(Effect.provide(Agent.defaultLayer)))
}
afterEach(async () => {
await Instance.disposeAll()
})
test("ask agent honors user MCP allow over generated ask rule", async () => {
await using tmp = await tmpdir({
config: {
mcp: {
context7: { type: "local", command: ["context7"] },
},
permission: {
"context7_query-docs": { "*": "allow" },
},
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const ask = await load(tmp.path, (svc) => svc.get("ask"))
expect(ask).toBeDefined()
expect(Permission.evaluate("context7_query-docs", "*", ask!.permission).action).toBe("allow")
},
})
})
test("plan agent honors user bash allow over read-only deny default", async () => {
await using tmp = await tmpdir({
config: {
permission: {
bash: { "cargo search *": "allow" },
},
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const plan = await load(tmp.path, (svc) => svc.get("plan"))
expect(plan).toBeDefined()
expect(Permission.evaluate("bash", "cargo search serde", plan!.permission).action).toBe("allow")
},
})
})
test("plan agent still hard-denies non-plan edits after user edit allow", async () => {
await using tmp = await tmpdir({
config: {
permission: {
edit: { "src/output.log": "allow" },
},
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const plan = await load(tmp.path, (svc) => svc.get("plan"))
expect(plan).toBeDefined()
expect(Permission.evaluate("edit", "src/output.log", plan!.permission).action).toBe("deny")
expect(Permission.evaluate("edit", ".kilo/plans/fix.md", plan!.permission).action).toBe("allow")
},
})
})
@@ -31,9 +31,8 @@ function askRulesetWithMcp(servers: string[], user: Permission.Ruleset = []) {
const sanitized = key.replace(/[^a-zA-Z0-9_-]/g, "_")
mcpRules[sanitized + "_*"] = "ask"
}
// Mirrors agent.ts merge order: user, ask-specific (with mcpRules), user denies last
// Mirrors Ask agent merge order: defaults, ask-specific guard, user config, user denies last.
return Permission.merge(
user,
Permission.fromConfig({
"*": "deny",
bash: readOnlyBash,
@@ -53,6 +52,7 @@ function askRulesetWithMcp(servers: string[], user: Permission.Ruleset = []) {
codebase_search: "allow",
...mcpRules,
}),
user,
user.filter((r) => r.action === "deny"),
)
}
@@ -257,6 +257,13 @@ describe("Ask agent MCP permissions", () => {
expect(result.action).toBe("ask")
})
test("user config allow overrides MCP ask rules", () => {
const allow = Permission.fromConfig({ "my-server_read_file": "allow" })
const ruleset = askRulesetWithMcp(["my-server"], allow)
const result = Permission.evaluate("my-server_read_file", "*", ruleset)
expect(result.action).toBe("allow")
})
test("MCP tools disabled without server config", () => {
const ruleset = askRuleset()
const result = Permission.disabled(["my-server_sometool"], ruleset)