diff --git a/package.json b/package.json
index aa7031bec7..a30b55be73 100644
--- a/package.json
+++ b/package.json
@@ -78,6 +78,8 @@
"url": "https://github.com/sst/opencode"
},
"license": "MIT",
+ "randomField": "hello from claude",
+ "anotherRandomField": "xkcd-927-compliance-level",
"prettier": {
"semi": false,
"printWidth": 120
diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts
index 876570fd6c..4a3506e25f 100644
--- a/packages/opencode/src/cli/cmd/run.ts
+++ b/packages/opencode/src/cli/cmd/run.ts
@@ -206,7 +206,7 @@ export const RunCommand = cmd({
const permission = event.properties
if (permission.sessionID !== sessionID) continue
const result = await select({
- message: `Permission required to run: ${permission.message}`,
+ message: `Permission required: ${permission.permission} (${permission.patterns.join(", ")})`,
options: [
{ value: "once", label: "Allow once" },
{ value: "always", label: "Always allow: " + permission.always.join(", ") },
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
index d770931af3..9089d525da 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -1390,7 +1390,7 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess
},
get permission() {
const permissions = sync.data.permission[props.message.sessionID] ?? []
- const permissionIndex = permissions.findIndex((x) => x.callID === props.part.callID)
+ const permissionIndex = permissions.findIndex((x) => x.tool?.callID === props.part.callID)
return permissions[permissionIndex]
},
get tool() {
@@ -1483,12 +1483,13 @@ function InlineTool(props: { icon: string; complete: any; pending: string; child
const sync = useSync()
const permission = createMemo(() => {
- const callID = sync.data.permission[ctx.sessionID]?.at(0)?.callID
+ const callID = sync.data.permission[ctx.sessionID]?.at(0)?.tool?.callID
if (!callID) return false
return callID === props.part.callID
})
const fg = createMemo(() => {
+ if (permission()) return theme.warning
if (props.complete) return theme.textMuted
return theme.text
})
@@ -1528,9 +1529,6 @@ function InlineTool(props: { icon: string; complete: any; pending: string; child
~ {props.pending}>} when={props.complete}>
{props.icon} {props.children}
-
- · Permission required
-
{error()}
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx
index 9bd4460280..34e7011144 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx
@@ -8,6 +8,7 @@ import { SplitBorder } from "../../component/border"
import { useSync } from "../../context/sync"
import path from "path"
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
+import { Locale } from "@/util/locale"
function normalizePath(input?: string) {
if (!input) return ""
@@ -30,9 +31,8 @@ function EditBody(props: { request: PermissionRequest }) {
const sync = useSync()
const dimensions = useTerminalDimensions()
- const metadata = props.request.metadata as { filepath?: string; diff?: string }
- const filepath = createMemo(() => metadata.filepath ?? "")
- const diff = createMemo(() => metadata.diff ?? "")
+ const filepath = createMemo(() => (props.request.metadata?.filepath as string) ?? "")
+ const diff = createMemo(() => (props.request.metadata?.diff as string) ?? "")
const view = createMemo(() => {
const diffStyle = sync.data.config.tui?.diff_style
@@ -44,7 +44,7 @@ function EditBody(props: { request: PermissionRequest }) {
return (
-
+
{"→"}
Edit {normalizePath(filepath())}
@@ -75,32 +75,50 @@ function EditBody(props: { request: PermissionRequest }) {
)
}
-function TextBody(props: { text: string }) {
+function TextBody(props: { title: string; description?: string; icon: string }) {
const { theme } = useTheme()
return (
-
-
- {"→"}
-
- {props.text}
-
+ <>
+
+
+ {props.icon}
+
+ {props.title}
+
+
+
+ {props.description}
+
+
+ >
)
}
export function PermissionPrompt(props: { request: PermissionRequest }) {
const sdk = useSDK()
+ const sync = useSync()
const [store, setStore] = createStore({
always: false,
})
- const metadata = props.request.metadata as { filepath?: string }
+ const input = createMemo(() => {
+ const tool = props.request.tool
+ if (!tool) return {}
+ const parts = sync.data.part[tool.messageID] ?? []
+ for (const part of parts) {
+ if (part.type === "tool" && part.callID === tool.callID && part.state.status !== "pending") {
+ return part.state.input ?? {}
+ }
+ }
+ return {}
+ })
return (
}
+ body={}
options={{ confirm: "Confirm", cancel: "Cancel" }}
onSelect={(option) => {
if (option === "cancel") {
@@ -114,27 +132,61 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
}}
/>
-
- }
- options={{ once: "Allow once", always: "Allow always", reject: "Reject" }}
- onSelect={(option) => {
- if (option === "always") {
- setStore("always", true)
- return
- }
- sdk.client.permission.reply({
- reply: option as "once" | "reject",
- requestID: props.request.id,
- })
- }}
- />
-
}
+ body={
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
options={{ once: "Allow once", always: "Allow always", reject: "Reject" }}
onSelect={(option) => {
if (option === "always") {
@@ -192,8 +244,8 @@ function Prompt>(props: {
borderColor={theme.warning}
customBorderChars={SplitBorder.customBorderChars}
>
-
-
+
+
{"△"}
{props.title}
diff --git a/packages/opencode/src/permission/next.ts b/packages/opencode/src/permission/next.ts
index fb551f9bef..54e3318dca 100644
--- a/packages/opencode/src/permission/next.ts
+++ b/packages/opencode/src/permission/next.ts
@@ -56,13 +56,17 @@ export namespace PermissionNext {
export const Request = z
.object({
id: Identifier.schema("permission"),
- callID: z.string().optional(),
sessionID: Identifier.schema("session"),
permission: z.string(),
patterns: z.string().array(),
- message: z.string(),
metadata: z.record(z.string(), z.any()),
always: z.string().array(),
+ tool: z
+ .object({
+ messageID: z.string(),
+ callID: z.string(),
+ })
+ .optional(),
})
.meta({
ref: "PermissionRequest",
diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts
index 6eb726a4d3..0558839445 100644
--- a/packages/opencode/src/session/processor.ts
+++ b/packages/opencode/src/session/processor.ts
@@ -157,7 +157,6 @@ export namespace SessionProcessor {
await PermissionNext.ask({
permission: "doom_loop",
patterns: [value.toolName],
- message: `Possible doom loop: "${value.toolName}" called ${DOOM_LOOP_THRESHOLD} times with identical arguments`,
sessionID: input.assistantMessage.sessionID,
metadata: {
tool: value.toolName,
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 4ead3f4ccf..2d4575a679 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -38,6 +38,8 @@ import { NamedError } from "@opencode-ai/util/error"
import { fn } from "@/util/fn"
import { SessionProcessor } from "./processor"
import { TaskTool } from "@/tool/task"
+import { Tool } from "@/tool/tool"
+import { PermissionNext } from "@/permission/next"
import { SessionStatus } from "./status"
import { LLM } from "./llm"
import { iife } from "@/util/iife"
@@ -349,28 +351,35 @@ export namespace SessionPrompt {
{ args: taskArgs },
)
let executionError: Error | undefined
- const result = await taskTool
- .execute(taskArgs, {
- agent: task.agent,
- messageID: assistantMessage.id,
- sessionID: sessionID,
- abort,
- async metadata(input) {
- await Session.updatePart({
- ...part,
- type: "tool",
- state: {
- ...part.state,
- ...input,
- },
- } satisfies MessageV2.ToolPart)
- },
- })
- .catch((error) => {
- executionError = error
- log.error("subtask execution failed", { error, agent: task.agent, description: task.description })
- return undefined
- })
+ const taskAgent = await Agent.get(task.agent)
+ const taskCtx: Tool.Context = {
+ agent: task.agent,
+ messageID: assistantMessage.id,
+ sessionID: sessionID,
+ abort,
+ async metadata(input) {
+ await Session.updatePart({
+ ...part,
+ type: "tool",
+ state: {
+ ...part.state,
+ ...input,
+ },
+ } satisfies MessageV2.ToolPart)
+ },
+ async ask(req) {
+ await PermissionNext.ask({
+ ...req,
+ sessionID: sessionID,
+ ruleset: taskAgent.permission,
+ })
+ },
+ }
+ const result = await taskTool.execute(taskArgs, taskCtx).catch((error) => {
+ executionError = error
+ log.error("subtask execution failed", { error, agent: task.agent, description: task.description })
+ return undefined
+ })
await Plugin.trigger(
"tool.execute.after",
{
@@ -604,14 +613,14 @@ export namespace SessionPrompt {
args,
},
)
- const result = await item.execute(args, {
+ const ctx: Tool.Context = {
sessionID: input.sessionID,
abort: options.abortSignal!,
messageID: input.processor.message.id,
callID: options.toolCallId,
extra: { model: input.model },
agent: input.agent.name,
- metadata: async (val) => {
+ metadata: async (val: { title?: string; metadata?: any }) => {
const match = input.processor.partFromToolCall(options.toolCallId)
if (match && match.state.status === "running") {
await Session.updatePart({
@@ -628,7 +637,16 @@ export namespace SessionPrompt {
})
}
},
- })
+ async ask(req) {
+ await PermissionNext.ask({
+ ...req,
+ sessionID: input.sessionID,
+ tool: { messageID: input.processor.message.id, callID: options.toolCallId },
+ ruleset: input.agent.permission,
+ })
+ },
+ }
+ const result = await item.execute(args, ctx)
await Plugin.trigger(
"tool.execute.after",
{
@@ -826,14 +844,16 @@ export namespace SessionPrompt {
await ReadTool.init()
.then(async (t) => {
const model = await Provider.getModel(info.model.providerID, info.model.modelID)
- const result = await t.execute(args, {
+ const readCtx: Tool.Context = {
sessionID: input.sessionID,
abort: new AbortController().signal,
agent: input.agent!,
messageID: info.id,
extra: { bypassCwdCheck: true, model },
metadata: async () => {},
- })
+ ask: async () => {},
+ }
+ const result = await t.execute(args, readCtx)
pieces.push({
id: Identifier.ascending("part"),
messageID: info.id,
@@ -885,16 +905,16 @@ export namespace SessionPrompt {
if (part.mime === "application/x-directory") {
const args = { path: filepath }
- const result = await ListTool.init().then((t) =>
- t.execute(args, {
- sessionID: input.sessionID,
- abort: new AbortController().signal,
- agent: input.agent!,
- messageID: info.id,
- extra: { bypassCwdCheck: true },
- metadata: async () => {},
- }),
- )
+ const listCtx: Tool.Context = {
+ sessionID: input.sessionID,
+ abort: new AbortController().signal,
+ agent: input.agent!,
+ messageID: info.id,
+ extra: { bypassCwdCheck: true },
+ metadata: async () => {},
+ ask: async () => {},
+ }
+ const result = await ListTool.init().then((t) => t.execute(args, listCtx))
return [
{
id: Identifier.ascending("part"),
diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts
index 6d942315b2..6671c939c4 100644
--- a/packages/opencode/src/tool/bash.ts
+++ b/packages/opencode/src/tool/bash.ts
@@ -6,13 +6,13 @@ import { Log } from "../util/log"
import { Instance } from "../project/instance"
import { lazy } from "@/util/lazy"
import { Language } from "web-tree-sitter"
-import { Agent } from "@/agent/agent"
+
import { $ } from "bun"
import { Filesystem } from "@/util/filesystem"
import { fileURLToPath } from "url"
import { Flag } from "@/flag/flag.ts"
import { Shell } from "@/shell/shell"
-import { PermissionNext } from "@/permission/next"
+
import { BashArity } from "@/permission/arity"
const MAX_OUTPUT_LENGTH = Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH || 30_000
@@ -80,8 +80,6 @@ export const BashTool = Tool.define("bash", async () => {
if (!tree) {
throw new Error("Failed to parse command")
}
- const agent = await Agent.get(ctx.agent)
-
const directories = new Set()
if (!Filesystem.contains(Instance.directory, cwd)) directories.add(cwd)
const patterns = new Set()
@@ -134,29 +132,20 @@ export const BashTool = Tool.define("bash", async () => {
}
if (directories.size > 0) {
- const dirs = Array.from(directories)
- await PermissionNext.ask({
- callID: ctx.callID,
+ await ctx.ask({
permission: "external_directory",
- message: `Requesting access to external directories: ${dirs.join(", ")}`,
patterns: Array.from(directories),
always: Array.from(directories).map((x) => x + "*"),
- sessionID: ctx.sessionID,
metadata: {},
- ruleset: agent.permission,
})
}
if (patterns.size > 0) {
- await PermissionNext.ask({
- callID: ctx.callID,
+ await ctx.ask({
permission: "bash",
patterns: Array.from(patterns),
always: Array.from(always),
- sessionID: ctx.sessionID,
- message: params.command,
metadata: {},
- ruleset: agent.permission,
})
}
diff --git a/packages/opencode/src/tool/codesearch.ts b/packages/opencode/src/tool/codesearch.ts
index c9259b2e85..369cdb4504 100644
--- a/packages/opencode/src/tool/codesearch.ts
+++ b/packages/opencode/src/tool/codesearch.ts
@@ -1,8 +1,6 @@
import z from "zod"
import { Tool } from "./tool"
import DESCRIPTION from "./codesearch.txt"
-import { PermissionNext } from "@/permission/next"
-import { Agent } from "@/agent/agent"
const API_CONFIG = {
BASE_URL: "https://mcp.exa.ai",
@@ -52,20 +50,14 @@ export const CodeSearchTool = Tool.define("codesearch", {
),
}),
async execute(params, ctx) {
- const agent = await Agent.get(ctx.agent)
- await PermissionNext.ask({
- callID: ctx.callID,
+ await ctx.ask({
permission: "codesearch",
- message: "Search code for: " + params.query,
patterns: [params.query],
always: ["*"],
- sessionID: ctx.sessionID,
metadata: {
query: params.query,
tokensNum: params.tokensNum,
},
-
- ruleset: agent.permission,
})
const codeRequest: McpCodeRequest = {
diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts
index 90fcf69ed0..787282ecd0 100644
--- a/packages/opencode/src/tool/edit.ts
+++ b/packages/opencode/src/tool/edit.ts
@@ -14,9 +14,7 @@ import { Bus } from "../bus"
import { FileTime } from "../file/time"
import { Filesystem } from "../util/filesystem"
import { Instance } from "../project/instance"
-import { Agent } from "../agent/agent"
import { Snapshot } from "@/snapshot"
-import { PermissionNext } from "@/permission/next"
const MAX_DIAGNOSTICS_PER_FILE = 20
@@ -41,24 +39,17 @@ 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 PermissionNext.ask({
- callID: ctx.callID,
+ await ctx.ask({
permission: "external_directory",
- message: `Edit file outside working directory: ${filePath}`,
patterns: [parentDir, path.join(parentDir, "*")],
always: [parentDir + "/*"],
- sessionID: ctx.sessionID,
metadata: {
filepath: filePath,
parentDir,
},
-
- ruleset: agent.permission,
})
}
@@ -69,19 +60,14 @@ export const EditTool = Tool.define("edit", {
if (params.oldString === "") {
contentNew = params.newString
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
- await PermissionNext.ask({
- callID: ctx.callID,
+ await ctx.ask({
permission: "edit",
- message: "Edit this file: " + path.relative(Instance.directory, filePath),
patterns: [path.relative(Instance.worktree, filePath)],
always: ["*"],
- sessionID: ctx.sessionID,
metadata: {
filepath: filePath,
diff,
},
-
- ruleset: agent.permission,
})
await Bun.write(filePath, params.newString)
await Bus.publish(File.Event.Edited, {
@@ -102,18 +88,14 @@ export const EditTool = Tool.define("edit", {
diff = trimDiff(
createTwoFilesPatch(filePath, filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew)),
)
- await PermissionNext.ask({
+ await ctx.ask({
permission: "edit",
- callID: ctx.callID,
- message: "Edit this file: " + path.relative(Instance.directory, filePath),
patterns: [path.relative(Instance.worktree, filePath)],
always: ["*"],
- sessionID: ctx.sessionID,
metadata: {
filepath: filePath,
diff,
},
- ruleset: agent.permission,
})
await file.write(contentNew)
@@ -127,6 +109,26 @@ export const EditTool = Tool.define("edit", {
FileTime.read(ctx.sessionID, filePath)
})
+ const filediff: Snapshot.FileDiff = {
+ file: filePath,
+ before: contentOld,
+ after: contentNew,
+ additions: 0,
+ deletions: 0,
+ }
+ for (const change of diffLines(contentOld, contentNew)) {
+ if (change.added) filediff.additions += change.count || 0
+ if (change.removed) filediff.deletions += change.count || 0
+ }
+
+ ctx.metadata({
+ metadata: {
+ diff,
+ filediff,
+ diagnostics: {},
+ },
+ })
+
let output = ""
await LSP.touchFile(filePath, true)
const diagnostics = await LSP.diagnostics()
@@ -140,18 +142,6 @@ export const EditTool = Tool.define("edit", {
output += `\nThis file has errors, please fix\n\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n\n`
}
- const filediff: Snapshot.FileDiff = {
- file: filePath,
- before: contentOld,
- after: contentNew,
- additions: 0,
- deletions: 0,
- }
- for (const change of diffLines(contentOld, contentNew)) {
- if (change.added) filediff.additions += change.count || 0
- if (change.removed) filediff.deletions += change.count || 0
- }
-
return {
metadata: {
diagnostics,
diff --git a/packages/opencode/src/tool/glob.ts b/packages/opencode/src/tool/glob.ts
index 1075db6f25..0c643796de 100644
--- a/packages/opencode/src/tool/glob.ts
+++ b/packages/opencode/src/tool/glob.ts
@@ -4,8 +4,6 @@ import { Tool } from "./tool"
import DESCRIPTION from "./glob.txt"
import { Ripgrep } from "../file/ripgrep"
import { Instance } from "../project/instance"
-import { Agent } from "@/agent/agent"
-import { PermissionNext } from "@/permission/next"
export const GlobTool = Tool.define("glob", {
description: DESCRIPTION,
@@ -19,20 +17,14 @@ export const GlobTool = Tool.define("glob", {
),
}),
async execute(params, ctx) {
- const agent = await Agent.get(ctx.agent)
- await PermissionNext.ask({
- callID: ctx.callID,
+ await ctx.ask({
permission: "glob",
- message: `Glob search: ${params.pattern}`,
patterns: [params.pattern],
always: ["*"],
- sessionID: ctx.sessionID,
metadata: {
pattern: params.pattern,
path: params.path,
},
-
- ruleset: agent.permission,
})
let search = params.path ?? Instance.directory
diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts
index c78fd83adb..4cbc5347f5 100644
--- a/packages/opencode/src/tool/grep.ts
+++ b/packages/opencode/src/tool/grep.ts
@@ -4,8 +4,6 @@ import { Ripgrep } from "../file/ripgrep"
import DESCRIPTION from "./grep.txt"
import { Instance } from "../project/instance"
-import { Agent } from "@/agent/agent"
-import { PermissionNext } from "@/permission/next"
const MAX_LINE_LENGTH = 2000
@@ -21,21 +19,15 @@ export const GrepTool = Tool.define("grep", {
throw new Error("pattern is required")
}
- const agent = await Agent.get(ctx.agent)
- await PermissionNext.ask({
- callID: ctx.callID,
+ await ctx.ask({
permission: "grep",
- message: `Grep search: ${params.pattern}`,
patterns: [params.pattern],
always: ["*"],
- sessionID: ctx.sessionID,
metadata: {
pattern: params.pattern,
path: params.path,
include: params.include,
},
-
- ruleset: agent.permission,
})
const searchPath = params.path || Instance.directory
diff --git a/packages/opencode/src/tool/ls.ts b/packages/opencode/src/tool/ls.ts
index a5494631c0..b8638b3e90 100644
--- a/packages/opencode/src/tool/ls.ts
+++ b/packages/opencode/src/tool/ls.ts
@@ -4,8 +4,6 @@ import * as path from "path"
import DESCRIPTION from "./ls.txt"
import { Instance } from "../project/instance"
import { Ripgrep } from "../file/ripgrep"
-import { Agent } from "@/agent/agent"
-import { PermissionNext } from "@/permission/next"
export const IGNORE_PATTERNS = [
"node_modules/",
@@ -45,19 +43,13 @@ export const ListTool = Tool.define("list", {
async execute(params, ctx) {
const searchPath = path.resolve(Instance.directory, params.path || ".")
- const agent = await Agent.get(ctx.agent)
- await PermissionNext.ask({
- callID: ctx.callID,
+ await ctx.ask({
permission: "list",
- message: `List directory: ${searchPath}`,
patterns: [searchPath],
always: ["*"],
- sessionID: ctx.sessionID,
metadata: {
path: searchPath,
},
-
- ruleset: agent.permission,
})
const ignoreGlobs = IGNORE_PATTERNS.map((p) => `!${p}*`).concat(params.ignore?.map((p) => `!${p}`) || [])
diff --git a/packages/opencode/src/tool/patch.ts b/packages/opencode/src/tool/patch.ts
index 05d1bd88fa..62d9f70f20 100644
--- a/packages/opencode/src/tool/patch.ts
+++ b/packages/opencode/src/tool/patch.ts
@@ -6,11 +6,9 @@ import { FileTime } from "../file/time"
import { Bus } from "../bus"
import { FileWatcher } from "../file/watcher"
import { Instance } from "../project/instance"
-import { Agent } from "../agent/agent"
import { Patch } from "../patch"
import { Filesystem } from "../util/filesystem"
import { createTwoFilesPatch } from "diff"
-import { PermissionNext } from "@/permission/next"
const PatchParams = z.object({
patchText: z.string().describe("The full patch text that describes all changes to be made"),
@@ -39,7 +37,6 @@ export const PatchTool = Tool.define("patch", {
}
// Validate file paths and check permissions
- const agent = await Agent.get(ctx.agent)
const fileChanges: Array<{
filePath: string
oldContent: string
@@ -55,19 +52,14 @@ export const PatchTool = Tool.define("patch", {
if (!Filesystem.contains(Instance.directory, filePath)) {
const parentDir = path.dirname(filePath)
- await PermissionNext.ask({
- callID: ctx.callID,
+ await ctx.ask({
permission: "external_directory",
- message: `Patch file outside working directory: ${filePath}`,
patterns: [parentDir, path.join(parentDir, "*")],
always: [parentDir + "/*"],
- sessionID: ctx.sessionID,
metadata: {
filepath: filePath,
parentDir,
},
-
- ruleset: agent.permission,
})
}
@@ -141,18 +133,13 @@ export const PatchTool = Tool.define("patch", {
}
// Check permissions if needed
- await PermissionNext.ask({
- callID: ctx.callID,
+ await ctx.ask({
permission: "edit",
- message: `Apply patch to ${fileChanges.length} files`,
patterns: fileChanges.map((c) => path.relative(Instance.worktree, c.filePath)),
always: ["*"],
- sessionID: ctx.sessionID,
metadata: {
diff: totalDiff,
},
-
- ruleset: agent.permission,
})
// Apply the changes
diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts
index 471e0cb5f2..847fe3ebe7 100644
--- a/packages/opencode/src/tool/read.ts
+++ b/packages/opencode/src/tool/read.ts
@@ -8,9 +8,7 @@ import DESCRIPTION from "./read.txt"
import { Filesystem } from "../util/filesystem"
import { Instance } from "../project/instance"
import { Identifier } from "../id/id"
-import { Agent } from "@/agent/agent"
import { iife } from "@/util/iife"
-import { PermissionNext } from "@/permission/next"
const DEFAULT_READ_LIMIT = 2000
const MAX_LINE_LENGTH = 2000
@@ -28,36 +26,25 @@ 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 PermissionNext.ask({
- callID: ctx.callID,
+ await ctx.ask({
permission: "external_directory",
- message: `Access file outside working directory: ${filepath}`,
patterns: [parentDir],
always: [parentDir + "/*"],
- sessionID: ctx.sessionID,
metadata: {
filepath,
parentDir,
},
-
- ruleset: agent.permission,
})
}
- await PermissionNext.ask({
- callID: ctx.callID,
+ await ctx.ask({
permission: "read",
- message: `Read file ${filepath}`,
patterns: [filepath],
always: ["*"],
- sessionID: ctx.sessionID,
metadata: {},
-
- ruleset: agent.permission,
})
const block = iife(() => {
diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts
index 2f9069797d..00a081eaca 100644
--- a/packages/opencode/src/tool/skill.ts
+++ b/packages/opencode/src/tool/skill.ts
@@ -2,9 +2,7 @@ import path from "path"
import z from "zod"
import { Tool } from "./tool"
import { Skill } from "../skill"
-import { Agent } from "../agent/agent"
import { ConfigMarkdown } from "../config/markdown"
-import { PermissionNext } from "@/permission/next"
export const SkillTool = Tool.define("skill", async () => {
const skills = await Skill.all()
@@ -46,8 +44,6 @@ export const SkillTool = Tool.define("skill", async () => {
.describe("The skill identifier from available_skills (e.g., 'code-review' or 'category/helper')"),
}),
async execute(params, ctx) {
- const agent = await Agent.get(ctx.agent)
-
const skill = await Skill.get(params.name)
if (!skill) {
@@ -55,15 +51,11 @@ export const SkillTool = Tool.define("skill", async () => {
throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`)
}
- await PermissionNext.ask({
- callID: ctx.callID,
+ await ctx.ask({
permission: "skill",
patterns: [params.name],
always: [params.name],
- sessionID: ctx.sessionID,
- message: `Activate skill ${params.name}`,
metadata: {},
- ruleset: agent.permission,
})
// Load and parse skill content
const parsed = await ConfigMarkdown.parse(skill.location)
diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts
index cd8f7bbe44..3bf800f112 100644
--- a/packages/opencode/src/tool/task.ts
+++ b/packages/opencode/src/tool/task.ts
@@ -10,7 +10,6 @@ import { SessionPrompt } from "../session/prompt"
import { iife } from "@/util/iife"
import { defer } from "@/util/defer"
import { Config } from "../config/config"
-import { PermissionNext } from "@/permission/next"
export const TaskTool = Tool.define("task", async () => {
const agents = await Agent.list().then((x) => x.filter((a) => a.mode !== "primary"))
@@ -30,19 +29,14 @@ export const TaskTool = Tool.define("task", async () => {
command: z.string().describe("The command that triggered this task").optional(),
}),
async execute(params, ctx) {
- const callingAgent = await Agent.get(ctx.agent)
- await PermissionNext.ask({
- callID: ctx.callID,
+ await ctx.ask({
permission: "task",
- message: `Launch task: ${params.description}`,
patterns: [params.subagent_type],
always: ["*"],
- sessionID: ctx.sessionID,
metadata: {
description: params.description,
subagent_type: params.subagent_type,
},
- ruleset: callingAgent.permission,
})
const agent = await Agent.get(params.subagent_type)
diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts
index acee24902c..434a3d4266 100644
--- a/packages/opencode/src/tool/tool.ts
+++ b/packages/opencode/src/tool/tool.ts
@@ -1,6 +1,7 @@
import z from "zod"
import type { MessageV2 } from "../session/message-v2"
import type { Agent } from "../agent/agent"
+import type { PermissionNext } from "../permission/next"
export namespace Tool {
interface Metadata {
@@ -19,6 +20,7 @@ export namespace Tool {
callID?: string
extra?: { [key: string]: any }
metadata(input: { title?: string; metadata?: M }): void
+ ask(input: Omit): Promise
}
export interface Info {
id: string
diff --git a/packages/opencode/src/tool/webfetch.ts b/packages/opencode/src/tool/webfetch.ts
index 1c7c8ef729..634c68f4ee 100644
--- a/packages/opencode/src/tool/webfetch.ts
+++ b/packages/opencode/src/tool/webfetch.ts
@@ -2,8 +2,6 @@ import z from "zod"
import { Tool } from "./tool"
import TurndownService from "turndown"
import DESCRIPTION from "./webfetch.txt"
-import { PermissionNext } from "@/permission/next"
-import { Agent } from "@/agent/agent"
const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB
const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
@@ -25,21 +23,15 @@ export const WebFetchTool = Tool.define("webfetch", {
throw new Error("URL must start with http:// or https://")
}
- const agent = await Agent.get(ctx.agent)
- await PermissionNext.ask({
- callID: ctx.callID,
+ await ctx.ask({
permission: "webfetch",
- message: "Fetch content from: " + params.url,
patterns: [params.url],
always: ["*"],
- sessionID: ctx.sessionID,
metadata: {
url: params.url,
format: params.format,
timeout: params.timeout,
},
-
- ruleset: agent.permission,
})
const timeout = Math.min((params.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000, MAX_TIMEOUT)
diff --git a/packages/opencode/src/tool/websearch.ts b/packages/opencode/src/tool/websearch.ts
index 31fb8da19e..f6df36f10f 100644
--- a/packages/opencode/src/tool/websearch.ts
+++ b/packages/opencode/src/tool/websearch.ts
@@ -1,8 +1,6 @@
import z from "zod"
import { Tool } from "./tool"
import DESCRIPTION from "./websearch.txt"
-import { PermissionNext } from "@/permission/next"
-import { Agent } from "@/agent/agent"
const API_CONFIG = {
BASE_URL: "https://mcp.exa.ai",
@@ -59,14 +57,10 @@ export const WebSearchTool = Tool.define("websearch", {
.describe("Maximum characters for context string optimized for LLMs (default: 10000)"),
}),
async execute(params, ctx) {
- const agent = await Agent.get(ctx.agent)
- await PermissionNext.ask({
- callID: ctx.callID,
+ await ctx.ask({
permission: "websearch",
- message: "Search web for: " + params.query,
patterns: [params.query],
always: ["*"],
- sessionID: ctx.sessionID,
metadata: {
query: params.query,
numResults: params.numResults,
@@ -74,8 +68,6 @@ export const WebSearchTool = Tool.define("websearch", {
type: params.type,
contextMaxCharacters: params.contextMaxCharacters,
},
-
- ruleset: agent.permission,
})
const searchRequest: McpSearchRequest = {
diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts
index 29e04e7c78..2f35f38f16 100644
--- a/packages/opencode/src/tool/write.ts
+++ b/packages/opencode/src/tool/write.ts
@@ -8,8 +8,6 @@ import { File } from "../file"
import { FileTime } from "../file/time"
import { Filesystem } from "../util/filesystem"
import { Instance } from "../project/instance"
-import { Agent } from "../agent/agent"
-import { PermissionNext } from "@/permission/next"
const MAX_DIAGNOSTICS_PER_FILE = 20
const MAX_PROJECT_DIAGNOSTICS_FILES = 5
@@ -21,37 +19,11 @@ 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)
/* TODO
if (!Filesystem.contains(Instance.directory, filepath)) {
const parentDir = path.dirname(filepath)
- if (agent.permission.external_directory === "ask") {
- await Permission.ask({
- type: "external_directory",
- pattern: [parentDir, path.join(parentDir, "*")],
- sessionID: ctx.sessionID,
- messageID: ctx.messageID,
- callID: ctx.callID,
- title: `Write file outside working directory: ${filepath}`,
- metadata: {
- filepath,
- parentDir,
- },
- })
- } else if (agent.permission.external_directory === "deny") {
- throw new Permission.RejectedError(
- ctx.sessionID,
- "external_directory",
- ctx.callID,
- {
- filepath: filepath,
- parentDir,
- },
- `File ${filepath} is not in the current working directory`,
- )
- }
+ ...
}
*/
@@ -59,16 +31,11 @@ export const WriteTool = Tool.define("write", {
const exists = await file.exists()
if (exists) await FileTime.assert(ctx.sessionID, filepath)
- await PermissionNext.ask({
- callID: ctx.callID,
+ await ctx.ask({
permission: "edit",
- message: `Create new file ${path.relative(Instance.directory, filepath)}`,
patterns: [path.relative(Instance.worktree, filepath)],
always: ["*"],
- sessionID: ctx.sessionID,
metadata: {},
-
- ruleset: agent.permission,
})
await Bun.write(filepath, params.content)
diff --git a/packages/opencode/test/permission/next.test.ts b/packages/opencode/test/permission/next.test.ts
index ed7c2b1304..31af4cd450 100644
--- a/packages/opencode/test/permission/next.test.ts
+++ b/packages/opencode/test/permission/next.test.ts
@@ -428,7 +428,6 @@ test("ask - resolves immediately when action is allow", async () => {
sessionID: "session_test",
permission: "bash",
patterns: ["ls"],
- message: "Run ls command",
metadata: {},
always: [],
ruleset: [{ permission: "bash", pattern: "*", action: "allow" }],
@@ -448,7 +447,6 @@ test("ask - throws RejectedError when action is deny", async () => {
sessionID: "session_test",
permission: "bash",
patterns: ["rm -rf /"],
- message: "Run dangerous command",
metadata: {},
always: [],
ruleset: [{ permission: "bash", pattern: "*", action: "deny" }],
@@ -467,7 +465,6 @@ test("ask - returns pending promise when action is ask", async () => {
sessionID: "session_test",
permission: "bash",
patterns: ["ls"],
- message: "Run ls command",
metadata: {},
always: [],
ruleset: [{ permission: "bash", pattern: "*", action: "ask" }],
@@ -491,7 +488,6 @@ test("reply - once resolves the pending ask", async () => {
sessionID: "session_test",
permission: "bash",
patterns: ["ls"],
- message: "Run ls command",
metadata: {},
always: [],
ruleset: [],
@@ -517,7 +513,6 @@ test("reply - reject throws RejectedError", async () => {
sessionID: "session_test",
permission: "bash",
patterns: ["ls"],
- message: "Run ls command",
metadata: {},
always: [],
ruleset: [],
@@ -543,7 +538,6 @@ test("reply - always persists approval and resolves", async () => {
sessionID: "session_test",
permission: "bash",
patterns: ["ls"],
- message: "Run ls command",
metadata: {},
always: ["ls"],
ruleset: [],
@@ -566,7 +560,6 @@ test("reply - always persists approval and resolves", async () => {
sessionID: "session_test2",
permission: "bash",
patterns: ["ls"],
- message: "Run ls command",
metadata: {},
always: [],
ruleset: [],
@@ -586,7 +579,6 @@ test("reply - reject cancels all pending for same session", async () => {
sessionID: "session_same",
permission: "bash",
patterns: ["ls"],
- message: "Run ls",
metadata: {},
always: [],
ruleset: [],
@@ -597,7 +589,6 @@ test("reply - reject cancels all pending for same session", async () => {
sessionID: "session_same",
permission: "edit",
patterns: ["foo.ts"],
- message: "Edit file",
metadata: {},
always: [],
ruleset: [],
@@ -630,7 +621,6 @@ test("ask - checks all patterns and stops on first deny", async () => {
sessionID: "session_test",
permission: "bash",
patterns: ["echo hello", "rm -rf /"],
- message: "Run commands",
metadata: {},
always: [],
ruleset: [
@@ -652,7 +642,6 @@ test("ask - allows all patterns when all match allow rules", async () => {
sessionID: "session_test",
permission: "bash",
patterns: ["echo hello", "ls -la", "pwd"],
- message: "Run safe commands",
metadata: {},
always: [],
ruleset: [{ permission: "bash", pattern: "*", action: "allow" }],
diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts
index 9ef7dfb9d8..31875d556b 100644
--- a/packages/opencode/test/tool/bash.test.ts
+++ b/packages/opencode/test/tool/bash.test.ts
@@ -12,6 +12,7 @@ const ctx = {
agent: "build",
abort: AbortSignal.any([]),
metadata: () => {},
+ ask: async () => {},
}
const projectRoot = path.join(__dirname, "../..")
diff --git a/packages/opencode/test/tool/grep.test.ts b/packages/opencode/test/tool/grep.test.ts
index f3da666a09..a79d931575 100644
--- a/packages/opencode/test/tool/grep.test.ts
+++ b/packages/opencode/test/tool/grep.test.ts
@@ -11,6 +11,7 @@ const ctx = {
agent: "build",
abort: AbortSignal.any([]),
metadata: () => {},
+ ask: async () => {},
}
const projectRoot = path.join(__dirname, "../..")
diff --git a/packages/opencode/test/tool/patch.test.ts b/packages/opencode/test/tool/patch.test.ts
index 6d7d6db87f..2a0df48f98 100644
--- a/packages/opencode/test/tool/patch.test.ts
+++ b/packages/opencode/test/tool/patch.test.ts
@@ -9,10 +9,11 @@ import * as fs from "fs/promises"
const ctx = {
sessionID: "test",
messageID: "",
- toolCallID: "",
+ callID: "",
agent: "build",
abort: AbortSignal.any([]),
metadata: () => {},
+ ask: async () => {},
}
const patchTool = await PatchTool.init()
diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts
index eb860d04fc..a808070a6d 100644
--- a/packages/opencode/test/tool/read.test.ts
+++ b/packages/opencode/test/tool/read.test.ts
@@ -11,6 +11,7 @@ const ctx = {
agent: "build",
abort: AbortSignal.any([]),
metadata: () => {},
+ ask: async () => {},
}
describe("tool.read external_directory permission", () => {
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index e3dcfbff10..f0f8cf9a5f 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -453,15 +453,17 @@ export type EventMessagePartRemoved = {
export type PermissionRequest = {
id: string
- callID?: string
sessionID: string
permission: string
patterns: Array
- message: string
metadata: {
[key: string]: unknown
}
always: Array
+ tool?: {
+ messageID: string
+ callID: string
+ }
}
export type EventPermissionNextAsked = {