Skip to content

Commit de565e5

Browse files
Harden provider debug option reads against traps
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 12150c5 commit de565e5

5 files changed

Lines changed: 78 additions & 2 deletions

File tree

currentState.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ HyperAgent exposes a TypeScript SDK for browser automation with three primary pa
153153
- Expanded settle trace regressions with cleaner log-capture coverage to ensure recording-video trace diagnostics remain validated without noisy test output.
154154
- Hardened settle debug-option lookup against trap-prone `getDebugOptions()` reads, with deterministic fallback trace defaults and sanitized warning diagnostics.
155155
- Hardened A11y DOM option ingestion (`useCache`, `onFrameChunk`, `filterAdTrackingFrames`) with trap-safe reads, so malformed option objects no longer break extraction setup.
156+
- Hardened OpenAI/Anthropic structured-schema debug-option reads so trap-prone debug-option access no longer interrupts structured invocation paths.
156157
- Hardened constructor LLM validation to reject malformed non-provider/non-client `llm` payloads instead of accepting invalid runtime objects, while preserving trap-safe config reads.
157158
- Added explicit constructor regression coverage for malformed partial `llm` objects to lock in fail-fast configuration behavior.
158159
- Hardened prompt final-goal rendering against malformed/trap-prone task inputs by normalizing non-string goals into bounded readable diagnostics instead of throwing.

src/llm/providers/anthropic.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ jest.mock("@/debug/options", () => ({
3939
getDebugOptions: jest.fn(() => debugOptions),
4040
}));
4141

42+
const { getDebugOptions } = jest.requireMock("@/debug/options") as {
43+
getDebugOptions: jest.Mock;
44+
};
45+
4246
describe("AnthropicClient", () => {
4347
beforeEach(() => {
4448
createMessageMock.mockReset();
@@ -346,6 +350,36 @@ describe("AnthropicClient", () => {
346350
}
347351
});
348352

353+
it("continues structured invocation when debug option getter traps", async () => {
354+
createMessageMock.mockResolvedValue({
355+
content: [
356+
{
357+
type: "tool_use",
358+
input: {
359+
result: {
360+
value: "ok",
361+
},
362+
},
363+
},
364+
],
365+
});
366+
getDebugOptions.mockImplementationOnce(() => {
367+
throw new Error("debug options trap");
368+
});
369+
370+
const client = new AnthropicClient({ model: "claude-test" });
371+
const result = await client.invokeStructured(
372+
{
373+
schema: z.object({
374+
value: z.string(),
375+
}),
376+
},
377+
[{ role: "user", content: "extract value" }]
378+
);
379+
380+
expect(result.parsed).toEqual({ value: "ok" });
381+
});
382+
349383
it("ignores reserved provider option overrides while preserving custom options", async () => {
350384
createMessageMock.mockResolvedValue({
351385
content: [{ type: "text", text: "ok" }],

src/llm/providers/anthropic.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,12 @@ const RESERVED_ANTHROPIC_PROVIDER_OPTION_KEYS = new Set([
3535
const MAX_ANTHROPIC_DIAGNOSTIC_CHARS = 300;
3636

3737
function shouldDebugStructuredSchema(): boolean {
38-
const opts = getDebugOptions();
38+
let opts: ReturnType<typeof getDebugOptions>;
39+
try {
40+
opts = getDebugOptions();
41+
} catch {
42+
return ENV_STRUCTURED_SCHEMA_DEBUG;
43+
}
3944
if (opts.enabled && typeof opts.structuredSchema === "boolean") {
4045
return opts.structuredSchema;
4146
}

src/llm/providers/openai.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ jest.mock("@/debug/options", () => ({
3333
getDebugOptions: jest.fn(() => debugOptions),
3434
}));
3535

36+
const { getDebugOptions } = jest.requireMock("@/debug/options") as {
37+
getDebugOptions: jest.Mock;
38+
};
39+
3640
describe("OpenAIClient", () => {
3741
beforeEach(() => {
3842
createCompletionMock.mockReset();
@@ -297,6 +301,33 @@ describe("OpenAIClient", () => {
297301
}
298302
});
299303

304+
it("continues structured invocation when debug option getter traps", async () => {
305+
createCompletionMock.mockResolvedValue({
306+
choices: [
307+
{
308+
message: {
309+
content: '{"ok":"yes"}',
310+
},
311+
},
312+
],
313+
});
314+
getDebugOptions.mockImplementationOnce(() => {
315+
throw new Error("debug options trap");
316+
});
317+
318+
const client = new OpenAIClient({ model: "gpt-test" });
319+
const result = await client.invokeStructured(
320+
{
321+
schema: z.object({
322+
ok: z.string(),
323+
}),
324+
},
325+
[{ role: "user", content: "hello" }]
326+
);
327+
328+
expect(result.parsed).toEqual({ ok: "yes" });
329+
});
330+
300331
it("throws readable error when completion choices are unreadable", async () => {
301332
const response = new Proxy(
302333
{},

src/llm/providers/openai.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,12 @@ function safeReadUsageTokens(
122122
}
123123

124124
function shouldDebugStructuredSchema(): boolean {
125-
const opts = getDebugOptions();
125+
let opts: ReturnType<typeof getDebugOptions>;
126+
try {
127+
opts = getDebugOptions();
128+
} catch {
129+
return ENV_STRUCTURED_SCHEMA_DEBUG;
130+
}
126131
if (opts.enabled && typeof opts.structuredSchema === "boolean") {
127132
return opts.structuredSchema;
128133
}

0 commit comments

Comments
 (0)