Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>` 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
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ ephemeral profile from `browser/runtime` (a signed-out browser) rather than
corrupting it.

`profile: "<name>"` (CLI `--profile <name>` 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/<name>`, with its own cookie jar, its own lock
(`browser/profiles/<name>.betterwright-lock`, a sibling of the directory), its
own [session daemon](sessions.md), and its own `exec` transcripts. Two
Expand Down
3 changes: 2 additions & 1 deletion docs/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 15 additions & 2 deletions src/pi-extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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 || "",
Expand All @@ -136,6 +137,7 @@ function resolvedBrowserOptions(options) {
...(options || {}),
...(timeout ? { defaultTimeout: timeout } : {}),
...(downloadPolicy ? { downloadPolicy } : {}),
...(profile ? { profile } : {}),
};
}

Expand Down Expand Up @@ -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 = "";
Expand Down Expand Up @@ -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(
Expand Down
14 changes: 14 additions & 0 deletions tests/node/pi-extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
1 change: 1 addition & 0 deletions types/pi-extension.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface PiExtensionOptions {
closeBrowserOnShutdown?: boolean;
guardrails?: Guardrails;
maxSteps?: number;
profile?: string;
requireEvidence?: boolean;
session?: string;
startUrl?: string;
Expand Down
Loading