Skip to content

Commit 2f1fe32

Browse files
lsr911claude
andcommitted
fix: cover C1 in sanitizeHookConsoleValue and fix config.test.ts lint
- Add C1 (0x80-0x9f) coverage to sanitizeHookConsoleValue to align with the sink-wide C1 pass - Add regression test: C1 characters in model/summary/status are replaced with space in hook warning console messages - Fix no-base-to-string lint error in config.test.ts by narrowing mock.calls type parameter from unknown to string Co-Authored-By: Claude <[email protected]>
1 parent c817223 commit 2f1fe32

3 files changed

Lines changed: 52 additions & 5 deletions

File tree

src/gateway/server-methods/config.test.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,9 @@ describe("config.openFile", () => {
155155

156156
const { logGateway } = await invokeConfigOpenFile();
157157

158-
const logged = String(
159-
(logGateway.warn as unknown as { mock: { calls: unknown[][] } }).mock.calls.at(-1)?.[0] ??
160-
"",
161-
);
158+
const logged =
159+
(logGateway.warn as unknown as { mock: { calls: string[][] } }).mock.calls.at(-1)?.[0] ??
160+
"";
162161
expect(logged.startsWith("config.openFile failed path=")).toBe(true);
163162
// The raw C1 byte is replaced with "?" before it reaches the warning log.
164163
expect(logged).toContain("cfg?.json");

src/gateway/server/hooks.agent-trust.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,54 @@ describe("dispatchAgentHook trust handling", () => {
362362
expect(requestHeartbeatMock).not.toHaveBeenCalled();
363363
});
364364

365+
it("strips C1 control characters from hook warning console messages", async () => {
366+
// U+009B is the C1 CSI introducer, equivalent to ESC [. A model or
367+
// status string containing it must not reach the hook console message.
368+
const CSI = String.fromCharCode(0x9b);
369+
runCronIsolatedAgentTurnMock.mockResolvedValueOnce({
370+
status: "error",
371+
summary: "tool failure",
372+
diagnostics: {
373+
summary: `model ${CSI}contaminated rejected`,
374+
entries: [
375+
{
376+
ts: 1,
377+
source: "cron-preflight",
378+
severity: "error",
379+
message: `model ${CSI}contaminated rejected`,
380+
},
381+
],
382+
},
383+
delivered: false,
384+
});
385+
386+
dispatchAgentHook({
387+
...buildAgentPayload("Model hook"),
388+
model: `gpt${CSI}4o`,
389+
});
390+
391+
await vi.waitFor(() =>
392+
expect(enqueueSystemEventMock).toHaveBeenCalledWith(
393+
`Hook Model hook (error): model ${CSI}contaminated rejected`,
394+
{
395+
sessionKey: "agent:main:main",
396+
},
397+
),
398+
);
399+
const meta = logWarnMetaFor(
400+
"hook agent run returned non-ok status",
401+
(candidate) => candidate.name === "Model hook",
402+
);
403+
// The raw C1 byte must never appear in the console message.
404+
expect(meta.consoleMessage).not.toContain(CSI);
405+
// The C1 byte in the model field is replaced with space before the log.
406+
expect(meta.consoleMessage).toContain("gpt 4o");
407+
// The diagnostics summary carries C1 through the system event
408+
// (resolveHookRunSummary), but the console message strips it.
409+
expect(meta.consoleMessage).toContain("model");
410+
expect(meta.consoleMessage).toContain("contaminated rejected");
411+
});
412+
365413
it("marks error events as untrusted and sanitizes hook names", async () => {
366414
runCronIsolatedAgentTurnMock.mockRejectedValueOnce(new Error("agent exploded"));
367415

src/gateway/server/hooks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ function sanitizeHookConsoleValue(value: string | undefined): string | undefined
6868
}
6969
const withoutControlChars = Array.from(normalized, (char) => {
7070
const code = char.charCodeAt(0);
71-
return code < 32 || code === 127 ? " " : char;
71+
return code < 32 || code === 127 || (code >= 0x80 && code <= 0x9f) ? " " : char;
7272
}).join("");
7373
return truncateUtf16Safe(withoutControlChars.replace(/\s+/gu, " ").trim(), 500);
7474
}

0 commit comments

Comments
 (0)