Skip to content

Commit c7bc88d

Browse files
Harden prompt base-message materialization against traps
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent bb159bd commit c7bc88d

3 files changed

Lines changed: 112 additions & 1 deletion

File tree

‎currentState.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ HyperAgent exposes a TypeScript SDK for browser automation with three primary pa
142142
- Added an additional open-tab fallback path: when the tab array becomes unreadable (e.g. trapped `length`), prompt assembly now still emits the current tab line instead of a blank/no-tabs summary.
143143
- Added constructor regression coverage for trap-prone `llm` config getters, ensuring fallback failure paths stay deterministic and readable.
144144
- Hardened CDP frame-filter URL normalization to support protocol-relative and scheme-less frame URLs while avoiding path-only false positives in host-based ad-domain detection.
145+
- Hardened prompt base-message materialization with trap-safe array reads so malformed/trap-prone seed message arrays no longer crash message assembly and readable entries are preserved.
145146
- Expanded top-level package exports for key workflow/config types at `@hyperbrowser/agent`.
146147
- Removed stale script entry (`build-dom-tree-script`) and improved README usage docs.
147148
- Added canonical single-action debug writer helper (`writePerformDebug`) while preserving deprecated alias compatibility.

‎src/agent/messages/builder.test.ts‎

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,98 @@ describe("buildAgentStepMessages", () => {
252252
expect(joined).toContain("=== Final Goal ===");
253253
});
254254

255+
it("continues when base message array length getter traps", async () => {
256+
const page = createFakePage("https://example.com/current", [
257+
"https://example.com/current",
258+
]);
259+
const trappedBaseMessages = new Proxy(
260+
[{ role: "system", content: "seed message" }],
261+
{
262+
get: (target, prop, receiver) => {
263+
if (prop === "length") {
264+
throw new Error("base message length trap");
265+
}
266+
return Reflect.get(target, prop, receiver);
267+
},
268+
}
269+
) as unknown as Parameters<typeof buildAgentStepMessages>[0];
270+
271+
const messages = await buildAgentStepMessages(
272+
trappedBaseMessages,
273+
[],
274+
"task",
275+
page,
276+
{
277+
elements: new Map(),
278+
domState: "dom",
279+
xpathMap: {},
280+
backendNodeMap: {},
281+
},
282+
undefined,
283+
[]
284+
);
285+
286+
const joined = messages
287+
.map((message) =>
288+
typeof message.content === "string" ? message.content : ""
289+
)
290+
.join("\n");
291+
expect(joined).toContain("=== Final Goal ===");
292+
expect(joined).not.toContain("seed message");
293+
});
294+
295+
it("keeps readable base messages when base message entry getter traps", async () => {
296+
const page = createFakePage("https://example.com/current", [
297+
"https://example.com/current",
298+
]);
299+
const trappedBaseMessages = new Proxy(
300+
[
301+
{ role: "system", content: "trapped message" },
302+
{ role: "system", content: "safe message" },
303+
],
304+
{
305+
get: (target, prop, receiver) => {
306+
if (prop === "0") {
307+
throw new Error("base message item trap");
308+
}
309+
return Reflect.get(target, prop, receiver);
310+
},
311+
}
312+
) as unknown as Parameters<typeof buildAgentStepMessages>[0];
313+
314+
const messages = await buildAgentStepMessages(
315+
trappedBaseMessages,
316+
[],
317+
"task",
318+
page,
319+
{
320+
elements: new Map(),
321+
domState: "dom",
322+
xpathMap: {},
323+
backendNodeMap: {},
324+
},
325+
undefined,
326+
[]
327+
);
328+
329+
expect(messages).toEqual(
330+
expect.arrayContaining([
331+
expect.objectContaining({
332+
role: "system",
333+
content: "safe message",
334+
}),
335+
])
336+
);
337+
expect(messages).not.toEqual(
338+
expect.arrayContaining([
339+
expect.objectContaining({
340+
role: "system",
341+
content: "trapped message",
342+
}),
343+
])
344+
);
345+
});
346+
255347
it("ignores unreadable step array entries when index getter traps", async () => {
256348
const page = createFakePage("https://example.com/current", [
257349
"https://example.com/current",

‎src/agent/messages/builder.ts‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,24 @@ function safeReadArrayItem<T>(value: unknown, index: number): T | undefined {
213213
}
214214
}
215215

216+
function materializeSafeBaseMessages(
217+
baseMessages: HyperAgentMessage[]
218+
): HyperAgentMessage[] {
219+
const total = safeArrayLength(baseMessages);
220+
if (total === 0) {
221+
return [];
222+
}
223+
224+
const normalizedMessages: HyperAgentMessage[] = [];
225+
for (let index = 0; index < total; index += 1) {
226+
const message = safeReadArrayItem<HyperAgentMessage>(baseMessages, index);
227+
if (typeof message !== "undefined") {
228+
normalizedMessages.push(message);
229+
}
230+
}
231+
return normalizedMessages;
232+
}
233+
216234
function getBoundedVariables(variables: HyperVariable[]): {
217235
visibleVariables: HyperVariable[];
218236
omittedCount: number;
@@ -439,7 +457,7 @@ export const buildAgentStepMessages = async (
439457
screenshot: string | undefined,
440458
variables: HyperVariable[]
441459
): Promise<HyperAgentMessage[]> => {
442-
const messages = [...baseMessages];
460+
const messages = materializeSafeBaseMessages(baseMessages);
443461
const normalizedSteps = materializeSafeSteps(steps);
444462

445463
// Add the final goal section

0 commit comments

Comments
 (0)