mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Tighten browser tool schemas
This commit is contained in:
@@ -13,6 +13,9 @@ const BROWSER_ID = "11111111-1111-4111-8111-111111111111";
|
||||
const BROWSER_ID_MESSAGE =
|
||||
"browserId must be a real id returned by browser_new_tab or browser_list_tabs";
|
||||
const WAIT_CONDITION_MESSAGE = "browser_wait requires exactly one of text or url";
|
||||
const HTTP_URL_MESSAGE = "URL must use http/https only";
|
||||
const WORKSPACE_CONTEXT_MESSAGE =
|
||||
"This browser tool needs a workspace. Start the agent from a Paseo workspace before calling browser_new_tab or browser_list_tabs.";
|
||||
|
||||
interface RegisteredTool {
|
||||
config: PaseoToolConfig;
|
||||
@@ -65,6 +68,10 @@ class BrowserToolHarness {
|
||||
return this.get(name).handler(parsed, {});
|
||||
}
|
||||
|
||||
public outputParses(name: string, output: unknown) {
|
||||
return schemaFor(this.get(name).config.outputSchema).safeParse(output);
|
||||
}
|
||||
|
||||
private get(name: string): RegisteredTool {
|
||||
const tool = this.tools.get(name);
|
||||
if (!tool) {
|
||||
@@ -103,6 +110,49 @@ function listTabsPayload(): Extract<BrowserToolsResponsePayload, { ok: true }> {
|
||||
};
|
||||
}
|
||||
|
||||
function newTabPayload(): Extract<BrowserToolsResponsePayload, { ok: true }> {
|
||||
return {
|
||||
requestId: "req-new-tab",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "new_tab",
|
||||
browserId: BROWSER_ID,
|
||||
workspaceId: "wks_workspace_a",
|
||||
url: "https://example.com",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function snapshotPayload(): Extract<BrowserToolsResponsePayload, { ok: true }> {
|
||||
return {
|
||||
requestId: "req-snapshot",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "snapshot",
|
||||
browserId: BROWSER_ID,
|
||||
workspaceId: "wks_workspace_a",
|
||||
url: "https://example.com",
|
||||
title: "Example",
|
||||
elements: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function screenshotPayload(): Extract<BrowserToolsResponsePayload, { ok: true }> {
|
||||
return {
|
||||
requestId: "req-screenshot",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "screenshot",
|
||||
browserId: BROWSER_ID,
|
||||
mimeType: "image/png",
|
||||
dataBase64: "iVBORw0KGgo=",
|
||||
width: 800,
|
||||
height: 600,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("registerBrowserTools", () => {
|
||||
test("list tabs sends workspace in the request envelope", async () => {
|
||||
const harness = new BrowserToolHarness();
|
||||
@@ -127,16 +177,7 @@ describe("registerBrowserTools", () => {
|
||||
|
||||
test("new tab sends workspace in the request envelope", async () => {
|
||||
const harness = new BrowserToolHarness();
|
||||
harness.broker.setResponse({
|
||||
requestId: "req-new-tab",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "new_tab",
|
||||
browserId: BROWSER_ID,
|
||||
workspaceId: "wks_workspace_a",
|
||||
url: "https://example.com",
|
||||
},
|
||||
});
|
||||
harness.broker.setResponse(newTabPayload());
|
||||
|
||||
const response = await harness.execute("browser_new_tab", { url: "https://example.com" });
|
||||
|
||||
@@ -156,6 +197,155 @@ describe("registerBrowserTools", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
name: "navigate accepts localhost without a scheme as http",
|
||||
toolName: "browser_navigate",
|
||||
input: { browserId: BROWSER_ID, url: "localhost:3000" },
|
||||
expected: { browserId: BROWSER_ID, url: "http://localhost:3000" },
|
||||
},
|
||||
{
|
||||
name: "download accepts an IP host without a scheme as http",
|
||||
toolName: "browser_download",
|
||||
input: { browserId: BROWSER_ID, url: "127.0.0.1:8081/admin" },
|
||||
expected: { browserId: BROWSER_ID, url: "http://127.0.0.1:8081/admin" },
|
||||
},
|
||||
{
|
||||
name: "new tab accepts a domain path without a scheme as http",
|
||||
toolName: "browser_new_tab",
|
||||
input: { url: "example.com/x" },
|
||||
expected: { url: "http://example.com/x" },
|
||||
},
|
||||
{
|
||||
name: "navigate keeps https URLs unchanged",
|
||||
toolName: "browser_navigate",
|
||||
input: { browserId: BROWSER_ID, url: "https://example.com/x" },
|
||||
expected: { browserId: BROWSER_ID, url: "https://example.com/x" },
|
||||
},
|
||||
])("$name", ({ toolName, input, expected }) => {
|
||||
const harness = new BrowserToolHarness();
|
||||
|
||||
const parsed = harness.validate(toolName, input);
|
||||
|
||||
expect(parsed).toEqual({ success: true, data: expected });
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
name: "navigate rejects file URLs",
|
||||
toolName: "browser_navigate",
|
||||
input: { browserId: BROWSER_ID, url: "file:///tmp/index.html" },
|
||||
},
|
||||
{
|
||||
name: "download rejects file URLs",
|
||||
toolName: "browser_download",
|
||||
input: { browserId: BROWSER_ID, url: "file:///tmp/archive.zip" },
|
||||
},
|
||||
{
|
||||
name: "new tab rejects file URLs",
|
||||
toolName: "browser_new_tab",
|
||||
input: { url: "file:///tmp/index.html" },
|
||||
},
|
||||
])("$name", ({ toolName, input }) => {
|
||||
const harness = new BrowserToolHarness();
|
||||
|
||||
const parsed = harness.validate(toolName, input);
|
||||
|
||||
expect(parsed).toMatchObject({
|
||||
success: false,
|
||||
error: { issues: [expect.objectContaining({ message: HTTP_URL_MESSAGE })] },
|
||||
});
|
||||
});
|
||||
|
||||
test("list tabs tells agents without a workspace how to proceed", async () => {
|
||||
const harness = new BrowserToolHarness({ id: "agent-1", cwd: "/repo" });
|
||||
|
||||
const response = await harness.execute("browser_list_tabs", {});
|
||||
|
||||
expect(harness.broker.calls).toEqual([]);
|
||||
expect(response.content).toEqual([{ type: "text", text: WORKSPACE_CONTEXT_MESSAGE }]);
|
||||
expect(response.structuredContent).toEqual({
|
||||
ok: false,
|
||||
error: {
|
||||
code: "browser_denied",
|
||||
message: WORKSPACE_CONTEXT_MESSAGE,
|
||||
retryable: false,
|
||||
},
|
||||
context: {
|
||||
agentId: "agent-1",
|
||||
cwd: "/repo",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("new tab tells agents without a workspace how to proceed", async () => {
|
||||
const harness = new BrowserToolHarness({ id: "agent-1", cwd: "/repo" });
|
||||
|
||||
const response = await harness.execute("browser_new_tab", {});
|
||||
|
||||
expect(harness.broker.calls).toEqual([]);
|
||||
expect(response.content).toEqual([{ type: "text", text: WORKSPACE_CONTEXT_MESSAGE }]);
|
||||
expect(response.structuredContent).toEqual({
|
||||
ok: false,
|
||||
error: {
|
||||
code: "browser_denied",
|
||||
message: WORKSPACE_CONTEXT_MESSAGE,
|
||||
retryable: false,
|
||||
},
|
||||
context: {
|
||||
agentId: "agent-1",
|
||||
cwd: "/repo",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("new tab declares the shape it returns", async () => {
|
||||
const harness = new BrowserToolHarness();
|
||||
harness.broker.setResponse(newTabPayload());
|
||||
|
||||
const response = await harness.execute("browser_new_tab", {});
|
||||
|
||||
expect(harness.outputParses("browser_new_tab", response.structuredContent)).toEqual({
|
||||
success: true,
|
||||
data: response.structuredContent,
|
||||
});
|
||||
});
|
||||
|
||||
test("list tabs declares the shape it returns", async () => {
|
||||
const harness = new BrowserToolHarness();
|
||||
|
||||
const response = await harness.execute("browser_list_tabs", {});
|
||||
|
||||
expect(harness.outputParses("browser_list_tabs", response.structuredContent)).toEqual({
|
||||
success: true,
|
||||
data: response.structuredContent,
|
||||
});
|
||||
});
|
||||
|
||||
test("snapshot declares the shape it returns", async () => {
|
||||
const harness = new BrowserToolHarness();
|
||||
harness.broker.setResponse(snapshotPayload());
|
||||
|
||||
const response = await harness.execute("browser_snapshot", { browserId: BROWSER_ID });
|
||||
|
||||
expect(harness.outputParses("browser_snapshot", response.structuredContent)).toEqual({
|
||||
success: true,
|
||||
data: response.structuredContent,
|
||||
});
|
||||
});
|
||||
|
||||
test("screenshot declares the shape it returns", async () => {
|
||||
const harness = new BrowserToolHarness();
|
||||
harness.broker.setResponse(screenshotPayload());
|
||||
|
||||
const response = await harness.execute("browser_screenshot", { browserId: BROWSER_ID });
|
||||
|
||||
expect(harness.outputParses("browser_screenshot", response.structuredContent)).toEqual({
|
||||
success: true,
|
||||
data: response.structuredContent,
|
||||
});
|
||||
});
|
||||
|
||||
test("snapshot rejects calls without a browser id", () => {
|
||||
const harness = new BrowserToolHarness();
|
||||
|
||||
@@ -180,18 +370,7 @@ describe("registerBrowserTools", () => {
|
||||
|
||||
test("snapshot sends browser id in command args only", async () => {
|
||||
const harness = new BrowserToolHarness();
|
||||
harness.broker.setResponse({
|
||||
requestId: "req-snapshot",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "snapshot",
|
||||
browserId: BROWSER_ID,
|
||||
workspaceId: "wks_workspace_a",
|
||||
url: "https://example.com",
|
||||
title: "Example",
|
||||
elements: [],
|
||||
},
|
||||
});
|
||||
harness.broker.setResponse(snapshotPayload());
|
||||
|
||||
const response = await harness.execute("browser_snapshot", { browserId: BROWSER_ID });
|
||||
|
||||
@@ -288,16 +467,41 @@ describe("registerBrowserTools", () => {
|
||||
expect(response.content).toEqual([{ type: "text", text: "Browser wait matched text." }]);
|
||||
});
|
||||
|
||||
test("tools keep empty context when there is no caller agent", async () => {
|
||||
test("tab tools keep empty context when there is no caller agent", async () => {
|
||||
const harness = new BrowserToolHarness(null, null);
|
||||
harness.broker.setResponse({
|
||||
requestId: "req-page-info",
|
||||
ok: true,
|
||||
result: {
|
||||
command: "page_info",
|
||||
tab: {
|
||||
browserId: BROWSER_ID,
|
||||
url: "https://example.com",
|
||||
title: "Example",
|
||||
isActive: false,
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await harness.execute("browser_list_tabs", {});
|
||||
const response = await harness.execute("browser_page_info", { browserId: BROWSER_ID });
|
||||
|
||||
expect(harness.broker.calls).toEqual([{ command: { command: "list_tabs", args: {} } }]);
|
||||
expect(harness.broker.calls).toEqual([
|
||||
{ command: { command: "page_info", args: { browserId: BROWSER_ID } } },
|
||||
]);
|
||||
expect(response.structuredContent).toEqual({
|
||||
ok: true,
|
||||
result: listTabsPayload().result,
|
||||
context: {},
|
||||
result: {
|
||||
command: "page_info",
|
||||
tab: {
|
||||
browserId: BROWSER_ID,
|
||||
url: "https://example.com",
|
||||
title: "Example",
|
||||
isActive: false,
|
||||
isLoading: false,
|
||||
},
|
||||
},
|
||||
context: { browserId: BROWSER_ID },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,36 @@
|
||||
import { z } from "zod";
|
||||
import { BrowserAutomationBrowserIdSchema } from "@getpaseo/protocol/browser-automation/rpc-schemas";
|
||||
import {
|
||||
BrowserAutomationBackResultSchema,
|
||||
BrowserAutomationBrowserIdSchema,
|
||||
BrowserAutomationCheckResultSchema,
|
||||
BrowserAutomationClearResultSchema,
|
||||
BrowserAutomationClickResultSchema,
|
||||
BrowserAutomationDownloadResultSchema,
|
||||
BrowserAutomationDragResultSchema,
|
||||
BrowserAutomationEnvironmentResultSchema,
|
||||
BrowserAutomationErrorSchema,
|
||||
BrowserAutomationFillResultSchema,
|
||||
BrowserAutomationFocusResultSchema,
|
||||
BrowserAutomationForwardResultSchema,
|
||||
BrowserAutomationFullPageScreenshotResultSchema,
|
||||
BrowserAutomationHoverResultSchema,
|
||||
BrowserAutomationKeypressResultSchema,
|
||||
BrowserAutomationListTabsResultSchema,
|
||||
BrowserAutomationLogsResultSchema,
|
||||
BrowserAutomationNavigateResultSchema,
|
||||
BrowserAutomationNewTabResultSchema,
|
||||
BrowserAutomationPageInfoResultSchema,
|
||||
BrowserAutomationPdfResultSchema,
|
||||
BrowserAutomationReloadResultSchema,
|
||||
BrowserAutomationScreenshotResultSchema,
|
||||
BrowserAutomationSelectResultSchema,
|
||||
BrowserAutomationSetBackgroundResultSchema,
|
||||
BrowserAutomationSnapshotResultSchema,
|
||||
BrowserAutomationStorageResultSchema,
|
||||
BrowserAutomationTypeResultSchema,
|
||||
BrowserAutomationUploadResultSchema,
|
||||
BrowserAutomationWaitResultSchema,
|
||||
} from "@getpaseo/protocol/browser-automation/rpc-schemas";
|
||||
import type { BrowserToolsBroker } from "./broker.js";
|
||||
import type { BrowserToolsResponsePayload } from "./errors.js";
|
||||
import type {
|
||||
@@ -29,26 +60,50 @@ export interface RegisterBrowserToolsOptions {
|
||||
resolveCallerAgent: () => CallerAgentContext | null;
|
||||
}
|
||||
|
||||
const BrowserToolOutputSchema = z.object({
|
||||
ok: z.boolean(),
|
||||
result: z.unknown().optional(),
|
||||
error: z
|
||||
.object({
|
||||
code: z.string(),
|
||||
message: z.string(),
|
||||
retryable: z.boolean(),
|
||||
})
|
||||
.optional(),
|
||||
context: z
|
||||
.object({
|
||||
agentId: z.string().optional(),
|
||||
cwd: z.string().optional(),
|
||||
workspaceId: z.string().optional(),
|
||||
browserId: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
const HTTP_URL_ONLY_MESSAGE = "URL must use http/https only";
|
||||
const WORKSPACE_CONTEXT_MESSAGE =
|
||||
"This browser tool needs a workspace. Start the agent from a Paseo workspace before calling browser_new_tab or browser_list_tabs.";
|
||||
const SCHEMELESS_HTTP_URL_PATTERN =
|
||||
/^(?:localhost|(?:\d{1,3}\.){3}\d{1,3}|\[[0-9a-f:.]+\]|[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+)(?::\d{1,5})?(?:[/?#].*)?$/i;
|
||||
|
||||
const BrowserToolContextSchema = z.object({
|
||||
agentId: z.string().optional(),
|
||||
cwd: z.string().optional(),
|
||||
workspaceId: z.string().optional(),
|
||||
browserId: z.string().optional(),
|
||||
});
|
||||
|
||||
const BrowserToolErrorOutputSchema = z.object({
|
||||
ok: z.literal(false),
|
||||
error: BrowserAutomationErrorSchema,
|
||||
context: BrowserToolContextSchema,
|
||||
});
|
||||
|
||||
function browserToolOutputSchema<ResultSchema extends z.ZodType>(resultSchema: ResultSchema) {
|
||||
return z.discriminatedUnion("ok", [
|
||||
z.object({
|
||||
ok: z.literal(true),
|
||||
result: resultSchema,
|
||||
context: BrowserToolContextSchema,
|
||||
}),
|
||||
BrowserToolErrorOutputSchema,
|
||||
]);
|
||||
}
|
||||
|
||||
const BrowserHttpUrlInputSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((value, context) => {
|
||||
const normalized = normalizeHttpUrlInput(value);
|
||||
if (!normalized) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: HTTP_URL_ONLY_MESSAGE,
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
return normalized;
|
||||
});
|
||||
const BrowserRefInputSchema = z.string().regex(/^@e\d+$/);
|
||||
const BrowserWaitInputSchema = z
|
||||
.object({
|
||||
@@ -61,18 +116,53 @@ const BrowserWaitInputSchema = z
|
||||
message: "browser_wait requires exactly one of text or url",
|
||||
});
|
||||
|
||||
const BrowserToolOutputs = {
|
||||
listTabs: browserToolOutputSchema(BrowserAutomationListTabsResultSchema),
|
||||
newTab: browserToolOutputSchema(BrowserAutomationNewTabResultSchema),
|
||||
pageInfo: browserToolOutputSchema(BrowserAutomationPageInfoResultSchema),
|
||||
snapshot: browserToolOutputSchema(BrowserAutomationSnapshotResultSchema),
|
||||
click: browserToolOutputSchema(BrowserAutomationClickResultSchema),
|
||||
fill: browserToolOutputSchema(BrowserAutomationFillResultSchema),
|
||||
wait: browserToolOutputSchema(BrowserAutomationWaitResultSchema),
|
||||
type: browserToolOutputSchema(BrowserAutomationTypeResultSchema),
|
||||
keypress: browserToolOutputSchema(BrowserAutomationKeypressResultSchema),
|
||||
navigate: browserToolOutputSchema(BrowserAutomationNavigateResultSchema),
|
||||
back: browserToolOutputSchema(BrowserAutomationBackResultSchema),
|
||||
forward: browserToolOutputSchema(BrowserAutomationForwardResultSchema),
|
||||
reload: browserToolOutputSchema(BrowserAutomationReloadResultSchema),
|
||||
screenshot: browserToolOutputSchema(BrowserAutomationScreenshotResultSchema),
|
||||
fullPageScreenshot: browserToolOutputSchema(BrowserAutomationFullPageScreenshotResultSchema),
|
||||
pdf: browserToolOutputSchema(BrowserAutomationPdfResultSchema),
|
||||
download: browserToolOutputSchema(BrowserAutomationDownloadResultSchema),
|
||||
upload: browserToolOutputSchema(BrowserAutomationUploadResultSchema),
|
||||
focus: browserToolOutputSchema(BrowserAutomationFocusResultSchema),
|
||||
clear: browserToolOutputSchema(BrowserAutomationClearResultSchema),
|
||||
check: browserToolOutputSchema(BrowserAutomationCheckResultSchema),
|
||||
select: browserToolOutputSchema(BrowserAutomationSelectResultSchema),
|
||||
hover: browserToolOutputSchema(BrowserAutomationHoverResultSchema),
|
||||
drag: browserToolOutputSchema(BrowserAutomationDragResultSchema),
|
||||
logs: browserToolOutputSchema(BrowserAutomationLogsResultSchema),
|
||||
storage: browserToolOutputSchema(BrowserAutomationStorageResultSchema),
|
||||
environment: browserToolOutputSchema(BrowserAutomationEnvironmentResultSchema),
|
||||
setBackground: browserToolOutputSchema(BrowserAutomationSetBackgroundResultSchema),
|
||||
} as const;
|
||||
|
||||
export function registerBrowserTools(options: RegisterBrowserToolsOptions): void {
|
||||
options.registerTool(
|
||||
"browser_list_tabs",
|
||||
{
|
||||
title: "List browser tabs",
|
||||
description:
|
||||
"List open Paseo desktop browser tabs for this agent's workspace context. Use the returned browserId values for tab-scoped browser tools.",
|
||||
"List open Paseo desktop browser tabs for this agent's workspace. Use returned browserId values with tab-scoped tools.",
|
||||
inputSchema: {},
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.listTabs,
|
||||
},
|
||||
async () => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
const missingWorkspace = requireWorkspaceContext(context);
|
||||
if (missingWorkspace) {
|
||||
return missingWorkspace;
|
||||
}
|
||||
const payload = await options.broker.execute({
|
||||
agentId: context.agentId,
|
||||
cwd: context.cwd,
|
||||
@@ -91,14 +181,18 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
{
|
||||
title: "Create browser tab",
|
||||
description:
|
||||
"Create and focus a new Paseo desktop browser tab in this agent's workspace. Optionally pass an http(s) URL to open immediately. Returns the browserId for later tab-scoped browser tools.",
|
||||
"Create and focus a new Paseo desktop browser tab in this agent's workspace. Pass an http(s) URL or a scheme-less host URL, which is treated as http; the returned browserId is used by tab-scoped tools.",
|
||||
inputSchema: {
|
||||
url: z.string().url().optional(),
|
||||
url: BrowserHttpUrlInputSchema.optional(),
|
||||
},
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.newTab,
|
||||
},
|
||||
async ({ url }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
const missingWorkspace = requireWorkspaceContext(context);
|
||||
if (missingWorkspace) {
|
||||
return missingWorkspace;
|
||||
}
|
||||
const payload = await options.broker.execute({
|
||||
agentId: context.agentId,
|
||||
cwd: context.cwd,
|
||||
@@ -117,11 +211,11 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
{
|
||||
title: "Get browser page info",
|
||||
description:
|
||||
"Get page info for a Paseo desktop browser tab. Pass browserId from browser_new_tab or browser_list_tabs.",
|
||||
"Get page info for a Paseo desktop browser tab. Use browserId from browser_new_tab or browser_list_tabs.",
|
||||
inputSchema: {
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
},
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.pageInfo,
|
||||
},
|
||||
async ({ browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -146,11 +240,11 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
{
|
||||
title: "Snapshot browser page",
|
||||
description:
|
||||
"Return a model-readable snapshot of a Paseo desktop browser tab. Pass browserId from browser_new_tab or browser_list_tabs. Snapshot refs like @e1 are valid until the page changes or a new snapshot is taken.",
|
||||
"Return a model-readable snapshot of a Paseo desktop browser tab. Use browserId from browser_new_tab or browser_list_tabs; refs come from the latest browser_snapshot of the same tab and expire when the page changes.",
|
||||
inputSchema: {
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
},
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.snapshot,
|
||||
},
|
||||
async ({ browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -175,12 +269,12 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
{
|
||||
title: "Click browser element",
|
||||
description:
|
||||
"Click an element ref from the latest browser_snapshot for this agent's workspace browser tab.",
|
||||
"Click an element in a Paseo desktop browser tab. Use browserId from browser_new_tab or browser_list_tabs; refs come from the latest browser_snapshot of the same tab and expire when the page changes.",
|
||||
inputSchema: {
|
||||
ref: BrowserRefInputSchema,
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
},
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.click,
|
||||
},
|
||||
async ({ ref, browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -206,13 +300,13 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
{
|
||||
title: "Fill browser element",
|
||||
description:
|
||||
"Fill an input-like element ref from the latest browser_snapshot for this agent's workspace browser tab.",
|
||||
"Fill an input-like element in a Paseo desktop browser tab. Use browserId from browser_new_tab or browser_list_tabs; refs come from the latest browser_snapshot of the same tab and expire when the page changes.",
|
||||
inputSchema: {
|
||||
ref: BrowserRefInputSchema,
|
||||
value: z.string(),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
},
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.fill,
|
||||
},
|
||||
async ({ ref, value, browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -239,9 +333,9 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
{
|
||||
title: "Wait for browser condition",
|
||||
description:
|
||||
"Wait until a Paseo desktop browser tab contains text or reaches a URL fragment. Pass browserId from browser_new_tab or browser_list_tabs.",
|
||||
"Wait until a Paseo desktop browser tab contains text or reaches a URL fragment. Use browserId from browser_new_tab or browser_list_tabs; waits up to 5s by default on the desktop side.",
|
||||
inputSchema: BrowserWaitInputSchema,
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.wait,
|
||||
},
|
||||
async ({ text, url, timeoutMs, browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -270,13 +364,13 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
{
|
||||
title: "Type into browser",
|
||||
description:
|
||||
"Type text into an element ref from the latest browser_snapshot, or into the currently focused browser element when ref is omitted.",
|
||||
"Type text into an element, or into the focused element when ref is omitted. Use browserId from browser_new_tab or browser_list_tabs; refs come from the latest browser_snapshot of the same tab and expire when the page changes.",
|
||||
inputSchema: {
|
||||
text: z.string(),
|
||||
ref: BrowserRefInputSchema.optional(),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
},
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.type,
|
||||
},
|
||||
async ({ text, ref, browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -303,13 +397,13 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
{
|
||||
title: "Press browser key",
|
||||
description:
|
||||
"Dispatch a keypress to an element ref from the latest browser_snapshot, or to the currently focused browser element when ref is omitted.",
|
||||
"Dispatch a keypress to an element, or to the focused element when ref is omitted. Use browserId from browser_new_tab or browser_list_tabs; refs come from the latest browser_snapshot of the same tab and expire when the page changes.",
|
||||
inputSchema: {
|
||||
key: z.string().min(1),
|
||||
ref: BrowserRefInputSchema.optional(),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
},
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.keypress,
|
||||
},
|
||||
async ({ key, ref, browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -336,9 +430,9 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
{
|
||||
title: "Navigate browser",
|
||||
description:
|
||||
"Navigate a Paseo desktop browser tab to a URL. Pass browserId from browser_new_tab or browser_list_tabs.",
|
||||
inputSchema: { url: z.string().min(1), browserId: BrowserAutomationBrowserIdSchema },
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
"Navigate a Paseo desktop browser tab to a URL. Use browserId from browser_new_tab or browser_list_tabs; pass an http(s) URL or a scheme-less host URL, which is treated as http.",
|
||||
inputSchema: { url: BrowserHttpUrlInputSchema, browserId: BrowserAutomationBrowserIdSchema },
|
||||
outputSchema: BrowserToolOutputs.navigate,
|
||||
},
|
||||
async ({ url, browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -365,9 +459,9 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
name,
|
||||
{
|
||||
title: `Browser ${command}`,
|
||||
description: `${command} a Paseo desktop browser tab. Pass browserId from browser_new_tab or browser_list_tabs.`,
|
||||
description: `${command} a Paseo desktop browser tab. Use browserId from browser_new_tab or browser_list_tabs.`,
|
||||
inputSchema: { browserId: BrowserAutomationBrowserIdSchema },
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs[command],
|
||||
},
|
||||
async ({ browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -393,9 +487,9 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
{
|
||||
title: "Capture browser screenshot",
|
||||
description:
|
||||
"Capture a PNG screenshot of a Paseo desktop browser tab. Pass browserId from browser_new_tab or browser_list_tabs.",
|
||||
"Capture a PNG screenshot of a Paseo desktop browser tab. Use browserId from browser_new_tab or browser_list_tabs.",
|
||||
inputSchema: { browserId: BrowserAutomationBrowserIdSchema },
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.screenshot,
|
||||
},
|
||||
async ({ browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -419,9 +513,10 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
"browser_full_page_screenshot",
|
||||
{
|
||||
title: "Capture full-page browser screenshot",
|
||||
description: "Capture a full-page PNG screenshot of a Paseo desktop browser tab.",
|
||||
description:
|
||||
"Capture a full-page PNG screenshot of a Paseo desktop browser tab. Use browserId from browser_new_tab or browser_list_tabs.",
|
||||
inputSchema: { browserId: BrowserAutomationBrowserIdSchema },
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.fullPageScreenshot,
|
||||
},
|
||||
async ({ browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -446,13 +541,13 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
{
|
||||
title: "Export browser page PDF",
|
||||
description:
|
||||
"Export a Paseo desktop browser tab as a PDF. Pass browserId from browser_new_tab or browser_list_tabs.",
|
||||
"Export a Paseo desktop browser tab as a PDF. Use browserId from browser_new_tab or browser_list_tabs.",
|
||||
inputSchema: {
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
landscape: z.boolean().optional(),
|
||||
printBackground: z.boolean().default(true),
|
||||
},
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.pdf,
|
||||
},
|
||||
async ({ browserId, landscape, printBackground }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -478,13 +573,14 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
"browser_download",
|
||||
{
|
||||
title: "Download file in browser",
|
||||
description: "Download a URL through the Paseo desktop browser session.",
|
||||
description:
|
||||
"Download a URL through a Paseo desktop browser tab. Use browserId from browser_new_tab or browser_list_tabs; pass an http(s) URL or a scheme-less host URL, which is treated as http.",
|
||||
inputSchema: {
|
||||
url: z.string().min(1),
|
||||
url: BrowserHttpUrlInputSchema,
|
||||
fileName: z.string().min(1).optional(),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
},
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.download,
|
||||
},
|
||||
async ({ url, fileName, browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -511,13 +607,13 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
{
|
||||
title: "Upload files in browser",
|
||||
description:
|
||||
"Set workspace files on a file input ref from the latest browser_snapshot. Paths must resolve inside the agent workspace.",
|
||||
"Set workspace files on a file input in a Paseo desktop browser tab. Use browserId from browser_new_tab or browser_list_tabs; refs come from the latest browser_snapshot of the same tab and expire when the page changes.",
|
||||
inputSchema: {
|
||||
ref: BrowserRefInputSchema,
|
||||
filePaths: z.array(z.string().min(1)).min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
},
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.upload,
|
||||
},
|
||||
async ({ ref, filePaths, browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -544,19 +640,22 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
name: "browser_focus",
|
||||
command: "focus",
|
||||
title: "Focus browser element",
|
||||
description: "Focus an element ref from the latest browser_snapshot.",
|
||||
description:
|
||||
"Focus an element in a Paseo desktop browser tab. Use browserId from browser_new_tab or browser_list_tabs; refs come from the latest browser_snapshot of the same tab and expire when the page changes.",
|
||||
},
|
||||
{
|
||||
name: "browser_clear",
|
||||
command: "clear",
|
||||
title: "Clear browser element",
|
||||
description: "Clear an input-like element ref from the latest browser_snapshot.",
|
||||
description:
|
||||
"Clear an input-like element in a Paseo desktop browser tab. Use browserId from browser_new_tab or browser_list_tabs; refs come from the latest browser_snapshot of the same tab and expire when the page changes.",
|
||||
},
|
||||
{
|
||||
name: "browser_hover",
|
||||
command: "hover",
|
||||
title: "Hover browser element",
|
||||
description: "Hover an element ref from the latest browser_snapshot.",
|
||||
description:
|
||||
"Hover an element in a Paseo desktop browser tab. Use browserId from browser_new_tab or browser_list_tabs; refs come from the latest browser_snapshot of the same tab and expire when the page changes.",
|
||||
},
|
||||
] as const) {
|
||||
options.registerTool(
|
||||
@@ -565,7 +664,7 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
title: toolConfig.title,
|
||||
description: toolConfig.description,
|
||||
inputSchema: { ref: BrowserRefInputSchema, browserId: BrowserAutomationBrowserIdSchema },
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs[toolConfig.command],
|
||||
},
|
||||
async ({ ref, browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -591,13 +690,14 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
"browser_check",
|
||||
{
|
||||
title: "Check browser control",
|
||||
description: "Set a checkbox or radio ref from the latest browser_snapshot.",
|
||||
description:
|
||||
"Set a checkbox or radio control in a Paseo desktop browser tab. Use browserId from browser_new_tab or browser_list_tabs; refs come from the latest browser_snapshot of the same tab and expire when the page changes.",
|
||||
inputSchema: {
|
||||
ref: BrowserRefInputSchema,
|
||||
checked: z.boolean().default(true),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
},
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.check,
|
||||
},
|
||||
async ({ ref, checked, browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -623,13 +723,14 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
"browser_select",
|
||||
{
|
||||
title: "Select browser option",
|
||||
description: "Set a select element ref from the latest browser_snapshot to a value.",
|
||||
description:
|
||||
"Set a select element in a Paseo desktop browser tab to a value. Use browserId from browser_new_tab or browser_list_tabs; refs come from the latest browser_snapshot of the same tab and expire when the page changes.",
|
||||
inputSchema: {
|
||||
ref: BrowserRefInputSchema,
|
||||
value: z.string(),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
},
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.select,
|
||||
},
|
||||
async ({ ref, value, browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -656,13 +757,13 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
{
|
||||
title: "Drag browser element",
|
||||
description:
|
||||
"Drag a source element ref onto a target element ref from the latest browser_snapshot.",
|
||||
"Drag one element onto another in a Paseo desktop browser tab. Use browserId from browser_new_tab or browser_list_tabs; refs come from the latest browser_snapshot of the same tab and expire when the page changes.",
|
||||
inputSchema: {
|
||||
sourceRef: BrowserRefInputSchema,
|
||||
targetRef: BrowserRefInputSchema,
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
},
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.drag,
|
||||
},
|
||||
async ({ sourceRef, targetRef, browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -689,12 +790,12 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
{
|
||||
title: "Read browser logs",
|
||||
description:
|
||||
"Read recent console messages and browser performance network entries for a Paseo desktop browser tab.",
|
||||
"Read recent console messages and browser performance network entries for a Paseo desktop browser tab. Use browserId from browser_new_tab or browser_list_tabs; maxEntries defaults to 50.",
|
||||
inputSchema: {
|
||||
maxEntries: z.number().int().positive().max(200).optional(),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
},
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.logs,
|
||||
},
|
||||
async ({ maxEntries, browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -720,9 +821,9 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
{
|
||||
title: "Read browser storage",
|
||||
description:
|
||||
"Read cookies plus localStorage and sessionStorage for a Paseo desktop browser tab.",
|
||||
"Read cookies plus localStorage and sessionStorage for a Paseo desktop browser tab. Use browserId from browser_new_tab or browser_list_tabs.",
|
||||
inputSchema: { browserId: BrowserAutomationBrowserIdSchema },
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.storage,
|
||||
},
|
||||
async ({ browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -746,7 +847,8 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
"browser_environment",
|
||||
{
|
||||
title: "Set/read browser environment",
|
||||
description: "Set or read viewport and geolocation for a Paseo desktop browser tab.",
|
||||
description:
|
||||
"Set or read viewport and geolocation for a Paseo desktop browser tab. Use browserId from browser_new_tab or browser_list_tabs.",
|
||||
inputSchema: {
|
||||
viewport: z
|
||||
.object({
|
||||
@@ -764,7 +866,7 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
.optional(),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
},
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.environment,
|
||||
},
|
||||
async ({ viewport, geolocation, browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -791,12 +893,12 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
||||
{
|
||||
title: "Set browser background",
|
||||
description:
|
||||
"Set the current page background color in a Paseo desktop browser tab. Pass browserId from browser_new_tab or browser_list_tabs.",
|
||||
"Set the current page background color in a Paseo desktop browser tab. Use browserId from browser_new_tab or browser_list_tabs.",
|
||||
inputSchema: {
|
||||
color: z.string().min(1),
|
||||
browserId: BrowserAutomationBrowserIdSchema,
|
||||
},
|
||||
outputSchema: BrowserToolOutputSchema,
|
||||
outputSchema: BrowserToolOutputs.setBackground,
|
||||
},
|
||||
async ({ color, browserId }) => {
|
||||
const context = resolveBrowserToolContext(options);
|
||||
@@ -831,6 +933,56 @@ function resolveBrowserToolContext(options: RegisterBrowserToolsOptions): {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeHttpUrlInput(value: string): string | null {
|
||||
if (value.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const explicitHttpUrl = /^https?:\/\//i.test(value);
|
||||
if (explicitHttpUrl) {
|
||||
return isValidHttpUrl(value) ? value : null;
|
||||
}
|
||||
|
||||
if (SCHEMELESS_HTTP_URL_PATTERN.test(value)) {
|
||||
const normalized = `http://${value}`;
|
||||
return isValidHttpUrl(normalized) ? normalized : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isValidHttpUrl(value: string): boolean {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.protocol === "http:" || url.protocol === "https:";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function requireWorkspaceContext(context: {
|
||||
agentId?: string;
|
||||
cwd?: string;
|
||||
workspaceId?: string;
|
||||
}): PaseoToolResult | null {
|
||||
if (context.workspaceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return browserToolResult({
|
||||
payload: {
|
||||
requestId: "browser-tools-workspace-context",
|
||||
ok: false,
|
||||
error: {
|
||||
code: "browser_denied",
|
||||
message: WORKSPACE_CONTEXT_MESSAGE,
|
||||
retryable: false,
|
||||
},
|
||||
},
|
||||
context,
|
||||
});
|
||||
}
|
||||
|
||||
function browserToolResult(params: {
|
||||
payload: BrowserToolsResponsePayload;
|
||||
context: { agentId?: string; cwd?: string; workspaceId?: string; browserId?: string };
|
||||
|
||||
Reference in New Issue
Block a user