pi-agent-browser-native 0.2.77 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +154 -0
- package/README.md +69 -35
- package/dist/extensions/agent-browser/index.js +911 -121
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -8
- package/dist/extensions/agent-browser/lib/argv-grammar.js +48 -7
- package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
- package/dist/extensions/agent-browser/lib/command-policy.js +1 -6
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -8
- package/dist/extensions/agent-browser/lib/config-policy.js +0 -5
- package/dist/extensions/agent-browser/lib/config.js +0 -6
- package/dist/extensions/agent-browser/lib/electron/cdp.js +6 -7
- package/dist/extensions/agent-browser/lib/electron/cleanup.js +0 -7
- package/dist/extensions/agent-browser/lib/electron/discovery.js +0 -7
- package/dist/extensions/agent-browser/lib/electron/launch.js +0 -7
- package/dist/extensions/agent-browser/lib/input-modes/electron.js +0 -5
- package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -9
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -7
- package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -29
- package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
- package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -17
- package/dist/extensions/agent-browser/lib/input-modes/shared.js +0 -5
- package/dist/extensions/agent-browser/lib/input-modes/types.js +0 -5
- package/dist/extensions/agent-browser/lib/json-schema.js +0 -5
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +12 -5
- package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -2
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -4
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +55 -48
- package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -9
- package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +48 -36
- package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -29
- package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -5
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +27 -26
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +58 -33
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +22 -14
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +148 -34
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +225 -61
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +74 -30
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +47 -26
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +40 -19
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +42 -1
- package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
- package/dist/extensions/agent-browser/lib/parsing.js +0 -7
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +36 -11
- package/dist/extensions/agent-browser/lib/playbook.js +20 -24
- package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
- package/dist/extensions/agent-browser/lib/process-identity.js +4 -9
- package/dist/extensions/agent-browser/lib/process.js +131 -48
- package/dist/extensions/agent-browser/lib/prompt-policy.js +171 -27
- package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -13
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +68 -11
- package/dist/extensions/agent-browser/lib/results/categories.js +7 -9
- package/dist/extensions/agent-browser/lib/results/confirmation.js +0 -7
- package/dist/extensions/agent-browser/lib/results/contracts.js +0 -7
- package/dist/extensions/agent-browser/lib/results/editable-ref-evidence.js +0 -7
- package/dist/extensions/agent-browser/lib/results/envelope.js +0 -7
- package/dist/extensions/agent-browser/lib/results/network.js +0 -7
- package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -35
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -44
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +66 -11
- package/dist/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.js +0 -5
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -5
- package/dist/extensions/agent-browser/lib/results/presentation/content.js +0 -5
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -6
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
- package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +0 -5
- package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -5
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -16
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
- package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -15
- package/dist/extensions/agent-browser/lib/results/presentation/skills.js +0 -5
- package/dist/extensions/agent-browser/lib/results/presentation.js +6 -8
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -7
- package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +0 -7
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -15
- package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -14
- package/dist/extensions/agent-browser/lib/results/snapshot-refs.js +0 -7
- package/dist/extensions/agent-browser/lib/results/snapshot-segments.js +0 -7
- package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +0 -7
- package/dist/extensions/agent-browser/lib/results/snapshot.js +0 -7
- package/dist/extensions/agent-browser/lib/results/text.js +0 -7
- package/dist/extensions/agent-browser/lib/runtime.js +119 -52
- package/dist/extensions/agent-browser/lib/session-page-state.js +62 -17
- package/dist/extensions/agent-browser/lib/string-enum-schema.js +0 -5
- package/dist/extensions/agent-browser/lib/temp.js +0 -7
- package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
- package/dist/extensions/agent-browser/lib/web-search.js +0 -5
- package/dist/extensions/agent-browser/script-worker.js +169 -0
- package/dist/scripts/agent-browser-target.mjs +3 -0
- package/docs/ARCHITECTURE.md +50 -31
- package/docs/COMMAND_REFERENCE.md +97 -41
- package/docs/ELECTRON.md +3 -19
- package/docs/RELEASE.md +8 -20
- package/docs/REQUIREMENTS.md +8 -6
- package/docs/SUPPORT_MATRIX.md +32 -25
- package/docs/TOOL_CONTRACT.md +107 -64
- package/docs/platform-smoke.md +1 -1
- package/package.json +7 -6
- package/platform-smoke.config.mjs +2 -2
- package/scripts/agent-browser-capability-baseline.mjs +24 -6
- package/scripts/agent-browser-target.mjs +3 -0
- package/scripts/build.mjs +41 -0
- package/scripts/doctor.mjs +8 -7
- package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
- package/scripts/platform-smoke/targets.mjs +12 -6
- package/dist/extensions/agent-browser/lib/electron/text.js +0 -13
- package/dist/extensions/agent-browser/lib/input-modes.js +0 -10
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.js +0 -5
- package/dist/extensions/agent-browser/lib/results/artifact-state.js +0 -13
- package/dist/extensions/agent-browser/lib/results.js +0 -14
|
@@ -1,39 +1,24 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Purpose: Define the Pi tool input schema for the native agent_browser wrapper.
|
|
3
|
-
* Responsibilities: Keep TypeBox schema construction separate from runtime execution and input-mode compilers.
|
|
4
|
-
* Scope: Schema-only; behavioral validation lives in the mode compilers.
|
|
5
|
-
*/
|
|
6
1
|
import { JsonSchema } from "../json-schema.js";
|
|
7
2
|
import { StringEnum as localStringEnum } from "../string-enum-schema.js";
|
|
3
|
+
import { AGENT_BROWSER_SCRIPT_CODE_MAX_BYTES } from "./script.js";
|
|
8
4
|
import { ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS, ELECTRON_DISCOVERY_MAX_RESULTS, } from "../electron/discovery.js";
|
|
9
5
|
import { AGENT_BROWSER_ELECTRON_HANDOFFS, AGENT_BROWSER_ELECTRON_TARGET_TYPES, AGENT_BROWSER_JOB_STEP_ACTIONS, AGENT_BROWSER_JOB_TYPE_DELAYED_TEXT_MAX_CHARACTERS, AGENT_BROWSER_QA_LOAD_STATES, AGENT_BROWSER_SEMANTIC_ACTIONS, AGENT_BROWSER_SEMANTIC_LOCATORS, DEFAULT_SESSION_MODE, SOURCE_LOOKUP_MAX_WORKSPACE_FILES, } from "./types.js";
|
|
10
6
|
// Keep descriptions terse: Pi sends this schema every turn; workflows belong in prompt guidance and docs.
|
|
11
|
-
// ponytail: the four electron.launch variants differ only in their single target field
|
|
12
|
-
// (appPath/appName/bundleId/executablePath); the action literal and shared launch fields
|
|
13
|
-
// are identical, so this helper keeps the schema variants in sync.
|
|
14
|
-
function electronLaunchVariant(Type, StringEnum, targetField) {
|
|
15
|
-
return Type.Object({
|
|
16
|
-
action: StringEnum(["launch"]),
|
|
17
|
-
...targetField,
|
|
18
|
-
appArgs: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
19
|
-
handoff: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_HANDOFFS)),
|
|
20
|
-
targetType: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_TARGET_TYPES)),
|
|
21
|
-
timeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
22
|
-
allow: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
23
|
-
deny: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
24
|
-
}, { additionalProperties: false });
|
|
25
|
-
}
|
|
26
7
|
export function createAgentBrowserParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
|
|
27
8
|
return Type.Object({
|
|
9
|
+
script: Type.Optional(Type.String({
|
|
10
|
+
description: "One-shot JavaScript orchestration with async browser({ args, stdin?, timeoutMs? }) and emit(value); isolated session and no host access.",
|
|
11
|
+
maxLength: AGENT_BROWSER_SCRIPT_CODE_MAX_BYTES,
|
|
12
|
+
})),
|
|
28
13
|
args: Type.Optional(Type.Array(Type.String(), {
|
|
29
14
|
description: "Raw agent-browser argv only: no binary, shell operators, or --json. Start with open → snapshot -i → act on current @refs; re-snapshot after page changes.",
|
|
30
15
|
minItems: 1,
|
|
31
16
|
})),
|
|
32
17
|
semanticAction: Type.Optional(Type.Object({
|
|
33
18
|
action: StringEnum(AGENT_BROWSER_SEMANTIC_ACTIONS),
|
|
34
|
-
locator: Type.Optional(StringEnum(AGENT_BROWSER_SEMANTIC_LOCATORS, { description: "Locator for check/click/fill." })),
|
|
35
|
-
value: Type.Optional(Type.String({ description: "Locator value
|
|
36
|
-
values: Type.Optional(Type.Array(Type.String(), { description: "Select options.", minItems: 1 })),
|
|
19
|
+
locator: Type.Optional(StringEnum(AGENT_BROWSER_SEMANTIC_LOCATORS, { description: "Locator for check/click/fill; select supports role or label." })),
|
|
20
|
+
value: Type.Optional(Type.String({ description: "Locator value or one select option; for select by label, this is the label text." })),
|
|
21
|
+
values: Type.Optional(Type.Array(Type.String(), { description: "Select options; required for select by label.", minItems: 1 })),
|
|
37
22
|
selector: Type.Optional(Type.String({ description: "Direct selector or @ref." })),
|
|
38
23
|
text: Type.Optional(Type.String({ description: "Fill text." })),
|
|
39
24
|
role: Type.Optional(Type.String({ description: "Role locator; alternative to value." })),
|
|
@@ -84,10 +69,19 @@ export function createAgentBrowserParamsSchema(Type = JsonSchema, StringEnum = l
|
|
|
84
69
|
query: Type.Optional(Type.String({ description: "Case-insensitive app filter.", minLength: 1 })),
|
|
85
70
|
maxResults: Type.Optional(Type.Integer({ description: `Result cap; default ${ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS}, values over ${ELECTRON_DISCOVERY_MAX_RESULTS} are clamped.`, minimum: 1 })),
|
|
86
71
|
}, { additionalProperties: false }),
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
72
|
+
Type.Object({
|
|
73
|
+
action: StringEnum(["launch"]),
|
|
74
|
+
appPath: Type.Optional(Type.String({ description: "macOS .app path.", minLength: 1 })),
|
|
75
|
+
appName: Type.Optional(Type.String({ description: "Name from electron.list.", minLength: 1 })),
|
|
76
|
+
bundleId: Type.Optional(Type.String({ description: "Bundle id from electron.list.", minLength: 1 })),
|
|
77
|
+
executablePath: Type.Optional(Type.String({ description: "Executable path.", minLength: 1 })),
|
|
78
|
+
appArgs: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
79
|
+
handoff: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_HANDOFFS)),
|
|
80
|
+
targetType: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_TARGET_TYPES)),
|
|
81
|
+
timeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
82
|
+
allow: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
83
|
+
deny: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
84
|
+
}, { additionalProperties: false }),
|
|
91
85
|
Type.Object({
|
|
92
86
|
action: StringEnum(["status", "cleanup"]),
|
|
93
87
|
launchId: Type.String({ description: "Tracked launch id.", minLength: 1 }),
|
|
@@ -128,7 +122,7 @@ export function createAgentBrowserParamsSchema(Type = JsonSchema, StringEnum = l
|
|
|
128
122
|
}, { additionalProperties: false }), { minItems: 1 }),
|
|
129
123
|
}, { additionalProperties: false, description: "Constrained multi-step batch." })),
|
|
130
124
|
stdin: Type.Optional(Type.String({ description: "Raw stdin for batch, eval --stdin, or auth save --password-stdin; unavailable with structured modes and electron." })),
|
|
131
|
-
outputPath: Type.Optional(Type.String({ description: "Workspace-relative or absolute result path.", minLength: 1 })),
|
|
125
|
+
outputPath: Type.Optional(Type.String({ description: "Workspace-relative or absolute result-data path; keep it distinct from screenshot, download, recording, and other browser artifact destinations.", minLength: 1 })),
|
|
132
126
|
timeoutMs: Type.Optional(Type.Integer({ description: "Wrapper timeout in ms; exceed explicit waits. Electron uses electron.timeoutMs.", minimum: 1 })),
|
|
133
127
|
sessionMode: Type.Optional(StringEnum(["auto", "fresh"], {
|
|
134
128
|
description: "auto reuses the managed session; fresh starts one for launch-only flags, then makes it the managed session.",
|
|
@@ -136,7 +130,7 @@ export function createAgentBrowserParamsSchema(Type = JsonSchema, StringEnum = l
|
|
|
136
130
|
})),
|
|
137
131
|
}, {
|
|
138
132
|
additionalProperties: false,
|
|
139
|
-
description: "Choose one input mode: args, semanticAction, job, qa, sourceLookup, networkSourceLookup, or electron.",
|
|
133
|
+
description: "Choose one input mode: script, args, semanticAction, job, qa, sourceLookup, networkSourceLookup, or electron.",
|
|
140
134
|
});
|
|
141
135
|
}
|
|
142
136
|
export const AGENT_BROWSER_PARAMS = createAgentBrowserParamsSchema();
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { parseArgvDescriptor } from "../argv-descriptor.js";
|
|
7
|
+
import { getFlagName } from "../argv-grammar.js";
|
|
8
|
+
import { needsManagedSession } from "../command-policy.js";
|
|
9
|
+
import { isCloseCommand } from "../command-taxonomy.js";
|
|
10
|
+
import { LAUNCH_SCOPED_FLAGS, MANAGED_RESTORE_INCOMPATIBLE_FLAGS } from "../launch-scoped-flags.js";
|
|
11
|
+
import { isRecord } from "../parsing.js";
|
|
12
|
+
import { validateToolArgs } from "../runtime.js";
|
|
13
|
+
export const AGENT_BROWSER_SCRIPT_CODE_MAX_BYTES = 64 * 1_024;
|
|
14
|
+
export const AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS = 120_000;
|
|
15
|
+
export const AGENT_BROWSER_SCRIPT_NAMESPACE = "";
|
|
16
|
+
export const AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS = 300_000;
|
|
17
|
+
export const AGENT_BROWSER_SCRIPT_MAX_CALLS = 25;
|
|
18
|
+
export const AGENT_BROWSER_SCRIPT_FINAL_OUTPUT_MAX_BYTES = 64 * 1_024;
|
|
19
|
+
export const AGENT_BROWSER_SCRIPT_IPC_MESSAGE_MAX_BYTES = 1 * 1_024 * 1_024;
|
|
20
|
+
export const AGENT_BROWSER_SCRIPT_IPC_CUMULATIVE_MAX_BYTES = 8 * 1_024 * 1_024;
|
|
21
|
+
export const AGENT_BROWSER_SCRIPT_SPILL_MAX_BYTES = 512 * 1_024;
|
|
22
|
+
function findPackageRoot(startDir) {
|
|
23
|
+
let currentDir = startDir;
|
|
24
|
+
for (;;) {
|
|
25
|
+
if (existsSync(join(currentDir, "package.json")))
|
|
26
|
+
return currentDir;
|
|
27
|
+
const parentDir = dirname(currentDir);
|
|
28
|
+
if (parentDir === currentDir)
|
|
29
|
+
throw new Error("Unable to resolve the pi-agent-browser-native package root.");
|
|
30
|
+
currentDir = parentDir;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function resolveScriptWorkerPath() {
|
|
34
|
+
const workerPath = join(findPackageRoot(dirname(fileURLToPath(import.meta.url))), "dist", "extensions", "agent-browser", "script-worker.js");
|
|
35
|
+
if (!existsSync(workerPath))
|
|
36
|
+
throw new Error("Compiled script worker is missing; run npm run build or reinstall pi-agent-browser-native.");
|
|
37
|
+
return workerPath;
|
|
38
|
+
}
|
|
39
|
+
const SCRIPT_SESSION_NAME_PATTERN = /^piab-script-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
40
|
+
const SCRIPT_ALLOWED_LAUNCH_FLAG = "--allowed-domains";
|
|
41
|
+
const SCRIPT_FORBIDDEN_COMMANDS = new Set(["attach", "auth", "batch", "connect", "script", "session", "state"]);
|
|
42
|
+
const SCRIPT_FORBIDDEN_FLAGS = new Set([
|
|
43
|
+
...LAUNCH_SCOPED_FLAGS.filter((flag) => flag !== SCRIPT_ALLOWED_LAUNCH_FLAG),
|
|
44
|
+
...MANAGED_RESTORE_INCOMPATIBLE_FLAGS.filter((flag) => flag !== SCRIPT_ALLOWED_LAUNCH_FLAG),
|
|
45
|
+
"--namespace",
|
|
46
|
+
"--session",
|
|
47
|
+
]);
|
|
48
|
+
export function compileAgentBrowserScript(input) {
|
|
49
|
+
if (typeof input !== "string")
|
|
50
|
+
return { error: "script must be a string." };
|
|
51
|
+
const bytes = Buffer.byteLength(input, "utf8");
|
|
52
|
+
return bytes > AGENT_BROWSER_SCRIPT_CODE_MAX_BYTES
|
|
53
|
+
? { error: `script must be ${AGENT_BROWSER_SCRIPT_CODE_MAX_BYTES} bytes or less.` }
|
|
54
|
+
: { compiled: { code: input } };
|
|
55
|
+
}
|
|
56
|
+
export function createAgentBrowserScriptSessionName() {
|
|
57
|
+
return `piab-script-${randomUUID()}`;
|
|
58
|
+
}
|
|
59
|
+
export function createAgentBrowserScriptCloseArgs(sessionName) {
|
|
60
|
+
return ["--namespace", AGENT_BROWSER_SCRIPT_NAMESPACE, "--session", sessionName, "close"];
|
|
61
|
+
}
|
|
62
|
+
export function isAgentBrowserScriptSessionName(value) {
|
|
63
|
+
return typeof value === "string" && SCRIPT_SESSION_NAME_PATTERN.test(value);
|
|
64
|
+
}
|
|
65
|
+
function getScriptCallPolicyError(args) {
|
|
66
|
+
const descriptor = parseArgvDescriptor(args);
|
|
67
|
+
const command = descriptor.commandInfo.command;
|
|
68
|
+
if (!command)
|
|
69
|
+
return "script browser call args must contain an agent-browser command.";
|
|
70
|
+
if (isCloseCommand(command))
|
|
71
|
+
return "script browser calls cannot close, quit, or exit their isolated session.";
|
|
72
|
+
if (SCRIPT_FORBIDDEN_COMMANDS.has(command))
|
|
73
|
+
return `script browser calls cannot use ${command}.`;
|
|
74
|
+
if (!needsManagedSession(descriptor))
|
|
75
|
+
return `script browser calls cannot use sessionless/local command ${command}.`;
|
|
76
|
+
for (const token of args) {
|
|
77
|
+
const flag = getFlagName(token);
|
|
78
|
+
if (SCRIPT_FORBIDDEN_FLAGS.has(flag)) {
|
|
79
|
+
return `script browser calls cannot use ${flag}; the parent owns the isolated session identity and launch policy.`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
export function validateAgentBrowserScriptBrowserParams(input) {
|
|
85
|
+
if (!isRecord(input))
|
|
86
|
+
return { error: "script browser(params) requires an object." };
|
|
87
|
+
const unsupportedField = Object.keys(input).find((field) => !["args", "stdin", "timeoutMs"].includes(field));
|
|
88
|
+
if (unsupportedField)
|
|
89
|
+
return { error: `script browser(params) does not support ${unsupportedField}; use only args, stdin, and timeoutMs.` };
|
|
90
|
+
if (!Array.isArray(input.args) || input.args.length === 0 || input.args.some((arg) => typeof arg !== "string")) {
|
|
91
|
+
return { error: "script browser(params).args must be a non-empty string array." };
|
|
92
|
+
}
|
|
93
|
+
if (input.stdin !== undefined && typeof input.stdin !== "string") {
|
|
94
|
+
return { error: "script browser(params).stdin must be a string when provided." };
|
|
95
|
+
}
|
|
96
|
+
if (input.timeoutMs !== undefined && (typeof input.timeoutMs !== "number" || !Number.isSafeInteger(input.timeoutMs) || input.timeoutMs <= 0)) {
|
|
97
|
+
return { error: "script browser(params).timeoutMs must be a positive integer when provided." };
|
|
98
|
+
}
|
|
99
|
+
const params = {
|
|
100
|
+
args: input.args,
|
|
101
|
+
stdin: input.stdin,
|
|
102
|
+
timeoutMs: input.timeoutMs,
|
|
103
|
+
};
|
|
104
|
+
const policyError = getScriptCallPolicyError(params.args);
|
|
105
|
+
if (policyError)
|
|
106
|
+
return { error: policyError, policyBlocked: true };
|
|
107
|
+
const validationError = validateToolArgs(params.args);
|
|
108
|
+
return validationError ? { error: validationError } : { params };
|
|
109
|
+
}
|
|
110
|
+
function buildRejectedCallEnvelope(error, policyBlocked) {
|
|
111
|
+
return {
|
|
112
|
+
data: null,
|
|
113
|
+
details: { failureCategory: policyBlocked ? "policy-blocked" : "validation-error", resultCategory: "failure" },
|
|
114
|
+
error,
|
|
115
|
+
failureCategory: policyBlocked ? "policy-blocked" : "validation-error",
|
|
116
|
+
ok: false,
|
|
117
|
+
resultCategory: "failure",
|
|
118
|
+
summary: error,
|
|
119
|
+
text: error,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function normalizeBrowserEnvelope(value) {
|
|
123
|
+
if (!isRecord(value)
|
|
124
|
+
|| typeof value.ok !== "boolean"
|
|
125
|
+
|| typeof value.text !== "string"
|
|
126
|
+
|| typeof value.summary !== "string"
|
|
127
|
+
|| (value.resultCategory !== "success" && value.resultCategory !== "failure")) {
|
|
128
|
+
return buildRejectedCallEnvelope("The ordinary agent_browser executor returned an invalid script envelope.", false);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
data: value.data ?? null,
|
|
132
|
+
details: isRecord(value.details) ? value.details : undefined,
|
|
133
|
+
error: typeof value.error === "string" ? value.error : undefined,
|
|
134
|
+
failureCategory: value.failureCategory,
|
|
135
|
+
nextActions: Array.isArray(value.nextActions) ? value.nextActions : undefined,
|
|
136
|
+
ok: value.ok,
|
|
137
|
+
resultCategory: value.resultCategory,
|
|
138
|
+
successCategory: value.successCategory,
|
|
139
|
+
summary: value.summary,
|
|
140
|
+
text: value.text,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function buildStepSummary(index, envelope) {
|
|
144
|
+
return {
|
|
145
|
+
failureCategory: envelope.failureCategory,
|
|
146
|
+
index,
|
|
147
|
+
ok: envelope.ok,
|
|
148
|
+
resultCategory: envelope.resultCategory,
|
|
149
|
+
successCategory: envelope.successCategory,
|
|
150
|
+
summary: envelope.summary,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function buildFailedRun(options) {
|
|
154
|
+
return { ...options, ok: false };
|
|
155
|
+
}
|
|
156
|
+
function describeScriptError(error) {
|
|
157
|
+
const name = typeof error?.name === "string" && error.name.length > 0 ? error.name.slice(0, 80) : "Error";
|
|
158
|
+
const message = typeof error?.message === "string" && error.message.length > 0
|
|
159
|
+
? error.message.replace(/[\r\n]+/g, " ").slice(0, 400)
|
|
160
|
+
: "Script execution failed.";
|
|
161
|
+
return `${name}: ${message}`;
|
|
162
|
+
}
|
|
163
|
+
function isScriptChildMessage(value) {
|
|
164
|
+
if (!isRecord(value) || typeof value.type !== "string")
|
|
165
|
+
return false;
|
|
166
|
+
if (value.type === "ready")
|
|
167
|
+
return true;
|
|
168
|
+
if (value.type === "call")
|
|
169
|
+
return typeof value.id === "number" && Number.isSafeInteger(value.id) && value.id > 0;
|
|
170
|
+
if (value.type === "emit")
|
|
171
|
+
return true;
|
|
172
|
+
return value.type === "complete";
|
|
173
|
+
}
|
|
174
|
+
function waitForChildExit(child) {
|
|
175
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
176
|
+
return Promise.resolve();
|
|
177
|
+
return new Promise((resolve) => {
|
|
178
|
+
child.once("exit", () => resolve());
|
|
179
|
+
child.once("error", () => resolve());
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function terminateChild(child) {
|
|
183
|
+
child.stdin.destroy();
|
|
184
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
185
|
+
child.kill("SIGTERM");
|
|
186
|
+
return setTimeout(() => {
|
|
187
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
188
|
+
child.kill("SIGKILL");
|
|
189
|
+
}, 250);
|
|
190
|
+
}
|
|
191
|
+
async function settleWithin(promise, timeoutMs) {
|
|
192
|
+
let timer;
|
|
193
|
+
await Promise.race([
|
|
194
|
+
promise.catch(() => undefined),
|
|
195
|
+
new Promise((resolve) => {
|
|
196
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
197
|
+
}),
|
|
198
|
+
]);
|
|
199
|
+
if (timer)
|
|
200
|
+
clearTimeout(timer);
|
|
201
|
+
}
|
|
202
|
+
function serializeFinalOutput(value) {
|
|
203
|
+
if (value === undefined)
|
|
204
|
+
return undefined;
|
|
205
|
+
return JSON.stringify(value);
|
|
206
|
+
}
|
|
207
|
+
export async function runAgentBrowserScript(options) {
|
|
208
|
+
const compiled = compileAgentBrowserScript(options.code);
|
|
209
|
+
if (compiled.error) {
|
|
210
|
+
return buildFailedRun({ callCount: 0, emitCount: 0, error: compiled.error, failureCategory: "validation-error", rejectedCallCount: 0, steps: [] });
|
|
211
|
+
}
|
|
212
|
+
const timeoutMs = options.timeoutMs ?? AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS;
|
|
213
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS) {
|
|
214
|
+
return buildFailedRun({ callCount: 0, emitCount: 0, error: `script timeoutMs must be between 1 and ${AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS}.`, failureCategory: "validation-error", rejectedCallCount: 0, steps: [] });
|
|
215
|
+
}
|
|
216
|
+
if (options.signal?.aborted) {
|
|
217
|
+
return buildFailedRun({ aborted: true, callCount: 0, emitCount: 0, error: "Script execution was aborted.", failureCategory: "aborted", rejectedCallCount: 0, steps: [] });
|
|
218
|
+
}
|
|
219
|
+
let workerPath;
|
|
220
|
+
try {
|
|
221
|
+
workerPath = resolveScriptWorkerPath();
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
const message = error instanceof Error ? error.message : "Compiled script worker is missing.";
|
|
225
|
+
return buildFailedRun({ callCount: 0, emitCount: 0, error: message, failureCategory: "missing-binary", rejectedCallCount: 0, steps: [] });
|
|
226
|
+
}
|
|
227
|
+
const child = spawn(process.execPath, [
|
|
228
|
+
"--permission",
|
|
229
|
+
"--max-old-space-size=64",
|
|
230
|
+
workerPath,
|
|
231
|
+
String(AGENT_BROWSER_SCRIPT_IPC_MESSAGE_MAX_BYTES),
|
|
232
|
+
String(AGENT_BROWSER_SCRIPT_IPC_CUMULATIVE_MAX_BYTES),
|
|
233
|
+
], {
|
|
234
|
+
env: {},
|
|
235
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
236
|
+
});
|
|
237
|
+
child.stdin.on("error", () => undefined);
|
|
238
|
+
let stdoutBuffer = Buffer.alloc(0);
|
|
239
|
+
let stderrBytes = 0;
|
|
240
|
+
let cumulativeBytes = 0;
|
|
241
|
+
let callCount = 0;
|
|
242
|
+
let rejectedCallCount = 0;
|
|
243
|
+
let leaseStarted = false;
|
|
244
|
+
let ready = false;
|
|
245
|
+
let stopping = false;
|
|
246
|
+
let activeCallController;
|
|
247
|
+
const emissions = [];
|
|
248
|
+
const steps = [];
|
|
249
|
+
const messages = [];
|
|
250
|
+
let draining = false;
|
|
251
|
+
let drainPromise = Promise.resolve();
|
|
252
|
+
let resolveResult;
|
|
253
|
+
const resultPromise = new Promise((resolve) => {
|
|
254
|
+
resolveResult = resolve;
|
|
255
|
+
});
|
|
256
|
+
const childExit = waitForChildExit(child);
|
|
257
|
+
let timeout;
|
|
258
|
+
let killTimer;
|
|
259
|
+
const sendParentMessage = async (message) => {
|
|
260
|
+
const line = `${JSON.stringify(message)}\n`;
|
|
261
|
+
const bytes = Buffer.byteLength(line, "utf8");
|
|
262
|
+
if (bytes > AGENT_BROWSER_SCRIPT_IPC_MESSAGE_MAX_BYTES || cumulativeBytes + bytes > AGENT_BROWSER_SCRIPT_IPC_CUMULATIVE_MAX_BYTES) {
|
|
263
|
+
throw new Error("Script IPC limit exceeded.");
|
|
264
|
+
}
|
|
265
|
+
cumulativeBytes += bytes;
|
|
266
|
+
await new Promise((resolve, reject) => {
|
|
267
|
+
child.stdin.write(line, (error) => error ? reject(error) : resolve());
|
|
268
|
+
});
|
|
269
|
+
};
|
|
270
|
+
const finish = async (result, waitForDrain) => {
|
|
271
|
+
if (stopping)
|
|
272
|
+
return;
|
|
273
|
+
stopping = true;
|
|
274
|
+
if (timeout)
|
|
275
|
+
clearTimeout(timeout);
|
|
276
|
+
options.signal?.removeEventListener("abort", abortListener);
|
|
277
|
+
activeCallController?.abort();
|
|
278
|
+
killTimer = terminateChild(child);
|
|
279
|
+
if (waitForDrain)
|
|
280
|
+
await settleWithin(drainPromise, 5_000);
|
|
281
|
+
await settleWithin(childExit, 1_000);
|
|
282
|
+
clearTimeout(killTimer);
|
|
283
|
+
resolveResult(result);
|
|
284
|
+
};
|
|
285
|
+
const fail = (error, failureCategory, flags = {}, waitForDrain = false) => finish(buildFailedRun({
|
|
286
|
+
...flags,
|
|
287
|
+
callCount,
|
|
288
|
+
emitCount: emissions.length,
|
|
289
|
+
error,
|
|
290
|
+
failureCategory,
|
|
291
|
+
rejectedCallCount,
|
|
292
|
+
steps,
|
|
293
|
+
}), waitForDrain);
|
|
294
|
+
const abortListener = () => {
|
|
295
|
+
void fail("Script execution was aborted.", "aborted", { aborted: true }, true);
|
|
296
|
+
};
|
|
297
|
+
options.signal?.addEventListener("abort", abortListener, { once: true });
|
|
298
|
+
timeout = setTimeout(() => {
|
|
299
|
+
void fail(`Script execution timed out after ${timeoutMs}ms.`, "timeout", { timedOut: true }, true);
|
|
300
|
+
}, timeoutMs);
|
|
301
|
+
const drainMessages = async () => {
|
|
302
|
+
if (draining)
|
|
303
|
+
return;
|
|
304
|
+
draining = true;
|
|
305
|
+
try {
|
|
306
|
+
while (!stopping && messages.length > 0) {
|
|
307
|
+
const message = messages.shift();
|
|
308
|
+
if (message.type === "ready") {
|
|
309
|
+
if (ready) {
|
|
310
|
+
await fail("Sandbox sent a duplicate ready message.", "upstream-error");
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
ready = true;
|
|
314
|
+
try {
|
|
315
|
+
await sendParentMessage({ code: options.code, type: "start" });
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
await fail("Unable to start the script sandbox.", "upstream-error");
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (!ready) {
|
|
324
|
+
await fail("Sandbox sent a message before it was ready.", "upstream-error");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (message.type === "emit") {
|
|
328
|
+
if (!Object.hasOwn(message, "value")) {
|
|
329
|
+
await fail("emit(value) requires a JSON-serializable value; undefined and functions are not supported.", "validation-error");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
emissions.push(message.value);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (message.type === "complete") {
|
|
336
|
+
if (message.error) {
|
|
337
|
+
await fail(describeScriptError(message.error), "script-error");
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const data = emissions.length === 0
|
|
341
|
+
? message.hasValue ? message.value : undefined
|
|
342
|
+
: emissions.length === 1 ? emissions[0] : emissions;
|
|
343
|
+
let serialized;
|
|
344
|
+
try {
|
|
345
|
+
serialized = serializeFinalOutput(data);
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
await fail("Final script output must be JSON-serializable.", "validation-error");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (serialized !== undefined && Buffer.byteLength(serialized, "utf8") > AGENT_BROWSER_SCRIPT_FINAL_OUTPUT_MAX_BYTES) {
|
|
352
|
+
await fail(`Final script output exceeds ${AGENT_BROWSER_SCRIPT_FINAL_OUTPUT_MAX_BYTES} bytes.`, "validation-error");
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
await finish({ callCount, data, emitCount: emissions.length, ok: true, rejectedCallCount, steps }, false);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
callCount += 1;
|
|
359
|
+
if (callCount > AGENT_BROWSER_SCRIPT_MAX_CALLS) {
|
|
360
|
+
await fail(`Script browser call limit exceeded (${AGENT_BROWSER_SCRIPT_MAX_CALLS}).`, "validation-error");
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const validated = validateAgentBrowserScriptBrowserParams(message.params);
|
|
364
|
+
let envelope;
|
|
365
|
+
if (!validated.params) {
|
|
366
|
+
rejectedCallCount += 1;
|
|
367
|
+
envelope = buildRejectedCallEnvelope(validated.error ?? "Invalid script browser call.", validated.policyBlocked === true);
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
if (!leaseStarted) {
|
|
371
|
+
try {
|
|
372
|
+
options.beforeFirstCall?.();
|
|
373
|
+
leaseStarted = true;
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
await fail("Unable to persist the isolated script session lease.", "upstream-error");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
activeCallController = new AbortController();
|
|
381
|
+
try {
|
|
382
|
+
envelope = normalizeBrowserEnvelope(await options.dispatch(validated.params, activeCallController.signal));
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
envelope = buildRejectedCallEnvelope("The ordinary agent_browser executor failed while dispatching this call.", false);
|
|
386
|
+
}
|
|
387
|
+
finally {
|
|
388
|
+
activeCallController = undefined;
|
|
389
|
+
}
|
|
390
|
+
if (stopping)
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
steps.push(buildStepSummary(callCount - 1, envelope));
|
|
394
|
+
try {
|
|
395
|
+
await sendParentMessage({ envelope, id: message.id, type: "response" });
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
await fail("Unable to return a browser result to the script sandbox.", "upstream-error");
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
finally {
|
|
404
|
+
draining = false;
|
|
405
|
+
if (!stopping && messages.length > 0)
|
|
406
|
+
scheduleDrain();
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
function scheduleDrain() {
|
|
410
|
+
if (draining || stopping)
|
|
411
|
+
return;
|
|
412
|
+
drainPromise = drainMessages();
|
|
413
|
+
}
|
|
414
|
+
child.stdout.on("data", (chunk) => {
|
|
415
|
+
if (stopping)
|
|
416
|
+
return;
|
|
417
|
+
stdoutBuffer = Buffer.concat([stdoutBuffer, chunk]);
|
|
418
|
+
if (stdoutBuffer.length > AGENT_BROWSER_SCRIPT_IPC_MESSAGE_MAX_BYTES) {
|
|
419
|
+
void fail("Script IPC message limit exceeded.", "validation-error", {}, true);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
for (;;) {
|
|
423
|
+
const newline = stdoutBuffer.indexOf(10);
|
|
424
|
+
if (newline < 0)
|
|
425
|
+
break;
|
|
426
|
+
const lineBuffer = stdoutBuffer.subarray(0, newline);
|
|
427
|
+
stdoutBuffer = stdoutBuffer.subarray(newline + 1);
|
|
428
|
+
const bytes = lineBuffer.length + 1;
|
|
429
|
+
if (bytes > AGENT_BROWSER_SCRIPT_IPC_MESSAGE_MAX_BYTES || cumulativeBytes + bytes > AGENT_BROWSER_SCRIPT_IPC_CUMULATIVE_MAX_BYTES) {
|
|
430
|
+
void fail("Script IPC limit exceeded.", "validation-error", {}, true);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
cumulativeBytes += bytes;
|
|
434
|
+
try {
|
|
435
|
+
const parsed = JSON.parse(lineBuffer.toString("utf8"));
|
|
436
|
+
if (!isScriptChildMessage(parsed))
|
|
437
|
+
throw new Error("invalid message");
|
|
438
|
+
messages.push(parsed);
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
void fail("Sandbox returned an invalid IPC message.", "upstream-error", {}, true);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
scheduleDrain();
|
|
446
|
+
});
|
|
447
|
+
child.stderr.on("data", (chunk) => {
|
|
448
|
+
stderrBytes += chunk.length;
|
|
449
|
+
if (stderrBytes > AGENT_BROWSER_SCRIPT_IPC_MESSAGE_MAX_BYTES && !stopping) {
|
|
450
|
+
void fail("Sandbox stderr limit exceeded.", "upstream-error", {}, true);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
child.once("error", () => {
|
|
454
|
+
if (!stopping)
|
|
455
|
+
void fail("Unable to start the script sandbox.", "upstream-error", {}, true);
|
|
456
|
+
});
|
|
457
|
+
child.once("exit", () => {
|
|
458
|
+
if (!stopping)
|
|
459
|
+
void fail("Script sandbox exited before completion.", "upstream-error", {}, true);
|
|
460
|
+
});
|
|
461
|
+
return await resultPromise;
|
|
462
|
+
}
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Purpose: Compile semanticAction shorthand inputs into upstream agent-browser commands.
|
|
3
|
-
* Responsibilities: Validate shorthand locator/action fields and expose compiled-command helpers.
|
|
4
|
-
* Scope: semanticAction mode only.
|
|
5
|
-
*/
|
|
6
1
|
import { isRecord } from "../parsing.js";
|
|
7
2
|
import { getSelectValues } from "./shared.js";
|
|
8
3
|
import { AGENT_BROWSER_SEMANTIC_ACTIONS, AGENT_BROWSER_SEMANTIC_LOCATORS, } from "./types.js";
|
|
@@ -14,7 +9,7 @@ export function getCompiledSemanticActionSessionPrefix(compiled) {
|
|
|
14
9
|
return commandIndex > 0 ? compiled.args.slice(0, commandIndex) : [];
|
|
15
10
|
}
|
|
16
11
|
export function isCompiledSemanticActionFindCommand(compiled) {
|
|
17
|
-
if (!compiled)
|
|
12
|
+
if (!compiled || compiled.action === "select")
|
|
18
13
|
return false;
|
|
19
14
|
return compiled.args[getCompiledSemanticActionCommandIndex(compiled)] === "find";
|
|
20
15
|
}
|
|
@@ -29,7 +24,7 @@ export function compileAgentBrowserSemanticAction(input) {
|
|
|
29
24
|
const selector = input.selector;
|
|
30
25
|
const text = input.text;
|
|
31
26
|
const role = input.role;
|
|
32
|
-
const name = input.name;
|
|
27
|
+
const name = typeof input.name === "string" && input.name.trim().length === 0 ? undefined : input.name;
|
|
33
28
|
const session = input.session;
|
|
34
29
|
if (typeof action !== "string" || !AGENT_BROWSER_SEMANTIC_ACTIONS.includes(action)) {
|
|
35
30
|
return { error: `semanticAction.action must be one of: ${AGENT_BROWSER_SEMANTIC_ACTIONS.join(", ")}.` };
|
|
@@ -38,20 +33,59 @@ export function compileAgentBrowserSemanticAction(input) {
|
|
|
38
33
|
return { error: "semanticAction.session must be a non-empty string when provided." };
|
|
39
34
|
}
|
|
40
35
|
if (action === "select") {
|
|
41
|
-
if (locator !== undefined || role !== undefined || name !== undefined) {
|
|
42
|
-
return { error: "semanticAction.locator, role, and name are not supported for select; use selector plus value or values." };
|
|
43
|
-
}
|
|
44
36
|
if (text !== undefined) {
|
|
45
37
|
return { error: "semanticAction.text is not supported for select; use value or values for option values." };
|
|
46
38
|
}
|
|
47
|
-
if (typeof selector
|
|
48
|
-
|
|
39
|
+
if (typeof selector === "string" && selector.trim().length > 0) {
|
|
40
|
+
if (locator !== undefined || role !== undefined || name !== undefined) {
|
|
41
|
+
return { error: "semanticAction.selector cannot be combined with locator, role, or name for select; use selector plus value/values, or locator fields plus values." };
|
|
42
|
+
}
|
|
43
|
+
const selectedValues = getSelectValues(input, "semanticAction");
|
|
44
|
+
if (selectedValues.error)
|
|
45
|
+
return { error: selectedValues.error };
|
|
46
|
+
const args = typeof session === "string" ? ["--session", session, "select", selector, ...selectedValues.values] : ["select", selector, ...selectedValues.values];
|
|
47
|
+
return { compiled: { action: "select", selector, values: selectedValues.values, args } };
|
|
48
|
+
}
|
|
49
|
+
if (selector !== undefined) {
|
|
50
|
+
return { error: "semanticAction.selector must be a non-empty string when provided." };
|
|
51
|
+
}
|
|
52
|
+
if (locator === undefined) {
|
|
53
|
+
return { error: "semanticAction.selector or semanticAction.locator is required for select." };
|
|
54
|
+
}
|
|
55
|
+
if (locator !== "role" && locator !== "label") {
|
|
56
|
+
return { error: "semanticAction select locator must be role or label; use selector plus value/values for other targets." };
|
|
57
|
+
}
|
|
58
|
+
if (locator === "role") {
|
|
59
|
+
if (typeof role !== "string" || !/^(?:combobox|listbox)$/i.test(role)) {
|
|
60
|
+
return { error: "semanticAction.role must be combobox or listbox for locator=role select." };
|
|
61
|
+
}
|
|
62
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
63
|
+
return { error: "semanticAction.name is required for locator=role select." };
|
|
64
|
+
}
|
|
65
|
+
const optionValues = getSelectValues({ value, values }, "semanticAction");
|
|
66
|
+
if (optionValues.error)
|
|
67
|
+
return { error: optionValues.error };
|
|
68
|
+
const args = typeof session === "string"
|
|
69
|
+
? ["--session", session, "find", "role", role, "select", ...optionValues.values, "--name", name]
|
|
70
|
+
: ["find", "role", role, "select", ...optionValues.values, "--name", name];
|
|
71
|
+
return { compiled: { action: "select", locator: "role", values: optionValues.values, args } };
|
|
72
|
+
}
|
|
73
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
74
|
+
return { error: "semanticAction.value must be the accessible label text for locator=label select." };
|
|
75
|
+
}
|
|
76
|
+
if (role !== undefined || name !== undefined) {
|
|
77
|
+
return { error: "semanticAction.role and name are only supported for locator=role select." };
|
|
78
|
+
}
|
|
79
|
+
const optionValues = getSelectValues({ values }, "semanticAction");
|
|
80
|
+
if (optionValues.error) {
|
|
81
|
+
return { error: optionValues.error.includes("required")
|
|
82
|
+
? "semanticAction.values is required for locator=label select (value is the label text)."
|
|
83
|
+
: optionValues.error };
|
|
49
84
|
}
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return { compiled: { action: "select", selector, values: selectedValues.values, args } };
|
|
85
|
+
const args = typeof session === "string"
|
|
86
|
+
? ["--session", session, "find", "label", value, "select", ...optionValues.values]
|
|
87
|
+
: ["find", "label", value, "select", ...optionValues.values];
|
|
88
|
+
return { compiled: { action: "select", locator: "label", values: optionValues.values, args } };
|
|
55
89
|
}
|
|
56
90
|
if (values !== undefined) {
|
|
57
91
|
return { error: "semanticAction.values is only supported for select actions." };
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Purpose: Hold tiny shared parsing helpers for structured agent_browser input modes.
|
|
3
|
-
* Responsibilities: Normalize common select values, batch result rows, and workspace scan limits.
|
|
4
|
-
* Scope: Generic input-mode helpers only; mode-specific policy stays in the owning module.
|
|
5
|
-
*/
|
|
6
1
|
import { isRecord } from "../parsing.js";
|
|
7
2
|
import { SOURCE_LOOKUP_DEFAULT_MAX_WORKSPACE_FILES, SOURCE_LOOKUP_MAX_WORKSPACE_FILES } from "./types.js";
|
|
8
3
|
export function getSelectValues(input, context) {
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Purpose: Define structured agent_browser input-mode constants and TypeScript contracts.
|
|
3
|
-
* Responsibilities: Share schema enums and compiled input-mode result types across input-mode modules.
|
|
4
|
-
* Scope: Types and constants only; validation and compilation live in sibling modules.
|
|
5
|
-
*/
|
|
6
1
|
export const DEFAULT_SESSION_MODE = "auto";
|
|
7
2
|
export const AGENT_BROWSER_SEMANTIC_ACTIONS = ["check", "click", "fill", "select"];
|
|
8
3
|
export const AGENT_BROWSER_SEMANTIC_LOCATORS = ["alt", "label", "placeholder", "role", "testid", "text", "title"];
|