mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-10-02 07:34:35 +08:00
Merge pull request #322 from starsstreaming/codex/request-help-safe-regex
fix(extension): 用线性正则匹配 request-help 的 URL 完成条件
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
"@browser-skill/ui": "workspace:*",
|
||||
"@browser-skill/vom": "workspace:*",
|
||||
"@remixicon/react": "^4.9.0",
|
||||
"re2js": "2.8.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { InteractionPreferenceStore } from "@/lib/interaction-preferences";
|
||||
import { SessionManager } from "@/session-manager/manager";
|
||||
@@ -544,6 +548,331 @@ describe("handleRequestHelp", () => {
|
||||
expect(res).toMatchObject({ outcome: "completed", completed_by: "system", tab_id: 5 });
|
||||
});
|
||||
|
||||
it.skipIf(process.env.BSK_URL_REGEX_CHILD === "1")(
|
||||
"bounds dangerous URL regex checks in a child process",
|
||||
() => {
|
||||
const require = createRequire(import.meta.url);
|
||||
const vitestCli = join(dirname(require.resolve("vitest/package.json")), "vitest.mjs");
|
||||
const child = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
vitestCli,
|
||||
"run",
|
||||
fileURLToPath(import.meta.url),
|
||||
"-t",
|
||||
"isolated dangerous URL regex checks",
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env, BSK_URL_REGEX_CHILD: "1" },
|
||||
encoding: "utf8",
|
||||
timeout: 10_000,
|
||||
},
|
||||
);
|
||||
expect(child.error, `${child.stdout}\n${child.stderr}`).toBeUndefined();
|
||||
expect(child.status, `${child.stdout}\n${child.stderr}`).toBe(0);
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.env.BSK_URL_REGEX_CHILD !== "1")(
|
||||
"isolated dangerous URL regex checks",
|
||||
async () => {
|
||||
const url = `https://app.example/${"a".repeat(28)}!`;
|
||||
const deps = baseDeps({
|
||||
sendToTab: vi.fn(async () => ({ type: "bsk-help-ack", ok: true })),
|
||||
tabsApi: {
|
||||
get: vi.fn(async () => ({ id: 5, windowId: 99, active: true, url })) as never,
|
||||
query: vi.fn(async () => [{ id: 5, windowId: 99, active: true }] as never),
|
||||
},
|
||||
});
|
||||
const started = performance.now();
|
||||
const res = await handleRequestHelp(
|
||||
fakeManager("abcd", 99, 5),
|
||||
baseParams({
|
||||
completion_criteria: {
|
||||
any: [{ url_matches: "(a+)+$" }, { url_matches: "^https://app.example/a+!$" }],
|
||||
stable_for_ms: 0,
|
||||
},
|
||||
}),
|
||||
deps,
|
||||
);
|
||||
expect(res).toMatchObject({ outcome: "completed", completed_by: "system" });
|
||||
expect(performance.now() - started).toBeLessThan(1_000);
|
||||
|
||||
const expanded = handleRequestHelp(
|
||||
fakeManager("abcd", 99, 5),
|
||||
baseParams({
|
||||
completion_criteria: { any: [{ url_matches: `(?:${"[ab]".repeat(20)}){1000}$` }] },
|
||||
}),
|
||||
deps,
|
||||
);
|
||||
await expect(expanded).resolves.toMatchObject({ code: "invalid_params" });
|
||||
|
||||
const nearLimit = handleRequestHelp(
|
||||
fakeManager("abcd", 99, 5),
|
||||
baseParams({
|
||||
completion_criteria: { any: [{ url_matches: `(?:${"a".repeat(118)}){1000}` }] },
|
||||
}),
|
||||
deps,
|
||||
);
|
||||
await expect(nearLimit).resolves.toMatchObject({ code: "invalid_params" });
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ label: "unsupported lookahead", pattern: "(?=a)" },
|
||||
{ label: "unsupported lookbehind", pattern: "(?<=a)b" },
|
||||
{ label: "unsupported Unicode escape", pattern: "\\u0061" },
|
||||
{ label: "unsupported large repetition", pattern: "a{1001}" },
|
||||
{ label: "unsupported empty negated class", pattern: "[^]" },
|
||||
{ label: "unsupported backreference", pattern: "(a)\\1" },
|
||||
{ label: "oversized pattern", pattern: "a".repeat(129) },
|
||||
])("rejects $label before starting help", async ({ pattern }) => {
|
||||
const deps = baseDeps();
|
||||
const res = await handleRequestHelp(
|
||||
fakeManager("abcd", 99, 5),
|
||||
baseParams({ completion_criteria: { any: [{ url_matches: pattern }] } }),
|
||||
deps,
|
||||
);
|
||||
expect(res).toMatchObject({ code: "invalid_params" });
|
||||
expect(deps.activateTab).not.toHaveBeenCalled();
|
||||
expect(deps.sendToTab).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects too many conditions before starting help", async () => {
|
||||
const deps = baseDeps();
|
||||
const res = await handleRequestHelp(
|
||||
fakeManager("abcd", 99, 5),
|
||||
baseParams({
|
||||
completion_criteria: {
|
||||
all: Array.from({ length: 5 }, () => ({ url_contains: "/done" })),
|
||||
any: Array.from({ length: 4 }, () => ({ selector_exists: "#done" })),
|
||||
},
|
||||
}),
|
||||
deps,
|
||||
);
|
||||
expect(res).toMatchObject({ code: "invalid_params" });
|
||||
expect(deps.tabsApi.get).not.toHaveBeenCalled();
|
||||
expect(deps.sendToTab).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects patterns whose total compiled program is too large", async () => {
|
||||
const deps = baseDeps();
|
||||
const res = await handleRequestHelp(
|
||||
fakeManager("abcd", 99, 5),
|
||||
baseParams({
|
||||
completion_criteria: {
|
||||
all: ["[ab]", "[ac]", "[ad]"].map((part) => ({
|
||||
url_matches: `(?:${part.repeat(3)}){1000}`,
|
||||
})),
|
||||
},
|
||||
}),
|
||||
deps,
|
||||
);
|
||||
expect(res).toMatchObject({ code: "invalid_params" });
|
||||
expect(deps.activateTab).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("charges repeated URL patterns once per condition", async () => {
|
||||
const deps = baseDeps();
|
||||
const pattern = "a.{1000}.{1000}.{1000}.{996}[#%]";
|
||||
const single = await handleRequestHelp(
|
||||
fakeManager("abcd", 99, 5),
|
||||
baseParams({ completion_criteria: { all: [{ url_matches: pattern }] } }),
|
||||
deps,
|
||||
);
|
||||
expect(single).toMatchObject({ outcome: "continued" });
|
||||
|
||||
const res = await handleRequestHelp(
|
||||
fakeManager("abcd", 99, 5),
|
||||
baseParams({
|
||||
completion_criteria: {
|
||||
all: Array.from({ length: 8 }, () => ({ url_matches: pattern })),
|
||||
},
|
||||
}),
|
||||
deps,
|
||||
);
|
||||
expect(res).toMatchObject({ code: "invalid_params" });
|
||||
expect(deps.activateTab).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ label: "null", urlMatches: null },
|
||||
{ label: "empty", urlMatches: "" },
|
||||
{ label: "omitted", urlMatches: undefined },
|
||||
])("treats $label URL regex as absent", async ({ urlMatches }) => {
|
||||
const deps = baseDeps({
|
||||
sendToTab: vi.fn(async () => ({ type: "bsk-help-ack", ok: true })),
|
||||
tabsApi: {
|
||||
get: vi.fn(async () => ({
|
||||
id: 5,
|
||||
windowId: 99,
|
||||
active: true,
|
||||
url: "https://app.example/done",
|
||||
})) as never,
|
||||
query: vi.fn(async () => [{ id: 5, windowId: 99, active: true }] as never),
|
||||
},
|
||||
});
|
||||
const res = await handleRequestHelp(
|
||||
fakeManager("abcd", 99, 5),
|
||||
baseParams({
|
||||
completion_criteria: {
|
||||
any: [{ url_matches: urlMatches, url_contains: "/done" }],
|
||||
stable_for_ms: 0,
|
||||
},
|
||||
}),
|
||||
deps,
|
||||
);
|
||||
expect(res).toMatchObject({ outcome: "completed" });
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ label: "unanchored substring", pattern: "/done" },
|
||||
{ label: "start and end anchors", pattern: "^https://app\\.example/done$" },
|
||||
])("matches a normal URL with $label", async ({ pattern }) => {
|
||||
const deps = baseDeps({
|
||||
sendToTab: vi.fn(async () => ({ type: "bsk-help-ack", ok: true })),
|
||||
tabsApi: {
|
||||
get: vi.fn(async () => ({
|
||||
id: 5,
|
||||
windowId: 99,
|
||||
active: true,
|
||||
url: "https://app.example/done",
|
||||
})) as never,
|
||||
query: vi.fn(async () => [{ id: 5, windowId: 99, active: true }] as never),
|
||||
},
|
||||
});
|
||||
const res = await handleRequestHelp(
|
||||
fakeManager("abcd", 99, 5),
|
||||
baseParams({ completion_criteria: { any: [{ url_matches: pattern }], stable_for_ms: 0 } }),
|
||||
deps,
|
||||
);
|
||||
expect(res).toMatchObject({ outcome: "completed" });
|
||||
});
|
||||
|
||||
it("does not match an anchored regex against a different URL", async () => {
|
||||
const abort = new AbortController();
|
||||
const tabsApi = {
|
||||
get: vi.fn(async () => ({
|
||||
id: 5,
|
||||
windowId: 99,
|
||||
active: true,
|
||||
url: "https://app.example/done/next",
|
||||
})) as never,
|
||||
query: vi.fn(async () => [{ id: 5, windowId: 99, active: true }] as never),
|
||||
};
|
||||
const deps = baseDeps({
|
||||
signal: abort.signal,
|
||||
tabsApi,
|
||||
sendToTab: vi.fn(async () => ({ type: "bsk-help-ack", ok: true })),
|
||||
});
|
||||
const pending = handleRequestHelp(
|
||||
fakeManager("abcd", 99, 5),
|
||||
baseParams({ completion_criteria: { any: [{ url_matches: "/done$" }], stable_for_ms: 0 } }),
|
||||
deps,
|
||||
);
|
||||
await vi.waitFor(() => expect(tabsApi.get).toHaveBeenCalledTimes(2));
|
||||
abort.abort();
|
||||
await expect(pending).resolves.toMatchObject({ code: "cancelled" });
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ label: "url_contains", condition: { url_contains: "/done" } },
|
||||
{ label: "url_matches", condition: { url_matches: "/done" } },
|
||||
])("keeps waiting for manual control when a URL exceeds the $label limit", async ({
|
||||
condition,
|
||||
}) => {
|
||||
const abort = new AbortController();
|
||||
const tabsApi = {
|
||||
get: vi.fn(async () => ({
|
||||
id: 5,
|
||||
windowId: 99,
|
||||
active: true,
|
||||
url: `https://app.example/done?${"x".repeat(8_192)}`,
|
||||
})) as never,
|
||||
query: vi.fn(async () => [{ id: 5, windowId: 99, active: true }] as never),
|
||||
};
|
||||
const deps = baseDeps({
|
||||
signal: abort.signal,
|
||||
tabsApi,
|
||||
sendToTab: vi.fn(async () => ({ type: "bsk-help-ack", ok: true })),
|
||||
});
|
||||
const pending = handleRequestHelp(
|
||||
fakeManager("abcd", 99, 5),
|
||||
baseParams({ completion_criteria: { any: [condition], stable_for_ms: 0 } }),
|
||||
deps,
|
||||
);
|
||||
await vi.waitFor(() => expect(tabsApi.get).toHaveBeenCalledTimes(2));
|
||||
abort.abort();
|
||||
await expect(pending).resolves.toMatchObject({ code: "cancelled" });
|
||||
});
|
||||
|
||||
it("handles cancellation after later URL completion polls", async () => {
|
||||
vi.useFakeTimers();
|
||||
const abort = new AbortController();
|
||||
const get = vi.fn(async () => ({
|
||||
id: 5,
|
||||
windowId: 99,
|
||||
active: true,
|
||||
url: "https://app.example/wait",
|
||||
}));
|
||||
const tabsApi = {
|
||||
get: get as never,
|
||||
query: vi.fn(async () => [{ id: 5, windowId: 99, active: true }] as never),
|
||||
};
|
||||
const deps = baseDeps({
|
||||
signal: abort.signal,
|
||||
tabsApi,
|
||||
sendToTab: vi.fn(async () => ({ type: "bsk-help-ack", ok: true })),
|
||||
});
|
||||
try {
|
||||
const pending = handleRequestHelp(
|
||||
fakeManager("abcd", 99, 5),
|
||||
baseParams({ completion_criteria: { any: [{ url_matches: "/done$" }] } }),
|
||||
deps,
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(1_100);
|
||||
expect(get.mock.calls.length).toBeGreaterThanOrEqual(3);
|
||||
abort.abort();
|
||||
await expect(pending).resolves.toMatchObject({ code: "cancelled" });
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not overlap completion checks while an earlier check is pending", async () => {
|
||||
vi.useFakeTimers();
|
||||
const abort = new AbortController();
|
||||
let releaseCheck!: (value: { result: { value: boolean } }) => void;
|
||||
const evaluation = new Promise<{ result: { value: boolean } }>((resolve) => {
|
||||
releaseCheck = resolve;
|
||||
});
|
||||
const send = vi.fn((_tabId: number, method: string) =>
|
||||
method === "Runtime.evaluate" ? evaluation : Promise.resolve({}),
|
||||
);
|
||||
const deps = baseDeps({
|
||||
signal: abort.signal,
|
||||
cdp: { send } as unknown as RequestHelpDeps["cdp"],
|
||||
sendToTab: vi.fn(async () => ({ type: "bsk-help-ack", ok: true })),
|
||||
});
|
||||
try {
|
||||
const pending = handleRequestHelp(
|
||||
fakeManager("abcd", 99, 5),
|
||||
baseParams({ completion_criteria: { any: [{ selector_exists: "#done" }] } }),
|
||||
deps,
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(send.mock.calls.filter(([, method]) => method === "Runtime.evaluate")).toHaveLength(1);
|
||||
await vi.advanceTimersByTimeAsync(1_500);
|
||||
expect(send.mock.calls.filter(([, method]) => method === "Runtime.evaluate")).toHaveLength(1);
|
||||
abort.abort();
|
||||
releaseCheck({ result: { value: true } });
|
||||
await expect(pending).resolves.toMatchObject({ code: "cancelled" });
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps user control active on new tabs without moving completion off the primary tab", async () => {
|
||||
vi.useFakeTimers();
|
||||
const chromeEvents = installHelpLifecycleChrome();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// `tool.request_help` — pause automation and let the human control a tab.
|
||||
|
||||
import { RE2JS } from "re2js";
|
||||
import {
|
||||
HELP_ACK,
|
||||
HELP_CANCEL,
|
||||
@@ -48,6 +49,11 @@ const HELP_REARM_RETRY_DELAY_MS = 400;
|
||||
const HELP_CLEANUP_TIMEOUT_MS = 1_000;
|
||||
const DEFAULT_COMPLETION_STABLE_MS = 1_000;
|
||||
const COMPLETION_POLL_MS = 500;
|
||||
const MAX_COMPLETION_CONDITIONS = 8;
|
||||
const MAX_URL_REGEX_LENGTH = 128;
|
||||
const MAX_URL_REGEX_PROGRAM_SIZE = 4_096;
|
||||
const MAX_TOTAL_URL_REGEX_PROGRAM_SIZE = 8_192;
|
||||
const MAX_COMPLETION_URL_LENGTH = 8_192;
|
||||
|
||||
export interface RequestHelpNotifications {
|
||||
create(id: string, options: chrome.notifications.NotificationOptions<true>): Promise<string>;
|
||||
@@ -83,6 +89,7 @@ interface ActiveHelpRequest {
|
||||
notificationId: string;
|
||||
resolvedTargets?: ResolvedTarget[];
|
||||
completionCriteria?: HelpCompletionCriteria;
|
||||
urlMatchers: Map<string, RE2JS>;
|
||||
deps: RequestHelpDeps;
|
||||
settled: boolean;
|
||||
unsubscribePreferences?: () => void;
|
||||
@@ -597,13 +604,10 @@ async function evaluateCompletionCondition(
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (url.length > MAX_COMPLETION_URL_LENGTH) return false;
|
||||
if (condition.url_contains && !url.includes(condition.url_contains)) return false;
|
||||
if (condition.url_matches) {
|
||||
try {
|
||||
if (!new RegExp(condition.url_matches).test(url)) return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (!help.urlMatchers.get(condition.url_matches)?.test(url)) return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -667,22 +671,30 @@ async function completionCriteriaMatches(help: ActiveHelpRequest): Promise<boole
|
||||
function startCompletionPolling(help: ActiveHelpRequest): void {
|
||||
if (!help.completionCriteria) return;
|
||||
const stableMs = help.completionCriteria.stable_for_ms ?? DEFAULT_COMPLETION_STABLE_MS;
|
||||
// A check can await tab or CDP work longer than the polling interval.
|
||||
let checking = false;
|
||||
const check = async () => {
|
||||
if (help.settled) return;
|
||||
const matched = await completionCriteriaMatches(help);
|
||||
const now = Date.now();
|
||||
if (!matched) {
|
||||
help.completionMatchedSince = null;
|
||||
return;
|
||||
if (help.settled || checking) return;
|
||||
checking = true;
|
||||
try {
|
||||
const matched = await completionCriteriaMatches(help);
|
||||
if (help.settled) return;
|
||||
const now = Date.now();
|
||||
if (!matched) {
|
||||
help.completionMatchedSince = null;
|
||||
return;
|
||||
}
|
||||
help.completionMatchedSince ??= now;
|
||||
if (now - help.completionMatchedSince < stableMs) return;
|
||||
void finishHelp(help, {
|
||||
outcome: "completed",
|
||||
completed_by: "system",
|
||||
tab_id: help.primaryTabId,
|
||||
resolved_targets: help.resolvedTargets,
|
||||
});
|
||||
} finally {
|
||||
checking = false;
|
||||
}
|
||||
help.completionMatchedSince ??= now;
|
||||
if (now - help.completionMatchedSince < stableMs) return;
|
||||
void finishHelp(help, {
|
||||
outcome: "completed",
|
||||
completed_by: "system",
|
||||
tab_id: help.primaryTabId,
|
||||
resolved_targets: help.resolvedTargets,
|
||||
});
|
||||
};
|
||||
help.completionTimer = setInterval(() => void check(), COMPLETION_POLL_MS);
|
||||
void check();
|
||||
@@ -715,6 +727,54 @@ export async function handleRequestHelp(
|
||||
});
|
||||
if (helpDisabled()) return disabledResult(params.tab_id ?? 0);
|
||||
if (deps.signal?.aborted) return { code: "cancelled", message: "request_help aborted" };
|
||||
const all = params.completion_criteria?.all ?? [];
|
||||
const any = params.completion_criteria?.any ?? [];
|
||||
if (
|
||||
!Array.isArray(all) ||
|
||||
!Array.isArray(any) ||
|
||||
all.length + any.length > MAX_COMPLETION_CONDITIONS
|
||||
) {
|
||||
return { code: "invalid_params", message: "completion_criteria supports at most 8 conditions" };
|
||||
}
|
||||
const conditions = [...all, ...any];
|
||||
for (const condition of conditions) {
|
||||
if (!condition || typeof condition !== "object" || Array.isArray(condition)) {
|
||||
return {
|
||||
code: "invalid_params",
|
||||
message: "completion_criteria contains an invalid condition",
|
||||
};
|
||||
}
|
||||
const pattern = condition.url_matches;
|
||||
if (pattern == null || pattern === "") continue;
|
||||
if (typeof pattern !== "string" || pattern.length > MAX_URL_REGEX_LENGTH) {
|
||||
return {
|
||||
code: "invalid_params",
|
||||
message: "url_matches must be a string of at most 128 characters",
|
||||
};
|
||||
}
|
||||
}
|
||||
const urlMatchers = new Map<string, RE2JS>();
|
||||
let totalProgramSize = 0;
|
||||
for (const condition of conditions) {
|
||||
const pattern = condition.url_matches;
|
||||
if (!pattern) continue;
|
||||
try {
|
||||
// Compilation is shared, but each condition runs a match on every poll.
|
||||
const matcher = urlMatchers.get(pattern) ?? RE2JS.compile(pattern);
|
||||
const size = matcher.programSize();
|
||||
if (
|
||||
size > MAX_URL_REGEX_PROGRAM_SIZE ||
|
||||
totalProgramSize + size > MAX_TOTAL_URL_REGEX_PROGRAM_SIZE
|
||||
) {
|
||||
return { code: "invalid_params", message: "url_matches is too complex" };
|
||||
}
|
||||
totalProgramSize += size;
|
||||
urlMatchers.set(pattern, matcher);
|
||||
} catch {
|
||||
return { code: "invalid_params", message: "url_matches uses unsupported regex syntax" };
|
||||
}
|
||||
}
|
||||
if (deps.signal?.aborted) return { code: "cancelled", message: "request_help aborted" };
|
||||
const target = await resolveTargetTab(manager, ctx, params.tab_id, deps.tabsApi);
|
||||
if (isRpcError(target)) return target;
|
||||
const denied = enforceAgentWindow(ctx, target, "request_help");
|
||||
@@ -765,6 +825,7 @@ export async function handleRequestHelp(
|
||||
notificationId,
|
||||
resolvedTargets,
|
||||
completionCriteria: params.completion_criteria,
|
||||
urlMatchers,
|
||||
deps,
|
||||
settled: false,
|
||||
resolve,
|
||||
|
||||
@@ -787,7 +787,7 @@ export type HelpOutcome =
|
||||
|
||||
export interface HelpCompletionCondition {
|
||||
url_contains?: string;
|
||||
url_matches?: string;
|
||||
url_matches?: string | null;
|
||||
selector_exists?: string;
|
||||
selector_missing?: string;
|
||||
text_exists?: string;
|
||||
|
||||
@@ -8,7 +8,21 @@ bsk request-help --session <id> --prompt "Please complete sign-in" --target @e3
|
||||
```
|
||||
|
||||
Use a precise prompt and fresh targets; omit `--target` when no control fits.
|
||||
Use completion criteria only for a clear, stable success signal.
|
||||
Use completion criteria only for a clear, stable success signal. `all` and `any`
|
||||
support at most eight conditions in total. For URL completion, `url_matches`
|
||||
accepts RE2-compatible regular expressions of at most 128 characters. A compiled
|
||||
pattern may contain at most 4096 instructions, and all URL-regex conditions together
|
||||
at most 8192, counting repeated patterns each time. Invalid or over-budget patterns
|
||||
return `invalid_params` before help starts. URL conditions do not auto-complete on URLs
|
||||
longer than 8192 characters;
|
||||
the user can still finish the help request manually. Empty, null, and omitted
|
||||
`url_matches` values impose no URL-regex condition.
|
||||
|
||||
RE2 rejects JavaScript patterns such as `\u0061`, `a{1001}`, `[^]`, lookahead,
|
||||
lookbehind, and backreferences. Some accepted patterns differ in meaning: `\a`
|
||||
is a control character in RE2 rather than `a`, and RE2's `\s` does not include
|
||||
Unicode whitespace. Rewrite patterns in RE2 syntax; use `url_contains` when a
|
||||
substring is enough.
|
||||
|
||||
| Result | Next step |
|
||||
| --- | --- |
|
||||
|
||||
@@ -95,10 +95,12 @@
|
||||
]
|
||||
},
|
||||
"url_matches": {
|
||||
"description": "RE2-compatible URL regex (maximum 128 characters). Backtracking-only syntax is rejected.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
],
|
||||
"maxLength": 128
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -112,7 +114,8 @@
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/HelpCompletionCondition"
|
||||
}
|
||||
},
|
||||
"maxItems": 8
|
||||
},
|
||||
"any": {
|
||||
"type": [
|
||||
@@ -121,7 +124,8 @@
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/HelpCompletionCondition"
|
||||
}
|
||||
},
|
||||
"maxItems": 8
|
||||
},
|
||||
"stable_for_ms": {
|
||||
"type": [
|
||||
|
||||
@@ -57,8 +57,10 @@ pub struct RequestHelpParams {
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct HelpCompletionCriteria {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(length(max = 8))]
|
||||
pub any: Option<Vec<HelpCompletionCondition>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(length(max = 8))]
|
||||
pub all: Option<Vec<HelpCompletionCondition>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(range(min = 0))]
|
||||
@@ -70,6 +72,8 @@ pub struct HelpCompletionCondition {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub url_contains: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(length(max = 128))]
|
||||
/// RE2-compatible URL regex (maximum 128 characters). Backtracking-only syntax is rejected.
|
||||
pub url_matches: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub selector_exists: Option<String>,
|
||||
|
||||
@@ -3,6 +3,20 @@
|
||||
With help enabled, use `browser_assist` action `request-help` for login, CAPTCHA,
|
||||
OTP, payment confirmation, consent, or after two attempts without progress. Supply
|
||||
a precise prompt and fresh targets; completion criteria need a stable success signal.
|
||||
`all` and `any` support at most eight conditions in total. `urlMatches` accepts
|
||||
RE2-compatible regular expressions of at most 128 characters. A compiled pattern
|
||||
may contain at most 4096 instructions, and all URL-regex conditions together at most
|
||||
8192, counting repeated patterns each time. Invalid or over-budget patterns return
|
||||
`invalid_params` before help starts.
|
||||
URL conditions do not auto-complete on URLs longer than 8192 characters; the user
|
||||
can still finish manually. The protocol treats empty, null, and omitted
|
||||
`url_matches` values as no URL-regex condition.
|
||||
|
||||
RE2 rejects JavaScript patterns such as `\u0061`, `a{1001}`, `[^]`, lookahead,
|
||||
lookbehind, and backreferences. Some accepted patterns differ in meaning: `\a`
|
||||
is a control character in RE2 rather than `a`, and RE2's `\s` does not include
|
||||
Unicode whitespace. Rewrite patterns in RE2 syntax; use `urlContains` when a
|
||||
substring is enough.
|
||||
Resume only on `continued` / `completed`, then observe. Cancellation/timeout blocks
|
||||
the step; do not repeat the request. Navigation alone is not success.
|
||||
|
||||
|
||||
@@ -300,7 +300,8 @@ const BROWSER_TOOL_SPECS: BrowserToolSpec[] = [
|
||||
completionCriteria: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
description: "Automatic completion detector for request-help.",
|
||||
description:
|
||||
"Automatic completion detector for request-help (at most 8 conditions across any and all).",
|
||||
properties: {
|
||||
any: {
|
||||
type: "array",
|
||||
@@ -309,7 +310,11 @@ const BROWSER_TOOL_SPECS: BrowserToolSpec[] = [
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
urlContains: { type: "string" },
|
||||
urlMatches: { type: "string" },
|
||||
urlMatches: {
|
||||
type: "string",
|
||||
description:
|
||||
"RE2-compatible URL regex (max 128 characters; compiled program max 4096 instructions).",
|
||||
},
|
||||
selectorExists: { type: "string" },
|
||||
selectorMissing: { type: "string" },
|
||||
textExists: { type: "string" },
|
||||
@@ -324,7 +329,11 @@ const BROWSER_TOOL_SPECS: BrowserToolSpec[] = [
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
urlContains: { type: "string" },
|
||||
urlMatches: { type: "string" },
|
||||
urlMatches: {
|
||||
type: "string",
|
||||
description:
|
||||
"RE2-compatible URL regex (max 128 characters; compiled program max 4096 instructions).",
|
||||
},
|
||||
selectorExists: { type: "string" },
|
||||
selectorMissing: { type: "string" },
|
||||
textExists: { type: "string" },
|
||||
|
||||
@@ -65,7 +65,11 @@ const HELP_CONDITION_SCHEMA = {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
urlContains: { type: "string" },
|
||||
urlMatches: { type: "string" },
|
||||
urlMatches: {
|
||||
type: "string",
|
||||
description:
|
||||
"RE2-compatible URL regex (max 128 characters; compiled program max 4096 instructions).",
|
||||
},
|
||||
selectorExists: { type: "string" },
|
||||
selectorMissing: { type: "string" },
|
||||
textExists: { type: "string" },
|
||||
@@ -168,7 +172,8 @@ export function registerPhaseOneSupportTools(
|
||||
completionCriteria: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
description: "Optional explicit success detector for automatic completion.",
|
||||
description:
|
||||
"Optional automatic completion detector (at most 8 conditions across any and all).",
|
||||
properties: {
|
||||
any: { type: "array", items: HELP_CONDITION_SCHEMA },
|
||||
all: { type: "array", items: HELP_CONDITION_SCHEMA },
|
||||
|
||||
Generated
+9
@@ -34,6 +34,9 @@ importers:
|
||||
'@remixicon/react':
|
||||
specifier: ^4.9.0
|
||||
version: 4.9.0(react@19.2.6)
|
||||
re2js:
|
||||
specifier: 2.8.6
|
||||
version: 2.8.6
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.6
|
||||
@@ -3446,6 +3449,10 @@ packages:
|
||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||
hasBin: true
|
||||
|
||||
re2js@2.8.6:
|
||||
resolution: {integrity: sha512-xLgQil4kIUCrAzVk9fRSkxkFNwmygLFjVxXrLc65aE1F0+Zsb8rxumFBy4XKyvgMCTL6kilDq3EZ0piE2dP/Dg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
react-dom@18.3.1:
|
||||
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
|
||||
peerDependencies:
|
||||
@@ -7178,6 +7185,8 @@ snapshots:
|
||||
minimist: 1.2.8
|
||||
strip-json-comments: 2.0.1
|
||||
|
||||
re2js@2.8.6: {}
|
||||
|
||||
react-dom@18.3.1(react@18.3.1):
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
Reference in New Issue
Block a user