Skip to content

Commit 26d360b

Browse files
committed
fix: preserve native hook relay policy fallback
1 parent 0d29b68 commit 26d360b

5 files changed

Lines changed: 88 additions & 7 deletions

File tree

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", () => {
@@ -3190,4 +3194,19 @@ describe("native hook relay command builder", () => {
31903194
"openclaw hooks relay --provider codex --relay-id relay-1 --generation generation-1 --event permission_request --timeout 5000",
31913195
);
31923196
});
3197+
3198+
it("includes explicit unavailable noop mode only for PreToolUse", () => {
3199+
expect(
3200+
buildNativeHookRelayCommand({
3201+
provider: "codex",
3202+
relayId: "relay-1",
3203+
generation: "generation-1",
3204+
event: "pre_tool_use",
3205+
preToolUseUnavailable: "noop",
3206+
executable: "openclaw",
3207+
}),
3208+
).toBe(
3209+
"openclaw hooks relay --provider codex --relay-id relay-1 --generation generation-1 --event pre_tool_use --pre-tool-use-unavailable noop --timeout 5000",
3210+
);
3211+
});
31933212
});

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

Lines changed: 13 additions & 4 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,17 +760,18 @@ 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") {
762-
// A stale or missing relay should not turn basic tool execution into a
763-
// hard failure when OpenClaw has no before-tool policy to enforce. If a
764-
// policy exists, keep the fail-closed behavior.
765-
if (!hasBeforeToolCallPolicy()) {
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") {
766775
return adapter.renderNoopResponse(event);
767776
}
768777
return adapter.renderPreToolUseBlockResponse(message);

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: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,13 @@ describe("native hook relay CLI", () => {
159159
);
160160

161161
expect(exitCode).toBe(0);
162-
expect(stdout.text()).toBe("");
162+
expect(JSON.parse(stdout.text())).toEqual({
163+
hookSpecificOutput: {
164+
hookEventName: "PreToolUse",
165+
permissionDecision: "deny",
166+
permissionDecisionReason: "Native hook relay unavailable",
167+
},
168+
});
163169
expect(stderr.text()).toContain("native hook relay unavailable");
164170
expect(stderr.text()).toContain("generation must be non-empty string");
165171
expect(callGateway).toHaveBeenCalledWith(
@@ -173,7 +179,13 @@ describe("native hook relay CLI", () => {
173179
it.each([
174180
{
175181
event: "pre_tool_use",
176-
stdout: null,
182+
stdout: {
183+
hookSpecificOutput: {
184+
hookEventName: "PreToolUse",
185+
permissionDecision: "deny",
186+
permissionDecisionReason: "Native hook relay unavailable",
187+
},
188+
},
177189
},
178190
{
179191
event: "permission_request",
@@ -265,7 +277,7 @@ describe("native hook relay CLI", () => {
265277
expect(callGateway).not.toHaveBeenCalled();
266278
});
267279

268-
it("keeps PreToolUse unavailable handling observational when there is no before-tool policy", async () => {
280+
it("fails closed for PreToolUse when the gateway relay is unavailable", async () => {
269281
const callGateway = vi.fn(async () => {
270282
throw new Error("gateway closed");
271283
});
@@ -282,6 +294,40 @@ describe("native hook relay CLI", () => {
282294
},
283295
);
284296

297+
expect(exitCode).toBe(0);
298+
expect(JSON.parse(stdout.text())).toEqual({
299+
hookSpecificOutput: {
300+
hookEventName: "PreToolUse",
301+
permissionDecision: "deny",
302+
permissionDecisionReason: "Native hook relay unavailable",
303+
},
304+
});
305+
expect(stderr.text()).toContain("native hook relay unavailable");
306+
});
307+
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+
285331
expect(exitCode).toBe(0);
286332
expect(stdout.text()).toBe("");
287333
expect(stderr.text()).toContain("native hook relay unavailable");

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)