




















@@ -1,5 +1,6 @@
11import {
22callGatewayTool,
3+runBeforeToolCallHook,
34type EmbeddedRunAttemptParams,
45} from "openclaw/plugin-sdk/agent-harness-runtime";
56import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -8,9 +9,14 @@ import { buildApprovalResponse, handleCodexAppServerApprovalRequest } from "./ap
89vi.mock("openclaw/plugin-sdk/agent-harness-runtime", async (importOriginal) => ({
910 ...(await importOriginal<typeof import("openclaw/plugin-sdk/agent-harness-runtime")>()),
1011callGatewayTool: vi.fn(),
12+runBeforeToolCallHook: vi.fn(async ({ params }: { params: unknown }) => ({
13+blocked: false,
14+ params,
15+})),
1116}));
12171318const mockCallGatewayTool = vi.mocked(callGatewayTool);
19+const mockRunBeforeToolCallHook = vi.mocked(runBeforeToolCallHook);
14201521function requireRecord(value: unknown, label: string): Record<string, unknown> {
1622if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -41,7 +47,13 @@ function gatewayCallMethod(callIndex = 0) {
41474248function findApprovalEvent(
4349params: EmbeddedRunAttemptParams,
44-fields: { status?: string; approvalId?: string; command?: string; reason?: string },
50+fields: {
51+status?: string;
52+approvalId?: string;
53+command?: string;
54+reason?: string;
55+message?: string;
56+},
4557) {
4658const onAgentEvent = params.onAgentEvent as unknown as { mock?: { calls?: unknown[][] } };
4759const calls = onAgentEvent.mock?.calls;
@@ -58,7 +70,8 @@ function findApprovalEvent(
5870(!fields.status || data.status === fields.status) &&
5971(!fields.approvalId || data.approvalId === fields.approvalId) &&
6072(!fields.command || data.command === fields.command) &&
61-(!fields.reason || data.reason === fields.reason)
73+(!fields.reason || data.reason === fields.reason) &&
74+(!fields.message || data.message === fields.message)
6275) {
6376return data;
6477}
@@ -81,6 +94,11 @@ function createParams(): EmbeddedRunAttemptParams {
8194describe("Codex app-server approval bridge", () => {
8295beforeEach(() => {
8396mockCallGatewayTool.mockReset();
97+mockRunBeforeToolCallHook.mockReset();
98+mockRunBeforeToolCallHook.mockImplementation(async ({ params }) => ({
99+blocked: false,
100+ params,
101+}));
84102});
8510386104it("routes command approvals through plugin approvals and accepts allowed commands", async () => {
@@ -116,10 +134,123 @@ describe("Codex app-server approval bridge", () => {
116134expect(requestPayload.turnSourceChannel).toBe("telegram");
117135expect(requestPayload.turnSourceTo).toBe("chat-1");
118136expect(gatewayCallOptions()).toEqual({ expectFinal: false });
137+expect(mockRunBeforeToolCallHook).toHaveBeenCalledWith({
138+toolName: "bash",
139+params: {
140+command: "pnpm test extensions/codex/src/app-server",
141+approval: {
142+threadId: "thread-1",
143+turnId: "turn-1",
144+itemId: "cmd-1",
145+command: "pnpm test extensions/codex/src/app-server",
146+},
147+},
148+toolCallId: "cmd-1",
149+approvalMode: "report",
150+signal: undefined,
151+ctx: {
152+agentId: "main",
153+sessionKey: "agent:main:session-1",
154+channelId: "telegram",
155+},
156+});
119157findApprovalEvent(params, { status: "pending", approvalId: "plugin:approval-1" });
120158findApprovalEvent(params, { status: "approved", approvalId: "plugin:approval-1" });
121159});
122160161+it("denies command approvals before prompting when OpenClaw tool policy blocks", async () => {
162+const params = createParams();
163+mockRunBeforeToolCallHook.mockResolvedValueOnce({
164+blocked: true,
165+kind: "veto",
166+deniedReason: "plugin-before-tool-call",
167+reason: "blocked by policy",
168+});
169+170+const result = await handleCodexAppServerApprovalRequest({
171+method: "item/commandExecution/requestApproval",
172+requestParams: {
173+threadId: "thread-1",
174+turnId: "turn-1",
175+itemId: "cmd-blocked",
176+command: "cat /tmp/private_key",
177+},
178+paramsForRun: params,
179+threadId: "thread-1",
180+turnId: "turn-1",
181+});
182+183+expect(result).toEqual({ decision: "decline" });
184+expect(mockCallGatewayTool).not.toHaveBeenCalled();
185+findApprovalEvent(params, { status: "denied" });
186+});
187+188+it("denies command approvals when OpenClaw tool policy rewrites params", async () => {
189+const params = createParams();
190+mockRunBeforeToolCallHook.mockResolvedValueOnce({
191+blocked: false,
192+params: {
193+command: "echo rewritten",
194+approval: {
195+threadId: "thread-1",
196+turnId: "turn-1",
197+itemId: "cmd-rewritten",
198+command: "echo rewritten",
199+},
200+},
201+});
202+203+const result = await handleCodexAppServerApprovalRequest({
204+method: "item/commandExecution/requestApproval",
205+requestParams: {
206+threadId: "thread-1",
207+turnId: "turn-1",
208+itemId: "cmd-rewritten",
209+command: "cat /tmp/private_key",
210+},
211+paramsForRun: params,
212+threadId: "thread-1",
213+turnId: "turn-1",
214+});
215+216+expect(result).toEqual({ decision: "decline" });
217+expect(mockCallGatewayTool).not.toHaveBeenCalled();
218+findApprovalEvent(params, {
219+status: "denied",
220+message:
221+"OpenClaw tool policy rewrote Codex app-server approval params; refusing original request.",
222+});
223+});
224+225+it("denies command approvals when OpenClaw tool policy requires approval", async () => {
226+const params = createParams();
227+mockRunBeforeToolCallHook.mockResolvedValueOnce({
228+blocked: true,
229+kind: "failure",
230+deniedReason: "plugin-approval",
231+reason: "Plugin approval required",
232+});
233+const result = await handleCodexAppServerApprovalRequest({
234+method: "item/commandExecution/requestApproval",
235+requestParams: {
236+threadId: "thread-1",
237+turnId: "turn-1",
238+itemId: "cmd-needs-approval",
239+command: "pnpm test",
240+},
241+paramsForRun: params,
242+threadId: "thread-1",
243+turnId: "turn-1",
244+});
245+246+expect(result).toEqual({ decision: "decline" });
247+expect(mockCallGatewayTool).not.toHaveBeenCalled();
248+findApprovalEvent(params, {
249+status: "denied",
250+message: "Plugin approval required",
251+});
252+});
253+123254it("describes command approvals from parsed command actions when available", async () => {
124255const params = createParams();
125256mockCallGatewayTool
@@ -143,6 +274,12 @@ describe("Codex app-server approval bridge", () => {
143274const requestPayload = gatewayRequestPayload();
144275expect(String(requestPayload.description)).toContain("Command: pnpm test extensions/codex");
145276expect(String(requestPayload.description)).not.toContain("bash -lc");
277+expect(mockRunBeforeToolCallHook.mock.calls.at(0)?.[0]).toMatchObject({
278+toolName: "bash",
279+params: {
280+command: "bash -lc 'pnpm test extensions/codex'",
281+},
282+});
146283findApprovalEvent(params, { command: "pnpm test extensions/codex" });
147284});
148285此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。