

























@@ -2,6 +2,7 @@ import {
22callGatewayTool,
33hasNativeHookRelayInvocation,
44invokeNativeHookRelay,
5+resolveNativeHookRelayDeferredToolApproval,
56runBeforeToolCallHook,
67type EmbeddedRunAttemptParams,
78} from "openclaw/plugin-sdk/agent-harness-runtime";
@@ -13,6 +14,7 @@ vi.mock("openclaw/plugin-sdk/agent-harness-runtime", async (importOriginal) => (
1314callGatewayTool: vi.fn(),
1415hasNativeHookRelayInvocation: vi.fn(() => false),
1516invokeNativeHookRelay: vi.fn(),
17+resolveNativeHookRelayDeferredToolApproval: vi.fn(),
1618runBeforeToolCallHook: vi.fn(async ({ params }: { params: unknown }) => ({
1719blocked: false,
1820 params,
@@ -22,6 +24,9 @@ vi.mock("openclaw/plugin-sdk/agent-harness-runtime", async (importOriginal) => (
2224const mockCallGatewayTool = vi.mocked(callGatewayTool);
2325const mockHasNativeHookRelayInvocation = vi.mocked(hasNativeHookRelayInvocation);
2426const mockInvokeNativeHookRelay = vi.mocked(invokeNativeHookRelay);
27+const mockResolveNativeHookRelayDeferredToolApproval = vi.mocked(
28+resolveNativeHookRelayDeferredToolApproval,
29+);
2530const mockRunBeforeToolCallHook = vi.mocked(runBeforeToolCallHook);
26312732function requireRecord(value: unknown, label: string): Record<string, unknown> {
@@ -103,6 +108,8 @@ describe("Codex app-server approval bridge", () => {
103108mockHasNativeHookRelayInvocation.mockReset();
104109mockHasNativeHookRelayInvocation.mockReturnValue(false);
105110mockInvokeNativeHookRelay.mockReset();
111+mockResolveNativeHookRelayDeferredToolApproval.mockReset();
112+mockResolveNativeHookRelayDeferredToolApproval.mockResolvedValue(undefined);
106113mockRunBeforeToolCallHook.mockReset();
107114mockRunBeforeToolCallHook.mockImplementation(async ({ params }) => ({
108115blocked: false,
@@ -132,7 +139,7 @@ describe("Codex app-server approval bridge", () => {
132139expect(mockRunBeforeToolCallHook).toHaveBeenCalledWith(
133140expect.objectContaining({
134141toolName: "exec",
135-approvalMode: "report",
142+approvalMode: "request",
136143}),
137144);
138145findApprovalEvent(params, {
@@ -212,7 +219,7 @@ describe("Codex app-server approval bridge", () => {
212219},
213220},
214221toolCallId: "cmd-1",
215-approvalMode: "report",
222+approvalMode: "request",
216223signal: undefined,
217224ctx: {
218225agentId: "main",
@@ -385,6 +392,11 @@ describe("Codex app-server approval bridge", () => {
385392expect(result).toEqual({ decision: "accept" });
386393expect(mockRunBeforeToolCallHook).not.toHaveBeenCalled();
387394expect(mockInvokeNativeHookRelay).toHaveBeenCalledTimes(1);
395+expect(mockResolveNativeHookRelayDeferredToolApproval).toHaveBeenCalledWith({
396+relayId: "relay-1",
397+toolUseId: "cmd-native-relay-noop",
398+signal: undefined,
399+});
388400expect(mockCallGatewayTool.mock.calls.map(([method]) => method)).toEqual([
389401"plugin.approval.request",
390402"plugin.approval.waitDecision",
@@ -432,12 +444,53 @@ describe("Codex app-server approval bridge", () => {
432444event: "pre_tool_use",
433445toolUseId: "cmd-native-relay-observed",
434446});
447+expect(mockResolveNativeHookRelayDeferredToolApproval).toHaveBeenCalledWith({
448+relayId: "relay-1",
449+toolUseId: "cmd-native-relay-observed",
450+signal: undefined,
451+});
435452expect(mockCallGatewayTool.mock.calls.map(([method]) => method)).toEqual([
436453"plugin.approval.request",
437454"plugin.approval.waitDecision",
438455]);
439456});
440457458+it("accepts command approvals from deferred native PreToolUse plugin approvals", async () => {
459+const params = createParams();
460+mockHasNativeHookRelayInvocation.mockReturnValueOnce(true);
461+mockResolveNativeHookRelayDeferredToolApproval.mockResolvedValueOnce({
462+handled: true,
463+outcome: "approved-once",
464+});
465+466+const result = await handleCodexAppServerApprovalRequest({
467+method: "item/commandExecution/requestApproval",
468+requestParams: {
469+threadId: "thread-1",
470+turnId: "turn-1",
471+itemId: "cmd-native-relay-deferred",
472+command: "pnpm test extensions/codex/src/app-server",
473+cwd: "/workspace",
474+},
475+paramsForRun: params,
476+threadId: "thread-1",
477+turnId: "turn-1",
478+nativeHookRelay: {
479+relayId: "relay-1",
480+allowedEvents: ["pre_tool_use"],
481+},
482+});
483+484+expect(result).toEqual({ decision: "accept" });
485+expect(mockRunBeforeToolCallHook).not.toHaveBeenCalled();
486+expect(mockInvokeNativeHookRelay).not.toHaveBeenCalled();
487+expect(mockCallGatewayTool).not.toHaveBeenCalled();
488+findApprovalEvent(params, {
489+status: "approved",
490+message: "Codex app-server approval granted for this turn.",
491+});
492+});
493+441494it("fails closed when the native hook relay returns unreadable approval output", async () => {
442495const params = createParams();
443496mockInvokeNativeHookRelay.mockResolvedValueOnce({
@@ -676,6 +729,43 @@ describe("Codex app-server approval bridge", () => {
676729});
677730});
678731732+it("keeps OpenClaw plugin allow-always approvals scoped to one Codex request", async () => {
733+const params = createParams();
734+mockRunBeforeToolCallHook.mockResolvedValueOnce({
735+blocked: false,
736+params: {
737+command: "pnpm test",
738+approval: {
739+threadId: "thread-1",
740+turnId: "turn-1",
741+itemId: "cmd-needs-approval",
742+command: "pnpm test",
743+},
744+},
745+approvalResolution: "allow-always",
746+});
747+748+const result = await handleCodexAppServerApprovalRequest({
749+method: "item/commandExecution/requestApproval",
750+requestParams: {
751+threadId: "thread-1",
752+turnId: "turn-1",
753+itemId: "cmd-needs-approval",
754+command: "pnpm test",
755+},
756+paramsForRun: params,
757+threadId: "thread-1",
758+turnId: "turn-1",
759+});
760+761+expect(result).toEqual({ decision: "accept" });
762+expect(mockCallGatewayTool).not.toHaveBeenCalled();
763+findApprovalEvent(params, {
764+status: "approved",
765+message: "Codex app-server approval granted for this turn.",
766+});
767+});
768+679769it("denies command approvals when OpenClaw tool policy requires approval", async () => {
680770const params = createParams();
681771mockRunBeforeToolCallHook.mockResolvedValueOnce({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。