diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index a6933708..f1050ae7 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -20,6 +20,8 @@ export namespace Agent { edit: Config.Permission, bash: z.record(z.string(), Config.Permission), webfetch: Config.Permission.optional(), + doom_loop: Config.Permission.optional(), + external_directory: Config.Permission.optional(), }), model: z .object({ @@ -45,6 +47,8 @@ export namespace Agent { "*": "allow", }, webfetch: "allow", + doom_loop: "ask", + external_directory: "ask", } const agentPermission = mergeAgentPermissions(defaultPermission, cfg.permission ?? {}) @@ -244,6 +248,8 @@ function mergeAgentPermissions(basePermission: any, overridePermission: any): Ag edit: merged.edit ?? "allow", webfetch: merged.webfetch ?? "allow", bash: mergedBash ?? { "*": "allow" }, + doom_loop: merged.doom_loop, + external_directory: merged.external_directory, } return result diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 7008141a..eaac1dd4 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -360,6 +360,8 @@ export namespace Config { edit: Permission.optional(), bash: z.union([Permission, z.record(z.string(), Permission)]).optional(), webfetch: Permission.optional(), + doom_loop: Permission.optional(), + external_directory: Permission.optional(), }) .optional(), }) @@ -574,6 +576,8 @@ export namespace Config { edit: Permission.optional(), bash: z.union([Permission, z.record(z.string(), Permission)]).optional(), webfetch: Permission.optional(), + doom_loop: Permission.optional(), + external_directory: Permission.optional(), }) .optional(), tools: z.record(z.string(), z.boolean()).optional(), diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 28afe61c..10a9727a 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1115,18 +1115,21 @@ export namespace SessionPrompt { JSON.stringify(p.state.input) === JSON.stringify(value.input), ) ) { - await Permission.ask({ - type: "doom-loop", - pattern: value.toolName, - sessionID: assistantMsg.sessionID, - messageID: assistantMsg.id, - callID: value.toolCallId, - title: `Possible doom loop: "${value.toolName}" called ${DOOM_LOOP_THRESHOLD} times with identical arguments`, - metadata: { - tool: value.toolName, - input: value.input, - }, - }) + const permission = await Agent.get(input.agent).then((x) => x.permission) + if (permission.doom_loop === "ask") { + await Permission.ask({ + type: "doom_loop", + pattern: value.toolName, + sessionID: assistantMsg.sessionID, + messageID: assistantMsg.id, + callID: value.toolCallId, + title: `Possible doom loop: "${value.toolName}" called ${DOOM_LOOP_THRESHOLD} times with identical arguments`, + metadata: { + tool: value.toolName, + input: value.input, + }, + }) + } } } break diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index ba3d2c0b..96c62b86 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -35,24 +35,27 @@ export const EditTool = Tool.define("edit", { 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) if (!Filesystem.contains(Instance.directory, filePath)) { const parentDir = path.dirname(filePath) - await Permission.ask({ - type: "external-directory", - pattern: parentDir, - sessionID: ctx.sessionID, - messageID: ctx.messageID, - callID: ctx.callID, - title: `Edit file outside working directory: ${filePath}`, - metadata: { - filepath: filePath, - parentDir, - }, - }) + if (agent.permission.external_directory === "ask") { + await Permission.ask({ + type: "external_directory", + pattern: parentDir, + sessionID: ctx.sessionID, + messageID: ctx.messageID, + callID: ctx.callID, + title: `Edit file outside working directory: ${filePath}`, + metadata: { + filepath: filePath, + parentDir, + }, + }) + } } - const agent = await Agent.get(ctx.agent) let diff = "" let contentOld = "" let contentNew = "" diff --git a/packages/opencode/src/tool/patch.ts b/packages/opencode/src/tool/patch.ts index f76b9474..0571cd35 100644 --- a/packages/opencode/src/tool/patch.ts +++ b/packages/opencode/src/tool/patch.ts @@ -55,18 +55,20 @@ export const PatchTool = Tool.define("patch", { if (!Filesystem.contains(Instance.directory, filePath)) { const parentDir = path.dirname(filePath) - await Permission.ask({ - type: "external-directory", - pattern: parentDir, - sessionID: ctx.sessionID, - messageID: ctx.messageID, - callID: ctx.callID, - title: `Patch file outside working directory: ${filePath}`, - metadata: { - filepath: filePath, - parentDir, - }, - }) + if (agent.permission.external_directory === "ask") { + await Permission.ask({ + type: "external_directory", + pattern: parentDir, + sessionID: ctx.sessionID, + messageID: ctx.messageID, + callID: ctx.callID, + title: `Patch file outside working directory: ${filePath}`, + metadata: { + filepath: filePath, + parentDir, + }, + }) + } } switch (hunk.type) { diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 963636fd..4d8e15bf 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -10,6 +10,7 @@ import { Instance } from "../project/instance" import { Provider } from "../provider/provider" import { Identifier } from "../id/id" import { Permission } from "../permission" +import { Agent } from "@/agent/agent" const DEFAULT_READ_LIMIT = 2000 const MAX_LINE_LENGTH = 2000 @@ -27,21 +28,24 @@ export const ReadTool = Tool.define("read", { filepath = path.join(process.cwd(), filepath) } const title = path.relative(Instance.worktree, filepath) + const agent = await Agent.get(ctx.agent) if (!ctx.extra?.["bypassCwdCheck"] && !Filesystem.contains(Instance.directory, filepath)) { const parentDir = path.dirname(filepath) - await Permission.ask({ - type: "external-directory", - pattern: parentDir, - sessionID: ctx.sessionID, - messageID: ctx.messageID, - callID: ctx.callID, - title: `Access file outside working directory: ${filepath}`, - metadata: { - filepath, - parentDir, - }, - }) + if (agent.permission.external_directory === "ask") { + await Permission.ask({ + type: "external_directory", + pattern: parentDir, + sessionID: ctx.sessionID, + messageID: ctx.messageID, + callID: ctx.callID, + title: `Access file outside working directory: ${filepath}`, + metadata: { + filepath, + parentDir, + }, + }) + } } const file = Bun.file(filepath) diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index acaa1239..58a0c177 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -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)"), }), 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) if (!Filesystem.contains(Instance.directory, filepath)) { const parentDir = path.dirname(filepath) - await Permission.ask({ - type: "external-directory", - pattern: parentDir, - sessionID: ctx.sessionID, - messageID: ctx.messageID, - callID: ctx.callID, - title: `Write file outside working directory: ${filepath}`, - metadata: { - filepath, - parentDir, - }, - }) + if (agent.permission.external_directory === "ask") { + await Permission.ask({ + type: "external_directory", + pattern: parentDir, + sessionID: ctx.sessionID, + messageID: ctx.messageID, + callID: ctx.callID, + title: `Write file outside working directory: ${filepath}`, + metadata: { + filepath, + parentDir, + }, + }) + } } const file = Bun.file(filepath) const exists = await file.exists() if (exists) await FileTime.assert(ctx.sessionID, filepath) - const agent = await Agent.get(ctx.agent) if (agent.permission.edit === "ask") await Permission.ask({ type: "write", diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index abbce39b..19f7a3a7 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -198,6 +198,8 @@ export type AgentConfig = { [key: string]: "ask" | "allow" | "deny" } webfetch?: "ask" | "allow" | "deny" + doom_loop?: "ask" | "allow" | "deny" + external_directory?: "ask" | "allow" | "deny" } [key: string]: | unknown @@ -216,6 +218,8 @@ export type AgentConfig = { [key: string]: "ask" | "allow" | "deny" } webfetch?: "ask" | "allow" | "deny" + doom_loop?: "ask" | "allow" | "deny" + external_directory?: "ask" | "allow" | "deny" } | undefined } @@ -463,6 +467,8 @@ export type Config = { [key: string]: "ask" | "allow" | "deny" } webfetch?: "ask" | "allow" | "deny" + doom_loop?: "ask" | "allow" | "deny" + external_directory?: "ask" | "allow" | "deny" } tools?: { [key: string]: boolean @@ -1043,6 +1049,8 @@ export type Agent = { [key: string]: "ask" | "allow" | "deny" } webfetch?: "ask" | "allow" | "deny" + doom_loop?: "ask" | "allow" | "deny" + external_directory?: "ask" | "allow" | "deny" } model?: { modelID: string