fix: allow user to configure doom loop & external dir perms (#4095)

This commit is contained in:
Aiden Cline
2025-11-09 18:21:38 -08:00
committed by GitHub
parent 7be8e16c33
commit 4e549b1c05
8 changed files with 95 additions and 62 deletions

View File

@@ -20,6 +20,8 @@ export namespace Agent {
edit: Config.Permission, edit: Config.Permission,
bash: z.record(z.string(), Config.Permission), bash: z.record(z.string(), Config.Permission),
webfetch: Config.Permission.optional(), webfetch: Config.Permission.optional(),
doom_loop: Config.Permission.optional(),
external_directory: Config.Permission.optional(),
}), }),
model: z model: z
.object({ .object({
@@ -45,6 +47,8 @@ export namespace Agent {
"*": "allow", "*": "allow",
}, },
webfetch: "allow", webfetch: "allow",
doom_loop: "ask",
external_directory: "ask",
} }
const agentPermission = mergeAgentPermissions(defaultPermission, cfg.permission ?? {}) const agentPermission = mergeAgentPermissions(defaultPermission, cfg.permission ?? {})
@@ -244,6 +248,8 @@ function mergeAgentPermissions(basePermission: any, overridePermission: any): Ag
edit: merged.edit ?? "allow", edit: merged.edit ?? "allow",
webfetch: merged.webfetch ?? "allow", webfetch: merged.webfetch ?? "allow",
bash: mergedBash ?? { "*": "allow" }, bash: mergedBash ?? { "*": "allow" },
doom_loop: merged.doom_loop,
external_directory: merged.external_directory,
} }
return result return result

View File

@@ -360,6 +360,8 @@ export namespace Config {
edit: Permission.optional(), edit: Permission.optional(),
bash: z.union([Permission, z.record(z.string(), Permission)]).optional(), bash: z.union([Permission, z.record(z.string(), Permission)]).optional(),
webfetch: Permission.optional(), webfetch: Permission.optional(),
doom_loop: Permission.optional(),
external_directory: Permission.optional(),
}) })
.optional(), .optional(),
}) })
@@ -574,6 +576,8 @@ export namespace Config {
edit: Permission.optional(), edit: Permission.optional(),
bash: z.union([Permission, z.record(z.string(), Permission)]).optional(), bash: z.union([Permission, z.record(z.string(), Permission)]).optional(),
webfetch: Permission.optional(), webfetch: Permission.optional(),
doom_loop: Permission.optional(),
external_directory: Permission.optional(),
}) })
.optional(), .optional(),
tools: z.record(z.string(), z.boolean()).optional(), tools: z.record(z.string(), z.boolean()).optional(),

View File

@@ -1115,18 +1115,21 @@ export namespace SessionPrompt {
JSON.stringify(p.state.input) === JSON.stringify(value.input), JSON.stringify(p.state.input) === JSON.stringify(value.input),
) )
) { ) {
await Permission.ask({ const permission = await Agent.get(input.agent).then((x) => x.permission)
type: "doom-loop", if (permission.doom_loop === "ask") {
pattern: value.toolName, await Permission.ask({
sessionID: assistantMsg.sessionID, type: "doom_loop",
messageID: assistantMsg.id, pattern: value.toolName,
callID: value.toolCallId, sessionID: assistantMsg.sessionID,
title: `Possible doom loop: "${value.toolName}" called ${DOOM_LOOP_THRESHOLD} times with identical arguments`, messageID: assistantMsg.id,
metadata: { callID: value.toolCallId,
tool: value.toolName, title: `Possible doom loop: "${value.toolName}" called ${DOOM_LOOP_THRESHOLD} times with identical arguments`,
input: value.input, metadata: {
}, tool: value.toolName,
}) input: value.input,
},
})
}
} }
} }
break break

View File

@@ -35,24 +35,27 @@ export const EditTool = Tool.define("edit", {
throw new Error("oldString and newString must be different") throw new Error("oldString and newString must be different")
} }
const agent = await Agent.get(ctx.agent)
const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath) const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
if (!Filesystem.contains(Instance.directory, filePath)) { if (!Filesystem.contains(Instance.directory, filePath)) {
const parentDir = path.dirname(filePath) const parentDir = path.dirname(filePath)
await Permission.ask({ if (agent.permission.external_directory === "ask") {
type: "external-directory", await Permission.ask({
pattern: parentDir, type: "external_directory",
sessionID: ctx.sessionID, pattern: parentDir,
messageID: ctx.messageID, sessionID: ctx.sessionID,
callID: ctx.callID, messageID: ctx.messageID,
title: `Edit file outside working directory: ${filePath}`, callID: ctx.callID,
metadata: { title: `Edit file outside working directory: ${filePath}`,
filepath: filePath, metadata: {
parentDir, filepath: filePath,
}, parentDir,
}) },
})
}
} }
const agent = await Agent.get(ctx.agent)
let diff = "" let diff = ""
let contentOld = "" let contentOld = ""
let contentNew = "" let contentNew = ""

View File

