diff --git a/extensions/codex/src/app-server/dynamic-tools.test.ts b/extensions/codex/src/app-server/dynamic-tools.test.ts index 0f28ecf6412..417332288ec 100644 --- a/extensions/codex/src/app-server/dynamic-tools.test.ts +++ b/extensions/codex/src/app-server/dynamic-tools.test.ts @@ -1,4 +1,9 @@ import type { AgentToolResult } from "openclaw/plugin-sdk/agent-core"; +import { + onInternalDiagnosticEvent, + waitForDiagnosticEventsDrained, + type DiagnosticEventPayload, +} from "openclaw/plugin-sdk/diagnostic-runtime"; import type { AnyAgentTool } from "openclaw/plugin-sdk/agent-harness"; import { HEARTBEAT_RESPONSE_TOOL_NAME, @@ -293,18 +298,33 @@ describe("createCodexDynamicToolBridge", () => { it("quarantines dynamic tools with unsupported input schemas", async () => { const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined); + const diagnosticEvents: DiagnosticEventPayload[] = []; + const unsubscribeDiagnostics = onInternalDiagnosticEvent((event) => + diagnosticEvents.push(event), + ); const badExecute = vi.fn(); - const bridge = createCodexDynamicToolBridge({ - tools: [ - createTool({ name: "message" }), - createTool({ - name: "dofbot_move_angles", - parameters: { type: "array", items: { type: "number" } }, - execute: badExecute, - }), - ], - signal: new AbortController().signal, - }); + let bridge!: ReturnType; + try { + bridge = createCodexDynamicToolBridge({ + tools: [ + createTool({ name: "message" }), + createTool({ + name: "dofbot_move_angles", + parameters: { type: "array", items: { type: "number" } }, + execute: badExecute, + }), + ], + signal: new AbortController().signal, + hookContext: { + runId: "run-1", + sessionId: "session-1", + sessionKey: "agent:main:session-1", + }, + }); + await waitForDiagnosticEventsDrained(); + } finally { + unsubscribeDiagnostics(); + } expect(bridge.availableSpecs.map((tool) => tool.name)).toEqual(["message"]); expect(bridge.specs.map((tool) => tool.name)).toEqual(["message"]); @@ -325,6 +345,23 @@ describe("createCodexDynamicToolBridge", () => { ], }), ); + const blockedEvents = diagnosticEvents.filter( + ( + event, + ): event is Extract => + event.type === "tool.execution.blocked", + ); + expect(blockedEvents).toContainEqual( + expect.objectContaining({ + type: "tool.execution.blocked", + runId: "run-1", + sessionId: "session-1", + sessionKey: "agent:main:session-1", + toolName: "dofbot_move_angles", + deniedReason: "unsupported_tool_schema", + reason: 'dofbot_move_angles.inputSchema.type must be "object"', + }), + ); const result = await bridge.handleToolCall({ threadId: "thread-1", diff --git a/extensions/codex/src/app-server/dynamic-tools.ts b/extensions/codex/src/app-server/dynamic-tools.ts index 606050cf597..d30743c3e03 100644 --- a/extensions/codex/src/app-server/dynamic-tools.ts +++ b/extensions/codex/src/app-server/dynamic-tools.ts @@ -1,4 +1,5 @@ import type { AgentToolResult } from "openclaw/plugin-sdk/agent-core"; +import { emitTrustedDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime"; import { createAgentToolResultMiddlewareRunner, createCodexAppServerToolResultExtensionRunner, @@ -118,6 +119,7 @@ export function createCodexDynamicToolBridge(params: { ...registeredProjection.quarantinedTools, ]); warnQuarantinedDynamicTools(quarantinedTools); + emitQuarantinedDynamicToolDiagnostics(quarantinedTools, params.hookContext); const telemetry: CodexDynamicToolBridge["telemetry"] = { didSendViaMessagingTool: false, messagingToolSentTexts: [], @@ -337,6 +339,23 @@ function warnQuarantinedDynamicTools(tools: readonly CodexDynamicToolSchemaQuara ); } +function emitQuarantinedDynamicToolDiagnostics( + tools: readonly CodexDynamicToolSchemaQuarantine[], + ctx: CodexDynamicToolHookContext | undefined, +): void { + for (const tool of tools) { + emitTrustedDiagnosticEvent({ + type: "tool.execution.blocked", + runId: ctx?.runId, + sessionId: ctx?.sessionId, + sessionKey: ctx?.sessionKey, + toolName: tool.tool, + deniedReason: "unsupported_tool_schema", + reason: tool.violations.join(", "), + }); + } +} + function dedupeQuarantinedDynamicTools( tools: readonly CodexDynamicToolSchemaQuarantine[], ): CodexDynamicToolSchemaQuarantine[] {