Explorar o código

tweak: make bash permissions key off of command pattern (#2592)

Aiden Cline hai 5 meses
pai
achega
c81624aef7

+ 23 - 5
packages/opencode/src/permission/index.ts

@@ -4,15 +4,25 @@ import { Log } from "../util/log"
 import { Identifier } from "../id/id"
 import { Identifier } from "../id/id"
 import { Plugin } from "../plugin"
 import { Plugin } from "../plugin"
 import { Instance } from "../project/instance"
 import { Instance } from "../project/instance"
+import { Wildcard } from "../util/wildcard"
 
 
 export namespace Permission {
 export namespace Permission {
   const log = Log.create({ service: "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
   export const Info = z
     .object({
     .object({
       id: z.string(),
       id: z.string(),
       type: z.string(),
       type: z.string(),
-      pattern: z.string().optional(),
+      pattern: z.union([z.string(), z.array(z.string())]).optional(),
       sessionID: z.string(),
       sessionID: z.string(),
       messageID: z.string(),
       messageID: z.string(),
       callID: z.string().optional(),
       callID: z.string().optional(),
@@ -83,7 +93,9 @@ export namespace Permission {
       toolCallID: input.callID,
       toolCallID: input.callID,
       pattern: input.pattern,
       pattern: input.pattern,
     })
     })
-    if (approved[input.sessionID]?.[input.type]) return
+    const approvedForSession = approved[input.sessionID] || {}
+    const keys = toKeys(input.pattern, input.type)
+    if (covered(keys, approvedForSession)) return
     const info: Info = {
     const info: Info = {
       id: Identifier.ascending("permission"),
       id: Identifier.ascending("permission"),
       type: input.type,
       type: input.type,
@@ -141,9 +153,15 @@ export namespace Permission {
     })
     })
     if (input.response === "always") {
     if (input.response === "always") {
       approved[input.sessionID] = approved[input.sessionID] || {}
       approved[input.sessionID] = approved[input.sessionID] || {}
-      approved[input.sessionID][match.info.type] = true
-      for (const item of Object.values(pending[input.sessionID])) {
-        if (item.info.type === match.info.type) {
+      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 })
           respond({ sessionID: item.info.sessionID, permissionID: item.info.id, response: input.response })
         }
         }
       }
       }

+ 30 - 5
packages/opencode/src/tool/bash.ts

@@ -59,7 +59,7 @@ export const BashTool = Tool.define("bash", {
     const tree = await parser().then((p) => p.parse(params.command))
     const tree = await parser().then((p) => p.parse(params.command))
     const permissions = await Agent.get(ctx.agent).then((x) => x.permission.bash)
     const permissions = await Agent.get(ctx.agent).then((x) => x.permission.bash)
 
 
-    let needsAsk = false
+    const askPatterns = new Set<string>()
     for (const node of tree.rootNode.descendantsOfType("command")) {
     for (const node of tree.rootNode.descendantsOfType("command")) {
       const command = []
       const command = []
       for (let i = 0; i < node.childCount; i++) {
       for (let i = 0; i < node.childCount; i++) {
@@ -96,27 +96,52 @@ export const BashTool = Tool.define("bash", {
       }
       }
 
 
       // always allow cd if it passes above check
       // always allow cd if it passes above check
-      if (!needsAsk && command[0] !== "cd") {
+      if (command[0] !== "cd") {
         const action = Wildcard.all(node.text, permissions)
         const action = Wildcard.all(node.text, permissions)
         if (action === "deny") {
         if (action === "deny") {
           throw new Error(
           throw new Error(
             `The user has specifically restricted access to this command, you are not allowed to execute it. Here is the configuration: ${JSON.stringify(permissions)}`,
             `The user has specifically restricted access to this command, you are not allowed to execute it. Here is the configuration: ${JSON.stringify(permissions)}`,
           )
           )
         }
         }
-        if (action === "ask") needsAsk = true
+        if (action === "ask") {
+          const pattern = (() => {
+            let head = ""
+            let sub: string | undefined
+            for (let i = 0; i < node.childCount; i++) {
+              const child = node.child(i)
+              if (!child) continue
+              if (child.type === "command_name") {
+                if (!head) {
+                  head = child.text
+                }
+                continue
+              }
+              if (!sub && child.type === "word") {
+                if (!child.text.startsWith("-")) sub = child.text
+              }
+            }
+            if (!head) return
+            return sub ? `${head} ${sub} *` : `${head} *`
+          })()
+          if (pattern) {
+            askPatterns.add(pattern)
+          }
+        }
       }
       }
     }
     }
 
 
-    if (needsAsk) {
+    if (askPatterns.size > 0) {
+      const patterns = Array.from(askPatterns)
       await Permission.ask({
       await Permission.ask({
         type: "bash",
         type: "bash",
-        pattern: params.command,
+        pattern: patterns,
         sessionID: ctx.sessionID,
         sessionID: ctx.sessionID,
         messageID: ctx.messageID,
         messageID: ctx.messageID,
         callID: ctx.callID,
         callID: ctx.callID,
         title: params.command,
         title: params.command,
         metadata: {
         metadata: {
           command: params.command,
           command: params.command,
+          patterns,
         },
         },
       })
       })
     }
     }

+ 0 - 1
packages/opencode/src/tool/edit.ts

@@ -82,7 +82,6 @@ export const EditTool = Tool.define("edit", {
           sessionID: ctx.sessionID,
           sessionID: ctx.sessionID,
           messageID: ctx.messageID,
           messageID: ctx.messageID,
           callID: ctx.callID,
           callID: ctx.callID,
-          pattern: filePath,
           title: "Edit this file: " + filePath,
           title: "Edit this file: " + filePath,
           metadata: {
           metadata: {
             filePath,
             filePath,

+ 0 - 1
packages/opencode/src/tool/webfetch.ts

@@ -28,7 +28,6 @@ export const WebFetchTool = Tool.define("webfetch", {
     if (cfg.permission?.webfetch === "ask")
     if (cfg.permission?.webfetch === "ask")
       await Permission.ask({
       await Permission.ask({
         type: "webfetch",
         type: "webfetch",
-        pattern: params.url,
         sessionID: ctx.sessionID,
         sessionID: ctx.sessionID,
         messageID: ctx.messageID,
         messageID: ctx.messageID,
         callID: ctx.callID,
         callID: ctx.callID,