@@ -55,18 +55,20 @@ export const PatchTool = Tool.define("patch", {
if (!Filesystem.contains(Instance.directory, filePath)) { if (!Filesystem.contains(Instance.directory, filePath)) {
const parentDir = path.dirname(filePath) const parentDir = path.dirname(filePath)
await Permission.ask({ if (agent.permission.external_directory === "ask") {
type: "external-directory", await Permission.ask({
pattern: parentDir, type: "external_directory",
sessionID: ctx.sessionID, pattern: parentDir,
messageID: ctx.messageID, sessionID: ctx.sessionID,
callID: ctx.callID, messageID: ctx.messageID,
title: `Patch file outside working directory: ${filePath}`, callID: ctx.callID,
metadata: { title: `Patch file outside working directory: ${filePath}`,
filepath: filePath, metadata: {
parentDir, filepath: filePath,
}, parentDir,
}) },
})
}
} }
switch (hunk.type) { switch (hunk.type) {

View File

@@ -10,6 +10,7 @@ import { Instance } from "../project/instance"
import { Provider } from "../provider/provider" import { Provider } from "../provider/provider"
import { Identifier } from "../id/id" import { Identifier } from "../id/id"
import { Permission } from "../permission" import { Permission } from "../permission"
import { Agent } from "@/agent/agent"
const DEFAULT_READ_LIMIT = 2000 const DEFAULT_READ_LIMIT = 2000
const MAX_LINE_LENGTH = 2000 const MAX_LINE_LENGTH = 2000
@@ -27,21 +28,24 @@ export const ReadTool = Tool.define("read", {
filepath = path.join(process.cwd(), filepath) filepath = path.join(process.cwd(), filepath)
} }
const title = path.relative(Instance.worktree, filepath) const title = path.relative(Instance.worktree, filepath)
const agent = await Agent.get(ctx.agent)
if (!ctx.extra?.["bypassCwdCheck"] && !Filesystem.contains(Instance.directory, filepath)) { if (!ctx.extra?.["bypassCwdCheck"] && !Filesystem.contains(Instance.directory, filepath)) {
const parentDir = path.dirname(filepath) const parentDir = path.dirname(filepath)
await Permission.ask({ if (agent.permission.external_directory === "ask") {
type: "external-directory", await Permission.ask({
pattern: parentDir, type: "external_directory",
sessionID: ctx.sessionID, pattern: parentDir,
messageID: ctx.messageID, sessionID: ctx.sessionID,
callID: ctx.callID, messageID: ctx.messageID,
title: `Access file outside working directory: ${filepath}`, callID: ctx.callID,
metadata: { title: `Access file outside working directory: ${filepath}`,
filepath, metadata: {
parentDir, filepath,
}, parentDir,
}) },
})
}
} }
const file = Bun.file(filepath) const file = Bun.file(filepath)

View File

@@ -18,28 +18,31 @@ export const WriteTool = Tool.define("write", {
filePath: z.string().describe("The absolute path to the file to write (must be absolute, not relative)"), filePath: z.string().describe("The absolute path to the file to write (must be absolute, not relative)"),
}), }),
async execute(params, ctx) { async execute(params, ctx) {
const agent = await Agent.get(ctx.agent)
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath) const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
if (!Filesystem.contains(Instance.directory, filepath)) { if (!Filesystem.contains(Instance.directory, filepath)) {
const parentDir = path.dirname(filepath) const parentDir = path.dirname(filepath)
await Permission.ask({ if (agent.permission.external_directory === "ask") {
type: "external-directory", await Permission.ask({
pattern: parentDir, type: "external_directory",
sessionID: ctx.sessionID, pattern: parentDir,
messageID: ctx.messageID, sessionID: ctx.sessionID,
callID: ctx.callID, messageID: ctx.messageID,
title: `Write file outside working directory: ${filepath}`, callID: ctx.callID,
metadata: { title: `Write file outside working directory: ${filepath}`,
filepath, metadata: {
parentDir, filepath,
}, parentDir,
}) },
})
}
} }
const file = Bun.file(filepath) const file = Bun.file(filepath)
const exists = await file.exists() const exists = await file.exists()
if (exists) await FileTime.assert(ctx.sessionID, filepath) if (exists) await FileTime.assert(ctx.sessionID, filepath)
const agent = await Agent.get(ctx.agent)
if (agent.permission.edit === "ask") if (agent.permission.edit === "ask")
await Permission.ask({ await Permission.ask({
type: "write", type: "write",

View File

@@ -198,6 +198,8 @@ export type AgentConfig = {
[key: string]: "ask" | "allow" | "deny" [key: string]: "ask" | "allow" | "deny"
} }
webfetch?: "ask" | "allow" | "deny" webfetch?: "ask" | "allow" | "deny"
doom_loop?: "ask" | "allow" | "deny"
external_directory?: "ask" | "allow" | "deny"
} }
[key: string]: [key: string]:
| unknown | unknown
@@ -216,6 +218,8 @@ export type AgentConfig = {
[key: string]: "ask" | "allow" | "deny" [key: string]: "ask" | "allow" | "deny"
} }
webfetch?: "ask" | "allow" | "deny" webfetch?: "ask" | "allow" | "deny"
doom_loop?: "ask" | "allow" | "deny"
external_directory?: "ask" | "allow" | "deny"
} }
| undefined | undefined
} }
@@ -463,6 +467,8 @@ export type Config = {
[key: string]: "ask" | "allow" | "deny" [key: string]: "ask" | "allow" | "deny"
} }
webfetch?: "ask" | "allow" | "deny" webfetch?: "ask" | "allow" | "deny"
doom_loop?: "ask" | "allow" | "deny"
external_directory?: "ask" | "allow" | "deny"
} }
tools?: { tools?: {
[key: string]: boolean [key: string]: boolean
@@ -1043,6 +1049,8 @@ export type Agent = {
[key: string]: "ask" | "allow" | "deny" [key: string]: "ask" | "allow" | "deny"
} }
webfetch?: "ask" | "allow" | "deny" webfetch?: "ask" | "allow" | "deny"
doom_loop?: "ask" | "allow" | "deny"
external_directory?: "ask" | "allow" | "deny"
} }
model?: { model?: {
modelID: string modelID: string