Skip to content

Commit fb64546

Browse files
fix: preserve no-policy native hook fallback
Keep selected no-policy Codex PreToolUse relay hooks installed with an explicit unavailable no-op marker, while unknown unavailable PreToolUse and PermissionRequest still fail closed. Refs #87543. Replaces #88620. Verification: - pnpm test extensions/codex/src/app-server/native-hook-relay.test.ts src/agents/harness/native-hook-relay.test.ts src/cli/native-hook-relay-cli.test.ts - pnpm lint --threads=8 - autoreview --mode branch --base origin/main - GitHub CI run 26729700996, Real behavior proof 26729874455, OpenGrep 26729701010, CodeQL high 26729701003 Co-authored-by: woodym-dotcom <[email protected]>
1 parent 83f2900 commit fb64546

7 files changed

Lines changed: 114 additions & 2 deletions

File tree

extensions/codex/src/app-server/native-hook-relay.test.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,41 @@ describe("Codex native hook relay config", () => {
185185
});
186186
});
187187

188+
it("keeps selected no-policy PreToolUse installed with an unavailable no-op marker", () => {
189+
expect(
190+
buildCodexNativeHookRelayConfig({
191+
relay: createRelay({ inactiveEvents: ["pre_tool_use"] }),
192+
events: ["pre_tool_use"],
193+
}),
194+
).toEqual({
195+
"features.hooks": true,
196+
"hooks.PreToolUse": [
197+
{
198+
hooks: [
199+
{
200+
type: "command",
201+
command:
202+
"openclaw hooks relay --provider codex --relay-id relay-1 --generation generation-1 --event pre_tool_use --pre-tool-use-unavailable noop",
203+
timeout: 5,
204+
async: false,
205+
statusMessage: "OpenClaw native hook relay",
206+
},
207+
],
208+
},
209+
],
210+
"hooks.state": {
211+
"/<session-flags>/config.toml:pre_tool_use:0:0": {
212+
enabled: true,
213+
trusted_hash: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
214+
},
215+
"<session-flags>/config.toml:pre_tool_use:0:0": {
216+
enabled: true,
217+
trusted_hash: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
218+
},
219+
},
220+
});
221+
});
222+
188223
it("clears omitted hook events when requested", () => {
189224
expect(
190225
buildCodexNativeHookRelayConfig({
@@ -276,7 +311,11 @@ function createRelay(options?: {
276311
expiresAtMs: Date.now() + 1000,
277312
shouldRelayEvent: (event) => !inactiveEvents.has(event),
278313
commandForEvent: (event) =>
279-
`openclaw hooks relay --provider codex --relay-id relay-1 --generation generation-1 --event ${event}`,
314+
`openclaw hooks relay --provider codex --relay-id relay-1 --generation generation-1 --event ${event}${
315+
event === "pre_tool_use" && inactiveEvents.has(event)
316+
? " --pre-tool-use-unavailable noop"
317+
: ""
318+
}`,
280319
renew: () => undefined,
281320
unregister: () => undefined,
282321
};

extensions/codex/src/app-server/native-hook-relay.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,11 @@ export function buildCodexNativeHookRelayConfig(params: {
231231
for (const event of CODEX_NATIVE_HOOK_RELAY_EVENTS) {
232232
const codexEvent = CODEX_HOOK_EVENT_BY_NATIVE_EVENT[event];
233233
const selected = selectedEvents.has(event);
234-
if (!selected || !params.relay.shouldRelayEvent(event)) {
234+
const shouldRelay = params.relay.shouldRelayEvent(event);
235+
// Keep no-policy PreToolUse commands installed with an explicit no-op marker;
236+
// otherwise a stale relay fallback cannot distinguish no policy from unknown policy.
237+
const selectedNoopPreToolUse = selected && event === "pre_tool_use" && !shouldRelay;
238+
if (!selected || (!shouldRelay && !selectedNoopPreToolUse)) {
235239
if (selected || params.clearOmittedEvents) {
236240
config[`hooks.${codexEvent}`] = [] satisfies JsonValue;
237241
}

src/agents/harness/native-hook-relay.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,10 @@ describe("native hook relay registry", () => {
525525
expect(relay.shouldRelayEvent("post_tool_use")).toBe(false);
526526
expect(relay.shouldRelayEvent("before_agent_finalize")).toBe(false);
527527
expect(relay.shouldRelayEvent("permission_request")).toBe(true);
528+
expect(relay.commandForEvent("pre_tool_use")).toBe(
529+
"/usr/local/bin/node '/opt/Open Claw/openclaw.mjs' hooks relay --provider codex --relay-id " +
530+
`${relay.relayId} --generation ${relay.generation} --event pre_tool_use --pre-tool-use-unavailable noop --timeout 1234`,
531+
);
528532
});
529533

530534
it("builds pre-tool relay commands only when before-tool policy is active", () => {
@@ -3196,4 +3200,19 @@ describe("native hook relay command builder", () => {
31963200
"openclaw hooks relay --provider codex --relay-id relay-1 --generation generation-1 --event permission_request --timeout 5000",
31973201
);
31983202
});
3203+
3204+
it("includes explicit unavailable noop mode only for PreToolUse", () => {
3205+
expect(
3206+
buildNativeHookRelayCommand({
3207+
provider: "codex",
3208+
relayId: "relay-1",
3209+
generation: "generation-1",
3210+
event: "pre_tool_use",
3211+
preToolUseUnavailable: "noop",
3212+
executable: "openclaw",
3213+
}),
3214+
).toBe(
3215+
"openclaw hooks relay --provider codex --relay-id relay-1 --generation generation-1 --event pre_tool_use --pre-tool-use-unavailable noop --timeout 5000",
3216+
);
3217+
});
31993218
});

src/agents/harness/native-hook-relay.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,10 @@ export function registerNativeHookRelay(
439439
relayId,
440440
generation: registration.generation,
441441
event,
442+
preToolUseUnavailable:
443+
event === "pre_tool_use" && !nativeHookRelayEventHasLocalWork(registration, event)
444+
? "noop"
445+
: undefined,
442446
nice: params.command?.nice,
443447
timeoutMs: params.command?.timeoutMs,
444448
executable: params.command?.executable,
@@ -517,6 +521,7 @@ export function buildNativeHookRelayCommand(params: {
517521
relayId: string;
518522
generation?: string;
519523
event: NativeHookRelayEvent;
524+
preToolUseUnavailable?: "noop";
520525
timeoutMs?: number;
521526
executable?: string;
522527
nice?: number | false;
@@ -541,6 +546,9 @@ export function buildNativeHookRelayCommand(params: {
541546
...(params.generation ? ["--generation", params.generation] : []),
542547
"--event",
543548
params.event,
549+
...(params.event === "pre_tool_use" && params.preToolUseUnavailable
550+
? ["--pre-tool-use-unavailable", params.preToolUseUnavailable]
551+
: []),
544552
"--timeout",
545553
String(timeoutMs),
546554
]);
@@ -752,13 +760,20 @@ export async function invokeNativeHookRelayBridge(
752760
export function renderNativeHookRelayUnavailableResponse(params: {
753761
provider: unknown;
754762
event: unknown;
763+
preToolUseUnavailable?: unknown;
755764
message?: string;
756765
}): NativeHookRelayProcessResponse {
757766
const provider = readNativeHookRelayProvider(params.provider);
758767
const event = readNativeHookRelayEvent(params.event);
759768
const adapter = getNativeHookRelayProviderAdapter(provider);
760769
const message = params.message?.trim() || "Native hook relay unavailable";
761770
if (event === "pre_tool_use") {
771+
// The standalone CLI cannot reconstruct the originating registration after
772+
// relay lookup fails, so unavailable PreToolUse must fail closed unless the
773+
// generated command explicitly recorded that no before-tool policy existed.
774+
if (params.preToolUseUnavailable === "noop") {
775+
return adapter.renderNoopResponse(event);
776+
}
762777
return adapter.renderPreToolUseBlockResponse(message);
763778
}
764779
if (event === "permission_request") {

src/cli/hooks-cli.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,10 @@ export function registerHooksCli(program: Command): void {
530530
.requiredOption("--relay-id <id>", "Native hook relay id")
531531
.option("--generation <generation>", "Native hook relay registration generation")
532532
.requiredOption("--event <event>", "Native hook event")
533+
.option(
534+
"--pre-tool-use-unavailable <mode>",
535+
"PreToolUse fallback mode when the originating relay is unavailable",
536+
)
533537
.option("--timeout <ms>", "Gateway timeout in ms", "5000")
534538
.action(async (opts: NativeHookRelayCliOptions) =>
535539
runHooksCliAction(async () => {

src/cli/native-hook-relay-cli.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,34 @@ describe("native hook relay CLI", () => {
305305
expect(stderr.text()).toContain("native hook relay unavailable");
306306
});
307307

308+
it("keeps PreToolUse unavailable handling observational only with an explicit no-policy marker", async () => {
309+
const callGateway = vi.fn(async () => {
310+
throw new Error("gateway closed");
311+
});
312+
const stdout = createWritableTextBuffer();
313+
const stderr = createWritableTextBuffer();
314+
315+
const exitCode = await runNativeHookRelayCli(
316+
{
317+
provider: "codex",
318+
relayId: "relay-1",
319+
generation: "generation-1",
320+
event: "pre_tool_use",
321+
preToolUseUnavailable: "noop",
322+
},
323+
{
324+
stdin: createReadableTextStream("{}"),
325+
stdout,
326+
stderr,
327+
callGateway: callGateway as never,
328+
},
329+
);
330+
331+
expect(exitCode).toBe(0);
332+
expect(stdout.text()).toBe("");
333+
expect(stderr.text()).toContain("native hook relay unavailable");
334+
});
335+
308336
it("fails closed for PermissionRequest when the gateway relay is unavailable", async () => {
309337
const callGateway = vi.fn(async () => {
310338
throw new Error("gateway closed");

src/cli/native-hook-relay-cli.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type NativeHookRelayCliOptions = {
1616
relayId?: string;
1717
generation?: string;
1818
event?: string;
19+
preToolUseUnavailable?: string;
1920
timeout?: string;
2021
};
2122

@@ -76,6 +77,7 @@ export async function runNativeHookRelayCli(
7677
const response = renderNativeHookRelayUnavailableResponse({
7778
provider,
7879
event,
80+
preToolUseUnavailable: opts.preToolUseUnavailable,
7981
message: "Native hook relay unavailable",
8082
});
8183
writeText(stdout, response.stdout);
@@ -101,6 +103,7 @@ export async function runNativeHookRelayCli(
101103
const response = renderNativeHookRelayUnavailableResponse({
102104
provider,
103105
event,
106+
preToolUseUnavailable: opts.preToolUseUnavailable,
104107
message: "Native hook relay unavailable",
105108
});
106109
writeText(stdout, response.stdout);

0 commit comments

Comments
 (0)