From 312ade4dced7619862c2bb8efaf74c73930b8f2c Mon Sep 17 00:00:00 2001 From: ArmirKS Date: Sun, 15 Feb 2026 20:06:32 +0100 Subject: [PATCH 1/2] fix: centralize tool plugin hooks via Tool.invoke() prompt.ts had duplicated inline Plugin.trigger calls for tool.execute.before/after at two call sites. Sub-tools inside batch never fired hooks, allowing plugin security policies to be silently bypassed. - Add Tool.invoke() to wrap execution with before/after hooks - Replace duplicated inline Plugin.trigger calls in prompt.ts - Add agent to hook input, status to after output - Fire after hook on error path (previously silent) - MCP tools kept inline (different result shape) --- packages/opencode/src/session/prompt.ts | 72 ++++++++----------------- packages/opencode/src/tool/tool.ts | 56 +++++++++++++++++++ packages/plugin/src/index.ts | 5 +- 3 files changed, 82 insertions(+), 51 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 5890a4a73249..d387e215332f 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -403,15 +403,6 @@ export namespace SessionPrompt { subagent_type: task.agent, command: task.command, } - await Plugin.trigger( - "tool.execute.before", - { - tool: "task", - sessionID, - callID: part.id, - }, - { args: taskArgs }, - ) let executionError: Error | undefined const taskAgent = await Agent.get(task.agent) const taskCtx: Tool.Context = { @@ -440,10 +431,18 @@ export namespace SessionPrompt { }) }, } - 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 + const result = await Tool.invoke({ + tool: "task", + sessionID, + callID: part.id, + agent: task.agent, + args: taskArgs, + fn: () => + taskTool.execute(taskArgs, taskCtx).catch((error) => { + executionError = error + log.error("subtask execution failed", { error, agent: task.agent, description: task.description }) + return undefined + }), }) const attachments = result?.attachments?.map((attachment) => ({ ...attachment, @@ -451,16 +450,6 @@ export namespace SessionPrompt { sessionID, messageID: assistantMessage.id, })) - await Plugin.trigger( - "tool.execute.after", - { - tool: "task", - sessionID, - callID: part.id, - args: taskArgs, - }, - result, - ) assistantMessage.finish = "tool-calls" assistantMessage.time.completed = Date.now() await Session.updateMessage(assistantMessage) @@ -791,38 +780,23 @@ export namespace SessionPrompt { inputSchema: jsonSchema(schema as any), async execute(args, options) { const ctx = context(args, options) - await Plugin.trigger( - "tool.execute.before", - { - tool: item.id, - sessionID: ctx.sessionID, - callID: ctx.callID, - }, - { - args, - }, - ) - const result = await item.execute(args, ctx) - const output = { - ...result, - attachments: result.attachments?.map((attachment) => ({ + const result = await Tool.invoke({ + tool: item.id, + sessionID: ctx.sessionID, + callID: ctx.callID, + agent: ctx.agent, + args, + fn: () => item.execute(args, ctx), + }) + return { + ...result!, + attachments: result?.attachments?.map((attachment) => ({ ...attachment, id: Identifier.ascending("part"), sessionID: ctx.sessionID, messageID: input.processor.message.id, })), } - await Plugin.trigger( - "tool.execute.after", - { - tool: item.id, - sessionID: ctx.sessionID, - callID: ctx.callID, - args, - }, - output, - ) - return output }, }) } diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index 0e78ba665cfc..793dae831a18 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -45,6 +45,62 @@ export namespace Tool { export type InferParameters = T extends Info ? z.infer

: never export type InferMetadata = T extends Info ? M : never + export type Result = Awaited>["execute"]>> + + let pending: Promise | undefined + export async function invoke(input: { + tool: string + sessionID: string + callID?: string + agent?: string + args: any + fn: () => Promise + }): Promise { + pending ??= import("../plugin") + const { Plugin } = await pending + await Plugin.trigger( + "tool.execute.before", + { + tool: input.tool, + sessionID: input.sessionID, + callID: input.callID ?? "", + agent: input.agent, + }, + { args: input.args }, + ) + let result: Result | undefined + try { + result = await input.fn() + } catch (e) { + await Plugin.trigger( + "tool.execute.after", + { + tool: input.tool, + sessionID: input.sessionID, + callID: input.callID ?? "", + args: input.args, + agent: input.agent, + }, + { title: "", output: "", metadata: { error: e }, status: "error" as const }, + ) + throw e + } + await Plugin.trigger( + "tool.execute.after", + { + tool: input.tool, + sessionID: input.sessionID, + callID: input.callID ?? "", + args: input.args, + agent: input.agent, + }, + result + ? { ...result, status: "success" as const } + : { title: "", output: "", metadata: {}, status: "error" as const }, + ) + return result + } + export function define( id: string, init: Info["init"] | Awaited["init"]>>, diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index bd4ba530498d..9914e40c1830 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -182,16 +182,17 @@ export interface Hooks { output: { parts: Part[] }, ) => Promise "tool.execute.before"?: ( - input: { tool: string; sessionID: string; callID: string }, + input: { tool: string; sessionID: string; callID: string; agent?: string }, output: { args: any }, ) => Promise "shell.env"?: (input: { cwd: string }, output: { env: Record }) => Promise "tool.execute.after"?: ( - input: { tool: string; sessionID: string; callID: string; args: any }, + input: { tool: string; sessionID: string; callID: string; args: any; agent?: string }, output: { title: string output: string metadata: any + status?: "success" | "error" }, ) => Promise "experimental.chat.messages.transform"?: ( From 5573dcbe0eb7888f720cda6278852a8fcdbb0624 Mon Sep 17 00:00:00 2001 From: ArmirKS Date: Thu, 19 Feb 2026 02:54:46 +0100 Subject: [PATCH 2/2] ci: retrigger checks