pi-agent-browser-native 0.2.49 → 0.2.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,7 +2,26 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
- No unreleased changes yet.
5
+ ## 0.2.51 - 2026-06-11
6
+
7
+ ### Fixed
8
+
9
+ - Made the source-package `prepare` lifecycle install dev dependencies with scripts disabled when Pi's `npm install --omit=dev` package path omits the compiler and peer type packages, so GitHub/source installs can still build `dist/` from a clean clone without changing runtime dependency policy.
10
+
11
+ ### Validation
12
+
13
+ - Reproduced the `pi install -l --approve https://github.com/fitchmultz/pi-agent-browser-native@v0.2.50` source-install failure, then verified production-dependency source builds, project-local GitHub install, project-local npm install, and release gates before publish.
14
+
15
+ ## 0.2.50 - 2026-06-11
16
+
17
+ ### Changed
18
+
19
+ - Keep visual/model-facing secret redaction and the native-tool bash guard while allowing loaded config credential sources and parent environment variables to pass through to upstream/provider runtime paths.
20
+ - Allow trusted project-local package config to provide web-search credential sources and browser profile/executable prompt guidance instead of limiting those capabilities to global or override config.
21
+
22
+ ### Validation
23
+
24
+ - Ran focused config/web-search/process/redaction/clipboard/extension tests, `npm run typecheck`, `npm run docs`, `npm run verify -- command-reference`, the default `npm run verify` gate, and the release gate through lifecycle, package Pi, and platform smoke validation.
6
25
 
7
26
  ## 0.2.49 - 2026-06-11
8
27
 
package/README.md CHANGED
@@ -183,7 +183,7 @@ npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-conf
183
183
  npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config show
184
184
  ```
185
185
 
186
- The optional `agent_browser_web_search` companion tool is registered only when a usable Exa or Brave credential source is configured or resolvable. It is not an `agent_browser` input mode and does not launch a browser; agents may use it whenever current/live external web information helps, then use `agent_browser` when they need page interaction, screenshots, authenticated/profile content, or DOM inspection. If both keys are available, the default provider is Exa because its `/search` endpoint returns agent-friendly highlights and search modes; set `webSearch.preferredProvider` to `"brave"` when you prefer Brave Search.
186
+ The optional `agent_browser_web_search` companion tool is available when a usable Exa or Brave credential source is configured or resolvable from startup config or trusted session config. It is not an `agent_browser` input mode and does not launch a browser; agents may use it whenever current/live external web information helps, then use `agent_browser` when they need page interaction, screenshots, authenticated/profile content, or DOM inspection. If both keys are available, the default provider is Exa because its `/search` endpoint returns agent-friendly highlights and search modes; set `webSearch.preferredProvider` to `"brave"` when you prefer Brave Search.
187
187
 
188
188
  Get an Exa API key from the [Exa dashboard](https://dashboard.exa.ai/api-keys) or a Brave Search API key from the [Brave Search API dashboard](https://api-dashboard.search.brave.com/). Most users can simply export `EXA_API_KEY` or `BRAVE_API_KEY` in the environment that launches `pi`; config is only needed when you want Pi-scoped secret references, a preferred provider, or to disable this built-in search tool.
189
189
 
@@ -227,14 +227,14 @@ cat > /tmp/pi-agent-browser-disable-web-search.json <<'JSON'
227
227
  JSON
228
228
  PI_AGENT_BROWSER_CONFIG=/tmp/pi-agent-browser-disable-web-search.json pi
229
229
 
230
- # Store a plaintext key in global Pi-scoped user config; output stays redacted.
230
+ # Store a plaintext key in Pi-scoped user config; output stays redacted.
231
231
  printf '%s' "$EXA_API_KEY" | npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config web-search set-key --provider exa --stdin
232
232
 
233
- # Store a global secret-manager command source.
233
+ # Store a secret-manager command source. Add --project when you want the repo config to own the source.
234
234
  npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config web-search set-command "op read 'op://Private/Brave Search/API Key'" --provider brave --global
235
235
  ```
236
236
 
237
- Config merges in this order: global → project → `PI_AGENT_BROWSER_CONFIG` override. Under Pi 0.79+, the globally installed or CLI-loaded extension still loads project-local `.pi/config/pi-agent-browser-native/config.json` by default because installed extensions are developer-trusted code; it skips that project layer only when Pi is launched with `--no-approve`. `webSearch.enabled` is evaluated after the loaded layers merge. Use `web-search disable --global` for a user default, `web-search disable --project` for one repo, and a `PI_AGENT_BROWSER_CONFIG` override with `{ "webSearch": { "enabled": false } }` when web search must stay off even if project config exists. Project-local plaintext, custom env aliases, interpolation-literal, malformed, and command-backed web-search keys are refused; project config may only use the matching provider env refs (`$EXA_API_KEY` / `${EXA_API_KEY}` for Exa and `$BRAVE_API_KEY` / `${BRAVE_API_KEY}` for Brave). `web-search set-key`, `set-command`, and `clear` require `--provider`; `set-env` infers Exa/Brave from `EXA_API_KEY` or `BRAVE_API_KEY` unless you pass `--provider`. The tool content, details, status output, and docs examples must not expose resolved keys.
237
+ Config merges in this order: global → project → `PI_AGENT_BROWSER_CONFIG` override. Under Pi 0.79+, the globally installed or CLI-loaded extension still loads project-local `.pi/config/pi-agent-browser-native/config.json` when Pi trust allows that project layer; it skips that project layer when Pi reports the project is untrusted or when Pi is launched with `--no-approve`. `webSearch.enabled` is evaluated after the loaded layers merge. Use `web-search disable --global` for a user default, `web-search disable --project` for one repo, and a `PI_AGENT_BROWSER_CONFIG` override with `{ "webSearch": { "enabled": false } }` when web search must stay off even if project config exists. Loaded config may use plaintext, custom environment aliases, interpolation literals, malformed-or-late-bound `$` values, and `!command` credential sources; the resolved secret is passed to the provider request while tool content, details, status output, and docs examples stay redacted. `web-search set-key`, `set-command`, and `clear` require `--provider`; `set-env` infers Exa/Brave from `EXA_API_KEY` or `BRAVE_API_KEY` unless you pass `--provider`.
238
238
 
239
239
  For Exa, the tool defaults to `searchType: "auto"` with `contents.highlights: true`. Agents may pass `searchType` (`fast`, `instant`, `deep-lite`, `deep`, or `deep-reasoning`) only when the task needs that latency/depth tradeoff; structured output schemas are intentionally not exposed yet.
240
240
 
@@ -248,7 +248,7 @@ npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-conf
248
248
  npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config browser executable set "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
249
249
  ```
250
250
 
251
- This adds agent guidance for signed-in/account-specific tasks; current releases do not auto-inject `--profile` or `--executable-path` for every launch. Configure profile/executable guidance globally or through `PI_AGENT_BROWSER_CONFIG`; project-local browser config is not trusted to steer host executable/profile prompt guidance. Ask the agent to run `agent_browser` with `args: ["profiles"]` and `args: ["doctor"]` when profile resolution fails. The upstream `profiles` command lists Chrome profiles from Chrome's user data directory; `Default` is not canonical on every machine. Use the displayed profile directory name, a full profile/user-data directory path when upstream accepts one, or a configured `browser.executablePath` plus `sessionMode: "fresh"` for a different Chromium-compatible browser.
251
+ This adds agent guidance for signed-in/account-specific tasks; current releases do not auto-inject `--profile` or `--executable-path` for every launch. Configure profile/executable guidance globally, in trusted project config, or through `PI_AGENT_BROWSER_CONFIG`. Ask the agent to run `agent_browser` with `args: ["profiles"]` and `args: ["doctor"]` when profile resolution fails. The upstream `profiles` command lists Chrome profiles from Chrome's user data directory; `Default` is not canonical on every machine. Use the displayed profile directory name, a full profile/user-data directory path when upstream accepts one, or a configured `browser.executablePath` plus `sessionMode: "fresh"` for a different Chromium-compatible browser.
252
252
 
253
253
  ## Common agent calls
254
254
 
@@ -575,7 +575,7 @@ For larger local handoffs or PR-ready confidence before expensive release/lifecy
575
575
  npm run verify -- pre-pr
576
576
  ```
577
577
 
578
- That mode composes the full default gate with `npm run verify -- package`, so package contents and forbidden archived/repo-only files are checked without launching Pi lifecycle, Crabbox, or live dogfood flows. Package, package-pi, lifecycle, platform-target, and startup-profile modes all build `dist/` first so clean checkouts do not validate stale or missing compiled output. GitHub/source installs run the package `prepare` script so the ignored `dist/` entrypoint exists before Pi loads the extension.
578
+ That mode composes the full default gate with `npm run verify -- package`, so package contents and forbidden archived/repo-only files are checked without launching Pi lifecycle, Crabbox, or live dogfood flows. Package, package-pi, lifecycle, platform-target, and startup-profile modes all build `dist/` first so clean checkouts do not validate stale or missing compiled output. GitHub/source installs run the package `prepare` script; when Pi installs with `npm install --omit=dev`, that script installs the source-build dev dependencies with lifecycle scripts disabled before building the ignored `dist/` entrypoint that Pi loads.
579
579
 
580
580
  The deterministic agent-efficiency benchmark’s **standalone JSON/Markdown accounting run** is not part of default or pre-PR `npm run verify` (only `npm run verify -- benchmark` or `npm run benchmark:agent-browser` invokes the script). The full unit suite still exercises `test/agent-browser.efficiency-benchmark.test.ts`. Use the script before and after agent-facing abstractions to prove call-count, output-size, stale-ref, artifact, failure-category coverage, success-rate, and elapsed-time effects before changing the wrapper UX:
581
581
 
@@ -8,7 +8,7 @@
8
8
  import { dirname, join, resolve } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { Text } from "@earendil-works/pi-tui";
11
- import { PROJECT_RULE_PROMPT, buildToolPromptGuidelines, } from "./lib/playbook.js";
11
+ import { PROJECT_RULE_PROMPT, buildBrowserDefaultProfileGuideline, buildBrowserExecutablePathGuideline, buildToolPromptGuidelines, } from "./lib/playbook.js";
12
12
  import { SessionPageState } from "./lib/session-page-state.js";
13
13
  import { createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractCommandTokens, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, extractExplicitSessionName, restoreManagedSessionStateFromBranch, validateToolArgs, } from "./lib/runtime.js";
14
14
  import { isRecord } from "./lib/parsing.js";
@@ -452,10 +452,9 @@ function shouldIncludeProjectConfig(ctx, argv = process.argv) {
452
452
  }
