Skip to content

Commit 00fb47c

Browse files
committed
fix(browser): accept tab aliases in action target ids
1 parent 3f289fd commit 00fb47c

2 files changed

Lines changed: 113 additions & 14 deletions

File tree

extensions/browser/src/browser/routes/agent.act.ts

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
} from "../navigation-guard.js";
2828
import { getBrowserProfileCapabilities } from "../profile-capabilities.js";
2929
import type { BrowserRouteContext } from "../server-context.js";
30+
import { resolveTargetIdFromTabs } from "../target-id.js";
3031
import { matchBrowserUrlPattern } from "../url-pattern.js";
3132
import { registerBrowserAgentActDownloadRoutes } from "./agent.act.download.js";
3233
import {
@@ -35,7 +36,7 @@ import {
3536
jsonActError,
3637
} from "./agent.act.errors.js";
3738
import { registerBrowserAgentActHookRoutes } from "./agent.act.hooks.js";
38-
import { normalizeActRequest, validateBatchTargetIds } from "./agent.act.normalize.js";
39+
import { normalizeActRequest } from "./agent.act.normalize.js";
3940
import { type ActKind, isActKind } from "./agent.act.shared.js";
4041
import {
4142
readBody,
@@ -357,6 +358,66 @@ function getExistingSessionUnsupportedMessage(action: BrowserActRequest): string
357358
throw new Error("Unsupported browser act kind");
358359
}
359360

361+
type TargetComparableTab = {
362+
targetId: string;
363+
suggestedTargetId?: string;
364+
tabId?: string;
365+
label?: string;
366+
};
367+
368+
function actionContainsTargetId(action: BrowserActRequest): boolean {
369+
if (action.targetId) {
370+
return true;
371+
}
372+
return action.kind === "batch" && action.actions.some(actionContainsTargetId);
373+
}
374+
375+
function actionTargetIdMatchesTab(params: {
376+
requestedTargetId: string;
377+
selectedTargetId: string;
378+
tabs: TargetComparableTab[];
379+
}): boolean {
380+
if (params.requestedTargetId === params.selectedTargetId) {
381+
return true;
382+
}
383+
const resolved = resolveTargetIdFromTabs(params.requestedTargetId, params.tabs);
384+
return resolved.ok && resolved.targetId === params.selectedTargetId;
385+
}
386+
387+
function normalizeActionTargetIdsForSelectedTab(params: {
388+
action: BrowserActRequest;
389+
selectedTargetId: string;
390+
tabs: TargetComparableTab[];
391+
mismatchMessage: string;
392+
}): string | null {
393+
if (params.action.targetId) {
394+
if (
395+
!actionTargetIdMatchesTab({
396+
requestedTargetId: params.action.targetId,
397+
selectedTargetId: params.selectedTargetId,
398+
tabs: params.tabs,
399+
})
400+
) {
401+
return params.mismatchMessage;
402+
}
403+
params.action.targetId = params.selectedTargetId;
404+
}
405+
if (params.action.kind === "batch") {
406+
for (const nestedAction of params.action.actions) {
407+
const nestedError = normalizeActionTargetIdsForSelectedTab({
408+
action: nestedAction,
409+
selectedTargetId: params.selectedTargetId,
410+
tabs: params.tabs,
411+
mismatchMessage: "batched action targetId must match request targetId",
412+
});
413+
if (nestedError) {
414+
return nestedError;
415+
}
416+
}
417+
}
418+
return null;
419+
}
420+
360421
/** Register browser action endpoints, including hook and download subroutes. */
361422
export function registerBrowserAgentActRoutes(
362423
app: BrowserRouteRegistrar,
@@ -412,6 +473,9 @@ export function registerBrowserAgentActRoutes(
412473
const hasNavigationResultPolicy = Boolean(
413474
withBrowserNavigationPolicy(ssrfPolicy).ssrfPolicy,
414475
);
476+
const targetComparableTabs = actionContainsTargetId(action)
477+
? await profileCtx.listTabs().catch(() => [] as TargetComparableTab[])
478+
: [];
415479
const jsonOk = async (
416480
extra?: Record<string, unknown>,
417481
options?: { resolveCurrentTarget?: boolean },
@@ -441,13 +505,14 @@ export function registerBrowserAgentActRoutes(
441505
...extra,
442506
});
443507
};
444-
if (action.targetId && action.targetId !== tab.targetId) {
445-
return jsonActError(
446-
res,
447-
403,
448-
ACT_ERROR_CODES.targetIdMismatch,
449-
"action targetId must match request targetId",
450-
);
508+
const targetIdError = normalizeActionTargetIdsForSelectedTab({
509+
action,
510+
selectedTargetId: tab.targetId,
511+
tabs: targetComparableTabs,
512+
mismatchMessage: "action targetId must match request targetId",
513+
});
514+
if (targetIdError) {
515+
return jsonActError(res, 403, ACT_ERROR_CODES.targetIdMismatch, targetIdError);
451516
}
452517
const profileName = profileCtx.profile.name;
453518
if (isExistingSession) {
@@ -661,12 +726,6 @@ export function registerBrowserAgentActRoutes(
661726
if (!pw) {
662727
return;
663728
}
664-
if (action.kind === "batch") {
665-
const targetIdError = validateBatchTargetIds(action.actions, tab.targetId);
666-
if (targetIdError) {
667-
return jsonActError(res, 403, ACT_ERROR_CODES.targetIdMismatch, targetIdError);
668-
}
669-
}
670729
const result = await pw.executeActViaPlaywright({
671730
cdpUrl,
672731
action,

extensions/browser/src/browser/server.agent-contract-core.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,46 @@ describe("browser control server", () => {
196196
slowTimeoutMs,
197197
);
198198

199+
it(
200+
"accepts tabId aliases for top-level action targetId",
201+
async () => {
202+
const base = await startServerAndBase();
203+
const response = await postJson<{ ok: boolean; targetId?: string }>(`${base}/act`, {
204+
kind: "click",
205+
ref: "5",
206+
targetId: "t1",
207+
});
208+
209+
expect(response.ok).toBe(true);
210+
expect(response.targetId).toBe("abcd1234");
211+
const actCall = mockFirstArg(pwMocks.executeActViaPlaywright, 0, "act");
212+
expectRecordFields(actCall, { targetId: "abcd1234" });
213+
expectRecordFields(actCall.action, { targetId: "abcd1234" });
214+
},
215+
slowTimeoutMs,
216+
);
217+
218+
it(
219+
"accepts tabId aliases for batched action targetIds",
220+
async () => {
221+
const base = await startServerAndBase();
222+
const response = await postJson<{ ok: boolean; targetId?: string }>(`${base}/act`, {
223+
kind: "batch",
224+
targetId: "t1",
225+
actions: [{ kind: "click", ref: "5", targetId: "t1" }],
226+
});
227+
228+
expect(response.ok).toBe(true);
229+
expect(response.targetId).toBe("abcd1234");
230+
const actCall = mockFirstArg(pwMocks.executeActViaPlaywright, 0, "act");
231+
expectRecordFields(actCall, { targetId: "abcd1234" });
232+
expectRecordFields(actCall.action, { targetId: "abcd1234" });
233+
const action = actCall.action as { actions?: Array<Record<string, unknown>> };
234+
expectRecordFields(action.actions?.[0], { targetId: "abcd1234" });
235+
},
236+
slowTimeoutMs,
237+
);
238+
199239
it(
200240
"returns the replacement targetId after an action-triggered target swap",
201241
async () => {

0 commit comments

Comments
 (0)