From 34e8e7025c835574d6a1f1fca7a11c437c61205a Mon Sep 17 00:00:00 2001 From: Johan Ekenberg Date: Wed, 29 Jul 2026 21:07:38 +0200 Subject: [PATCH] pi: honor BETTERWRIGHT_PROFILE for a separate persistent identity The CLI and the MCP server already read BETTERWRIGHT_PROFILE so two processes on one home can run as different signed-in identities; the Pi extension silently ignored it, so a second Pi session always contended for the default profile and fell over to an ephemeral, signed-out one. Resolve the profile eagerly (options.profile over the env var) so an invalid name fails at extension load, and pass it to the browser like the other env-resolved options. --- README.md | 2 +- docs/architecture.md | 2 +- docs/sessions.md | 3 ++- src/pi-extension.ts | 17 +++++++++++++++-- tests/node/pi-extension.test.ts | 14 ++++++++++++++ types/pi-extension.d.ts | 1 + 6 files changed, 34 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c87f19e..3d625de 100644 --- a/README.md +++ b/README.md @@ -305,7 +305,7 @@ new BetterWright({ profile: "review" }); // the reading account, concurrently Omitting `profile` keeps the single default profile, unchanged. The vault and artifacts are shared across profiles, so a credential saved once fills anywhere. CLI: `--profile ` or `BETTERWRIGHT_PROFILE`, which the MCP -server reads too. See +server and the Pi extension read too. See [docs/sessions.md](docs/sessions.md#sessions-vs-profiles). ## Docs diff --git a/docs/architecture.md b/docs/architecture.md index 22de4a8..db96fbf 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -163,7 +163,7 @@ ephemeral profile from `browser/runtime` (a signed-out browser) rather than corrupting it. `profile: ""` (CLI `--profile ` or `BETTERWRIGHT_PROFILE`, which -the MCP server reads too) selects a **separate identity**: an independent persistent profile at +the MCP server and the Pi extension read too) selects a **separate identity**: an independent persistent profile at `browser/profiles/`, with its own cookie jar, its own lock (`browser/profiles/.betterwright-lock`, a sibling of the directory), its own [session daemon](sessions.md), and its own `exec` transcripts. Two diff --git a/docs/sessions.md b/docs/sessions.md index 81241b7..4055f89 100644 --- a/docs/sessions.md +++ b/docs/sessions.md @@ -64,7 +64,8 @@ betterwright close --all # stops every profile's daemon ``` `BETTERWRIGHT_PROFILE=social` sets the identity for a whole shell (and is the -only way to set it for the MCP server); `--profile` beats it. Both run at once +only way to set it for the MCP server and the Pi extension); `--profile` +beats it. Both run at once and both stay signed in, because each profile locks its own directory under `$BETTERWRIGHT_HOME/browser/profiles/`. Two runs of the *same* profile still serialize: the second gets an isolated, signed-out ephemeral diff --git a/src/pi-extension.ts b/src/pi-extension.ts index abc8c94..145b63f 100644 --- a/src/pi-extension.ts +++ b/src/pi-extension.ts @@ -8,6 +8,7 @@ import { piImageContent, piPrimaryImageArtifact, } from "./pi.js"; +import { resolveProfileName } from "./profile-name.js"; import { agentSystemPrompt } from "./prompt.js"; import { piBrowserToolParameters, piLoginToolParameters } from "./tool-schemas.js"; @@ -125,7 +126,7 @@ function normalizedMaxSteps(value) { return parsed; } -function resolvedBrowserOptions(options) { +function resolvedBrowserOptions(options, profile) { const timeout = envPositiveInteger("BETTERWRIGHT_PI_TIMEOUT_SECONDS", 0); const downloadPolicy = String( process.env.BETTERWRIGHT_PI_DOWNLOAD_POLICY || "", @@ -136,6 +137,7 @@ function resolvedBrowserOptions(options) { ...(options || {}), ...(timeout ? { defaultTimeout: timeout } : {}), ...(downloadPolicy ? { downloadPolicy } : {}), + ...(profile ? { profile } : {}), }; } @@ -412,6 +414,15 @@ export function createPiExtension(options: any = {}) { const startUrl = normalizedStartUrl( options.startUrl ?? process.env.BETTERWRIGHT_PI_START_URL, ); + // Same identity knob as the CLI and MCP server: a named profile is a + // separate persistent cookie jar with its own lock, so two Pi sessions + // with different profiles run concurrently, both signed in. Resolved + // eagerly so an invalid name surfaces at extension load, not as a + // mysteriously signed-out browser later. + const profile = resolveProfileName( + String(options.profile ?? process.env.BETTERWRIGHT_PROFILE ?? "").trim() || + undefined, + ); let browser = options.browser || null; let startPromise = null; let pendingStartWarning = ""; @@ -447,7 +458,9 @@ export function createPiExtension(options: any = {}) { async function getBrowser() { if (!browser) - browser = new BetterWright(resolvedBrowserOptions(options.browserOptions)); + browser = new BetterWright( + resolvedBrowserOptions(options.browserOptions, profile), + ); if (startUrl && !startPromise) { startPromise = browser .run( diff --git a/tests/node/pi-extension.test.ts b/tests/node/pi-extension.test.ts index e3c7b4f..e6387af 100644 --- a/tests/node/pi-extension.test.ts +++ b/tests/node/pi-extension.test.ts @@ -432,6 +432,20 @@ test("native Pi extension reports invalid configuration and recovers from start () => createPiExtension({ startUrl: "file:///tmp/page.html" })(new FakePi()), /http or https/, ); + // Profile names become path segments; an invalid one must fail at + // extension load (option or BETTERWRIGHT_PROFILE env), not at first browse. + assert.throws( + () => createPiExtension({ profile: "../escape" })(new FakePi()), + /profile/i, + ); + process.env.BETTERWRIGHT_PROFILE = "bad name"; + try { + assert.throws(() => createPiExtension({})(new FakePi()), /profile/i); + } finally { + delete process.env.BETTERWRIGHT_PROFILE; + } + // A valid name is accepted (identity wiring is covered by resolvedBrowserOptions). + createPiExtension({ browser: new FakeBrowser({}), profile: "work" })(new FakePi()); const { dir, screenshot } = await fixture(); const browser = new FakeBrowser({ startFails: true, screenshot }); diff --git a/types/pi-extension.d.ts b/types/pi-extension.d.ts index d21342b..0421989 100644 --- a/types/pi-extension.d.ts +++ b/types/pi-extension.d.ts @@ -12,6 +12,7 @@ export interface PiExtensionOptions { closeBrowserOnShutdown?: boolean; guardrails?: Guardrails; maxSteps?: number; + profile?: string; requireEvidence?: boolean; session?: string; startUrl?: string;