Fix browser tool workspace context

This commit is contained in:
Mohamed Boudra
2026-07-02 18:39:10 +02:00
parent 58fce6622b
commit 3eed8c31a2
5 changed files with 245 additions and 96 deletions
@@ -1495,6 +1495,7 @@ describe("executeAutomationCommand", () => {
it("sets workspace files on a file input ref through CDP", async () => {
const debugCommands: Array<{ command: string; params?: Record<string, unknown> }> = [];
const workspaceRoot = "/tmp/paseo-workspace-a";
const workspaceOpaqueId = "wks_workspace_a";
const tab = fakeTab({
id: 25,
executeJavaScript: async () =>
@@ -1519,18 +1520,20 @@ describe("executeAutomationCommand", () => {
},
});
const registry = createRegistry({
getWorkspaceActiveTabContents: (workspaceId) =>
workspaceId === workspaceRoot ? tab : null,
getWorkspaceActiveBrowserId: (workspaceId) => (workspaceId === workspaceRoot ? "a" : null),
getBrowserWorkspaceId: (id) => (id === "a" ? workspaceRoot : null),
getWorkspaceActiveTabContents: (requestedWorkspaceId) =>
requestedWorkspaceId === workspaceOpaqueId ? tab : null,
getWorkspaceActiveBrowserId: (requestedWorkspaceId) =>
requestedWorkspaceId === workspaceOpaqueId ? "a" : null,
getBrowserWorkspaceId: (id) => (id === "a" ? workspaceOpaqueId : null),
});
const snapshotEngine = new BrowserSnapshotEngine();
const snapshot = await executeAutomationCommand(
{
type: "browser.automation.execute.request",
requestId: "r-snapshot-upload",
workspaceId: workspaceRoot,
command: { command: "snapshot", args: { workspaceId: workspaceRoot } },
cwd: workspaceRoot,
workspaceId: workspaceOpaqueId,
command: { command: "snapshot", args: { workspaceId: workspaceOpaqueId } },
},
registry,
{ snapshotEngine },
@@ -1548,10 +1551,11 @@ describe("executeAutomationCommand", () => {
{
type: "browser.automation.execute.request",
requestId: "r-upload",
workspaceId: workspaceRoot,
cwd: workspaceRoot,
workspaceId: workspaceOpaqueId,
command: {
command: "upload",
args: { workspaceId: workspaceRoot, ref, filePaths: ["uploads/file.txt"] },
args: { workspaceId: workspaceOpaqueId, ref, filePaths: ["uploads/file.txt"] },
},
},
registry,
@@ -1576,6 +1580,7 @@ describe("executeAutomationCommand", () => {
it("rejects upload paths outside the workspace", async () => {
const workspaceRoot = "/tmp/paseo-workspace-a";
const workspaceOpaqueId = "wks_workspace_a";
const debugCommands: Array<{ command: string; params?: Record<string, unknown> }> = [];
const tab = fakeTab({
id: 26,
@@ -1601,18 +1606,20 @@ describe("executeAutomationCommand", () => {
},
});
const registry = createRegistry({
getWorkspaceActiveTabContents: (workspaceId) =>
workspaceId === workspaceRoot ? tab : null,
getWorkspaceActiveBrowserId: (workspaceId) => (workspaceId === workspaceRoot ? "a" : null),
getBrowserWorkspaceId: (id) => (id === "a" ? workspaceRoot : null),
getWorkspaceActiveTabContents: (requestedWorkspaceId) =>
requestedWorkspaceId === workspaceOpaqueId ? tab : null,
getWorkspaceActiveBrowserId: (requestedWorkspaceId) =>
requestedWorkspaceId === workspaceOpaqueId ? "a" : null,
getBrowserWorkspaceId: (id) => (id === "a" ? workspaceOpaqueId : null),
});
const snapshotEngine = new BrowserSnapshotEngine();
const snapshot = await executeAutomationCommand(
{
type: "browser.automation.execute.request",
requestId: "r-snapshot-upload-outside",
workspaceId: workspaceRoot,
command: { command: "snapshot", args: { workspaceId: workspaceRoot } },
cwd: workspaceRoot,
workspaceId: workspaceOpaqueId,
command: { command: "snapshot", args: { workspaceId: workspaceOpaqueId } },
},
registry,
{ snapshotEngine },
@@ -1629,10 +1636,11 @@ describe("executeAutomationCommand", () => {
{
type: "browser.automation.execute.request",
requestId: "r-upload-outside",
workspaceId: workspaceRoot,
cwd: workspaceRoot,
workspaceId: workspaceOpaqueId,
command: {
command: "upload",
args: { workspaceId: workspaceRoot, ref, filePaths: ["../secret.txt"] },
args: { workspaceId: workspaceOpaqueId, ref, filePaths: ["../secret.txt"] },
},
},
registry,
@@ -1652,6 +1660,86 @@ describe("executeAutomationCommand", () => {
},
});
});
it("rejects upload when the request has no cwd", async () => {
const workspaceOpaqueId = "wks_workspace_a";
const debugCommands: Array<{ command: string; params?: Record<string, unknown> }> = [];
const tab = fakeTab({
id: 28,
executeJavaScript: async () =>
JSON.stringify([
{
role: "textbox",
tagName: "input",
text: "",
selector: "#file",
attributes: { id: "file", type: "file" },
},
]),
sendDebugCommand: async (command, params) => {
debugCommands.push({ command, params });
if (command === "DOM.getDocument") {
return { root: { nodeId: 1 } };
}
if (command === "DOM.querySelector") {
return { nodeId: 2 };
}
return {};
},
});
const registry = createRegistry({
getWorkspaceActiveTabContents: (requestedWorkspaceId) =>
requestedWorkspaceId === workspaceOpaqueId ? tab : null,
getWorkspaceActiveBrowserId: (requestedWorkspaceId) =>
requestedWorkspaceId === workspaceOpaqueId ? "a" : null,
getBrowserWorkspaceId: (id) => (id === "a" ? workspaceOpaqueId : null),
});
const snapshotEngine = new BrowserSnapshotEngine();
const snapshot = await executeAutomationCommand(
{
type: "browser.automation.execute.request",
requestId: "r-snapshot-upload-missing-cwd",
workspaceId: workspaceOpaqueId,
command: { command: "snapshot", args: { workspaceId: workspaceOpaqueId } },
},
registry,
{ snapshotEngine },
);
if (!snapshot.ok || snapshot.result.command !== "snapshot") {
throw new Error("snapshot failed");
}
const ref = snapshot.result.elements[0]?.ref;
if (!ref) {
throw new Error("missing upload ref");
}
const result = await executeAutomationCommand(
{
type: "browser.automation.execute.request",
requestId: "r-upload-missing-cwd",
workspaceId: workspaceOpaqueId,
command: {
command: "upload",
args: { workspaceId: workspaceOpaqueId, ref, filePaths: ["uploads/file.txt"] },
},
},
registry,
{ snapshotEngine },
);
expect(debugCommands).not.toContainEqual(
expect.objectContaining({ command: "DOM.setFileInputFiles" }),
);
expect(result).toEqual({
requestId: "r-upload-missing-cwd",
ok: false,
error: {
code: "browser_unsupported",
message: "browser_upload requires request cwd",
retryable: false,
},
});
});
});
describe("navigation", () => {
@@ -319,6 +319,7 @@ const commandHandlers: Partial<Record<BrowserAutomationCommand["command"], Comma
const uploadCommand = command as Extract<BrowserAutomationCommand, { command: "upload" }>;
return executeUpload(
requestId,
request.cwd,
workspaceId,
uploadCommand.args.browserId ?? request.browserId,
{ ref: uploadCommand.args.ref, filePaths: uploadCommand.args.filePaths },
@@ -1273,6 +1274,7 @@ function isAllowedPageUrl(value: string): boolean {
async function executeUpload(
requestId: string,
cwd: string | undefined,
workspaceId: string | undefined,
browserId: string | undefined,
input: { ref: string; filePaths: string[] },
@@ -1309,9 +1311,9 @@ async function executeUpload(
if (typeof queried.nodeId !== "number" || queried.nodeId <= 0) {
return staleRefFailure(requestId, input.ref);
}
const workspaceRoot = resolveUploadWorkspaceRoot({ workspaceId, target, registry });
const workspaceRoot = resolveUploadWorkspaceRoot(cwd);
if (!workspaceRoot) {
return fail(requestId, "browser_unsupported", "browser_upload requires a workspace target");
return fail(requestId, "browser_unsupported", "browser_upload requires request cwd");
}
const filePaths = resolveWorkspaceFilePaths(input.filePaths, workspaceRoot);
if (!filePaths) {
@@ -1338,14 +1340,8 @@ async function executeUpload(
};
}
function resolveUploadWorkspaceRoot(params: {
workspaceId: string | undefined;
target: ResolvedTabTarget;
registry: BrowserRegistry;
}): string | null {
const workspaceRoot =
params.workspaceId ?? params.registry.getBrowserWorkspaceId(params.target.browserId);
return workspaceRoot ? resolvePath(workspaceRoot) : null;
function resolveUploadWorkspaceRoot(cwd: string | undefined): string | null {
return cwd ? resolvePath(cwd) : null;
}
function resolveWorkspaceFilePaths(filePaths: string[], workspaceRoot: string): string[] | null {