diff --git a/packages/pi-extension/CHANGELOG.md b/packages/pi-extension/CHANGELOG.md index 7c9925d..e9e428e 100644 --- a/packages/pi-extension/CHANGELOG.md +++ b/packages/pi-extension/CHANGELOG.md @@ -2,6 +2,17 @@ ## Unreleased +- Flags name the domain rather than an acronym: `--cua-tools` is now + `--browser-tools`, `--cua-coordinates` is `--browser-coordinates`, and the + commands are `/browser` and `/browser-tools`. +- Browser configuration is one `--browser-options` JSON object forwarded verbatim + to Kernel's browser-create call, replacing `--cua-profile-id`, + `--cua-profile-save-changes`, `--cua-proxy-id`, and `--cua-browser-timeout`. A + flag per create-call field grows every time the SDK does; JSON tracks it for + free. The only default is `timeout_seconds: 600`. `--browser-session` still + attaches an existing browser and cannot be combined with `--browser-options`. + Note that `stealth` is no longer forced on — pass it in the JSON if you want it. + - `@onkernel/cua-cli` and the `cua` binary are removed. Everything the CLI built because it needed an agent front-end — sessions and resume, skills, the TUI, print and RPC modes, model selection — pi supplies, so the extension replaces diff --git a/packages/pi-extension/README.md b/packages/pi-extension/README.md index e7891c6..1cd6c39 100644 --- a/packages/pi-extension/README.md +++ b/packages/pi-extension/README.md @@ -25,17 +25,17 @@ No selector means no Kernel tool is active and no browser is provisioned. ```sh pi -p --provider openai --model gpt-5.6-sol \ - --cua-tools browser,browser-act "Open example.com and report its heading" + --browser-tools browser,browser-act "Open example.com and report its heading" -pi --mode rpc --no-session --provider openai --model gpt-5.6-sol --cua-tools browser +pi --mode rpc --no-session --provider openai --model gpt-5.6-sol --browser-tools browser -pi -p --provider anthropic --model claude-opus-5 --cua-tools anthropic-computer \ +pi -p --provider anthropic --model claude-opus-5 --browser-tools anthropic-computer \ "Open example.com and report its heading" ``` ### The menu -Eight entries, one per capability. Availability is per model, and `/cua-tools` +Eight entries, one per capability. Availability is per model, and `/browser-tools` tells you which apply to the one you selected. | entry | tools | works on | @@ -52,16 +52,16 @@ tells you which apply to the one you selected. `anthropic-browser` and `anthropic-computer` cannot be selected together: Anthropic rejects the pair because the browser tool addresses a viewport coordinate frame and the computer tool a display frame. The catalog compiler -refuses it before the request goes out, and `/cua-tools` reports it as a conflict +refuses it before the request goes out, and `/browser-tools` reports it as a conflict rather than as unavailability. -`--cua-coordinates` selects `pixels` (default) or `normalized-1000` for the +`--browser-coordinates` selects `pixels` (default) or `normalized-1000` for the `computer` entry's coordinate contract. ### Commands -- `/cua` — current selectors, active tools, and browser status. -- `/cua-tools` — with no argument, list every selector for the current model, +- `/browser` — current selectors, active tools, and browser status. +- `/browser-tools` — with no argument, list every selector for the current model, marking the selected ones and showing the compiler's own reason for any that this model cannot take. With an argument, replace the selection. `none` clears it. @@ -80,10 +80,21 @@ no browser is created, and the model answers from memory with exit 0. | flag | effect | | --- | --- | -| `--cua-browser-session` | attach an existing session; never deleted on exit | -| `--cua-profile-id`, `--cua-profile-save-changes` | load and optionally persist a profile | -| `--cua-proxy-id` | route through a Kernel proxy | -| `--cua-browser-timeout` | owned-browser timeout in seconds (default 300) | +| `--browser-session` | attach an existing session; never deleted on exit | +| `--browser-options` | JSON forwarded verbatim to Kernel's browser-create call | + +`--browser-options` is one JSON object rather than a flag per field, so it tracks +the Kernel SDK without this extension growing an option every time the SDK does: + +```sh +pi -p --browser-tools browser \ + --browser-options '{"stealth":true,"profile":{"id":"p1","save_changes":true},"proxy_id":"px1"}' \ + "open example.com" +``` + +The only default is `timeout_seconds: 600` — the failure it prevents is a browser +vanishing mid-task. Override it in the same JSON. `--browser-session` attaches an +existing browser, so it cannot be combined with `--browser-options`. One browser is provisioned lazily per session, on first tool execution. Compiling declarations, generating headers, and transforming a payload never diff --git a/packages/pi-extension/src/browser-runtime.ts b/packages/pi-extension/src/browser-runtime.ts index 2ded790..6440363 100644 --- a/packages/pi-extension/src/browser-runtime.ts +++ b/packages/pi-extension/src/browser-runtime.ts @@ -1,13 +1,22 @@ import Kernel from "@onkernel/sdk"; import { CuaExecutionResources } from "@onkernel/cua-agent"; +/** + * Browser configuration, passed as one JSON object rather than a flag per field. + * + * `create` is forwarded to Kernel's browser-create call as-is, so it tracks the + * SDK without this extension growing a flag every time the SDK does. The only + * default is a generous timeout: the failure it prevents is a browser vanishing + * mid-task. + */ export interface BrowserOptions { + /** Attach an existing session instead of creating one. Never deleted on exit. */ sessionId?: string; - profileId?: string; - proxyId?: string; - timeoutSeconds: number; - saveProfileChanges: boolean; + /** Forwarded verbatim to `client.browsers.create`. */ + create: Record; } + +export const DEFAULT_BROWSER_TIMEOUT_SECONDS = 600; export interface BrowserStatus { sessionId?: string; owned?: boolean; @@ -62,11 +71,9 @@ export class CuaBrowserRuntime { const browser = attached ? await client.browsers.retrieve(this.options.sessionId!) : await client.browsers.create({ - stealth: true, - timeout_seconds: this.options.timeoutSeconds, - ...(this.options.profileId ? { profile: { id: this.options.profileId, save_changes: this.options.saveProfileChanges } } : {}), - ...(this.options.proxyId ? { proxy_id: this.options.proxyId } : {}), - }); + timeout_seconds: DEFAULT_BROWSER_TIMEOUT_SECONDS, + ...this.options.create, + } as never); this.client = client; this.status = { sessionId: browser.session_id, diff --git a/packages/pi-extension/src/index.ts b/packages/pi-extension/src/index.ts index f758076..fab9c84 100644 --- a/packages/pi-extension/src/index.ts +++ b/packages/pi-extension/src/index.ts @@ -24,13 +24,10 @@ import { CONFIG_ENTRY, restoreConfig, type PersistedConfig } from "./state"; import { availabilityText, statusText } from "./render"; export default function cuaPiExtension(pi: ExtensionAPI): void { - pi.registerFlag("cua-tools", { type: "string", description: "Comma-separated explicit CUA tool selectors" }); - pi.registerFlag("cua-coordinates", { type: "string", description: "pixels or normalized-1000", default: "pixels" }); - pi.registerFlag("cua-browser-session", { type: "string", description: "Attach an existing Kernel browser session" }); - pi.registerFlag("cua-profile-id", { type: "string", description: "Kernel browser profile id" }); - pi.registerFlag("cua-proxy-id", { type: "string", description: "Kernel proxy id" }); - pi.registerFlag("cua-browser-timeout", { type: "string", description: "Owned browser timeout in seconds", default: "300" }); - pi.registerFlag("cua-profile-save-changes", { type: "boolean", description: "Save owned browser profile changes", default: false }); + pi.registerFlag("browser-tools", { type: "string", description: "Comma-separated tool selectors; see /browser-tools for this model's menu" }); + pi.registerFlag("browser-coordinates", { type: "string", description: "pixels or normalized-1000", default: "pixels" }); + pi.registerFlag("browser-session", { type: "string", description: "Attach an existing Kernel browser session instead of creating one" }); + pi.registerFlag("browser-options", { type: "string", description: "JSON forwarded to Kernel's browser-create call, e.g. {\"stealth\":true}" }); // Parsed flag values are unavailable until after the extension factory returns, // but session_start errors do not stop print/RPC provider calls. validateRawCliFlags(); @@ -54,7 +51,7 @@ export default function cuaPiExtension(pi: ExtensionAPI): void { for (const [name, spec] of allSpecs) { const conflict = pi.getAllTools().find((tool) => tool.name === name); if (conflict && conflict.sourceInfo.path !== extensionPath) { - throw new Error(`cannot register CUA tool "${name}": already owned by ${conflict.sourceInfo.source}`); + throw new Error(`cannot register browser tool "${name}": already owned by ${conflict.sourceInfo.source}`); } pi.registerTool({ name: spec.name, @@ -63,9 +60,9 @@ export default function cuaPiExtension(pi: ExtensionAPI): void { parameters: spec.declaration.parameters, executionMode: "sequential", async execute(toolCallId, input, signal) { - if (!activeNames.has(name)) throw new Error(`CUA tool "${name}" is not active`); + if (!activeNames.has(name)) throw new Error(`browser tool "${name}" is not active`); const selected = currentSpecs().find((candidate) => candidate.name === name); - if (!selected || compatibilityError) throw new Error(compatibilityError ?? `CUA tool "${name}" is no longer selected`); + if (!selected || compatibilityError) throw new Error(compatibilityError ?? `browser tool "${name}" is no longer selected`); const resources = await ensureRuntime().get(signal); return resources.materialize(selected).execute(toolCallId, input, signal); }, @@ -73,7 +70,7 @@ export default function cuaPiExtension(pi: ExtensionAPI): void { } } function ensureRuntime(): CuaBrowserRuntime { - if (!sessionActive) throw new Error("CUA browser runtime is unavailable outside an active pi session"); + if (!sessionActive) throw new Error("the browser runtime is unavailable outside an active pi session"); return (runtime ??= new CuaBrowserRuntime(browserOptions)); } function currentSpecs(): CuaToolSpec[] { @@ -121,13 +118,13 @@ export default function cuaPiExtension(pi: ExtensionAPI): void { } initialized = true; if (ctx.mode === "tui") { - ctx.ui.setStatus("cua", statusText(selection.selectors, [...activeNames], runtime?.getStatus() ?? {}, compatibilityError)); + ctx.ui.setStatus("browser-tools", statusText(selection.selectors, [...activeNames], runtime?.getStatus() ?? {}, compatibilityError)); } else if (compatibilityError && compatibilityError !== warnedError) { // Print and RPC have no status line, and silence here is the worst failure // this extension can produce: the tools vanish, no browser is created, and // the model answers from memory with exit 0. Say so on stderr, once per // distinct reason so a multi-turn run does not repeat itself. - process.stderr.write(`cua: no browser tool is active — ${compatibilityError}\n`); + process.stderr.write(`browser tools: none active — ${compatibilityError}\n`); warnedError = compatibilityError; } if (!compatibilityError) warnedError = undefined; @@ -185,21 +182,21 @@ export default function cuaPiExtension(pi: ExtensionAPI): void { registerCuaProviders(); - pi.registerCommand("cua", { - description: "Show CUA tool and browser status", + pi.registerCommand("browser", { + description: "Show the selected browser tools and browser status", handler: async (_args, ctx) => { reconcile(ctx); notifyStatus(ctx); }, }); - pi.registerCommand("cua-tools", { - description: "Replace this session's explicit CUA selectors, or list what this model can take", + pi.registerCommand("browser-tools", { + description: "Replace this session's tool selection, or list what this model can take", handler: async (args, ctx) => { // No argument lists the menu instead of clearing the selection, because // clearing is the more destructive reading of an empty command. if (!args?.trim()) { if (!ctx.model) { - ctx.ui.notify("cua: no pi model is selected", "error"); + ctx.ui.notify("browser tools: no pi model is selected", "error"); return; } ctx.ui.notify(availabilityText(selectorAvailability(ctx.model, selection)), "info"); @@ -227,10 +224,10 @@ export default function cuaPiExtension(pi: ExtensionAPI): void { const known = saved.selectors.filter((selector) => CUA_SELECTORS.includes(selector)); const dropped = saved.selectors.filter((selector) => !CUA_SELECTORS.includes(selector)); if (dropped.length) { - process.stderr.write(`cua: ignoring retired tool selector(s) from this session: ${dropped.join(", ")}\n`); + process.stderr.write(`browser tools: ignoring retired selector(s) from this session: ${dropped.join(", ")}\n`); } // Always apply what was restored, even when nothing survives. A persisted - // selection came from `/cua-tools`, which deliberately overrides the flags, + // selection came from `/browser-tools`, which deliberately overrides the flags, // so falling back to them would re-enable tools this session had replaced. // An empty selection with the note above is the honest outcome. selection = parseSelection(known.join(",") || undefined, saved.coordinates); @@ -270,7 +267,7 @@ export default function cuaPiExtension(pi: ExtensionAPI): void { pi.on("tool_call", (event) => { if (!allSpecs.has(event.toolName)) return; if (!activeNames.has(event.toolName) || compatibilityError) - return { block: true, reason: compatibilityError ?? `CUA tool "${event.toolName}" is inactive` }; + return { block: true, reason: compatibilityError ?? `browser tool "${event.toolName}" is inactive` }; }); pi.on("session_shutdown", async () => { sessionActive = false; @@ -293,26 +290,41 @@ function validateRawCliFlags(argv = process.argv.slice(2)): void { const index = argv.indexOf(`--${name}`); return index >= 0 && !argv[index + 1]?.startsWith("--") ? argv[index + 1] : undefined; }; - parseSelection(read("cua-tools"), read("cua-coordinates") ?? "pixels"); - const sessionId = trim(read("cua-browser-session")); - if (sessionId && (trim(read("cua-profile-id")) || trim(read("cua-proxy-id")))) - throw new Error("--cua-browser-session cannot be combined with --cua-profile-id or --cua-proxy-id"); - positiveSeconds(read("cua-browser-timeout")); + parseSelection(read("browser-tools"), read("browser-coordinates") ?? "pixels"); + parseBrowserOptions(read("browser-session"), read("browser-options")); } function readFlags(pi: ExtensionAPI): { selection: CuaSelection; browserOptions: BrowserOptions } { - const browserOptions: BrowserOptions = { - sessionId: trim(asString(pi.getFlag("cua-browser-session"))), - profileId: trim(asString(pi.getFlag("cua-profile-id"))), - proxyId: trim(asString(pi.getFlag("cua-proxy-id"))), - timeoutSeconds: positiveSeconds(asString(pi.getFlag("cua-browser-timeout"))), - saveProfileChanges: pi.getFlag("cua-profile-save-changes") === true, + return { + selection: parseSelection(asString(pi.getFlag("browser-tools")), asString(pi.getFlag("browser-coordinates"))), + browserOptions: parseBrowserOptions(asString(pi.getFlag("browser-session")), asString(pi.getFlag("browser-options"))), }; - if (browserOptions.sessionId && (browserOptions.profileId || browserOptions.proxyId)) - throw new Error("--cua-browser-session cannot be combined with --cua-profile-id or --cua-proxy-id"); - return { selection: parseSelection(asString(pi.getFlag("cua-tools")), asString(pi.getFlag("cua-coordinates"))), browserOptions }; } function defaultBrowserOptions(): BrowserOptions { - return { timeoutSeconds: 300, saveProfileChanges: false }; + return { create: {} }; +} + +/** + * One JSON flag instead of a flag per create-call field, so this extension does + * not grow an option every time the Kernel SDK does. + */ +export function parseBrowserOptions(sessionId: string | undefined, optionsJson: string | undefined): BrowserOptions { + const attach = trim(sessionId); + const raw = trim(optionsJson); + let create: Record = {}; + if (raw) { + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch (error) { + throw new Error(`--browser-options must be valid JSON: ${error instanceof Error ? error.message : String(error)}`); + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("--browser-options must be a JSON object"); + create = parsed as Record; + } + if (attach && Object.keys(create).length > 0) { + throw new Error("--browser-session attaches an existing browser, so --browser-options cannot also configure a new one"); + } + return { ...(attach ? { sessionId: attach } : {}), create }; } function asString(value: boolean | string | undefined): string | undefined { return typeof value === "string" ? value : undefined; @@ -321,13 +333,6 @@ function trim(value: string | undefined): string | undefined { const result = value?.trim(); return result || undefined; } -function positiveSeconds(value: string | undefined): number { - const seconds = Number(value ?? "300"); - if (!Number.isSafeInteger(seconds) || seconds < 1 || seconds > 259200) - throw new Error("--cua-browser-timeout must be a whole number from 1 to 259200"); - return seconds; -} - function withoutCuaToolSchemas(payload: unknown, cuaSpecs: ReadonlyMap): unknown { if (!isRecord(payload) || !Array.isArray(payload.tools)) return payload; const tools: unknown[] = []; diff --git a/packages/pi-extension/src/render.ts b/packages/pi-extension/src/render.ts index 4c8e74f..6f43e9d 100644 --- a/packages/pi-extension/src/render.ts +++ b/packages/pi-extension/src/render.ts @@ -6,7 +6,7 @@ export function statusText(selectors: readonly string[], active: readonly string const browserText = browser.sessionId ? `${browser.owned ? "owned" : "attached"} ${browser.sessionId}${browser.liveUrl ? ` ${browser.liveUrl}` : ""}` : "not provisioned"; - return `cua: selected=${selectors.join(",") || "none"}; active=${tools}; browser=${browserText}${error ? `; unavailable=${error}` : ""}`; + return `browser tools: selected=${selectors.join(",") || "none"}; active=${tools}; browser=${browserText}${error ? `; unavailable=${error}` : ""}`; } /** One line per selector, so an unavailable one carries the compiler's own reason. */ @@ -17,5 +17,5 @@ export function availabilityText(entries: readonly SelectorAvailability[]): stri const conflict = entry.conflictsWith.length ? ` (cannot combine with ${entry.conflictsWith.join(", ")})` : ""; return `${mark} ${entry.selector}${conflict}`; }); - return ["cua selectors for this model (* = selected):", ...lines].join("\n"); + return ["browser tool selectors for this model (* = selected):", ...lines].join("\n"); } diff --git a/packages/pi-extension/src/selection.ts b/packages/pi-extension/src/selection.ts index 8b816a2..d13f47a 100644 --- a/packages/pi-extension/src/selection.ts +++ b/packages/pi-extension/src/selection.ts @@ -51,16 +51,16 @@ export const CUA_SELECTORS: readonly string[] = Object.freeze(Object.keys(MENU)) export function parseSelection(value: string | undefined, coordinates: string | undefined): CuaSelection { const coordinateMode = coordinates ?? "pixels"; if (coordinateMode !== "pixels" && coordinateMode !== "normalized-1000") { - throw new Error('--cua-coordinates must be "pixels" or "normalized-1000"'); + throw new Error('--browser-coordinates must be "pixels" or "normalized-1000"'); } const selectors = value ?.split(",") .map((item) => item.trim()) .filter(Boolean) ?? []; - if (new Set(selectors).size !== selectors.length) throw new Error("--cua-tools contains duplicate selectors"); + if (new Set(selectors).size !== selectors.length) throw new Error("--browser-tools contains duplicate selectors"); for (const selector of selectors) { - if (!CUA_SELECTORS.includes(selector)) throw new Error(`unknown CUA tool selector "${selector}"`); + if (!CUA_SELECTORS.includes(selector)) throw new Error(`unknown browser tool selector "${selector}"`); } return Object.freeze({ selectors: Object.freeze(selectors), coordinates: coordinateMode }); } @@ -85,12 +85,12 @@ export function expandSelection(selection: CuaSelection): CuaToolSpec[] { const result: CuaToolSpec[] = []; for (const selector of selection.selectors) { const entry = MENU[selector]; - if (!entry) throw new Error(`unknown CUA tool selector "${selector}"`); + if (!entry) throw new Error(`unknown browser tool selector "${selector}"`); result.push(...entry(coordinates)); } const identities = new Set(); for (const spec of result) { - if (identities.has(spec.identity)) throw new Error(`CUA selection contains duplicate tool identity "${spec.identity}"`); + if (identities.has(spec.identity)) throw new Error(`selection contains duplicate tool identity "${spec.identity}"`); identities.add(spec.identity); } return result; diff --git a/packages/pi-extension/test/extension.test.ts b/packages/pi-extension/test/extension.test.ts index 35d7957..22e827b 100644 --- a/packages/pi-extension/test/extension.test.ts +++ b/packages/pi-extension/test/extension.test.ts @@ -97,10 +97,8 @@ const anthropicCtx = { ...ctx, model: getCuaModel("anthropic:claude-fable-5") } describe("pi extension activation", () => { it("reads parsed flags at session_start, installs selectable batch tools, and preserves unrelated tools", async () => { const pi = makePi({ - "cua-tools": "browser", - "cua-coordinates": "pixels", - "cua-browser-timeout": "300", - "cua-profile-save-changes": false, + "browser-tools": "browser", + "browser-coordinates": "pixels", }); extension(pi.api); await getHandler(pi, "session_start")({}, ctx); @@ -110,21 +108,17 @@ describe("pi extension activation", () => { it("rejects invalid parsed flags instead of silently activating no tools", () => { const pi = makePi({ - "cua-tools": "nope", - "cua-coordinates": "pixels", - "cua-browser-timeout": "300", - "cua-profile-save-changes": false, + "browser-tools": "nope", + "browser-coordinates": "pixels", }); extension(pi.api); - expect(() => getHandler(pi, "session_start")({}, ctx)).toThrow('unknown CUA tool selector "nope"'); + expect(() => getHandler(pi, "session_start")({}, ctx)).toThrow('unknown browser tool selector "nope"'); }); it("registers the CUA Anthropic provider and serializes native computer use", async () => { const pi = makePi({ - "cua-tools": "anthropic-computer", - "cua-coordinates": "pixels", - "cua-browser-timeout": "300", - "cua-profile-save-changes": false, + "browser-tools": "anthropic-computer", + "browser-coordinates": "pixels", }); extension(pi.api); expect(pi.providers.map((provider) => provider.id)).toContain("anthropic"); @@ -144,10 +138,8 @@ describe("pi extension activation", () => { const get = vi.spyOn(CuaBrowserRuntime.prototype, "get"); try { const pi = makePi({ - "cua-tools": "anthropic-computer", - "cua-coordinates": "pixels", - "cua-browser-timeout": "300", - "cua-profile-save-changes": false, + "browser-tools": "anthropic-computer", + "browser-coordinates": "pixels", }); extension(pi.api); await getHandler(pi, "session_start")({}, anthropicCtx); @@ -171,10 +163,8 @@ describe("pi extension activation", () => { it("applies provider transforms only for the active CUA subset", async () => { const pi = makePi({ - "cua-tools": "playwright", - "cua-coordinates": "pixels", - "cua-browser-timeout": "300", - "cua-profile-save-changes": false, + "browser-tools": "playwright", + "browser-coordinates": "pixels", }); extension(pi.api); await getHandler(pi, "session_start")({}, ctx); @@ -183,7 +173,7 @@ describe("pi extension activation", () => { const transformed = await getHandler(pi, "before_provider_request")({ payload: { tools: [] } }, ctx); expect(transformed).toEqual({ tools: [] }); - const inactive = makePi({ "cua-coordinates": "pixels", "cua-browser-timeout": "300", "cua-profile-save-changes": false }); + const inactive = makePi({ "browser-coordinates": "pixels" }); extension(inactive.api); await getHandler(inactive, "session_start")({}, ctx); expect(await getHandler(inactive, "before_provider_request")({ payload: { tools: [] } }, ctx)).toBeUndefined(); @@ -191,17 +181,15 @@ describe("pi extension activation", () => { it("does not persist a flag baseline and restores only command-origin selections", async () => { const pi = makePi({ - "cua-tools": "playwright", - "cua-coordinates": "pixels", - "cua-browser-timeout": "300", - "cua-profile-save-changes": false, + "browser-tools": "playwright", + "browser-coordinates": "pixels", }); extension(pi.api); await getHandler(pi, "session_start")({}, ctx); await getHandler(pi, "session_shutdown")({}, ctx); expect(pi.entries).toEqual([]); - await getCommand(pi, "cua-tools").handler("computer", ctx); + await getCommand(pi, "browser-tools").handler("computer", ctx); expect(pi.entries).toEqual([ { type: "custom", @@ -211,10 +199,8 @@ describe("pi extension activation", () => { ]); const resumed = makePi({ - "cua-tools": "playwright", - "cua-coordinates": "pixels", - "cua-browser-timeout": "300", - "cua-profile-save-changes": false, + "browser-tools": "playwright", + "browser-coordinates": "pixels", }); const resumedCtx = { ...ctx, sessionManager: { getBranch: () => pi.entries } } as unknown as ExtensionContext; extension(resumed.api); @@ -223,10 +209,8 @@ describe("pi extension activation", () => { expect(resumed.active).not.toContain("browser_snapshot"); const legacy = makePi({ - "cua-tools": "playwright", - "cua-coordinates": "pixels", - "cua-browser-timeout": "300", - "cua-profile-save-changes": false, + "browser-tools": "playwright", + "browser-coordinates": "pixels", }); const legacyCtx = { ...ctx, @@ -251,10 +235,8 @@ describe("pi extension activation", () => { // allowlist's removal: an unknown provider now compiles fine, but Anthropic's // native computer still cannot reach an OpenAI model. const pi = makePi({ - "cua-tools": "anthropic-computer", - "cua-coordinates": "pixels", - "cua-browser-timeout": "300", - "cua-profile-save-changes": false, + "browser-tools": "anthropic-computer", + "browser-coordinates": "pixels", }); extension(pi.api); await getHandler(pi, "session_start")({}, anthropicCtx); @@ -277,10 +259,8 @@ describe("pi extension activation", () => { it("keeps an ordinary function tool active on a model the registry does not carry", async () => { const pi = makePi({ - "cua-tools": "playwright", - "cua-coordinates": "pixels", - "cua-browser-timeout": "300", - "cua-profile-save-changes": false, + "browser-tools": "playwright", + "browser-coordinates": "pixels", }); extension(pi.api); await getHandler(pi, "session_start")({}, ctx); @@ -303,10 +283,8 @@ describe("pi extension activation", () => { }) as never); try { const pi = makePi({ - "cua-tools": "playwright", - "cua-coordinates": "pixels", - "cua-browser-timeout": "300", - "cua-profile-save-changes": false, + "browser-tools": "playwright", + "browser-coordinates": "pixels", }); extension(pi.api); // `browser-batch` was a selector before the menu shrank to eight entries. A @@ -327,7 +305,7 @@ describe("pi extension activation", () => { await getHandler(pi, "session_start")({}, resumedCtx); expect(pi.active).toContain("computer_click"); - expect(written.join("")).toMatch(/ignoring retired tool selector\(s\).*browser-batch/); + expect(written.join("")).toMatch(/ignoring retired selector\(s\).*browser-batch/); } finally { write.mockRestore(); } @@ -341,10 +319,8 @@ describe("pi extension activation", () => { }) as never); try { const pi = makePi({ - "cua-tools": "playwright", - "cua-coordinates": "pixels", - "cua-browser-timeout": "300", - "cua-profile-save-changes": false, + "browser-tools": "playwright", + "browser-coordinates": "pixels", }); extension(pi.api); const resumedCtx = { @@ -362,10 +338,10 @@ describe("pi extension activation", () => { await getHandler(pi, "session_start")({}, resumedCtx); - // The persisted selection came from /cua-tools, which overrides the flags. + // The persisted selection came from /browser-tools, which overrides the flags. // Reviving `playwright` here would re-enable a tool this session replaced. expect(pi.active).not.toContain("playwright_execute"); - expect(written.join("")).toMatch(/ignoring retired tool selector\(s\).*browser-batch/); + expect(written.join("")).toMatch(/ignoring retired selector\(s\).*browser-batch/); } finally { write.mockRestore(); } @@ -379,10 +355,8 @@ describe("pi extension activation", () => { }) as never); try { const pi = makePi({ - "cua-tools": "anthropic-computer", - "cua-coordinates": "pixels", - "cua-browser-timeout": "300", - "cua-profile-save-changes": false, + "browser-tools": "anthropic-computer", + "browser-coordinates": "pixels", }); extension(pi.api); // An OpenAI model cannot take Anthropic's native computer tool. Without this @@ -391,7 +365,7 @@ describe("pi extension activation", () => { await getHandler(pi, "session_start")({}, ctx); expect(pi.active).not.toContain("computer"); - expect(written.join("")).toMatch(/cua: no browser tool is active — .*requires a anthropic model/); + expect(written.join("")).toMatch(/browser tools: none active — .*requires a anthropic model/); // One warning per distinct reason, not once per reconcile. const before = written.length; await getHandler(pi, "before_agent_start")({}, ctx); @@ -404,10 +378,8 @@ describe("pi extension activation", () => { it("lists selector availability without changing the selection, and clears it only on request", async () => { const notices: string[] = []; const pi = makePi({ - "cua-tools": "playwright", - "cua-coordinates": "pixels", - "cua-browser-timeout": "300", - "cua-profile-save-changes": false, + "browser-tools": "playwright", + "browser-coordinates": "pixels", }); // An OpenAI model so a native selector it cannot take shows up unavailable. const listingCtx = { @@ -418,7 +390,7 @@ describe("pi extension activation", () => { extension(pi.api); await getHandler(pi, "session_start")({}, listingCtx); - await getCommand(pi, "cua-tools").handler("", listingCtx); + await getCommand(pi, "browser-tools").handler("", listingCtx); const listing = notices.at(-1) ?? ""; expect(listing).toContain("* playwright"); // The reason comes from the catalog compiler, not from a rule restated here. @@ -427,23 +399,21 @@ describe("pi extension activation", () => { expect(pi.active).toContain("playwright_execute"); expect(pi.entries).toEqual([]); - await getCommand(pi, "cua-tools").handler("none", listingCtx); + await getCommand(pi, "browser-tools").handler("none", listingCtx); expect(pi.active).not.toContain("playwright_execute"); }); it("re-registers declarations when a new session changes coordinate mode", async () => { const flags: Record = { - "cua-tools": "computer", - "cua-coordinates": "pixels", - "cua-browser-timeout": "300", - "cua-profile-save-changes": false, + "browser-tools": "computer", + "browser-coordinates": "pixels", }; const pi = makePi(flags); extension(pi.api); await getHandler(pi, "session_start")({}, ctx); expect(pi.tools.find((tool) => tool.name === "computer_click")?.description).not.toContain("[0, 1000]"); - flags["cua-coordinates"] = "normalized-1000"; + flags["browser-coordinates"] = "normalized-1000"; await getHandler(pi, "session_start")({}, ctx); expect(pi.tools.find((tool) => tool.name === "computer_click")?.description).toContain("[0, 1000]"); }); diff --git a/packages/pi-extension/test/pi-modes.test.ts b/packages/pi-extension/test/pi-modes.test.ts index 6ebfccf..89ee767 100644 --- a/packages/pi-extension/test/pi-modes.test.ts +++ b/packages/pi-extension/test/pi-modes.test.ts @@ -197,7 +197,7 @@ describe("pi modes", () => { KERNEL_BASE_URL: server.url, KERNEL_API_KEY: "test-key", }; - const args = ["--extension", extension, "--provider", "openai", "--model", "gpt-5.6-sol", "--cua-tools", "playwright"]; + const args = ["--extension", extension, "--provider", "openai", "--model", "gpt-5.6-sol", "--browser-tools", "playwright"]; const print = await runPrint([...args, "-p", "run the browser tool"], env, directory); expect(print.code, `${print.stdout}\n${print.stderr}`).toBe(0); diff --git a/packages/pi-extension/test/provider-stream.test.ts b/packages/pi-extension/test/provider-stream.test.ts index 537de34..c1c29ac 100644 --- a/packages/pi-extension/test/provider-stream.test.ts +++ b/packages/pi-extension/test/provider-stream.test.ts @@ -63,10 +63,8 @@ const ctx = { describe("provider stream ownership", () => { it("streams a native surface with the derived transport and the incoming plan", async () => { const pi = makePi({ - "cua-tools": "openai-computer", - "cua-coordinates": "pixels", - "cua-browser-timeout": "300", - "cua-profile-save-changes": false, + "browser-tools": "openai-computer", + "browser-coordinates": "pixels", }); extension(pi.api); const openaiCtx = { ...ctx, model: getCuaModel("openai:gpt-5.6-sol") } as ExtensionContext; diff --git a/packages/pi-extension/test/selection.test.ts b/packages/pi-extension/test/selection.test.ts index 6c56493..da79fea 100644 --- a/packages/pi-extension/test/selection.test.ts +++ b/packages/pi-extension/test/selection.test.ts @@ -1,6 +1,8 @@ import { getCuaModel } from "@onkernel/cua-ai"; import { describe, expect, it } from "vitest"; import { compileSpecs, CUA_SELECTORS, expandSelection, parseSelection, selectorAvailability } from "../src/selection"; +import { DEFAULT_BROWSER_TIMEOUT_SECONDS } from "../src/browser-runtime"; +import { parseBrowserOptions } from "../src/index"; describe("CUA pi selectors", () => { it("has stable exact browser and computer entry membership, batch included", () => { @@ -58,7 +60,7 @@ describe("CUA pi selectors", () => { // Packaging variants are gone: the batch tool ships inside its generic entry, // and the 37 individual tool names are no longer selectable on their own. for (const retired of ["mixed", "browser-batch", "computer-batch", "browser_snapshot", "computer_click"]) { - expect(() => parseSelection(retired, "pixels")).toThrow(/unknown CUA tool selector/); + expect(() => parseSelection(retired, "pixels")).toThrow(/unknown browser tool selector/); } expect(expandSelection(parseSelection("browser-act,playwright", "pixels")).map((tool) => tool.name)).toEqual([ "browser_act", @@ -144,3 +146,23 @@ describe("CUA pi selectors", () => { expect(byName.get("playwright")?.reason).toBeUndefined(); }); }); + +describe("browser options", () => { + it("takes one JSON object and defaults only the timeout", () => { + expect(parseBrowserOptions(undefined, undefined)).toEqual({ create: {} }); + expect(parseBrowserOptions(undefined, '{"stealth":true,"proxy_id":"p1"}')).toEqual({ + create: { stealth: true, proxy_id: "p1" }, + }); + // Forwarded verbatim, so a field the SDK adds needs no flag here. + expect(parseBrowserOptions(undefined, '{"invented_future_field":42}').create).toMatchObject({ invented_future_field: 42 }); + expect(DEFAULT_BROWSER_TIMEOUT_SECONDS).toBe(600); + }); + + it("rejects input that would silently do nothing", () => { + expect(() => parseBrowserOptions(undefined, "not json")).toThrow(/must be valid JSON/); + expect(() => parseBrowserOptions(undefined, "[1,2]")).toThrow(/must be a JSON object/); + // Attaching an existing browser and configuring a new one are contradictory. + expect(() => parseBrowserOptions("sess_1", '{"stealth":true}')).toThrow(/attaches an existing browser/); + expect(parseBrowserOptions("sess_1", undefined)).toEqual({ sessionId: "sess_1", create: {} }); + }); +});