From 8834211ef3011aad807fc980b0d0d2613f24f2fa Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:24:00 +0000 Subject: [PATCH 1/2] Add --proxy flag to route the browser through a Kernel proxy Accepts a proxy id or name, resolved via proxies.retrieve with a list-by-name fallback; ambiguous names and unknown selectors are errors (proxies are never auto-created, unlike --profile). Applies to one-shot subcommands and session start; named sessions persist the resolved proxy_id in their metadata. --- packages/cli/src/cli-harness.ts | 3 ++ packages/cli/src/cli.ts | 6 +++ packages/cli/src/harness-browser.ts | 28 +++++++++++++ packages/cli/src/harness-named-sessions.ts | 11 +++++- packages/cli/test/harness-browser.test.ts | 46 ++++++++++++++++++++++ skills/cua-cli/SKILL.md | 5 +++ 6 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 packages/cli/test/harness-browser.test.ts diff --git a/packages/cli/src/cli-harness.ts b/packages/cli/src/cli-harness.ts index 66e351d..2de6d20 100644 --- a/packages/cli/src/cli-harness.ts +++ b/packages/cli/src/cli-harness.ts @@ -187,6 +187,7 @@ export interface HarnessCliFlags { model?: string; thinking?: string; browserProfile?: string; + browserProxy?: string; browserTimeout?: number; maxSteps?: number; out?: string; @@ -257,6 +258,7 @@ export async function provisionForFlags(flags: HarnessCliFlags, auth: KernelAuth timeoutSeconds: flags.browserTimeout, profileSelector: flags.browserProfile, saveChanges: flags.profileSaveChanges, + proxySelector: flags.browserProxy, }); if (flags.verbose) { stderr.write(`[cua] browser session=${handle.browser.session_id}\n`); @@ -668,6 +670,7 @@ export async function runSessionSubcommand(args: string[], flags: HarnessCliFlag browserTimeoutSeconds: flags.browserTimeout, profileSelector: flags.browserProfile, saveProfileChanges: flags.profileSaveChanges, + proxySelector: flags.browserProxy, model: flags.model ? resolveCuaModelRef(flags.model) : undefined, }); stdout.write(`name=${meta.name}\n`); diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index f678ead..e474b56 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -58,6 +58,8 @@ Options: --thinking Thinking level: off | minimal | low | medium | high | xhigh (default: low; applies to providers that support it) --profile Kernel browser profile to load + --proxy Kernel proxy to route the browser through + (must already exist; never auto-created) --profile-no-save-changes Do not persist changes back to the profile --browser-timeout Browser inactivity timeout in seconds (default 300) --max-steps Max turns for action subcommands (default 3) @@ -130,6 +132,7 @@ interface CliFlags { model?: string; thinking?: string; browserProfile?: string; + browserProxy?: string; browserTimeout?: number; maxSteps?: number; out?: string; @@ -159,6 +162,7 @@ function parseCliArgs(argv: string[]): CliFlags { model: { type: "string", short: "m" }, thinking: { type: "string" }, profile: { type: "string" }, + proxy: { type: "string" }, "profile-no-save-changes": { type: "boolean", default: false }, "browser-timeout": { type: "string" }, "max-steps": { type: "string" }, @@ -223,6 +227,7 @@ function parseCliArgs(argv: string[]): CliFlags { model: parsed.values.model as string | undefined, thinking: parsed.values.thinking as string | undefined, browserProfile: parsed.values.profile as string | undefined, + browserProxy: parsed.values.proxy as string | undefined, browserTimeout: Number.isFinite(browserTimeout) ? browserTimeout : undefined, maxSteps: Number.isFinite(maxSteps) ? maxSteps : undefined, out: parsed.values.out as string | undefined, @@ -259,6 +264,7 @@ function toHarnessFlags(flags: CliFlags): HarnessCliFlags { model: flags.model, thinking: flags.thinking, browserProfile: flags.browserProfile, + browserProxy: flags.browserProxy, browserTimeout: flags.browserTimeout, maxSteps: flags.maxSteps, out: flags.out, diff --git a/packages/cli/src/harness-browser.ts b/packages/cli/src/harness-browser.ts index ede47b9..8674457 100644 --- a/packages/cli/src/harness-browser.ts +++ b/packages/cli/src/harness-browser.ts @@ -20,6 +20,8 @@ export interface ProvisionBrowserOptions { profileId?: string; /** Persist changes back to the profile when the session ends. Defaults to false. */ saveChanges?: boolean; + /** Proxy id or name. Must already exist; never auto-created. */ + proxySelector?: string; } const CUID2_LENGTH = 24; @@ -53,6 +55,29 @@ export async function resolveProfileId(client: Kernel, selector: string): Promis } } +/** + * Resolve a proxy selector (id or name) to a proxy id. Unlike profiles, + * proxies are never auto-created — creating one requires type/location/ + * credential configuration that does not fit a bare name. + */ +export async function resolveProxyId(client: Kernel, selector: string): Promise { + const trimmed = selector.trim(); + if (!trimmed) throw new Error("proxy selector is empty"); + try { + const existing = await client.proxies.retrieve(trimmed); + if (existing.id) return existing.id; + } catch (err) { + if (!(err instanceof NotFoundError)) { + throw new Error(`looking up proxy "${trimmed}": ${(err as Error).message}`, { cause: err }); + } + } + const proxies = await client.proxies.list(); + const matches = proxies.filter((proxy) => proxy.name === trimmed && proxy.id); + if (matches.length === 1) return matches[0]!.id!; + if (matches.length > 1) throw new Error(`proxy name "${trimmed}" is ambiguous (${matches.length} proxies); pass the proxy id`); + throw new Error(`proxy "${trimmed}" was not found; create one first (e.g. kernel proxies create)`); +} + /** Create a Kernel SDK client with the supplied auth. */ export function createKernelClient(apiKey: string, baseUrl?: string): Kernel { return new Kernel({ apiKey, ...(baseUrl ? { baseURL: baseUrl } : {}) }); @@ -75,6 +100,9 @@ export async function provisionBrowser(opts: ProvisionBrowserOptions): Promise new NotFoundError(404, { message: "not found" }, "not found", new Headers()); + +function fakeClient(overrides: { + retrieve?: (id: string) => Promise<{ id?: string }>; + list?: () => Promise>; +}): Kernel { + return { + proxies: { + retrieve: overrides.retrieve ?? (async () => Promise.reject(notFound())), + list: overrides.list ?? (async () => []), + }, + } as unknown as Kernel; +} + +describe("resolveProxyId", () => { + it("returns the id when the selector is an existing proxy id", async () => { + const client = fakeClient({ retrieve: async (id) => ({ id }) }); + await expect(resolveProxyId(client, "proxy_abc")).resolves.toBe("proxy_abc"); + }); + + it("falls back to a unique name match from the proxy list", async () => { + const client = fakeClient({ list: async () => [{ id: "proxy_1", name: "residential-us" }, { id: "proxy_2", name: "other" }] }); + await expect(resolveProxyId(client, "residential-us")).resolves.toBe("proxy_1"); + }); + + it("rejects an ambiguous name instead of guessing", async () => { + const client = fakeClient({ list: async () => [{ id: "proxy_1", name: "us" }, { id: "proxy_2", name: "us" }] }); + await expect(resolveProxyId(client, "us")).rejects.toThrow(/ambiguous/); + }); + + it("never auto-creates: an unknown selector is an error", async () => { + const client = fakeClient({}); + await expect(resolveProxyId(client, "does-not-exist")).rejects.toThrow(/was not found/); + }); + + it("propagates non-404 lookup failures", async () => { + const client = fakeClient({ + retrieve: async () => Promise.reject(new Error("boom")), + }); + await expect(resolveProxyId(client, "proxy_abc")).rejects.toThrow(/looking up proxy/); + }); +}); diff --git a/skills/cua-cli/SKILL.md b/skills/cua-cli/SKILL.md index c45dea7..361917d 100644 --- a/skills/cua-cli/SKILL.md +++ b/skills/cua-cli/SKILL.md @@ -53,6 +53,11 @@ Useful flags: a bare model id that matches exactly one entry. - `--max-steps ` — bound the agent loop on `cua do` (default 3). - `--filter interactive` — restrict `cua snapshot` to interactive elements. +- `--proxy ` — route the browser through a Kernel proxy. + The proxy must already exist (create one via the Kernel API/CLI first); + unlike `--profile`, an unknown name is an error, never auto-created. For + named sessions pass it to `session start`; later `-s` calls attach to the + same browser and inherit it. - `--profile ` — load a Kernel browser profile for cookies / storage. Existing ids or names are reused; a non-id name is created if it does not exist. Use this whenever logged-in state or other persisted browser From 8d6d83b1177ab7a668c152fc2dfeb6bbcfc984a5 Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:25:20 +0000 Subject: [PATCH 2/2] Bump cua-cli 0.3.1 for the --proxy flag release --- package-lock.json | 2 +- packages/cli/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index ef40724..466eb1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6141,7 +6141,7 @@ }, "packages/cli": { "name": "@onkernel/cua-cli", - "version": "0.3.0", + "version": "0.3.1", "license": "MIT", "dependencies": { "@earendil-works/pi-coding-agent": "0.80.3", diff --git a/packages/cli/package.json b/packages/cli/package.json index f8db938..fdc31b4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@onkernel/cua-cli", - "version": "0.3.0", + "version": "0.3.1", "description": "Kernel-cloud-browser computer-use TUI built on @onkernel/cua-agent and pi-tui", "license": "MIT", "type": "module",