From c8fd7b7078da841d5537aee9334291887445889f Mon Sep 17 00:00:00 2001 From: Igor Soarez Date: Sun, 30 Aug 2026 16:27:22 +0100 Subject: fix(profile): never prompt for a profile when driving a remote browser Profile selection works by launching the browser with --profile-directory and letting ProcessSingleton delegate to the running process, which only reaches a browser on this machine. discoverEndpoint() already withholds userDataDir for a remote target, but resolveUserDataDir() fell through to the local candidate dirs, so the picker offered this disk's Chromium profiles for a browser on another host - on castle, Brave's profiles for a Chrome on :9223. Pinning one launched a local browser and polled the remote for a sentinel that could never appear, failing after the seed deadline; seedProfileWindow()'s mismatch guard missed it because connectedDir is undefined on the remote path. Setup now returns early instead of prompting - the question had no actionable answer, and since cancelling stores nothing it returned every session. /browser-profile and seedProfileWindow() refuse with the reason, the latter catching a pin left over from a local session. --- FORK.md | 40 ++++++++++++++++++++++++++++++++++++++++ src/cdp/current-target.ts | 27 +++++++++++++++++++++++++++ src/profile/bind.ts | 16 ++++++++++++++++ src/profile/command.ts | 24 ++++++++++++++++++++++++ 4 files changed, 107 insertions(+) create mode 100644 src/cdp/current-target.ts diff --git a/FORK.md b/FORK.md index 4e78478..68a57bc 100644 --- a/FORK.md +++ b/FORK.md @@ -117,6 +117,46 @@ needed at all. Note the branch here is `master` (matching the other repos on castle) while upstream uses `main`. +## Browser profiles are local-only + +Upstream's `/browser-profile` does not work against a remote target, and this +fork now says so instead of guessing. + +Choosing a profile is not a CDP operation. `Target.createTarget` rejects another +profile's `browserContextId`, and a bare createTarget lands in the focus-derived +default context, so the only way in is the browser's own command line — +`--profile-directory`, delegated to the running browser by Chromium's +ProcessSingleton (`src/profile/launch.ts`). That is a local-process mechanism +with no remote equivalent. + +`discoverEndpoint()` already returns no `userDataDir` for a remote target, but +`resolveUserDataDir()` used to fall through to `userDataDirCandidates()` — the +Chromium user-data-dirs on *this* disk. On castle that meant the picker offering +Brave's profiles while the harness was connected to castle's Chrome on :9223, +and `seedProfileWindow()` could not catch it: its mismatch guard is +`connectedDir && connectedDir !== pin.userDataDir`, and `connectedDir` is +undefined on the remote path. Pinning launched a local Brave window and then +polled the remote browser for a sentinel that could never appear there, failing +only after the 15s seed deadline. + +So, for a remote target: + +- **setup never prompts.** `promptForProfileIfUnset()` returns early. A prompt + with no actionable answer is bad enough; because cancelling stores nothing, it + also came back every single session. +- **`/browser-profile` refuses**, naming the target and pointing at + `/browser-target local`. +- **`seedProfileWindow()` refuses**, which catches a pin left over from a local + session rather than letting it launch a browser on the wrong machine. + +The default against a remote browser is therefore unpinned — whichever window is +focused. castle's `~/.chrome-cdp-profile` has exactly one profile (`Default`), +so there is nothing to disambiguate. + +Making profiles selectable on castle needs the `--profile-directory` launch to +happen *on castle*, next to the browser — not a patch to this path, a separate +piece of machinery. Unbuilt so far. + ## Verifying after a merge With a browser exposed at `$BU_CDP_HTTP`: diff --git a/src/cdp/current-target.ts b/src/cdp/current-target.ts new file mode 100644 index 0000000..82bebe6 --- /dev/null +++ b/src/cdp/current-target.ts @@ -0,0 +1,27 @@ +/** + * The target the harness is actually pointed at, resolved with I/O. + * + * `resolveCdpTarget` in endpoint.ts is deliberately pure, so every caller that + * needs the *effective* target has to read the stored choice first. This wraps + * that pair so callers outside the CDP layer — profile selection in + * particular — can ask "am I driving a browser on this machine?" without + * duplicating the precedence rules. + */ + +import { type CdpTarget, resolveCdpTarget } from "./endpoint"; +import { readTarget } from "./target-store"; + +/** The effective target: BU_CDP_HTTP, else /browser-target, else the default. */ +export const currentCdpTarget = async (): Promise => + resolveCdpTarget(process.env, await readTarget()); + +/** + * True when the harness is driving a browser on another host. + * + * Profile selection is meaningless in that case: it works by launching the + * browser binary with --profile-directory and letting ProcessSingleton hand + * the argv to the running browser (see profile/launch.ts), which only reaches + * a browser on this machine. + */ +export const drivingRemoteBrowser = async (): Promise => + (await currentCdpTarget()).kind === "remote"; diff --git a/src/profile/bind.ts b/src/profile/bind.ts index 95cffaa..5382547 100644 --- a/src/profile/bind.ts +++ b/src/profile/bind.ts @@ -1,6 +1,8 @@ import type { BrowserClient } from "../client"; import type { ResultOf } from "../cdp/commands"; import { type CdpError, cdpError } from "../cdp/errors"; +import { currentCdpTarget } from "../cdp/current-target"; +import { describeCdpTarget } from "../cdp/endpoint"; import { type Result, err, ok } from "../util/result"; import { detectRunningBrowser, resolveBrowserExecutable } from "./browser-process"; import { openProfileWindow } from "./launch"; @@ -25,6 +27,20 @@ export const seedProfileWindow = async ( client: BrowserClient, pin: ProfilePin, ): Promise> => { + // A pin left over from a local session is not actionable against a browser on + // another host: the launch below would start a browser HERE and then poll the + // REMOTE one for a sentinel that cannot appear, failing only after the seed + // deadline. Refuse up front and say why. + const target = await currentCdpTarget(); + if (target.kind === "remote") { + return err(cdpError( + "discovery_failed", + `the selected profile "${pin.label}" belongs to a browser on this machine, but the harness is driving ` + + `${describeCdpTarget(target)}. Run /browser-profile and clear the selection, or /browser-target local to ` + + "drive this machine's browser.", + )); + } + const connectedDir = client.userDataDir(); if (connectedDir && connectedDir !== pin.userDataDir) { return err(cdpError( diff --git a/src/profile/command.ts b/src/profile/command.ts index 270b42f..ea0ddcc 100644 --- a/src/profile/command.ts +++ b/src/profile/command.ts @@ -1,6 +1,8 @@ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; import type { BrowserClient } from "../client"; import { discoverEndpoint } from "../cdp/discovery"; +import { currentCdpTarget } from "../cdp/current-target"; +import { describeCdpTarget } from "../cdp/endpoint"; import { type Result, err, ok } from "../util/result"; import { type BrowserProfile, listProfiles } from "./list"; import { browserNameForUserDataDir, userDataDirCandidates } from "./paths"; @@ -60,6 +62,21 @@ const resolveUserDataDir = async (client: BrowserClient): Promise 0) withProfiles.push(dir); @@ -164,6 +181,13 @@ export const promptForProfileIfUnset = async ( client.setProfilePin(stored); return undefined; } + // Never prompt for a remote browser. Profile selection cannot act on one (see + // resolveUserDataDir), so asking would be a blocking question with no useful + // answer — and every cancelled prompt stores nothing, so it would come back + // every session. Silence is correct here: the browser keeps its own default + // profile, and /browser-status already reports which browser is being driven. + if ((await currentCdpTarget()).kind === "remote") return undefined; + if (!ctx?.hasUI) { return "No browser profile selected — using whichever window is focused. Run /browser-profile to pin one."; } -- cgit v1.2.3