mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-22 18:24:21 +01:00
tweak: make bash permissions key off of command pattern (#2592)
This commit is contained in:
@@ -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
|
const approveKeys = toKeys(match.info.pattern, match.info.type)
|
||||||
for (const item of Object.values(pending[input.sessionID])) {
|
for (const k of approveKeys) {
|
||||||
if (item.info.type === match.info.type) {
|
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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user