453
453
  export default function agentBrowserExtension(pi) {
454
454
  const ephemeralSessionSeed = createEphemeralSessionSeed();
455
- const startupProjectConfigAllowed = shouldIncludeProjectConfig(undefined);
456
455
  const agentBrowserConfig = loadAgentBrowserConfigSync({
457
456
  cwd: process.cwd(),
458
- includeProjectConfig: startupProjectConfigAllowed,
457
+ includeProjectConfig: false,
459
458
  });
460
459
  const webSearchToolAvailable = canRegisterWebSearchTool(agentBrowserConfig);
461
460
  const toolPromptGuidelines = buildToolPromptGuidelines({
@@ -466,6 +465,7 @@ export default function agentBrowserExtension(pi) {
466
465
  });
467
466
  const implicitSessionIdleTimeoutMs = String(getImplicitSessionIdleTimeoutMs());
468
467
  const implicitSessionCloseTimeoutMs = getImplicitSessionCloseTimeoutMs();
468
+ let webSearchToolRegistered = false;
469
469
  let managedSessionActive = false;
470
470
  let managedSessionBaseName = createImplicitSessionName(undefined, process.cwd(), ephemeralSessionSeed);
471
471
  let managedSessionName = managedSessionBaseName;
@@ -543,9 +543,26 @@ export default function agentBrowserExtension(pi) {
543
543
  markBranchOwned: true,
544
544
  });
545
545
  };
546
+ const registerWebSearchToolIfAvailable = (configState) => {
547
+ if (webSearchToolRegistered || !canRegisterWebSearchTool(configState))
548
+ return;
549
+ pi.registerTool(createAgentBrowserWebSearchTool(configState, {
550
+ loadConfigState(ctx) {
551
+ return loadAgentBrowserConfigSync({
552
+ cwd: ctx.cwd,
553
+ includeProjectConfig: shouldIncludeProjectConfig(ctx),
554
+ });
555
+ },
556
+ }));
557
+ webSearchToolRegistered = true;
558
+ };
546
559
  pi.on("session_start", async (_event, ctx) => {
547
560
  restoreBranchBackedState(ctx, { resetRuntimeOwnership: true });
548
561
  electronChildProcesses = new Map();
562
+ registerWebSearchToolIfAvailable(loadAgentBrowserConfigSync({
563
+ cwd: ctx.cwd,
564
+ includeProjectConfig: shouldIncludeProjectConfig(ctx),
565
+ }));
549
566
  });
