|
|
@@ -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<string, boolean>): 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<typeof Info>
|
|
|
-
|
|
|
- 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<void>((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<typeof Response>
|
|
|
-
|
|
|
- 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<string, any>,
|
|
|
- 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.`,
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
-}
|