From 963f407062ccde868b6abb5a21178dea861bc4ca Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 1 Jan 2026 21:01:00 -0500 Subject: [PATCH] tui: improve permission error handling and evaluation logic --- .opencode/opencode.jsonc | 5 +++ .../src/cli/cmd/tui/routes/session/index.tsx | 2 +- packages/opencode/src/permission/next.ts | 31 +++++++++++-------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index ad9925767d..6008ab9bc0 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -10,6 +10,11 @@ "options": {}, }, }, + "permission": { + "bash": { + "ls foo": "ask", + }, + }, "mcp": { "context7": { "type": "remote", diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 94ba05614c..4b3b67a317 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1406,7 +1406,7 @@ function InlineTool(props: { icon: string; complete: any; pending: string; child const error = createMemo(() => (props.part.state.status === "error" ? props.part.state.error : undefined)) - const denied = createMemo(() => error()?.includes("rejected permission")) + const denied = createMemo(() => error()?.includes("rejected permission") || error()?.includes("specified a rule")) return ( Wildcard.match(request.permission, r.permission))) + if (rule.action === "ask") { const id = input.id ?? Identifier.ascending("permission") return new Promise((resolve, reject) => { const info: Request = { @@ -139,7 +140,7 @@ export namespace PermissionNext { Bus.publish(Event.Asked, info) }) } - if (action === "allow") continue + if (rule.action === "allow") continue } }, ) @@ -195,7 +196,7 @@ export namespace PermissionNext { for (const [id, pending] of Object.entries(s.pending)) { if (pending.info.sessionID !== sessionID) continue const ok = pending.info.patterns.every( - (pattern) => evaluate(pending.info.permission, pattern, s.approved) === "allow", + (pattern) => evaluate(pending.info.permission, pattern, s.approved).action === "allow", ) if (!ok) continue delete s.pending[id] @@ -215,13 +216,13 @@ export namespace PermissionNext { }, ) - export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Action { + export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Rule { const merged = merge(...rulesets) log.info("evaluate", { permission, pattern, ruleset: merged }) const match = merged.findLast( (rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern), ) - return match?.action ?? "ask" + return match ?? { action: "allow", permission, pattern: "*" } } const EDIT_TOOLS = ["edit", "write", "patch", "multiedit"] @@ -230,7 +231,7 @@ export namespace PermissionNext { const result = new Set() for (const tool of tools) { const permission = EDIT_TOOLS.includes(tool) ? "edit" : tool - if (evaluate(permission, "*", ruleset) === "deny") { + if (evaluate(permission, "*", ruleset).action === "deny") { result.add(tool) } } @@ -238,11 +239,15 @@ export namespace PermissionNext { } export class RejectedError extends Error { - constructor(public readonly reason?: string) { + constructor() { + super(`The user rejected permission to use this specific tool call. You may try again with different parameters.`) + } + } + + export class AutoRejectedError extends Error { + constructor(public readonly ruleset: Ruleset) { super( - reason !== undefined - ? reason - : `The user rejected permission to use this specific tool call. You may try again with different parameters.`, + `The user has specified a rule which prevents you from using this specific tool call. Here are some of the relevant rules ${JSON.stringify(ruleset)}`, ) } }