























@@ -1,3 +1,5 @@
1+import { statSync, writeFileSync } from "node:fs";
2+import { createServer } from "node:http";
13import { afterEach, describe, expect, it, vi } from "vitest";
24import {
35initializeGlobalHookRunner,
@@ -8,6 +10,7 @@ import {
810__testing,
911buildNativeHookRelayCommand,
1012invokeNativeHookRelay,
13+invokeNativeHookRelayBridge,
1114registerNativeHookRelay,
1215} from "./native-hook-relay.js";
1316@@ -17,6 +20,17 @@ afterEach(() => {
1720__testing.clearNativeHookRelaysForTests();
1821});
192223+async function waitForNativeHookRelayBridgeRecord(
24+relayId: string,
25+): Promise<Record<string, unknown>> {
26+let record: Record<string, unknown> | undefined;
27+await vi.waitFor(() => {
28+record = __testing.getNativeHookRelayBridgeRecordForTests(relayId);
29+expect(record).toBeDefined();
30+});
31+return record!;
32+}
33+2034describe("native hook relay registry", () => {
2135it("registers a short-lived relay and builds hidden CLI commands", () => {
2236const relay = registerNativeHookRelay({
@@ -46,6 +60,198 @@ describe("native hook relay registry", () => {
4660);
4761});
486263+it("allows callers to replace a relay at a stable id", () => {
64+const first = registerNativeHookRelay({
65+provider: "codex",
66+relayId: "codex-stable-session",
67+sessionId: "session-1",
68+runId: "run-1",
69+allowedEvents: ["pre_tool_use"],
70+});
71+72+const second = registerNativeHookRelay({
73+provider: "codex",
74+relayId: "codex-stable-session",
75+sessionId: "session-1",
76+runId: "run-2",
77+allowedEvents: ["post_tool_use"],
78+});
79+80+expect(second.relayId).toBe(first.relayId);
81+expect(__testing.getNativeHookRelayRegistrationForTests(first.relayId)).toMatchObject({
82+runId: "run-2",
83+allowedEvents: ["post_tool_use"],
84+});
85+});
86+87+it("exposes registered relays through the direct hook bridge", async () => {
88+const relay = registerNativeHookRelay({
89+provider: "codex",
90+relayId: "codex-bridge-session",
91+sessionId: "session-1",
92+runId: "run-1",
93+allowedEvents: ["pre_tool_use"],
94+});
95+96+const response = await invokeNativeHookRelayBridge({
97+provider: "codex",
98+relayId: relay.relayId,
99+event: "pre_tool_use",
100+timeoutMs: 2_000,
101+rawPayload: {
102+hook_event_name: "PreToolUse",
103+tool_name: "Bash",
104+tool_input: { command: "pnpm test" },
105+},
106+});
107+108+expect(response).toEqual({ stdout: "", stderr: "", exitCode: 0 });
109+expect(__testing.getNativeHookRelayInvocationsForTests()).toEqual([
110+expect.objectContaining({
111+relayId: relay.relayId,
112+event: "pre_tool_use",
113+runId: "run-1",
114+}),
115+]);
116+});
117+118+it("keeps direct bridge registry files private and loopback-only", async () => {
119+const relay = registerNativeHookRelay({
120+provider: "codex",
121+relayId: "codex-private-bridge-session",
122+sessionId: "session-1",
123+runId: "run-1",
124+allowedEvents: ["pre_tool_use"],
125+});
126+127+const record = await waitForNativeHookRelayBridgeRecord(relay.relayId);
128+const bridgeDir = __testing.getNativeHookRelayBridgeDirForTests();
129+const registryPath = __testing.getNativeHookRelayBridgeRegistryPathForTests(relay.relayId);
130+expect(statSync(bridgeDir).mode & 0o077).toBe(0);
131+expect(statSync(registryPath).mode & 0o077).toBe(0);
132+133+writeFileSync(
134+registryPath,
135+`${JSON.stringify({
136+ ...record,
137+ hostname: "192.0.2.1",
138+ expiresAtMs: Date.now() + 10_000,
139+ })}\n`,
140+{ mode: 0o600 },
141+);
142+143+await expect(
144+invokeNativeHookRelayBridge({
145+provider: "codex",
146+relayId: relay.relayId,
147+event: "pre_tool_use",
148+registrationTimeoutMs: 1,
149+timeoutMs: 50,
150+rawPayload: {
151+hook_event_name: "PreToolUse",
152+tool_name: "Bash",
153+tool_input: { command: "pnpm test" },
154+},
155+}),
156+).rejects.toThrow("native hook relay bridge not found");
157+});
158+159+it("binds direct bridge tokens to the relay they were issued for", async () => {
160+const first = registerNativeHookRelay({
161+provider: "codex",
162+relayId: "codex-first-bridge-session",
163+sessionId: "session-1",
164+runId: "run-1",
165+allowedEvents: ["pre_tool_use"],
166+});
167+const second = registerNativeHookRelay({
168+provider: "codex",
169+relayId: "codex-second-bridge-session",
170+sessionId: "session-2",
171+runId: "run-2",
172+allowedEvents: ["pre_tool_use"],
173+});
174+175+const firstRecord = await waitForNativeHookRelayBridgeRecord(first.relayId);
176+await waitForNativeHookRelayBridgeRecord(second.relayId);
177+writeFileSync(
178+__testing.getNativeHookRelayBridgeRegistryPathForTests(second.relayId),
179+`${JSON.stringify({
180+ ...firstRecord,
181+ relayId: second.relayId,
182+ expiresAtMs: Date.now() + 10_000,
183+ })}\n`,
184+{ mode: 0o600 },
185+);
186+187+await expect(
188+invokeNativeHookRelayBridge({
189+provider: "codex",
190+relayId: second.relayId,
191+event: "pre_tool_use",
192+timeoutMs: 500,
193+rawPayload: {
194+hook_event_name: "PreToolUse",
195+tool_name: "Bash",
196+tool_input: { command: "pnpm test" },
197+},
198+}),
199+).rejects.toThrow("native hook relay bridge target mismatch");
200+expect(__testing.getNativeHookRelayInvocationsForTests()).toEqual([]);
201+});
202+203+it("rejects oversized direct bridge responses", async () => {
204+const relay = registerNativeHookRelay({
205+provider: "codex",
206+relayId: "codex-oversized-bridge-response",
207+sessionId: "session-1",
208+runId: "run-1",
209+allowedEvents: ["pre_tool_use"],
210+});
211+const record = await waitForNativeHookRelayBridgeRecord(relay.relayId);
212+const server = createServer((_req, res) => {
213+res.writeHead(200, { "content-type": "application/json" });
214+res.end("x".repeat(5_000_001));
215+});
216+await new Promise<void>((resolve) => {
217+server.listen(0, "127.0.0.1", resolve);
218+});
219+try {
220+const address = server.address();
221+if (!address || typeof address === "string") {
222+throw new Error("test bridge server address unavailable");
223+}
224+writeFileSync(
225+__testing.getNativeHookRelayBridgeRegistryPathForTests(relay.relayId),
226+`${JSON.stringify({
227+ ...record,
228+ port: address.port,
229+ token: "test-token",
230+ expiresAtMs: Date.now() + 10_000,
231+ })}\n`,
232+{ mode: 0o600 },
233+);
234+235+await expect(
236+invokeNativeHookRelayBridge({
237+provider: "codex",
238+relayId: relay.relayId,
239+event: "pre_tool_use",
240+timeoutMs: 500,
241+rawPayload: {
242+hook_event_name: "PreToolUse",
243+tool_name: "Bash",
244+tool_input: { command: "pnpm test" },
245+},
246+}),
247+).rejects.toThrow("native hook relay bridge response too large");
248+} finally {
249+await new Promise<void>((resolve) => {
250+server.close(() => resolve());
251+});
252+}
253+});
254+49255it("accepts an allowed Codex invocation and preserves raw payload", async () => {
50256const relay = registerNativeHookRelay({
51257provider: "codex",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。