diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts deleted file mode 100644 index f1cd43fdbe5..00000000000 --- a/packages/opencode/src/permission/index.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { BusEvent } from "@/bus/bus-event" -import { Bus } from "@/bus" -import z from "zod" -import { Log } from "../util/log" -import { Identifier } from "../id/id" -import { Plugin } from "../plugin" -import { Instance } from "../project/instance" -import { Wildcard } from "../util/wildcard" - -export namespace Permission { - const log = Log.create({ service: "permission" }) - - function toKeys(pattern: Info["pattern"], type: string): string[] { - return pattern === undefined ? [type] : Array.isArray(pattern) ? pattern : [pattern] - } - - function covered(keys: string[], approved: Record): boolean { - const pats = Object.keys(approved) - return keys.every((k) => pats.some((p) => Wildcard.match(k, p))) - } - - export const Info = z - .object({ - id: z.string(), - type: z.string(), - pattern: z.union([z.string(), z.array(z.string())]).optional(), - sessionID: z.string(), - messageID: z.string(), - callID: z.string().optional(), - message: z.string(), - metadata: z.record(z.string(), z.any()), - time: z.object({ - created: z.number(), - }), - }) - .meta({ - ref: "Permission", - }) - export type Info = z.infer - - export const Event = { - Updated: BusEvent.define("permission.updated", Info), - Replied: BusEvent.define( - "permission.replied", - z.object({ - sessionID: z.string(), - permissionID: z.string(), - response: z.string(), - }), - ), - } - - const state = Instance.state( - () => { - const pending: { - [sessionID: string]: { - [permissionID: string]: { - info: Info - resolve: () => void - reject: (e: any) => void - } - } - } = {} - - const approved: { - [sessionID: string]: { - [permissionID: string]: boolean - } - } = {} - - return { - pending, - approved, - } - }, - async (state) => { - for (const pending of Object.values(state.pending)) { - for (const item of Object.values(pending)) { - item.reject(new RejectedError(item.info.sessionID, item.info.id, item.info.callID, item.info.metadata)) - } - } - }, - ) - - export function pending() { - return state().pending - } - - export function list() { - const { pending } = state() - const result: Info[] = [] - for (const items of Object.values(pending)) { - for (const item of Object.values(items)) { - result.push(item.info) - } - } - return result.sort((a, b) => a.id.localeCompare(b.id)) - } - - export async function ask(input: { - type: Info["type"] - message: Info["message"] - pattern?: Info["pattern"] - callID?: Info["callID"] - sessionID: Info["sessionID"] - messageID: Info["messageID"] - metadata: Info["metadata"] - }) { - const { pending, approved } = state() - log.info("asking", { - sessionID: input.sessionID, - messageID: input.messageID, - toolCallID: input.callID, - pattern: input.pattern, - }) - const approvedForSession = approved[input.sessionID] || {} - const keys = toKeys(input.pattern, input.type) - if (covered(keys, approvedForSession)) return - const info: Info = { - id: Identifier.ascending("permission"), - type: input.type, - pattern: input.pattern, - sessionID: input.sessionID, - messageID: input.messageID, - callID: input.callID, - message: input.message, - metadata: input.metadata, - time: { - created: Date.now(), - }, - } - - switch ( - await Plugin.trigger("permission.ask", info, { - status: "ask", - }).then((x) => x.status) - ) { - case "deny": - throw new RejectedError(info.sessionID, info.id, info.callID, info.metadata) - case "allow": - return - } - - pending[input.sessionID] = pending[input.sessionID] || {} - return new Promise((resolve, reject) => { - pending[input.sessionID][info.id] = { - info, - resolve, - reject, - } - Bus.publish(Event.Updated, info) - }) - } - - export const Response = z.enum(["once", "always", "reject"]) - export type Response = z.infer - - export function respond(input: { sessionID: Info["sessionID"]; permissionID: Info["id"]; response: Response }) { - log.info("response", input) - const { pending, approved } = state() - const match = pending[input.sessionID]?.[input.permissionID] - if (!match) return - delete pending[input.sessionID][input.permissionID] - Bus.publish(Event.Replied, { - sessionID: input.sessionID, - permissionID: input.permissionID, - response: input.response, - }) - if (input.response === "reject") { - match.reject(new RejectedError(input.sessionID, input.permissionID, match.info.callID, match.info.metadata)) - return - } - match.resolve() - if (input.response === "always") { - approved[input.sessionID] = approved[input.sessionID] || {} - const approveKeys = toKeys(match.info.pattern, match.info.type) - for (const k of approveKeys) { - approved[input.sessionID][k] = true - } - const items = pending[input.sessionID] - if (!items) return - for (const item of Object.values(items)) { - const itemKeys = toKeys(item.info.pattern, item.info.type) - if (covered(itemKeys, approved[input.sessionID])) { - respond({ - sessionID: item.info.sessionID, - permissionID: item.info.id, - response: input.response, - }) - } - } - } - } - - export class RejectedError extends Error { - constructor( - public readonly sessionID: string, - public readonly permissionID: string, - public readonly toolCallID?: string, - public readonly metadata?: Record, - public readonly reason?: string, - ) { - super( - reason !== undefined - ? reason - : `The user rejected permission to use this specific tool call. You may try again with different parameters.`, - ) - } - } -} diff --git a/packages/opencode/src/permission/next.ts b/packages/opencode/src/permission/next.ts index f95aaf34525..919989b2b48 100644 --- a/packages/opencode/src/permission/next.ts +++ b/packages/opencode/src/permission/next.ts @@ -127,11 +127,30 @@ export namespace PermissionNext { throw new DeniedError(ruleset.filter((r) => 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 = { + const info: Request = { + id, + ...request, + } + const { Plugin } = await import("@/plugin") + const hook = await Plugin.trigger( + "permission.ask", + { id, - ...request, - } + type: request.permission, + pattern: request.patterns, + sessionID: request.sessionID, + messageID: request.tool?.messageID ?? "", + callID: request.tool?.callID, + title: request.permission, + metadata: request.metadata, + time: { created: Date.now() }, + }, + { status: "ask" as "ask" | "deny" | "allow" }, + ).catch(() => ({ status: "ask" as const })) + if (hook.status === "deny") + throw new DeniedError(ruleset.filter((r) => Wildcard.match(request.permission, r.permission))) + if (hook.status === "allow") continue + return new Promise((resolve, reject) => { s.pending[id] = { info, resolve,