mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(browser): guard act hooks on current tab URL (#104095)
* fix(browser): guard act hooks on current tab URL * test(browser): align navigation policy hook mock * test(browser): exercise real hook navigation guard --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
co-authored by
Peter Steinberger
parent
b683a0a5cf
commit
4bf4cacb37
@@ -0,0 +1,145 @@
|
||||
// Browser tests cover agent.act hook current-tab navigation guard behavior.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { toBrowserErrorResponse } from "../errors.js";
|
||||
import { createBrowserRouteApp, createBrowserRouteResponse } from "./test-helpers.js";
|
||||
|
||||
const chromeMcpMocks = vi.hoisted(() => ({
|
||||
evaluateChromeMcpScript: vi.fn(async () => true),
|
||||
uploadChromeMcpFile: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
const pathMocks = vi.hoisted(() => ({
|
||||
resolveExistingUploadPaths: vi.fn(async ({ requestedPaths }: { requestedPaths: string[] }) => ({
|
||||
ok: true,
|
||||
paths: requestedPaths,
|
||||
})),
|
||||
}));
|
||||
|
||||
const pwMocks = vi.hoisted(() => ({
|
||||
armDialogViaPlaywright: vi.fn(async () => {}),
|
||||
armFileUploadViaPlaywright: vi.fn(async () => {}),
|
||||
clickViaPlaywright: vi.fn(async () => {}),
|
||||
setInputFilesViaPlaywright: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
vi.mock("../chrome-mcp.js", () => ({
|
||||
evaluateChromeMcpScript: chromeMcpMocks.evaluateChromeMcpScript,
|
||||
uploadChromeMcpFile: chromeMcpMocks.uploadChromeMcpFile,
|
||||
}));
|
||||
|
||||
vi.mock("../paths.js", () => pathMocks);
|
||||
|
||||
vi.mock("../pw-ai-module.js", () => ({
|
||||
getPwAiModule: vi.fn(async () => pwMocks),
|
||||
}));
|
||||
|
||||
const { registerBrowserAgentActHookRoutes } = await import("./agent.act.hooks.js");
|
||||
|
||||
function createProfileContext() {
|
||||
return {
|
||||
profile: {
|
||||
cdpIsLoopback: true,
|
||||
cdpUrl: "http://127.0.0.1:9222",
|
||||
driver: "openclaw" as const,
|
||||
name: "default",
|
||||
},
|
||||
ensureTabAvailable: vi.fn(async () => ({
|
||||
targetId: "tab-1",
|
||||
title: "Internal Admin",
|
||||
url: "http://127.0.0.1:8080/admin",
|
||||
type: "page",
|
||||
})),
|
||||
listTabs: vi.fn(async () => []),
|
||||
};
|
||||
}
|
||||
|
||||
function createRouteContext(profileCtx: ReturnType<typeof createProfileContext>) {
|
||||
return {
|
||||
forProfile: () => profileCtx,
|
||||
mapTabError: vi.fn(toBrowserErrorResponse),
|
||||
state: () => ({
|
||||
resolved: {
|
||||
actionTimeoutMs: 60_000,
|
||||
extraArgs: [],
|
||||
ssrfPolicy: { dangerouslyAllowPrivateNetwork: false },
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async function callHook(params: {
|
||||
path: "/hooks/file-chooser" | "/hooks/dialog";
|
||||
body: Record<string, unknown>;
|
||||
profileCtx: ReturnType<typeof createProfileContext>;
|
||||
}) {
|
||||
const { app, postHandlers } = createBrowserRouteApp();
|
||||
registerBrowserAgentActHookRoutes(app, createRouteContext(params.profileCtx) as never);
|
||||
const handler = postHandlers.get(params.path);
|
||||
expect(handler).toBeTypeOf("function");
|
||||
|
||||
const response = createBrowserRouteResponse();
|
||||
await handler?.(
|
||||
{
|
||||
params: {},
|
||||
query: {},
|
||||
body: params.body,
|
||||
},
|
||||
response.res,
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
const blockedHookCases = [
|
||||
{
|
||||
label: "file chooser",
|
||||
path: "/hooks/file-chooser" as const,
|
||||
body: { paths: ["/tmp/upload.txt"], ref: "upload-button" },
|
||||
sideEffects: [
|
||||
pathMocks.resolveExistingUploadPaths,
|
||||
chromeMcpMocks.uploadChromeMcpFile,
|
||||
pwMocks.armFileUploadViaPlaywright,
|
||||
pwMocks.clickViaPlaywright,
|
||||
pwMocks.setInputFilesViaPlaywright,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "dialog",
|
||||
path: "/hooks/dialog" as const,
|
||||
body: { accept: true },
|
||||
sideEffects: [chromeMcpMocks.evaluateChromeMcpScript, pwMocks.armDialogViaPlaywright],
|
||||
},
|
||||
];
|
||||
|
||||
describe("agent act hook current URL guard", () => {
|
||||
beforeEach(() => {
|
||||
for (const fn of Object.values(chromeMcpMocks)) {
|
||||
fn.mockClear();
|
||||
}
|
||||
for (const fn of Object.values(pathMocks)) {
|
||||
fn.mockClear();
|
||||
}
|
||||
for (const fn of Object.values(pwMocks)) {
|
||||
fn.mockClear();
|
||||
}
|
||||
});
|
||||
|
||||
it.each(blockedHookCases)(
|
||||
"blocks $label hooks before page side effects on a disallowed current tab",
|
||||
async ({ path, body, sideEffects }) => {
|
||||
const profileCtx = createProfileContext();
|
||||
|
||||
const response = await callHook({
|
||||
path,
|
||||
body,
|
||||
profileCtx,
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.body).toEqual({ error: expect.stringMatching(/blocked|private/i) });
|
||||
expect(profileCtx.ensureTabAvailable).toHaveBeenCalledOnce();
|
||||
for (const sideEffect of sideEffects) {
|
||||
expect(sideEffect).not.toHaveBeenCalled();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -55,6 +55,7 @@ export function registerBrowserAgentActHookRoutes(
|
||||
res,
|
||||
ctx,
|
||||
targetId,
|
||||
enforceCurrentUrlAllowed: true,
|
||||
run: async ({ profileCtx, cdpUrl, tab, signal }) => {
|
||||
const resolvedResult = await resolveExistingUploadPaths({ requestedPaths: paths });
|
||||
if (!resolvedResult.ok) {
|
||||
@@ -147,6 +148,7 @@ export function registerBrowserAgentActHookRoutes(
|
||||
res,
|
||||
ctx,
|
||||
targetId,
|
||||
enforceCurrentUrlAllowed: true,
|
||||
run: async ({ profileCtx, cdpUrl, tab, signal }) => {
|
||||
if (getBrowserProfileCapabilities(profileCtx.profile).usesChromeMcp) {
|
||||
if (dialogId) {
|
||||
|
||||
Reference in New Issue
Block a user