Skip to content

Commit a6cffe8

Browse files
Harden constructor custom action ingestion against traps
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent c7bc88d commit a6cffe8

3 files changed

Lines changed: 95 additions & 2 deletions

File tree

currentState.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ HyperAgent exposes a TypeScript SDK for browser automation with three primary pa
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.
145145
- 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.
146+
- Hardened constructor custom-action ingestion with trap-safe array reads so unreadable custom-action entries are skipped while valid entries continue to register.
146147
- Expanded top-level package exports for key workflow/config types at `@hyperbrowser/agent`.
147148
- Removed stale script entry (`build-dom-tree-script`) and improved README usage docs.
148149
- Added canonical single-action debug writer helper (`writePerformDebug`) while preserving deprecated alias compatibility.

src/agent/__tests__/hyperagent-constructor.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,62 @@ describe("HyperAgent constructor and task controls", () => {
134134
}
135135
});
136136

137+
it("continues constructor when customActions length getter traps", () => {
138+
const trappedCustomActions = new Proxy(
139+
[
140+
{
141+
type: "trappedAction",
142+
actionParams: z.object({}),
143+
run: async () => ({ success: true, message: "noop" }),
144+
},
145+
],
146+
{
147+
get: (target, prop, receiver) => {
148+
if (prop === "length") {
149+
throw new Error("customActions length trap");
150+
}
151+
return Reflect.get(target, prop, receiver);
152+
},
153+
}
154+
);
155+
156+
expect(
157+
() =>
158+
new HyperAgent({
159+
llm: createMockLLM(),
160+
customActions: trappedCustomActions as unknown as AgentActionDefinition[],
161+
})
162+
).not.toThrow();
163+
});
164+
165+
it("registers readable custom actions when some customActions entries trap", () => {
166+
const safeAction: AgentActionDefinition = {
167+
type: "safeAction",
168+
actionParams: z.object({}),
169+
run: async () => ({ success: true, message: "ok" }),
170+
};
171+
const trappedCustomActions = new Proxy([{}, safeAction], {
172+
get: (target, prop, receiver) => {
173+
if (prop === "0") {
174+
throw new Error("customActions item trap");
175+
}
176+
return Reflect.get(target, prop, receiver);
177+
},
178+
});
179+
180+
const agent = new HyperAgent({
181+
llm: createMockLLM(),
182+
customActions: trappedCustomActions as unknown as AgentActionDefinition[],
183+
});
184+
const internalAgent = agent as unknown as {
185+
actions: Array<{ type?: string }>;
186+
};
187+
188+
expect(
189+
internalAgent.actions.some((action) => action.type === "safeAction")
190+
).toBe(true);
191+
});
192+
137193
it("throws synchronously for reserved custom action names", () => {
138194
const reservedAction: AgentActionDefinition = {
139195
type: "complete",

src/agent/index.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,40 @@ export class HyperAgent<T extends BrowserProviders = "Local"> {
365365
}
366366
}
367367

368+
private safeArrayLength(value: unknown): number {
369+
if (!Array.isArray(value)) {
370+
return 0;
371+
}
372+
try {
373+
const length = value.length;
374+
if (!Number.isFinite(length) || length < 0) {
375+
return 0;
376+
}
377+
return Math.floor(length);
378+
} catch {
379+
return 0;
380+
}
381+
}
382+
383+
private safeArrayValues<T>(value: unknown): T[] {
384+
const length = this.safeArrayLength(value);
385+
if (length === 0 || !Array.isArray(value)) {
386+
return [];
387+
}
388+
const values: T[] = [];
389+
for (let index = 0; index < length; index += 1) {
390+
try {
391+
const item = value[index] as T | undefined;
392+
if (typeof item !== "undefined") {
393+
values.push(item);
394+
}
395+
} catch {
396+
continue;
397+
}
398+
}
399+
return values;
400+
}
401+
368402
private async startBrowserProvider(): Promise<Browser> {
369403
const startMethod = this.safeReadField(this.browserProvider, "start");
370404
if (typeof startMethod !== "function") {
@@ -1198,8 +1232,10 @@ export class HyperAgent<T extends BrowserProviders = "Local"> {
11981232
)
11991233
) as T extends "Hyperbrowser" ? HyperbrowserProvider : LocalBrowserProvider;
12001234

1201-
const customActions = this.safeReadField(params, "customActions");
1202-
if (Array.isArray(customActions)) {
1235+
const customActions = this.safeArrayValues<AgentActionDefinition>(
1236+
this.safeReadField(params, "customActions")
1237+
);
1238+
if (customActions.length > 0) {
12031239
customActions.forEach((action) => {
12041240
this.registerAction(action);
12051241
});

0 commit comments

Comments
 (0)