550
567
  pi.on("session_tree", async (_event, ctx) => {
551
568
  await managedSessionExecutionQueue.run(async () => {
@@ -594,12 +611,27 @@ export default function agentBrowserExtension(pi) {
594
611
  ownedManagedSessions.clear();
595
612
  await cleanupSecureTempArtifacts({ preservePaths: preservedElectronProfileDirs });
596
613
  });
597
- pi.on("before_agent_start", async (event) => {
614
+ pi.on("before_agent_start", async (event, ctx) => {
598
615
  if (!shouldAppendBrowserSystemPrompt(event.prompt)) {
599
616
  return undefined;
600
617
  }
618
+ const runtimeConfig = loadAgentBrowserConfigSync({
619
+ cwd: ctx.cwd,
620
+ includeProjectConfig: shouldIncludeProjectConfig(ctx),
621
+ });
622
+ const browserGuidance = [
623
+ runtimeConfig.browserExecutablePathScope === "project"
624
+ ? buildBrowserExecutablePathGuideline(runtimeConfig.browserExecutablePath)
625
+ : undefined,
626
+ runtimeConfig.browserDefaultProfileScope === "project"
627
+ ? buildBrowserDefaultProfileGuideline(runtimeConfig.browserDefaultProfile)
628
+ : undefined,
629
+ ].filter((line) => typeof line === "string" && line.length > 0);
630
+ const runtimeConfigPrompt = browserGuidance.length > 0
631
+ ? `\n\nProject agent_browser config guidance:\n${browserGuidance.map((line) => `- ${line}`).join("\n")}`
632
+ : "";
601
633
  return {
602
- systemPrompt: `${event.systemPrompt}\n\n${PROJECT_RULE_PROMPT}`,
634
+ systemPrompt: `${event.systemPrompt}\n\n${PROJECT_RULE_PROMPT}${runtimeConfigPrompt}`,
603
635
  };
604
636
  });
605
637
  pi.on("tool_call", async (event, ctx) => {
@@ -772,14 +804,5 @@ export default function agentBrowserExtension(pi) {
772
804
  : runBrowserCommand();
773
805
  },
774
806
  });
775
- if (webSearchToolAvailable) {
776
- pi.registerTool(createAgentBrowserWebSearchTool(agentBrowserConfig, {
777
- loadConfigState(ctx) {
778
- return loadAgentBrowserConfigSync({
779
- cwd: ctx.cwd,
780
- includeProjectConfig: shouldIncludeProjectConfig(ctx),
781
- });
782
- },
783
- }));
784
- }
807
+ registerWebSearchToolIfAvailable(agentBrowserConfig);
785
808
  }
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
  /**
3
3
  * Purpose: Canonical pi-agent-browser-native config policy shared by runtime and setup CLI.
4
- * Responsibilities: Own config paths, provider descriptors, project-local credential safety, layer validation/merge, status projection, and redacted summaries.
4
+ * Responsibilities: Own config paths, provider descriptors, credential source parsing, layer validation/merge, status projection, and redacted summaries.
5
5
  * Scope: Pure configuration policy plus synchronous status loading; secret command execution and browser/web-search runtime calls live elsewhere.
6
6
  */
7
7
  import { existsSync, readFileSync } from "node:fs";
@@ -22,7 +22,7 @@ import { join, resolve } from "node:path";
22
22
  /** @typedef {{ kind: CredentialSourceKind; provider?: WebSearchProvider; rawValue: string; scope: AgentBrowserConfigScope }} CredentialSource */
23
23
  /** @typedef {{ global: string; project: string; override?: string }} AgentBrowserConfigPaths */
24
24
  /** @typedef {{ cwd?: string; env?: NodeJS.ProcessEnv; includeProjectConfig?: boolean }} AgentBrowserConfigLoadOptions */
25
- /** @typedef {{ browserDefaultProfile?: Required<BrowserDefaultProfileConfig>; browserDefaultProfileScope?: ConfigLayerScope; browserExecutablePath?: string; browserExecutablePathScope?: ConfigLayerScope; trustedBrowserDefaultProfile?: Required<BrowserDefaultProfileConfig>; trustedBrowserDefaultProfileScope?: Exclude<ConfigLayerScope, "project">; trustedBrowserExecutablePath?: string; trustedBrowserExecutablePathScope?: Exclude<ConfigLayerScope, "project">; config: AgentBrowserConfig; webSearchCredentialSources: Partial<Record<WebSearchProvider, CredentialSource>>; webSearchEnabled: boolean; webSearchPreferredProvider: WebSearchProvider; errors: string[]; layers: ConfigLayer[]; paths: AgentBrowserConfigPaths; projectConfigIncluded: boolean; warnings: string[] }} AgentBrowserConfigState */
25
+ /** @typedef {{ browserDefaultProfile?: Required<BrowserDefaultProfileConfig>; browserDefaultProfileScope?: ConfigLayerScope; browserExecutablePath?: string; browserExecutablePathScope?: ConfigLayerScope; trustedBrowserDefaultProfile?: Required<BrowserDefaultProfileConfig>; trustedBrowserDefaultProfileScope?: ConfigLayerScope; trustedBrowserExecutablePath?: string; trustedBrowserExecutablePathScope?: ConfigLayerScope; config: AgentBrowserConfig; webSearchCredentialSources: Partial<Record<WebSearchProvider, CredentialSource>>; webSearchEnabled: boolean; webSearchPreferredProvider: WebSearchProvider; errors: string[]; layers: ConfigLayer[]; paths: AgentBrowserConfigPaths; projectConfigIncluded: boolean; warnings: string[] }} AgentBrowserConfigState */
26
26
  /** @typedef {{ scope: string; path: string; exists: boolean }} ConfigFileSummary */
27
27
  export const AGENT_BROWSER_CONFIG_ENV = "PI_AGENT_BROWSER_CONFIG";
28
28
  export const BRAVE_API_KEY_ENV = "BRAVE_API_KEY";
@@ -237,9 +237,8 @@ export function isPlaintextCredentialValue(rawValue) {
237
237
  * @param {WebSearchProvider} provider
238
238
  */
239
239
  export function isProjectSafeCredentialValueForProvider(rawValue, provider) {
240
- const envName = getWebSearchProviderEnvVar(provider);
241
- const trimmed = rawValue.trim();
242
- return trimmed === `$${envName}` || trimmed === `\${${envName}}`;
240
+ void provider;
241
+ return rawValue.trim().length > 0;
243
242
  }
244
243
  /**
245
244
  * @param {unknown} value
@@ -277,9 +276,6 @@ export function validateAgentBrowserConfig(value, path, scope, errors, warnings)
277
276
  const apiKey = validateString(value.webSearch[descriptor.configKey], `${path}.webSearch.${descriptor.configKey}`, errors);
278
277
  if (apiKey !== undefined) {
279
278
  webSearch[descriptor.configKey] = apiKey;
280
- if (scope === "project" && !isProjectSafeCredentialValueForProvider(apiKey, provider)) {
281
- errors.push(`${path}.webSearch.${descriptor.configKey} must be exactly $${descriptor.apiKeyEnv} or \${${descriptor.apiKeyEnv}} in project-local config; plaintext, custom env aliases, interpolation literals, malformed env references, and command-backed project secrets are not allowed.`);
282
- }
283
279
  }
284
280
  }
285
281
  if (Object.keys(webSearch).length > 0)
@@ -295,16 +291,10 @@ export function validateAgentBrowserConfig(value, path, scope, errors, warnings)
295
291
  const defaultProfile = validateBrowserDefaultProfile(value.browser.defaultProfile, `${path}.browser.defaultProfile`, errors);
296
292
  if (defaultProfile) {
297
293
  config.browser.defaultProfile = defaultProfile;
298
- if (scope === "project" && defaultProfile.policy !== "explicit-only") {
299
- warnings.push(`${path}.browser.defaultProfile is project-local; authenticated/always profile prompt guidance is emitted only from global or override config.`);
300
- }
301
294
  }
302
295
  const executablePath = validateString(value.browser.executablePath, `${path}.browser.executablePath`, errors)?.trim();
303
296
  if (executablePath) {
304
297
  config.browser.executablePath = executablePath;
305
- if (scope === "project") {
306
- warnings.push(`${path}.browser.executablePath is project-local; executable launch prompt guidance is emitted only from global or override config.`);
307
- }
308
298
  }
309
299
  const defaultLaunchArgs = validateStringArray(value.browser.defaultLaunchArgs, `${path}.browser.defaultLaunchArgs`, errors);
310
300
  if (defaultLaunchArgs) {
@@ -397,31 +387,31 @@ function getBrowserExecutablePathScope(layers) {
397
387
  }
398
388
  /**
399
389
  * @param {ConfigLayer[]} layers
400
- * @returns {{ profile: Required<BrowserDefaultProfileConfig>; scope: Exclude<ConfigLayerScope, "project"> } | undefined}
390
+ * @returns {{ profile: Required<BrowserDefaultProfileConfig>; scope: ConfigLayerScope } | undefined}
401
391
  */
402
392
  function getTrustedBrowserDefaultProfile(layers) {
403
393
  for (let index = layers.length - 1; index >= 0; index -= 1) {
404
394
  const layer = layers[index];
405
- if (!layer || layer.scope === "project")
395
+ if (!layer)
406
396
  continue;
407
397
  const profile = getBrowserDefaultProfile(layer.config);
408
398
  if (profile)
409
- return { profile, scope: /** @type {Exclude<ConfigLayerScope, "project">} */ (layer.scope) };
399
+ return { profile, scope: layer.scope };
410
400
  }
411
401
  return undefined;
412
402
  }
413
403
  /**
414
404
  * @param {ConfigLayer[]} layers
415
- * @returns {{ executablePath: string; scope: Exclude<ConfigLayerScope, "project"> } | undefined}
405
+ * @returns {{ executablePath: string; scope: ConfigLayerScope } | undefined}
416
406
  */
417
407
  function getTrustedBrowserExecutablePath(layers) {
418
408
  for (let index = layers.length - 1; index >= 0; index -= 1) {
419
409
  const layer = layers[index];
420
- if (!layer || layer.scope === "project")
410
+ if (!layer)
421
411
  continue;
422
412
  const executablePath = getBrowserExecutablePath(layer.config);
423
413
  if (executablePath)
424
- return { executablePath, scope: /** @type {Exclude<ConfigLayerScope, "project">} */ (layer.scope) };
414
+ return { executablePath, scope: layer.scope };
425
415
  }
426
416
  return undefined;
427
417
  }
@@ -655,11 +645,7 @@ export function formatBrowserProfileStatus(state) {
655
645
  if (!profile)
656
646
  return "not configured";
657
647
  const scope = state.browserDefaultProfileScope ?? "unknown";
658
- const base = `${profile.name} (policy: ${profile.policy}; ${scope})`;
659
- if (scope !== "project")
660
- return base;
661
- const trustedText = state.trustedBrowserDefaultProfile ? `; trusted guidance: ${state.trustedBrowserDefaultProfile.name} (${state.trustedBrowserDefaultProfileScope})` : "";
662
- return `${base}; ignored for prompt guidance${trustedText}`;
648
+ return `${profile.name} (policy: ${profile.policy}; ${scope})`;
663
649
  }
664
650
  /** @param {AgentBrowserConfigState} state */
665
651
  export function formatBrowserExecutableStatus(state) {
@@ -667,10 +653,7 @@ export function formatBrowserExecutableStatus(state) {
667
653
  if (!executablePath)
668
654
  return "not configured";
669
655
  const scope = state.browserExecutablePathScope ?? "unknown";
670
- if (scope !== "project")
671
- return `${executablePath} (${scope})`;
672
- const trustedText = state.trustedBrowserExecutablePath ? `; trusted guidance: ${state.trustedBrowserExecutablePath} (${state.trustedBrowserExecutablePathScope})` : "";
673
- return `${executablePath} (${scope}; ignored for prompt guidance${trustedText})`;
656
+ return `${executablePath} (${scope})`;
674
657
  }
675
658
  /**
676
659
  * @param {AgentBrowserConfigState} state
@@ -2,7 +2,7 @@
2
2
  * Purpose: Load pi-agent-browser-native package configuration from Pi-scoped global, project, or explicit paths.
3
3
  * Responsibilities: Resolve config layers, resolve secrets without exposing values, and provide redacted status for tools/CLIs.
4
4
  * Scope: Package-owned configuration only; canonical config policy lives in config-policy.js, browser command execution and web-search API calls live in focused modules.
5
- * Invariants/Assumptions: Raw project-local plaintext credentials are unsafe and rejected by the shared config policy; command credentials are resolved lazily at execution time.
5
+ * Invariants/Assumptions: Credential sources from loaded config are passed through to the runtime; command credentials are resolved lazily at execution time and displayed values stay redacted.
6
6
  */
7
7
  import { exec as execCallback } from "node:child_process";
8
8
  import { readFile } from "node:fs/promises";
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Purpose: Execute the upstream agent-browser binary for the pi-agent-browser extension.
3
- * Responsibilities: Spawn the agent-browser subprocess, forward a curated environment surface, stream optional stdin, bound in-memory output buffering, spill oversized stdout safely to a private temp file under a disk budget, and honor abort signals.
3
+ * Responsibilities: Spawn the agent-browser subprocess, forward parent environment variables plus wrapper overrides, stream optional stdin, bound in-memory output buffering, spill oversized stdout safely to a private temp file under a disk budget, and honor abort signals.
4
4
  * Scope: Process execution only; argument planning, output formatting, and pi tool registration live elsewhere.
5
5
  * Usage: Called by the extension tool after argument validation and session planning are complete.
6
6
  * Invariants/Assumptions: The binary name is always `agent-browser`; Windows routes through PowerShell to invoke npm launchers with escaped argv; callers handle semantic success/error interpretation.
@@ -22,72 +22,6 @@ export const SAFE_AGENT_BROWSER_OPERATION_TIMEOUT_MS = 25_000;
22
22
  const DEFAULT_AGENT_BROWSER_PROCESS_TIMEOUT_MS = 35_000;
23
23
  /** Grace period after `exit` before resolving when `close` is delayed by inherited stdio handles. */
24
24
  const EXIT_STDIO_GRACE_MS = 100;
25
- const httpProxyEnvName = "http_proxy";
26
- const httpsProxyEnvName = "https_proxy";
27
- const allProxyEnvName = "all_proxy";
28
- const noProxyEnvName = "no_proxy";
29
- const INHERITED_ENV_NAMES = new Set([
30
- "ALL_PROXY",
31
- "APPDATA",
32
- "CI",
33
- "COLORTERM",
34
- "COMSPEC",
35
- "DBUS_SESSION_BUS_ADDRESS",
36
- "DISPLAY",
37
- "FORCE_COLOR",
38
- "HOME",
39
- "HOMEDRIVE",
40
- "HOMEPATH",
41
- "HTTPS_PROXY",
42
- "HTTP_PROXY",
43
- "LANG",
44
- "LC_ALL",
45
- "LC_CTYPE",
46
- "LOCALAPPDATA",
47
- "LOGNAME",
48
- "NO_COLOR",
49
- "NO_PROXY",
50
- "NODE_EXTRA_CA_CERTS",
51
- "NODE_TLS_REJECT_UNAUTHORIZED",
52
- "OS",
53
- "PATH",
54
- "PATHEXT",
55
- "PWD",
56
- "SHELL",
57
- "SSL_CERT_DIR",
58
- "SSL_CERT_FILE",
59
- "SYSTEMROOT",
60
- "TEMP",
61
- "TERM",
62
- "TMP",
63
- "TMPDIR",
64
- "TZ",
65
- "USER",
66
- "USERNAME",
67
- "USERPROFILE",
68
- "WAYLAND_DISPLAY",
69
- "XAUTHORITY",
70
- "AWS_ACCESS_KEY_ID",
71
- "AWS_SECRET_ACCESS_KEY",
72
- "AWS_SESSION_TOKEN",
73
- "AWS_PROFILE",
74
- "AWS_REGION",
75
- "AWS_DEFAULT_REGION",
76
- httpProxyEnvName,
77
- httpsProxyEnvName,
78
- allProxyEnvName,
79
- noProxyEnvName,
80
- ]);
81
- const INHERITED_ENV_PREFIXES = [
82
- "AGENT_BROWSER_",
83
- "AGENTCORE_",
84
- "AI_GATEWAY_",
85
- "BROWSERBASE_",
86
- "BROWSERLESS_",
87
- "BROWSER_USE_",
88
- "KERNEL_",
89
- "XDG_",
90
- ];
91
25
  function appendTail(text, addition, maxChars) {
92
26
  const combined = text + addition;
93
27
  return combined.length <= maxChars ? combined : combined.slice(combined.length - maxChars);
@@ -230,37 +164,18 @@ async function ensureAgentBrowserSocketDir(socketDir) {
230
164
  return false;
231
165
  }
232
166
  }
233
- function getChildEnvName(name) {
234
- if (processPlatform === "win32") {
235
- const upperName = name.toUpperCase();
236
- if (INHERITED_ENV_NAMES.has(upperName))
237
- return upperName;
238
- return INHERITED_ENV_PREFIXES.some((prefix) => upperName.startsWith(prefix)) ? upperName : undefined;
239
- }
240
- if (INHERITED_ENV_NAMES.has(name) || INHERITED_ENV_PREFIXES.some((prefix) => name.startsWith(prefix))) {
241
- return name;
242
- }
243
- return undefined;
244
- }
245
167
  export function buildAgentBrowserProcessEnv(baseEnv = processEnv, overrides = undefined) {
246
168
  const childEnv = {};
247
169
  for (const [name, value] of Object.entries(baseEnv)) {
248
- const childName = getChildEnvName(name);
249
- if (value !== undefined && childName) {
250
- childEnv[childName] = value;
251
- }
252
- }
253
- if (!overrides) {
254
- clampUpstreamDefaultTimeout(childEnv);
255
- return childEnv;
170
+ if (value !== undefined)
171
+ childEnv[name] = value;
256
172
  }
257
- for (const [name, value] of Object.entries(overrides)) {
258
- const childName = getChildEnvName(name) ?? name;
173
+ for (const [name, value] of Object.entries(overrides ?? {})) {
259
174
  if (value === undefined) {
260
- delete childEnv[childName];
175
+ delete childEnv[name];
261
176
  }
262
177
  else {
263
- childEnv[childName] = value;
178
+ childEnv[name] = value;
264
179
  }
265
180
  }
266
181
  clampUpstreamDefaultTimeout(childEnv);
@@ -27,7 +27,7 @@ export function redactModelFacingText(text) {
27
27
  return redactSensitiveText(text);
28
28
  }
29
29
  export function redactModelFacingTextIfSensitive(text) {
30
- return /(?:@|\b(?:api[_-]?key|auth|authorization|basic|bearer|cookie|pass(?:word)?|secret|session[_-]?id|token)\b)/i.test(text)
30
+ return /(?:@|\b(?:access[_-]?key|api[_-]?key|auth|authorization|basic|bearer|connection[_-]?string|cookie|database[_-]?url|db[_-]?url|mongo(?:db)?[_-]?uri|pass(?:word)?|private[_-]?key|redis[_-]?url|secret|session[_-]?id|token)\b)/i.test(text)
31
31
  ? redactModelFacingText(text)
32
32
  : text;
33
33
  }
@@ -4,7 +4,7 @@
4
4
  * Scope: Diagnostic/result-state command presentation only; core orchestration stays in presentation.ts.
5
5
  */
6
6
  import { isRecord } from "../../parsing.js";
7
- import { redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
7
+ import { isSensitiveFieldName, redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
8
8
  import { classifyNetworkRequestFailure, isApiLikeNetworkRequest, isNetworkArtifactNoiseRequest, summarizeNetworkFailures } from "../network.js";
9
9
  import { withOptionalSessionArgs } from "../next-actions.js";
10
10
  import { stringifyUnknown, truncateText } from "../text.js";
@@ -39,7 +39,6 @@ const NETWORK_FILTER_SENSITIVE_SEGMENT_TERMS = [
39
39
  "session",
40
40
  "token",
41
41
  ];
42
- const SENSITIVE_PRESENTATION_FIELD_PATTERN = /^(?:access(?:_|-)?token|api(?:_|-)?key|auth(?:orization)?|bearer|client(?:_|-)?secret|cookie|id(?:_|-)?token|pass(?:word)?|proxy(?:_|-)?authorization|refresh(?:_|-)?token|secret|session(?:_|-)?id|set(?:_|-)?cookie|sig(?:nature)?|token|x(?:_|-)?api(?:_|-)?key)$/i;
43
42
  const NETWORK_FILTER_OPAQUE_SEGMENT_PATTERN = /^(?:[A-Fa-f0-9]{16,}|(?=.*[A-Za-z])(?=.*\d)[A-Za-z0-9_-]{16,})$/;
44
43
  const NETWORK_PREVIEW_FIELD_CANDIDATES = {
45
44
  request: ["postData"],
@@ -873,9 +872,6 @@ function formatStateText(data) {
873
872
  return "State cleared.";
874
873
  return undefined;
875
874
  }
876
- function isSensitivePresentationField(key) {
877
- return SENSITIVE_PRESENTATION_FIELD_PATTERN.test(key);
878
- }
879
875
  function redactStructuredPresentationValue(value) {
880
876
  if (typeof value === "string")
881
877
  return redactModelFacingTextIfSensitive(value);
@@ -885,7 +881,7 @@ function redactStructuredPresentationValue(value) {
885
881
  return value;
886
882
  return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [
887
883
  key,
888
- isSensitivePresentationField(key) ? "[REDACTED]" : redactStructuredPresentationValue(entryValue),
884
+ isSensitiveFieldName(key) ? "[REDACTED]" : redactStructuredPresentationValue(entryValue),
889
885
  ]));
890
886
  }
891
887
  function redactStatefulValues(value, sensitiveKeys) {
@@ -25,7 +25,8 @@ const DEFAULT_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS = 5_000;
25
25
  const INSPECTION_FLAGS = new Set(["--help", "-h", "--version", "-V"]);
26
26
  const SENSITIVE_VALUE_FLAGS = new Set(["--body", "--headers", "--password", "--proxy"]);
27
27
  const SENSITIVE_QUERY_PARAM_PATTERN = /^(?:access(?:_|-)?token|api(?:_|-)?key|auth|authorization|bearer|client(?:_|-)?secret|code|cookie|id(?:_|-)?token|key|pass(?:word)?|refresh(?:_|-)?token|secret|sentry(?:_|-)?key|session(?:_|-)?id|sig(?:nature)?|token|write(?:_|-)?key)$/i;
28
- const SENSITIVE_FIELD_NAME_PATTERN = /^(?:access(?:_|-)?token|api(?:_|-)?key|auth(?:orization)?|bearer|client(?:_|-)?secret|cookie|id(?:_|-)?token|pass(?:word)?|proxy(?:_|-)?authorization|refresh(?:_|-)?token|secret|sentry(?:_|-)?key|session(?:_|-)?id|set(?:_|-)?cookie|sig(?:nature)?|token|write(?:_|-)?key|x(?:_|-)?api(?:_|-)?key)$/i;
28
+ const SENSITIVE_FIELD_NAME_PATTERN = /^(?:[A-Za-z0-9_-]*(?:api[_-]?key|access[_-]?key|private[_-]?key|secret(?:[_-]?(?:key|access[_-]?key))?|token|password|passwd|credentials?|database[_-]?url|db[_-]?url|connection[_-]?string|mongo(?:db)?[_-]?uri|redis[_-]?url)|[A-Za-z0-9]*(?:apiKey|ApiKey|apikey|privateKey|PrivateKey|databaseUrl|DatabaseUrl|dbUrl|DbUrl|connectionString|ConnectionString|mongoUri|MongoUri|mongodbUri|MongodbUri|mongoDbUri|MongoDbUri|redisUrl|RedisUrl|Token|Secret|Password|Credential|Credentials)|auth(?:orization)?|bearer|client(?:_|-)?secret|cookie|id(?:_|-)?token|pass(?:word)?|proxy(?:_|-)?authorization|refresh(?:_|-)?token|sentry(?:_|-)?key|session(?:_|-)?id|set(?:_|-)?cookie|sig(?:nature)?|write(?:_|-)?key|x(?:_|-)?api(?:_|-)?key)$/i;
29
+ const ENV_SECRET_ASSIGNMENT_PATTERN = /\b((?:export\s+)?([A-Za-z_][A-Za-z0-9_-]*)(\s*[:=]\s*))(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;]+)/g;
29
30
  const DEFAULT_HEADLESS_COMPAT_USER_AGENT_BY_PLATFORM = {
30
31
  darwin: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
31
32
  linux: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
@@ -40,7 +41,7 @@ function isStringArray(value) {
40
41
  return Array.isArray(value) && value.every((item) => typeof item === "string");
41
42
  }
42
43
  function shouldRedactQueryParam(name) {
43
- return SENSITIVE_QUERY_PARAM_PATTERN.test(name);
44
+ return SENSITIVE_QUERY_PARAM_PATTERN.test(name) || isSensitiveFieldName(name);
44
45
  }
45
46
  function redactUrlToken(token) {
46
47
  let parsed;
@@ -50,9 +51,6 @@ function redactUrlToken(token) {
50
51
  catch {
51
52
  return token;
52
53
  }
53
- if (!["http:", "https:", "ws:", "wss:"].includes(parsed.protocol)) {
54
- return token;
55
- }
56
54
  let mutated = false;
57
55
  if (parsed.username.length > 0) {
58
56
  parsed.username = "[REDACTED]";
@@ -83,8 +81,31 @@ function redactUrlToken(token) {
83
81
  }
84
82
  return parsed.toString();
85
83
  }
84
+ function redactLooseUrlParameterText(text) {
85
+ return text.replace(/([?#&])([^=&#\s]+)=([^&#\s]*)/g, (match, separator, rawName) => {
86
+ let name = rawName;
87
+ try {
88
+ name = decodeURIComponent(rawName.replace(/\+/g, " "));
89
+ }
90
+ catch {
91
+ // Keep the raw name when percent decoding fails.
92
+ }
93
+ if (!shouldRedactQueryParam(name))
94
+ return match;
95
+ return `${separator}${rawName}=[REDACTED]`;
96
+ });
97
+ }
98
+ function redactLooseUrlUserinfo(text) {
99
+ return text.replace(/\b([A-Za-z][A-Za-z0-9+.-]*:\/\/)([^\s"'`/@]+)@([^\s"'`]+)/g, (match, prefix, userinfo, suffix) => {
100
+ if (/%5Bredacted%5D/i.test(userinfo))
101
+ return match;
102
+ if (userinfo.includes("[REDACTED]"))
103
+ return redactLooseUrlParameterText(match);
104
+ return redactLooseUrlParameterText(`${prefix}${userinfo.includes(":") ? "[REDACTED]:[REDACTED]" : "[REDACTED]"}@${suffix}`);
105
+ });
106
+ }
86
107
  function redactLooseUrlMatches(text) {
87
- return text.replace(/\b(?:https?|wss?):\/\/[^\s"'`<>\])]+/g, (match) => redactUrlToken(match));
108
+ return text.replace(/\b[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s"'`<>\])]+/g, (match) => redactUrlToken(match));
88
109
  }
89
110
  function findBalancedJsonEnd(text, startIndex) {
90
111
  const opener = text[startIndex];
@@ -188,10 +209,28 @@ function redactBearerCredentials(text) {
188
209
  return formatRedactedCredential(label, credential, trailing);
189
210
  });
190
211
  }
212
+ export function isSensitiveFieldName(key) {
213
+ SENSITIVE_FIELD_NAME_PATTERN.lastIndex = 0;
214
+ return SENSITIVE_FIELD_NAME_PATTERN.test(key);
215
+ }
216
+ function isEnvSecretAssignmentKey(key) {
217
+ if (!isSensitiveFieldName(key))
218
+ return false;
219
+ if (key.includes("_") || key.includes("-") || key === key.toUpperCase())
220
+ return true;
221
+ return /(?:apiKey|ApiKey|privateKey|PrivateKey|databaseUrl|DatabaseUrl|dbUrl|DbUrl|connectionString|ConnectionString|mongoUri|MongoUri|mongodbUri|MongodbUri|mongoDbUri|MongoDbUri|redisUrl|RedisUrl|Token|Secret|Password|Credential|Credentials)$/.test(key);
222
+ }
223
+ function redactEnvSecretAssignments(text) {
224
+ return text.replace(ENV_SECRET_ASSIGNMENT_PATTERN, (match, prefix, key) => {
225
+ if (!isEnvSecretAssignmentKey(key))
226
+ return match;
227
+ return `${prefix}[REDACTED]`;
228
+ });
229
+ }
191
230
  export function redactSensitiveText(text) {
192
- return redactEmbeddedStructuredText(redactStandaloneBasicCredential(redactBearerCredentials(redactLooseUrlMatches(text))
231
+ return redactEmbeddedStructuredText(redactEnvSecretAssignments(redactStandaloneBasicCredential(redactBearerCredentials(redactLooseUrlUserinfo(redactLooseUrlMatches(text)))
193
232
  .replace(/\b(Authorization\s*:\s*Basic)\s+[^\s",]+/gi, "$1 [REDACTED]")
194
- .replace(/\b(Cookie|Set-Cookie)\s*:\s*[^\n\r"]+/gi, "$1: [REDACTED]")));
233
+ .replace(/\b(Cookie|Set-Cookie)\s*:\s*[^\n\r"]+/gi, "$1: [REDACTED]"))));
195
234
  }
196
235
  export function redactSensitiveValue(value) {
197
236
  if (typeof value === "string") {
@@ -204,7 +243,7 @@ export function redactSensitiveValue(value) {
204
243
  return value;
205
244
  }
206
245
  return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => {
207
- if (SENSITIVE_FIELD_NAME_PATTERN.test(key)) {
246
+ if (isSensitiveFieldName(key)) {
208
247
  return [key, "[REDACTED]"];
209
248
  }
210
249
  return [key, redactSensitiveValue(entryValue)];
@@ -23,7 +23,7 @@ V1 exposes one native browser tool:
23
23
 
24
24
  It may also expose one optional companion tool:
25
25
 
26
- - `agent_browser_web_search`, registered only when an Exa or Brave Search credential source is configured or resolvable and `webSearch.enabled` is not false
26
+ - `agent_browser_web_search`, available when an Exa or Brave Search credential source is configured or resolvable from startup config or trusted session config and runtime `webSearch.enabled` is not false
27
27
 
28
28
  Why:
29
29
  - keeps browser automation centered on `agent_browser`
@@ -68,11 +68,11 @@ Pi docs use `settings.json` for package/resource loading and filtering, not arbi
68
68
  - project-local: `.pi/config/pi-agent-browser-native/config.json`
69
69
  - explicit override: `PI_AGENT_BROWSER_CONFIG=/path/to/config.json`
70
70
 
71
- Config layers merge in that order: global, project, override. The shared policy module (`extensions/agent-browser/lib/config-policy.js`) owns provider descriptors, environment variable names, config keys, project-local credential safety, developer-trusted project layer inclusion, layer validation/merge, redacted status projection, and credential summaries for both runtime config loading and the package config helper. Under Pi 0.79+, globally installed or CLI-loaded extensions are developer-trusted code, so this extension reads `.pi/config/pi-agent-browser-native/config.json` by default and skips that project layer when Pi reports the project is untrusted or when launched with `--no-approve`. Global config and explicit `PI_AGENT_BROWSER_CONFIG` overrides remain available either way. The config reader accepts v1 fields for `webSearch.enabled`, `webSearch.preferredProvider`, `webSearch.exaApiKey`, `webSearch.braveApiKey`, and conservative browser defaults such as `browser.defaultProfile` and `browser.executablePath`. Web-search key fields follow Pi model/provider-style value resolution for trusted global/override config: literal values, `$ENV_VAR` / `${ENV_VAR}` interpolation, escapes (`$$`, `$!`), and leading `!command` resolved at request time. Project-local plaintext, interpolation-literal, malformed, and command-backed web-search keys are rejected because project config can be copied, committed, or supplied by a repository; project config may use only the matching provider env ref (`$EXA_API_KEY` / `${EXA_API_KEY}` for Exa, `$BRAVE_API_KEY` / `${BRAVE_API_KEY}` for Brave), so a repository cannot redirect search credentials to arbitrary host env vars. `EXA_API_KEY` and `BRAVE_API_KEY` remain environment fallbacks when no config credential source exists for that provider. Browser default values keep their source scope; profile/executable prompt guidance is emitted only from trusted global or explicit override config, not from project-local config that could steer host profile or executable choices.
71
+ Config layers merge in that order: global, project, override. The shared policy module (`extensions/agent-browser/lib/config-policy.js`) owns provider descriptors, environment variable names, config keys, credential source parsing, developer-trusted project layer inclusion, layer validation/merge, redacted status projection, and credential summaries for both runtime config loading and the package config helper. Under Pi 0.79+, globally installed or CLI-loaded extensions are developer-trusted code, so this extension reads `.pi/config/pi-agent-browser-native/config.json` by default and skips that project layer when Pi reports the project is untrusted or when launched with `--no-approve`. Global config and explicit `PI_AGENT_BROWSER_CONFIG` overrides remain available either way. The config reader accepts v1 fields for `webSearch.enabled`, `webSearch.preferredProvider`, `webSearch.exaApiKey`, `webSearch.braveApiKey`, and conservative browser defaults such as `browser.defaultProfile` and `browser.executablePath`. Web-search key fields follow Pi model/provider-style value resolution from any loaded layer: literal values, `$ENV_VAR` / `${ENV_VAR}` interpolation, escapes (`$$`, `$!`), and leading `!command` resolved at request time. `EXA_API_KEY` and `BRAVE_API_KEY` remain environment fallbacks when no config credential source exists for that provider. Browser default values keep their source scope; prompt guidance is emitted from the highest-priority loaded layer, including project config when Pi trust/loading allows it.
72
72
 
73
- `agent_browser_web_search` registration is conditional. `webSearch.enabled: false` disables registration even when environment keys are present, but it is evaluated after the available config layers merge. A global disable is the normal user default and can still be overridden by project config or `PI_AGENT_BROWSER_CONFIG`; a project disable applies to one repo; an explicit `PI_AGENT_BROWSER_CONFIG` file with `webSearch.enabled: false` is the highest-priority hard disable for that run. Literal and env-backed sources must resolve at startup; command-backed sources are considered configured without running the command until tool execution, so secret managers do not slow startup or prompt unexpectedly. The tool resolves the selected key lazily, chooses Exa or Brave from available credentials (preferring Exa by default unless `webSearch.preferredProvider` says otherwise), then follows one provider-agnostic execution path through provider adapters for request building, HTTP JSON fetch, response normalization, and provider-specific detail fields. It calls Exa `/search` with highlights or Brave Search and returns compact result details without exposing keys.
73
+ `agent_browser_web_search` availability is conditional. Startup registration uses global, override, and environment fallback config without reading project-local config before Pi trust context exists; trusted project config can register the companion tool on `session_start`, and every execution reloads the final session config so `webSearch.enabled: false` still prevents a request even if a startup credential made the tool visible. A global disable is the normal user default and can still be overridden by project config or `PI_AGENT_BROWSER_CONFIG`; a project disable applies to one repo; an explicit `PI_AGENT_BROWSER_CONFIG` file with `webSearch.enabled: false` is the highest-priority hard disable for that run. Literal and env-backed sources must resolve before they make the tool available; command-backed sources are considered configured without running the command until tool execution, so secret managers do not slow startup or prompt unexpectedly. The tool resolves the selected key lazily, chooses Exa or Brave from available credentials (preferring Exa by default unless `webSearch.preferredProvider` says otherwise), then follows one provider-agnostic execution path through provider adapters for request building, HTTP JSON fetch, response normalization, and provider-specific detail fields. It calls Exa `/search` with highlights or Brave Search and returns compact result details without exposing keys.
74
74
 
75
- Browser default config is intentionally conservative. It can add prompt guidance for signed-in/account-specific tasks and alternate Chromium-compatible executables, but current releases do not auto-inject `--profile` or `--executable-path` into every launch. Project-local browser config is status-only for launch guidance unless the profile policy is `explicit-only`; executable guidance must come from global or explicit override config. Automatic launch-default mutation would affect privacy, browser state, and host executable choice, so it needs a separate explicit design and test pass.
75
+ Browser default config is intentionally advisory. It can add prompt guidance for signed-in/account-specific tasks and alternate Chromium-compatible executables, but current releases do not auto-inject `--profile` or `--executable-path` into every launch. Loaded project config can provide the same guidance as global and override config; Pi/project trust decides whether that project layer is loaded. Automatic launch-default mutation would affect privacy, browser state, and host executable choice, so it needs a separate explicit design and test pass.
76
76
 
77
77
  ### Prompt guidance budget
78
78
 
@@ -199,7 +199,7 @@ This keeps the product centered on native tool usage instead of auxiliary skill
199
199
  ### `pi-agent-browser-native` owns
200
200
 
201
201
  - tool registration and schema (including the optional `semanticAction` compilation path to upstream `find` or `select`)
202
- - subprocess execution and JSON parsing through a filtered child environment (`buildAgentBrowserProcessEnv` in `extensions/agent-browser/lib/process.ts`): copies an allowlisted inherited-name set plus every parent `AGENT_BROWSER_*` variable and provider-related prefixes (`AGENTCORE_*`, `AI_GATEWAY_*`, `BROWSERBASE_*`, `BROWSERLESS_*`, `BROWSER_USE_*`, `KERNEL_*`, `XDG_*`) instead of cloning the full parent process environment
202
+ - subprocess execution and JSON parsing through `buildAgentBrowserProcessEnv` in `extensions/agent-browser/lib/process.ts`: copies the parent process environment so user-approved provider credentials and other runtime variables reach upstream, then applies wrapper overrides such as the managed socket directory and clamped default operation timeout
203
203
  - clear missing-binary errors
204
204
  - compact result summaries, including presentation-time redaction: stateful browser-context commands (`auth`, `cookies`, `storage`, `dialog`, `frame`, `state`) use field-aware value redaction and compact formatters, while other structured upstream JSON (for example `network`, `diff`, `trace` / `profiler` / `record`, `console` / `errors` / `highlight` / `inspect` / `clipboard`, `stream`, `dashboard`, and `chat`) is passed through `redactPresentationData` in `extensions/agent-browser/lib/results/presentation.ts` so model-facing `details.data` and batch roll-ups stay compact and do not echo bearer tokens, proxy passwords, or similar fields verbatim; `redactInvocationArgs` in `extensions/agent-browser/lib/runtime.ts` masks trailing values for sensitive global flags such as `--body`, `--headers`, `--password`, and `--proxy`, preserves positional rules for `cookies set` and `storage local|session set`, and nested `batch` steps use the same argv and error-body scrubbing before echoing commands or errors
205
205
  - bounded machine-readable outcome metadata on tool `details` (`resultCategory`, `successCategory`, `failureCategory`, optional `nextActions`, optional `pageChangeSummary` with per-step summaries on `batch`, optional `artifactVerification` with the same shape on each successful `batchSteps[]` row) so agents can branch without parsing prose; enums, classifier precedence, and generic follow-up payloads are implemented under `extensions/agent-browser/lib/results/` in focused modules (`contracts.ts` for shared types, `categories.ts` for `classifyAgentBrowserSuccessCategory` / `classifyAgentBrowserFailureCategory` / `buildAgentBrowserResultCategoryDetails`, `action-recommendations.ts` for `buildAgentBrowserNextActions`, `next-actions.ts` for the `AgentBrowserNextAction` shape and merge helpers, `recovery-actions.ts` for recovery id registries and `buildRecoveryNextActions`, `network.ts` for `classifyNetworkRequestFailure` / `summarizeNetworkFailures`, and related helpers; `shared.ts` in that directory is a **re-export-only** barrel so historical `./results/shared.js` imports keep working without growing a monolith). Per-session tab target, `refSnapshot` alignment, invalidation, and tab pinning observations flow through `extensions/agent-browser/lib/session-page-state.ts` from `extensions/agent-browser/index.ts`. Compact page-change summaries and artifact verification rollups are built in `extensions/agent-browser/lib/results/presentation.ts` (`buildPageChangeSummary`, `buildArtifactVerificationSummary`), and the human contract lives in [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details). Real Pi custom tools otherwise only mark a row failed when `execute` throws, so the extension also registers `pi.on("tool_result", …)` and patches `agent_browser` results whose `details.resultCategory` is `failure` to set `isError: true`. Prose results also receive a short category notice, while caller-requested `--json` results with parseable JSON content keep that text unchanged so JSONL transcripts, UI affordances, and the machine-readable contract stay aligned for wrapper-side reclassifications such as `qa-failure` (`buildAgentBrowserToolResultPatch` in `extensions/agent-browser/lib/pi-tool-rendering.ts`; transcript semantics in the same contract doc)
@@ -748,7 +748,7 @@ npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-conf
748
748
  npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config browser executable set "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
749
749
  ```
750
750
 
751
- The optional `agent_browser_web_search` tool is registered only when Exa or Brave credentials are available and the final available config has not set `webSearch.enabled` to `false`. It is a separate custom tool, not an `agent_browser` input mode, and does not launch a browser. Use it when current/live external web information would help; use `agent_browser` for browser interaction, screenshots, authenticated/profile pages, and DOM inspection. Disable scope is explicit: `web-search disable --global` sets the normal user default, `web-search disable --project` disables it for one repo, and a `PI_AGENT_BROWSER_CONFIG` override containing `{ "version": 1, "webSearch": { "enabled": false } }` wins over both for a hard per-run disable. Project-local plaintext, custom env aliases, interpolation-literal, malformed, and command-backed web-search keys are refused; project config may only use the matching provider env refs (`$EXA_API_KEY` / `${EXA_API_KEY}` for Exa and `$BRAVE_API_KEY` / `${BRAVE_API_KEY}` for Brave). `web-search set-key`, `set-command`, and `clear` require `--provider`; `set-env` infers Exa/Brave from `EXA_API_KEY` or `BRAVE_API_KEY` unless you pass `--provider`. For Exa, the tool defaults to `searchType: "auto"` with `contents.highlights: true`; use `fast`, `instant`, `deep-lite`, `deep`, or `deep-reasoning` only when the task needs that latency/depth tradeoff.
751
+ The optional `agent_browser_web_search` tool is available when Exa or Brave credentials are visible from startup config or trusted session config and the runtime config has not set `webSearch.enabled` to `false`. It is a separate custom tool, not an `agent_browser` input mode, and does not launch a browser. Use it when current/live external web information would help; use `agent_browser` for browser interaction, screenshots, authenticated/profile pages, and DOM inspection. Disable scope is explicit: `web-search disable --global` sets the normal user default, `web-search disable --project` disables it for one repo, and a `PI_AGENT_BROWSER_CONFIG` override containing `{ "version": 1, "webSearch": { "enabled": false } }` wins over both for a hard per-run disable. Loaded config may use plaintext, custom env aliases, interpolation literals, malformed-or-late-bound `$` values, and command-backed web-search keys; the resolved secret reaches the provider request while model-facing tool output and status text stay redacted. `web-search set-key`, `set-command`, and `clear` require `--provider`; `set-env` infers Exa/Brave from `EXA_API_KEY` or `BRAVE_API_KEY` unless you pass `--provider`. For Exa, the tool defaults to `searchType: "auto"` with `contents.highlights: true`; use `fast`, `instant`, `deep-lite`, `deep`, or `deep-reasoning` only when the task needs that latency/depth tradeoff.
752
752
 
753
753
  Example config:
754
754
 
@@ -771,7 +771,7 @@ Example config:
771
771
  }
772
772
  ```
773
773
 
774
- Browser default config is conservative: it adds agent guidance for signed-in/account-specific tasks and alternate Chromium-compatible executables; current releases do not auto-inject `--profile` or `--executable-path` for every launch. Configure profile/executable guidance globally or through `PI_AGENT_BROWSER_CONFIG`; project-local browser config is loaded by default but is never trusted to steer host executable/profile prompt guidance. Ask the agent to run `profiles` and `doctor` when profile resolution fails, then use the reported Chrome profile directory name, a full profile/user-data directory path if upstream accepts one, or the configured `browser.executablePath` with top-level `sessionMode: "fresh"`.
774
+ Browser default config is conservative: it adds agent guidance for signed-in/account-specific tasks and alternate Chromium-compatible executables; current releases do not auto-inject `--profile` or `--executable-path` for every launch. Configure profile/executable guidance globally, in trusted project config, or through `PI_AGENT_BROWSER_CONFIG`. Ask the agent to run `profiles` and `doctor` when profile resolution fails, then use the reported Chrome profile directory name, a full profile/user-data directory path if upstream accepts one, or the configured `browser.executablePath` with top-level `sessionMode: "fresh"`.
775
775
 
776
776
  ## Important global flags, config, and environment
777
777
 
@@ -819,7 +819,7 @@ Browser default config is conservative: it adds agent guidance for signed-in/acc
819
819
  - `--confirm-interactive`: interactive confirmations; auto-denies when stdin is not a TTY. Environment: `AGENT_BROWSER_CONFIRM_INTERACTIVE`.
820
820
  - `-p, --provider <name>`: provider such as `ios`, `browserbase`, `kernel`, `browseruse`, `browserless`, or `agentcore`. Environment: `AGENT_BROWSER_PROVIDER`.
821
821
  - `--device <name>`: iOS device name. Environment: `AGENT_BROWSER_IOS_DEVICE`.
822
- - Provider-specific iOS examples from upstream include `agent-browser -p ios device list`, `agent-browser -p ios swipe up`, and `agent-browser -p ios tap @e1`; in pi, pass those tokens through `args` rather than bash. iOS requires external Xcode/Appium setup, and cloud providers (`browserbase`, `kernel`, `browseruse`, `browserless`, `agentcore`) require their upstream accounts, credentials, and provider-specific environment variables. Common forwarded provider variables include `BROWSERBASE_API_KEY`, `BROWSERBASE_PROJECT_ID`, `BROWSERLESS_API_KEY`, `BROWSERLESS_API_URL`, `BROWSERLESS_BROWSER_TYPE`, `BROWSERLESS_STEALTH`, `BROWSERLESS_TTL`, `BROWSER_USE_API_KEY`, `KERNEL_API_KEY`, `KERNEL_HEADLESS`, `KERNEL_STEALTH`, `KERNEL_TIMEOUT_SECONDS`, `KERNEL_PROFILE_NAME`, `AGENTCORE_API_KEY`, `AGENTCORE_REGION`, `AGENTCORE_BROWSER_ID`, `AGENTCORE_PROFILE_ID`, `AGENTCORE_SESSION_TIMEOUT`, plus AWS names used by AgentCore such as `AWS_PROFILE`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_REGION`, and `AWS_DEFAULT_REGION`. The wrapper forwards provider flags/env and stays thin; it does not emulate provider setup or cloud browser behavior.
822
+ - Provider-specific iOS examples from upstream include `agent-browser -p ios device list`, `agent-browser -p ios swipe up`, and `agent-browser -p ios tap @e1`; in pi, pass those tokens through `args` rather than bash. iOS requires external Xcode/Appium setup, and cloud providers (`browserbase`, `kernel`, `browseruse`, `browserless`, `agentcore`) require their upstream accounts, credentials, and provider-specific environment variables. Common provider variables include `BROWSERBASE_API_KEY`, `BROWSERBASE_PROJECT_ID`, `BROWSERLESS_API_KEY`, `BROWSERLESS_API_URL`, `BROWSERLESS_BROWSER_TYPE`, `BROWSERLESS_STEALTH`, `BROWSERLESS_TTL`, `BROWSER_USE_API_KEY`, `KERNEL_API_KEY`, `KERNEL_HEADLESS`, `KERNEL_STEALTH`, `KERNEL_TIMEOUT_SECONDS`, `KERNEL_PROFILE_NAME`, `AGENTCORE_API_KEY`, `AGENTCORE_REGION`, `AGENTCORE_BROWSER_ID`, `AGENTCORE_PROFILE_ID`, `AGENTCORE_SESSION_TIMEOUT`, plus AWS names used by AgentCore such as `AWS_PROFILE`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_REGION`, and `AWS_DEFAULT_REGION`. The wrapper forwards parent environment variables plus provider flags and stays thin; it does not emulate provider setup or cloud browser behavior.
823
823
  - `--model <name>`: AI model for `chat`. Environment: `AI_GATEWAY_MODEL`.
824
824
  - `-v, --verbose`: show tool commands and raw output.
825
825
  - `-q, --quiet`: show only AI text responses.
@@ -837,7 +837,7 @@ Browser default config is conservative: it adds agent guidance for signed-in/acc
837
837
 
838
838
  Use `--config <path>` to load a specific config file. Boolean flags accept optional `true` or `false` values, such as `--headed false`, to override config. Browser extensions from user and project configs are merged rather than replaced.
839
839
 
840
- Other useful environment variables include `AGENT_BROWSER_DEFAULT_TIMEOUT`, `AGENT_BROWSER_STREAM_PORT`, `AGENT_BROWSER_IDLE_TIMEOUT_MS`, `AGENT_BROWSER_ENCRYPTION_KEY`, `AGENT_BROWSER_STATE_EXPIRE_DAYS`, `AGENT_BROWSER_IOS_DEVICE`, `AGENT_BROWSER_IOS_UDID`, `AI_GATEWAY_URL`, `AI_GATEWAY_API_KEY`, the provider credential names listed above, and AWS credential names when using AgentCore. The upstream child also receives every parent variable whose name starts with `AGENT_BROWSER_`, `AGENTCORE_`, `AI_GATEWAY_`, `BROWSERBASE_`, `BROWSERLESS_`, `BROWSER_USE_`, `KERNEL_`, or `XDG_`, plus the explicit inherited-name allowlist in `buildAgentBrowserProcessEnv` (`extensions/agent-browser/lib/process.ts`).
840
+ Other useful environment variables include `AGENT_BROWSER_DEFAULT_TIMEOUT`, `AGENT_BROWSER_STREAM_PORT`, `AGENT_BROWSER_IDLE_TIMEOUT_MS`, `AGENT_BROWSER_ENCRYPTION_KEY`, `AGENT_BROWSER_STATE_EXPIRE_DAYS`, `AGENT_BROWSER_IOS_DEVICE`, `AGENT_BROWSER_IOS_UDID`, `AI_GATEWAY_URL`, `AI_GATEWAY_API_KEY`, provider credential names, and AWS credential names when using AgentCore. The upstream child receives the parent environment plus wrapper overrides such as the managed socket directory and clamped default operation timeout (`buildAgentBrowserProcessEnv` in `extensions/agent-browser/lib/process.ts`). Model-facing output still redacts recognized secret values.
841
841
 
842
842
  ## Wrapper-specific behavior worth knowing
843
843
 
package/docs/RELEASE.md CHANGED
@@ -178,7 +178,7 @@ Maintainer constraints for evolving scenarios and version bumps are summarized u
178
178
  - `LICENSE` exists in the repo and the packed tarball
179
179
  - canonical published docs are present
180
180
  - `npm pack --json --dry-run` runs the `prepack` build and packs the compiled `dist/extensions/agent-browser/index.js` entrypoint
181
- - GitHub/source installs run the package `prepare` build so Pi can load the ignored compiled `dist/extensions/agent-browser/index.js` entrypoint from a fresh clone
181
+ - GitHub/source installs run the package `prepare` build; when Pi installs with `npm install --omit=dev`, `scripts/prepare.mjs` installs source-build dev dependencies with lifecycle scripts disabled before building so Pi can load the ignored compiled `dist/extensions/agent-browser/index.js` entrypoint from a fresh clone
182
182
  - the package-level doctor command and capability baseline are present
183
183
  - compiled extension runtime files are present, including the split result-rendering modules required by the published facade
184
184
  - source-only, agent-only, and superseded docs are absent from the tarball
@@ -106,7 +106,7 @@ The design should comfortably support workflows such as:
106
106
  - isolated authenticated browser sessions
107
107
  - headless authenticated `chat.com` / ChatGPT / OpenAI browsing without forcing `--headed` or `--auto-connect`
108
108
  - upstream profile/debug workflows without adding a local profile-cloning layer in this package
109
- - provider-backed or iOS device launches where upstream owns credentials, env, and setup; the wrapper forwards argv and a curated provider-related environment without emulating those backends
109
+ - provider-backed or iOS device launches where upstream owns credentials, env, and setup; the wrapper forwards argv and the parent environment without emulating those backends
110
110
  - desktop Electron targets using top-level `electron` for discover → isolated launch → attach → probe/cleanup, or raw `args: ["connect", …]` when the operator launches the real app with a debug port for signed-in state (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#electron) and [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#electron-desktop-apps))
111
111
 
112
112
  ## Implications for the implementation
@@ -73,7 +73,7 @@ Runtime floor note: package metadata keeps Pi core package peer ranges wildcard
73
73
  | Sessions, state, tabs, frames, dialogs, and windows | 20 canonical tokens from baseline section `state-tabs-frames-dialogs`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#session-state-frames-dialogs-windows-and-inspection-commands). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#session-state-frames-dialogs-windows-and-inspection-commands), stateful workflow notes, [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details). | Stateful summaries/redaction, state artifact handling, sessionless local command planning, managed-session restore, tab target pinning, and close alias cleanup. | Extension-validation stateful matrix, runtime session/resume tests, presentation redaction tests, lifecycle harness. | Supported. External profile/auth state remains operator-owned. |
74
74
  | Network, storage, artifacts, diagnostics, and performance | 42 canonical tokens from baseline section `network-storage-artifacts-diagnostics`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#page-state-finding-mouse-settings-network-and-storage). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#page-state-finding-mouse-settings-network-and-storage), diagnostic sections, [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details). | Thin passthrough plus compact diagnostics, route-mock warnings, useful-but-redacted storage output, stream idempotency normalization, artifact metadata, missing-ffmpeg warnings, sensitive-data redaction, timeout bounds, and cleanup-pair guidance. | Fake non-core matrix and safe real-upstream coverage for network/HAR, diff, trace/profiler, console/errors/highlight, stream, vitals, and React missing-renderer. | Supported. Environment-sensitive operations need suitable local/browser state. |
75
75
  | Batch, auth, confirmations, setup, dashboard, devices, and AI commands | 24 canonical tokens from baseline section `batch-auth-setup-ai`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#batch-auth-confirmations-sessions-chat-dashboard-devices-and-setup). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#batch-auth-confirmations-sessions-chat-dashboard-devices-and-setup), README security notes, release docs. | Native-tool batch stdin, generated `job`/`qa`/lookup batch plans, auth/confirmation redaction, sessionless local auth/setup/dashboard/doctor planning, timeout/cleanup guidance. | Unit/fake batch/auth/confirmation/dashboard/chat/doctor tests; extension-validation for structured input modes; efficiency benchmark scenarios. | Supported. Interactive side-effecting setup/auth/chat remains upstream-owned. |
76
- | Global flags, config, providers, policy, and environment | 120 canonical tokens from baseline section `options-and-env`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#important-global-flags-config-and-environment). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#important-global-flags-config-and-environment), README provider/setup notes, [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#sessionmode), architecture/runtime docs. | Runtime handles command discovery, value-flag prevalidation, launch-scoped flags, redacted echoes, fresh-session recovery hints, explicit sessions, provider/device launch-scoping, curated env forwarding, subprocess completion, and package-owned Pi-scoped config for optional companion features. | Runtime tests for flags/planning/redaction/session behavior; process tests for env and stdio-linger completion; config/web-search/CLI tests; fake provider/specialized-skill matrix; package doctor. | Supported. Provider clouds, iOS/Appium, proxies, profiles, and credentials require external setup. |
76
+ | Global flags, config, providers, policy, and environment | 120 canonical tokens from baseline section `options-and-env`; see [`scripts/agent-browser-capability-baseline.mjs`](../scripts/agent-browser-capability-baseline.mjs) and generated [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#important-global-flags-config-and-environment). | [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#important-global-flags-config-and-environment), README provider/setup notes, [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#sessionmode), architecture/runtime docs. | Runtime handles command discovery, value-flag prevalidation, launch-scoped flags, redacted echoes, fresh-session recovery hints, explicit sessions, provider/device launch-scoping, parent env forwarding with wrapper overrides, subprocess completion, and package-owned Pi-scoped config for optional companion features. | Runtime tests for flags/planning/redaction/session behavior; process tests for env and stdio-linger completion; config/web-search/CLI tests; fake provider/specialized-skill matrix; package doctor. | Supported. Provider clouds, iOS/Appium, proxies, profiles, and credentials require external setup. |
77
77
 
78
78
  ## Follow-up decision after closure
79
79
 
@@ -36,7 +36,7 @@ Agent-facing efficiency claims are measured with `npm run benchmark:agent-browse
36
36
 
37
37
  ## Optional companion web search
38
38
 
39
- `agent_browser_web_search` is a separate custom tool, not an `agent_browser` input mode. It is registered only when the extension can see at least one configured/resolvable Exa or Brave credential source from `~/.pi/config/pi-agent-browser-native/config.json`, `.pi/config/pi-agent-browser-native/config.json`, `PI_AGENT_BROWSER_CONFIG`, or the `EXA_API_KEY` / `BRAVE_API_KEY` environment fallbacks, and only when the final available merged config does not set `webSearch.enabled` to `false`. Config layers merge global → project → `PI_AGENT_BROWSER_CONFIG` override; under Pi 0.79+, globally installed and CLI-loaded copies read `.pi/config/...` by default because extensions are developer-trusted code, and they skip the project layer when Pi reports the project is untrusted or when launched with `--no-approve`. Disable scope is explicit: a global disable is a normal user default, a project disable applies to one repo, and an override file with `webSearch.enabled: false` is the highest-priority hard disable for that run. Command credential sources such as `"!op read 'op://Private/Exa/API Key'"` are allowed only from trusted global or explicit-override config; they make the tool available without running the command at startup, and the key is resolved when the tool executes. Project-local config may use only matching provider env refs (`$EXA_API_KEY` / `${EXA_API_KEY}` for Exa and `$BRAVE_API_KEY` / `${BRAVE_API_KEY}` for Brave); custom env aliases, interpolation literals, and malformed `$` values are rejected. Browser profile/executable config uses the same paths but only trusted global or explicit override values are emitted as host launch prompt guidance; project-local browser config is loaded by default but is not trusted to steer local profiles or executable paths.
39
+ `agent_browser_web_search` is a separate custom tool, not an `agent_browser` input mode. It is available when the extension can see at least one configured/resolvable Exa or Brave credential source from `~/.pi/config/pi-agent-browser-native/config.json`, `.pi/config/pi-agent-browser-native/config.json`, `PI_AGENT_BROWSER_CONFIG`, or the `EXA_API_KEY` / `BRAVE_API_KEY` environment fallbacks, and runtime execution still checks that the final available merged config has not set `webSearch.enabled` to `false`. Config layers merge global → project → `PI_AGENT_BROWSER_CONFIG` override; under Pi 0.79+, globally installed and CLI-loaded copies read `.pi/config/...` when Pi trust allows that project layer, and they skip the project layer when Pi reports the project is untrusted or when launched with `--no-approve`. Disable scope is explicit: a global disable is a normal user default, a project disable applies to one repo, and an override file with `webSearch.enabled: false` is the highest-priority hard disable for that run. Credential sources may be plaintext, `$ENV_VAR` / `${ENV_VAR}` interpolation, escaped literals, or command sources such as `"!op read 'op://Private/Exa/API Key'"` from any loaded config layer; they make the tool available without exposing the value in status text, and command values resolve when the tool executes. Browser profile/executable config uses the same paths and emits prompt guidance from the highest-priority loaded layer, including project config when that layer is loaded.
40
40
 
41
41
  Use it when live/current external web information would help answer a task, find current docs/news, or discover candidate URLs. Use `agent_browser` when the task needs browser interaction, screenshots, authenticated/profile content, page inspection, or DOM work. The search tool is namespaced to avoid colliding with generic `web_search`, chooses Exa or Brave automatically from available credentials, defaults to Exa when both are available (unless `webSearch.preferredProvider` is set), and must not expose resolved API keys in content, details, errors, status output, docs examples, logs, or PR artifacts.
42
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-browser-native",
3
- "version": "0.2.49",
3
+ "version": "0.2.51",
4
4
  "description": "pi extension that exposes agent-browser as a native tool for browser automation",
5
5
  "type": "module",
6
6
  "author": "Mitch Fultz (https://github.com/fitchmultz)",
@@ -35,6 +35,7 @@
35
35
  "platform-smoke.config.mjs",
36
36
  "scripts/config.mjs",
37
37
  "scripts/doctor.mjs",
38
+ "scripts/prepare.mjs",
38
39
  "scripts/agent-browser-capability-baseline.mjs",
39
40
  "scripts/platform-smoke.mjs",
40
41
  "scripts/platform-smoke",
@@ -65,7 +66,7 @@
65
66
  "@earendil-works/pi-ai": "0.79.1",
66
67
  "@earendil-works/pi-coding-agent": "0.79.1",
67
68
  "@earendil-works/pi-tui": "0.79.1",
68
- "@types/node": "^25.6.1",
69
+ "@types/node": "^25.9.3",
69
70
  "tsx": "^4.21.0",
70
71
  "typebox": "^1.1.38",
71
72
  "typescript": "^6.0.3"
@@ -92,7 +93,7 @@
92
93
  "build": "node ./scripts/build.mjs",
93
94
  "startup-profile": "node ./scripts/profile-startup.mjs",
94
95
  "prepack": "npm run build",
95
- "prepare": "npm run build"
96
+ "prepare": "node ./scripts/prepare.mjs"
96
97
  },
97
98
  "packageManager": "npm@11.14.0"
98
99
  }
@@ -28,7 +28,6 @@ const {
28
28
  getWebSearchProviderConfigKey,
29
29
  getWebSearchProviderEnvVar,
30
30
  getWebSearchProviderLabel,
31
- isProjectSafeCredentialValueForProvider,
32
31
  isWebSearchProvider,
33
32
  loadAgentBrowserConfigStateSync,
34
33
  summarizeConfigFiles,
@@ -50,9 +49,9 @@ Usage through npm exec:
50
49
  npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config paths
51
50
  npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config show
52
51
  npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config web-search status
53
- npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config web-search set-key --stdin --provider <exa|brave> [--global]
52
+ npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config web-search set-key --stdin --provider <exa|brave> [--global|--project]
54
53
  npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config web-search set-env <ENV_VAR> [--provider brave|exa] [--global|--project]
55
- npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config web-search set-command <command> --provider <exa|brave> [--global]
54
+ npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config web-search set-command <command> --provider <exa|brave> [--global|--project]
56
55
  npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config web-search clear --provider <exa|brave|all> [--global|--project]
57
56
  npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config web-search prefer <exa|brave|auto> [--global|--project]
58
57
  npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config web-search enable [--global|--project]
@@ -61,14 +60,14 @@ Usage through npm exec:
61
60
  npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config browser profile set <name|path> [--policy explicit-only|authenticated-only|always] [--global|--project]
62
61
  npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config browser profile clear [--global|--project]
63
62
  npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config browser executable status
64
- npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config browser executable set <path> [--global]
63
+ npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config browser executable set <path> [--global|--project]
65
64
  npm exec --yes --package pi-agent-browser-native@latest -- pi-agent-browser-config browser executable clear [--global|--project]
66
65
 
67
66
  Notes:
68
67
  Global config: ~/.pi/config/pi-agent-browser-native/config.json
69
68
  Project config: .pi/config/pi-agent-browser-native/config.json
70
69
  Override: ${AGENT_BROWSER_CONFIG_ENV}=/path/to/config.json
71
- Project-local plaintext, custom env aliases, interpolation-literal, malformed, and command-backed web-search keys are refused; use matching ${EXA_API_KEY_ENV} or ${BRAVE_API_KEY_ENV} set-env references there.
70
+ Loaded config may use plaintext, environment interpolation, or !command credential sources; displayed status redacts resolved keys.
72
71
  Use --provider for set-key, set-command, and clear; set-env infers exa/brave from ${EXA_API_KEY_ENV} or ${BRAVE_API_KEY_ENV}.
73
72
  `;
74
73
  }
@@ -212,13 +211,12 @@ async function handleWebSearch(args, flags) {
212
211
  }
213
212
  if (action === "set-key") {
214
213
  const provider = getWebSearchProvider(flags);
215
- if (flags.get("--project")) throw new UsageError(`Plaintext ${getWebSearchProviderLabel(provider)} keys cannot be written to project-local config. Use set-env or set-command.`);
216
214
  const key = await readSecretFromStdin(Boolean(flags.get("--stdin")));
217
- const { path } = selectWritePath(flags);
215
+ const { path, scope } = selectWritePath(flags);
218
216
  mutateConfig(path, (config) => {
219
217
  setWebSearchCredential(config, provider, key);
220
218
  });
221
- console.log(`Saved ${getWebSearchProviderLabel(provider)} key to global config: ${path}`);
219
+ console.log(`Saved ${getWebSearchProviderLabel(provider)} key to ${scope} config: ${path}`);
222
220
  return;
223
221
  }
224
222
  if (action === "set-env") {
@@ -226,9 +224,6 @@ async function handleWebSearch(args, flags) {
226
224
  if (!envName || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(envName)) throw new UsageError("set-env requires a valid environment variable name.");
227
225
  const provider = getWebSearchProvider(flags, { envName });
228
226
  const envReference = `$${envName}`;
229
- if (flags.get("--project") && !isProjectSafeCredentialValueForProvider(envReference, provider)) {
230
- throw new UsageError(`Project-local ${getWebSearchProviderLabel(provider)} env references must use ${getWebSearchProviderEnvVar(provider)} exactly; custom env aliases belong in global config or ${AGENT_BROWSER_CONFIG_ENV}.`);
231
- }
232
227
  const { path, scope } = selectWritePath(flags);
233
228
  mutateConfig(path, (config) => {
234
229
  setWebSearchCredential(config, provider, envReference);
@@ -238,7 +233,6 @@ async function handleWebSearch(args, flags) {
238
233
  }
239
234
  if (action === "set-command") {
240
235
  const provider = getWebSearchProvider(flags);
241
- if (flags.get("--project")) throw new UsageError(`Command-backed ${getWebSearchProviderLabel(provider)} keys cannot be written to project-local config. Use set-env there.`);
242
236
  const command = args.slice(1).join(" ").trim();
243
237
  if (!command) throw new UsageError("set-command requires a command string.");
244
238
  const { path, scope } = selectWritePath(flags);
@@ -297,9 +291,6 @@ function handleBrowser(args, flags) {
297
291
  if (!name) throw new UsageError("browser profile set requires a profile name or profile directory path.");
298
292
  const policy = flags.get("--policy") || "authenticated-only";
299
293
  if (!["explicit-only", "authenticated-only", "always"].includes(policy)) throw new UsageError("Invalid --policy value.");
300
- if (flags.get("--project") && policy !== "explicit-only") {
301
- throw new UsageError("Project-local browser profile config may only use --policy explicit-only; authenticated or always profile guidance must be configured globally or through PI_AGENT_BROWSER_CONFIG.");
302
- }
303
294
  const { path, scope } = selectWritePath(flags);
304
295
  mutateConfig(path, (config) => {
305
296
  config.browser = { ...(config.browser ?? {}), defaultProfile: { name, policy } };
@@ -325,9 +316,6 @@ function handleBrowser(args, flags) {
325
316
  if (action === "set") {
326
317
  const executablePath = args.slice(2).join(" ").trim();
327
318
  if (!executablePath) throw new UsageError("browser executable set requires a browser executable path.");
328
- if (flags.get("--project")) {
329
- throw new UsageError("Project-local browser executable config cannot steer host launch guidance; configure it globally or through PI_AGENT_BROWSER_CONFIG.");
330
- }
331
319
  const { path, scope } = selectWritePath(flags);
332
320
  mutateConfig(path, (config) => {
333
321
  config.browser = { ...(config.browser ?? {}), executablePath };
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Purpose: Build generated dist output for GitHub/source installs even when Pi invokes npm install --omit=dev.
4
+ * Responsibilities: Detect missing source-build dependencies, install dev dependencies with lifecycle scripts disabled, then run the canonical build.
5
+ * Scope: Package install lifecycle only; npm tarball contents and runtime behavior remain owned by scripts/build.mjs.
6
+ * Usage: package.json prepare script.
7
+ */
8
+
9
+ import { execFile as execFileCallback } from "node:child_process";
10
+ import { createRequire } from "node:module";
11
+ import { join } from "node:path";
12
+ import process from "node:process";
13
+ import { promisify } from "node:util";
14
+
15
+ const execFile = promisify(execFileCallback);
16
+ const require = createRequire(import.meta.url);
17
+ const REQUIRED_SOURCE_BUILD_MODULES = [
18
+ "typescript",
19
+ "typebox",
20
+ "@earendil-works/pi-coding-agent",
21
+ "@earendil-works/pi-tui",
22
+ ];
23
+
24
+ function canResolveBuildDependencies() {
25
+ return REQUIRED_SOURCE_BUILD_MODULES.every((moduleName) => {
26
+ try {
27
+ require.resolve(moduleName);
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ });
33
+ }
34
+
35
+ async function runNpmInstallDevDependencies() {
36
+ const npmExecPath = process.env.npm_execpath;
37
+ const options = process.platform === "win32" ? { shell: true } : {};
38
+ if (npmExecPath) {
39
+ await execFile(process.execPath, [npmExecPath, "install", "--include=dev", "--ignore-scripts"], {
40
+ ...options,
41
+ cwd: process.cwd(),
42
+ maxBuffer: 20 * 1024 * 1024,
43
+ });
44
+ return;
45
+ }
46
+ await execFile("npm", ["install", "--include=dev", "--ignore-scripts"], {
47
+ ...options,
48
+ cwd: process.cwd(),
49
+ maxBuffer: 20 * 1024 * 1024,
50
+ });
51
+ }
52
+
53
+ async function main() {
54
+ if (!canResolveBuildDependencies()) {
55
+ await runNpmInstallDevDependencies();
56
+ }
57
+ await execFile(process.execPath, [join(process.cwd(), "scripts", "build.mjs")], {
58
+ cwd: process.cwd(),
59
+ maxBuffer: 20 * 1024 * 1024,
60
+ });
61
+ }
62
+
63
+ main().catch((error) => {
64
+ if (error?.stdout) process.stdout.write(error.stdout);
65
+ if (error?.stderr) process.stderr.write(error.stderr);
66
+ console.error(error instanceof Error ? error.message : String(error));
67
+ process.exitCode = 1;
68
+ });