diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index dd67e2bc27ca..d05d872f5f35 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -80,6 +80,14 @@ export const layer = Layer.effect( let needsAsk = false for (const pattern of request.patterns) { + // Check config deny rules first — they cannot be overridden by approved rules + const configRule = evaluate(request.permission, pattern, ruleset) + if (configRule.action === "deny") { + yield* Effect.logInfo("evaluated", { permission: request.permission, pattern, action: configRule }) + return yield* new PermissionV1.DeniedError({ + ruleset: ruleset.filter((rule) => Wildcard.match(request.permission, rule.permission)), + }) + } const rule = evaluate(request.permission, pattern, ruleset, approved) yield* Effect.logInfo("evaluated", { permission: request.permission, pattern, action: rule }) if (rule.action === "deny") { diff --git a/packages/opencode/test/permission/next.test.ts b/packages/opencode/test/permission/next.test.ts index e784350055a1..caf1630496d1 100644 --- a/packages/opencode/test/permission/next.test.ts +++ b/packages/opencode/test/permission/next.test.ts @@ -449,6 +449,19 @@ test("evaluate - merges multiple rulesets", () => { expect(result.action).toBe("deny") }) +test("evaluate - approved wildcard allow overrides config deny (documents bug)", () => { + // This test documents the evaluate() behavior that enables the bug. + // The fix is in Permission.ask() which checks config deny rules first. + const config: PermissionV1.Ruleset = [ + { permission: "edit", pattern: "*", action: "ask" }, + { permission: "edit", pattern: "*/AGENTS.md", action: "deny" }, + ] + const approved: PermissionV1.Ruleset = [{ permission: "edit", pattern: "*", action: "allow" }] + const result = Permission.evaluate("edit", "Users/someone/workspace/AGENTS.md", config, approved) + // evaluate() alone still returns "allow" — the fix is in ask() which checks config deny first + expect(result.action).toBe("allow") +}) + // disabled tests test("disabled - returns empty set when all tools allowed", () => { @@ -593,6 +606,44 @@ it.instance( { git: true }, ) +it.instance( + "ask - config deny is not overridden by approved wildcard allow", + () => + Effect.gen(function* () { + // First, approve an edit with "always" to populate the approved list + const fiber = yield* ask({ + id: PermissionV1.ID.make("per_deny_override_1"), + sessionID: SessionID.make("session_test"), + permission: "edit", + patterns: ["some/file.ts"], + metadata: {}, + always: ["*"], + ruleset: [{ permission: "edit", pattern: "*", action: "ask" }], + }).pipe(Effect.forkScoped) + + yield* waitForPending(1) + yield* reply({ requestID: PermissionV1.ID.make("per_deny_override_1"), reply: "always" }) + yield* Fiber.join(fiber) + + // Now attempt an edit that config explicitly denies — should still be denied + const err = yield* fail( + ask({ + sessionID: SessionID.make("session_test2"), + permission: "edit", + patterns: ["Users/someone/workspace/AGENTS.md"], + metadata: {}, + always: [], + ruleset: [ + { permission: "edit", pattern: "*", action: "ask" }, + { permission: "edit", pattern: "*/AGENTS.md", action: "deny" }, + ], + }), + ) + expect(err).toBeInstanceOf(PermissionV1.DeniedError) + }), + { git: true }, +) + it.instance( "ask - stays pending when action is ask", () =>