Add agent-level permissions with whitelist/blacklist support (#1862)

This commit is contained in:
Dax
2025-08-12 11:39:39 -04:00
committed by GitHub
parent ccaebdcd16
commit 10735f93ca
18 changed files with 344 additions and 54 deletions

View File

@@ -5,12 +5,12 @@ import { Tool } from "./tool"
import DESCRIPTION from "./bash.txt"
import { App } from "../app/app"
import { Permission } from "../permission"
import { Config } from "../config/config"
import { Filesystem } from "../util/filesystem"
import { lazy } from "../util/lazy"
import { Log } from "../util/log"
import { Wildcard } from "../util/wildcard"
import { $ } from "bun"
import { Agent } from "../agent/agent"
const MAX_OUTPUT_LENGTH = 30000
const DEFAULT_TIMEOUT = 1 * 60 * 1000
@@ -40,20 +40,8 @@ export const BashTool = Tool.define("bash", {
async execute(params, ctx) {
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
const app = App.info()
const cfg = await Config.get()
const tree = await parser().then((p) => p.parse(params.command))
const permissions = (() => {
const value = cfg.permission?.bash
if (!value)
return {
"*": "allow",
}
if (typeof value === "string")
return {
"*": value,
}
return value
})()
const permissions = await Agent.get(ctx.agent).then((x) => x.permission.bash)
let needsAsk = false
for (const node of tree.rootNode.descendantsOfType("command")) {
@@ -93,17 +81,10 @@ export const BashTool = Tool.define("bash", {
// always allow cd if it passes above check
if (!needsAsk && command[0] !== "cd") {
const action = (() => {
for (const [pattern, value] of Object.entries(permissions)) {
const match = Wildcard.match(node.text, pattern)
log.info("checking", { text: node.text.trim(), pattern, match })
if (match) return value
}
return "ask"
})()
const action = Wildcard.all(node.text, permissions)
if (action === "deny") {
throw new Error(
"The user has specifically restricted access to this command, you are not allowed to execute it.",
`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

View File

@@ -14,8 +14,8 @@ import { App } from "../app/app"
import { File } from "../file"
import { Bus } from "../bus"
import { FileTime } from "../file/time"
import { Config } from "../config/config"
import { Filesystem } from "../util/filesystem"
import { Agent } from "../agent/agent"
export const EditTool = Tool.define("edit", {
description: DESCRIPTION,
@@ -40,7 +40,7 @@ export const EditTool = Tool.define("edit", {
throw new Error(`File ${filePath} is not in the current working directory`)
}
const cfg = await Config.get()
const agent = await Agent.get(ctx.agent)
let diff = ""
let contentOld = ""
let contentNew = ""
@@ -48,7 +48,7 @@ export const EditTool = Tool.define("edit", {
if (params.oldString === "") {
contentNew = params.newString
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
if (cfg.permission?.edit === "ask") {
if (agent.permission.edit === "ask") {
await Permission.ask({
type: "edit",
sessionID: ctx.sessionID,
@@ -77,7 +77,7 @@ export const EditTool = Tool.define("edit", {
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
if (cfg.permission?.edit === "ask") {
if (agent.permission.edit === "ask") {
await Permission.ask({
type: "edit",
sessionID: ctx.sessionID,

View File

@@ -11,7 +11,7 @@ import { TodoWriteTool, TodoReadTool } from "./todo"
import { WebFetchTool } from "./webfetch"
import { WriteTool } from "./write"
import { InvalidTool } from "./invalid"
import { Config } from "../config/config"
import type { Agent } from "../agent/agent"
export namespace ToolRegistry {
const ALL = [
@@ -66,20 +66,23 @@ export namespace ToolRegistry {
return result
}
export async function enabled(_providerID: string, _modelID: string): Promise<Record<string, boolean>> {
const cfg = await Config.get()
export async function enabled(
_providerID: string,
_modelID: string,
agent: Agent.Info,
): Promise<Record<string, boolean>> {
const result: Record<string, boolean> = {}
result["patch"] = false
if (cfg.permission?.edit === "deny") {
if (agent.permission.edit === "deny") {
result["edit"] = false
result["patch"] = false
result["write"] = false
}
if (cfg?.permission?.bash === "deny") {
if (agent.permission.bash["*"] === "deny" && Object.keys(agent.permission.bash).length === 1) {
result["bash"] = false
}
if (cfg?.permission?.webfetch === "deny") {
if (agent.permission.webfetch === "deny") {
result["webfetch"] = false
}

View File

@@ -7,6 +7,7 @@ export namespace Tool {
export type Context<M extends Metadata = Metadata> = {
sessionID: string
messageID: string
agent: string
callID?: string
abort: AbortSignal
metadata(input: { title?: string; metadata?: M }): void

View File

@@ -8,8 +8,8 @@ import { App } from "../app/app"
import { Bus } from "../bus"
import { File } from "../file"
import { FileTime } from "../file/time"
import { Config } from "../config/config"
import { Filesystem } from "../util/filesystem"
import { Agent } from "../agent/agent"
export const WriteTool = Tool.define("write", {
description: DESCRIPTION,
@@ -28,8 +28,8 @@ export const WriteTool = Tool.define("write", {
const exists = await file.exists()
if (exists) await FileTime.assert(ctx.sessionID, filepath)
const cfg = await Config.get()
if (cfg.permission?.edit === "ask")
const agent = await Agent.get(ctx.agent)
if (agent.permission.edit === "ask")
await Permission.ask({
type: "write",
sessionID: ctx.sessionID,