pi-agent-browser-native 0.2.76 → 0.2.77

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
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.77 - 2026-08-04
4
+
5
+ ### Fixed
6
+
7
+ - Successful `connect`, `--cdp`, and `--auto-connect` sessions, including environment-configured and wrapper-launched Electron attachments, now keep their attached browser across native-tool follow-ups and cleanup instead of resending local-launch defaults that made upstream replace the connection and prompt again. Content-bearing first use is blocked until the attachment URL is verified, established attachments live-check `get url` before later page reads or interactions so external tab drift cannot expose a local target, and every child clears the file-access environment override even when attached reuse omits the canonical launch flags.
8
+
3
9
  ## 0.2.76 - 2026-08-04
4
10
 
5
11
  ### Fixed
package/README.md CHANGED
@@ -510,8 +510,8 @@ Use these rules:
510
510
  - Do not assume `--profile Default` is correct. Ask the agent to run `profiles` to list Chrome profile directory names, then `doctor` if profile/user-data-dir resolution still fails.
511
511
  - For non-Chrome Chromium browsers such as Brave, Edge, Arc, or Vivaldi, use `--executable-path <path>` when upstream can launch that executable. If you need that browser's existing login state, use the browser's real profile/user-data directory path when upstream accepts it, or attach with `--auto-connect` / `connect` to a debug-enabled running browser when appropriate.
512
512
  - Use `sessionMode: "fresh"` when switching from public browsing to `--allowed-domains`, `--profile`, `--executable-path`, `--webgpu`, `--restore`, `--restore-save`, restore check flags, `--namespace`, `--session-name`, `--cdp`, `--state`, `--auto-connect`, `--init-script`, `--enable`, `-p` / `--provider`, or iOS `--device`.
513
- - Use `--session` when you want to manage a live upstream session name yourself.
514
- - Do not treat an arbitrary `--session` name alone as persisted auth after `close`, `quit`, or `exit`. Wrapper-owned managed sessions automatically set a Git-checkout-generation-stable `AGENT_BROWSER_RESTORE` key so cookies/localStorage/sessionStorage survive browser relaunches across Pi chats in the same checkout; the key follows a renamed checkout but changes when that path is replaced or copied, and automatic restore fails closed outside a Git checkout. The wrapper combines the checkout-root and Git-admin filesystem identities with a generation UUID in the Git admin directory and never adopts older cwd-only keys; a bare caller `--session` name does not get that injection, and the wrapper reserves `piab-*` names case-insensitively so another Pi process cannot attach to a managed authenticated browser through a case alias. Disable with `PI_AGENT_BROWSER_MANAGED_SESSION_RESTORE=0`. For explicit non-managed sessions use `--session <id> --restore`, `--profile`, or `--state`. SSO/2FA such as Okta Touch ID may still need one human approval (often `--headed` the first time); after that, managed restore should keep the session without a manual `state save` dance. Any upstream `agent-browser.json` / `AGENT_BROWSER_CONFIG` / `--config` discovered while planning blocks browser-backed native calls without reading it, while accepted browser-backed spawns pin a process-private empty config to close config-creation races. This is separate from this package's trusted Pi-scoped config; sessionless local/setup commands retain upstream config behavior. Raw batch argv, batch stdin containing nested `connect`/`batch`, browser mutation flag, or matching launch-mutation env disables automatic managed restore rather than risking restored auth in a caller-customized or attached browser. Every accepted browser-backed subprocess, including wrapper-owned close, pins `AGENT_BROWSER_CONFIG` to that process-private empty config (`0400` on POSIX) in the marked secure-temp lifecycle so a project or user config created between planning and spawn cannot change the browser. A user-private immutable ticket-claim lock serializes each same-identity daemon inspection through the receiving spawn and bridges the pre-update v2 lock path; every lock winner re-inspects the live daemon, and abandoned v2 locks fail closed rather than being reclaimed unsafely. POSIX process identity probes use absolute `/bin/ps` then `/usr/bin/ps` paths. Before an incompatible call, the wrapper inspects the actual same-identity daemon and blocks when it retains any restore key, cannot be inspected, or reports restore-disabled policy without current-process provenance, including daemons missing from transcript state and sessions launched with explicit restore keys. Same-process `session_tree` transitions retain recorded provenance; extension reload, restart, and `/resume` deliberately do not trust transcript-only provenance for a still-live restore-disabled daemon, so close it first, omit the explicit session and use `sessionMode: "fresh"`, or choose a distinct explicit session. If inspection instead proves the old daemon inactive, the next owned no-restore spawn records that null policy so subsequent follow-ups remain usable. Wrapper-owned subprocesses pin the canonical namespace, including an explicit empty default, so a parent `AGENT_BROWSER_NAMESPACE` cannot redirect close or helper calls; Electron status target reads and current-managed probes also acquire the same daemon-policy lock, verify the live URL before title/content reads, and apply the same restore decision to every underlying read. A probe whose reads all fail is an `upstream-error`, not a successful empty partial result. Current-managed probe results persist their namespace and ref state for Pi reload/branch replay. Upstream restore files live under `~/.agent-browser/` and are plaintext unless you set `AGENT_BROWSER_ENCRYPTION_KEY`; on POSIX the wrapper canonicalizes and pins `HOME` after caller env merging, requires owner-trusted non-writable ancestry, requires stable device/inode/birth-time metadata for both checkout and Git-admin directories, enforces mode `0700` without silently tightening unsafe existing directories, and rejects symlinks/non-directories along the exact restore `sessions` path and its `.tmp` write area before automatic managed restore. Windows automatic managed restore requires an absolute `USERPROFILE` and the documented 64-character hex `AGENT_BROWSER_ENCRYPTION_KEY` because POSIX mode checks cannot verify profile ACLs. Wrapper-owned close commands discard caller config/restore globals, preserve the live daemon's existing restore key instead of injecting one derived from a possibly replaced checkout, and record a returned old-generation snapshot against that observed wrapper key. If a fresh command starts agent-browser but then fails, the wrapper probes that exact identity and retains a live or uninspectable daemon for shutdown cleanup instead of abandoning it. After a wrapper-owned managed session closes successfully, the wrapper persists the returned state path as an atomic record in a lockless convergent per-key ownership directory (`0700`, with `0600` records, on POSIX), keeps the two newest proven snapshots for its exact restore key across Pi restarts, self-heals malformed regular records, removes additional proven snapshots older than 30 days, expires ownership-proven snapshots and empty manifests from older restore-key generations only when a private lineage record proves the same canonical checkout path, after 30 days, and caps young close churn at 256 records per restore key; unrecorded matching files and the current checkout key remain untouched. Managed restore keys and key-bearing paths are redacted from tool output and transcripts. `session list` and `state list` hide wrapper-managed rows; cross-checkout managed `--restore` / `--state` / state-file access, broad `state clear`, `state clean`, and managed save/rename targets are rejected before spawn. Browser access to `.agent-browser` storage is blocked through command-specific file operands (including dash-prefixed values), every path-bearing upstream environment mirror (including state/profile/config, executable/extension/init-script, action-policy, artifact, skills, and socket paths), encoded, nested-file-scheme, Windows-aliased, or symlinked targets (including not-yet-created descendants of symlinked directories), content-returning local-URL commands, protected artifact destinations and top-level `outputPath`, local-page follow-ups, and persisted unverified top-level or batch tab/attachment/script/state-load transitions. Raw batch command strings are split on literal ASCII spaces exactly like upstream and inspected recursively just like batch stdin arrays; Electron launch handoffs, probes, and later capture share the same boundary: snapshot/tabs handoff and probes verify the live URL before tab/title/content helpers, and cancellation during handoff closes the managed session plus process/profile. The wrapper rejects enabled `--allow-file-access` argv/env plus file-access-enabling or protected-path `--args` / `AGENT_BROWSER_ARGS` values, removes caller file-access occurrences, clears raw-args env, and adds canonical `--args "" --allow-file-access false` defaults so project/user config cannot silently preserve local-page filesystem access; an explicit safe CLI `--args` value may still override the empty default. Post-transition summaries, including after arbitrary `eval`, verify the live URL before title and fail implicit transitions to local file pages; failed navigation attempts remain unverified, and stale concurrent completions cannot overwrite newer unknown page state. `get url`, `tab list`, non-content `tab <id>` selection, explicit safe navigation away, and session/tab close remain available for recovery; tab selection stays unverified until `get url` succeeds. The wrapper repeats checkout, storage, environment, managed-session ownership, and managed-state access validation after async config/socket setup immediately before spawn. On POSIX the selected daemon socket directory must be absolute, current-user-owned, mode `0700`, under trusted ancestry, and free of symlink, foreign-owner, or special planted entries. Pre-existing unsafe modes are rejected rather than repaired, and the check is repeated immediately before spawn. On native Windows, command-first launcher reordering moves only syntactically valid leading globals, rewrites a valued `--restore <name>` as `--restore=<name>` to preserve upstream optional-value semantics, and leaves invalid or command-scoped leading tokens untouched. Upstream periodically saves restore-enabled cookies/localStorage while the browser is open; `AGENT_BROWSER_AUTOSAVE_INTERVAL_MS` defaults to `30000`, `0` disables periodic saves but keeps save-on-close, and the `never` value for `--restore-save` disables automatic saves for that restore session.
513
+ - Use `--session` when you want to manage a live upstream session name yourself. For CDP, connect once, verify with `get url`, keep using that session without repeating `--cdp`, and close it explicitly when done. The wrapper preserves the established attachment across follow-ups instead of resending local-launch defaults, and live-checks the URL before later page reads or interactions because an attached browser can change tabs outside Pi.
514
+ - Do not treat an arbitrary `--session` name alone as persisted auth after `close`, `quit`, or `exit`. Wrapper-owned managed sessions automatically set a Git-checkout-generation-stable `AGENT_BROWSER_RESTORE` key so cookies/localStorage/sessionStorage survive browser relaunches across Pi chats in the same checkout; the key follows a renamed checkout but changes when that path is replaced or copied, and automatic restore fails closed outside a Git checkout. The wrapper combines the checkout-root and Git-admin filesystem identities with a generation UUID in the Git admin directory and never adopts older cwd-only keys; a bare caller `--session` name does not get that injection, and the wrapper reserves `piab-*` names case-insensitively so another Pi process cannot attach to a managed authenticated browser through a case alias. Disable with `PI_AGENT_BROWSER_MANAGED_SESSION_RESTORE=0`. For explicit non-managed sessions use `--session <id> --restore`, `--profile`, or `--state`. SSO/2FA such as Okta Touch ID may still need one human approval (often `--headed` the first time); after that, managed restore should keep the session without a manual `state save` dance. Any upstream `agent-browser.json` / `AGENT_BROWSER_CONFIG` / `--config` discovered while planning blocks browser-backed native calls without reading it, while accepted browser-backed spawns pin a process-private empty config to close config-creation races. This is separate from this package's trusted Pi-scoped config; sessionless local/setup commands retain upstream config behavior. Raw batch argv, batch stdin containing nested `connect`/`batch`, browser mutation flag, or matching launch-mutation env disables automatic managed restore rather than risking restored auth in a caller-customized or attached browser. Every accepted browser-backed subprocess, including wrapper-owned close, pins `AGENT_BROWSER_CONFIG` to that process-private empty config (`0400` on POSIX) in the marked secure-temp lifecycle so a project or user config created between planning and spawn cannot change the browser. A user-private immutable ticket-claim lock serializes each same-identity daemon inspection through the receiving spawn and bridges the pre-update v2 lock path; every lock winner re-inspects the live daemon, and abandoned v2 locks fail closed rather than being reclaimed unsafely. POSIX process identity probes use absolute `/bin/ps` then `/usr/bin/ps` paths. Before an incompatible call, the wrapper inspects the actual same-identity daemon and blocks when it retains any restore key, cannot be inspected, or reports restore-disabled policy without current-process provenance, including daemons missing from transcript state and sessions launched with explicit restore keys. Same-process `session_tree` transitions retain recorded provenance; extension reload, restart, and `/resume` deliberately do not trust transcript-only provenance for a still-live restore-disabled daemon, so close it first, omit the explicit session and use `sessionMode: "fresh"`, or choose a distinct explicit session. If inspection instead proves the old daemon inactive, the next owned no-restore spawn records that null policy so subsequent follow-ups remain usable. Wrapper-owned subprocesses pin the canonical namespace, including an explicit empty default, so a parent `AGENT_BROWSER_NAMESPACE` cannot redirect close or helper calls; Electron status target reads and current-managed probes also acquire the same daemon-policy lock, verify the live URL before title/content reads, and apply the same restore decision to every underlying read. A probe whose reads all fail is an `upstream-error`, not a successful empty partial result. Current-managed probe results persist their namespace and ref state for Pi reload/branch replay. Upstream restore files live under `~/.agent-browser/` and are plaintext unless you set `AGENT_BROWSER_ENCRYPTION_KEY`; on POSIX the wrapper canonicalizes and pins `HOME` after caller env merging, requires owner-trusted non-writable ancestry, requires stable device/inode/birth-time metadata for both checkout and Git-admin directories, enforces mode `0700` without silently tightening unsafe existing directories, and rejects symlinks/non-directories along the exact restore `sessions` path and its `.tmp` write area before automatic managed restore. Windows automatic managed restore requires an absolute `USERPROFILE` and the documented 64-character hex `AGENT_BROWSER_ENCRYPTION_KEY` because POSIX mode checks cannot verify profile ACLs. Wrapper-owned close commands discard caller config/restore globals, preserve the live daemon's existing restore key instead of injecting one derived from a possibly replaced checkout, and record a returned old-generation snapshot against that observed wrapper key. If a fresh command starts agent-browser but then fails, the wrapper probes that exact identity and retains a live or uninspectable daemon for shutdown cleanup instead of abandoning it. After a wrapper-owned managed session closes successfully, the wrapper persists the returned state path as an atomic record in a lockless convergent per-key ownership directory (`0700`, with `0600` records, on POSIX), keeps the two newest proven snapshots for its exact restore key across Pi restarts, self-heals malformed regular records, removes additional proven snapshots older than 30 days, expires ownership-proven snapshots and empty manifests from older restore-key generations only when a private lineage record proves the same canonical checkout path, after 30 days, and caps young close churn at 256 records per restore key; unrecorded matching files and the current checkout key remain untouched. Managed restore keys and key-bearing paths are redacted from tool output and transcripts. `session list` and `state list` hide wrapper-managed rows; cross-checkout managed `--restore` / `--state` / state-file access, broad `state clear`, `state clean`, and managed save/rename targets are rejected before spawn. Browser access to `.agent-browser` storage is blocked through command-specific file operands (including dash-prefixed values), every path-bearing upstream environment mirror (including state/profile/config, executable/extension/init-script, action-policy, artifact, skills, and socket paths), encoded, nested-file-scheme, Windows-aliased, or symlinked targets (including not-yet-created descendants of symlinked directories), content-returning local-URL commands, protected artifact destinations and top-level `outputPath`, local-page follow-ups, and persisted unverified top-level or batch tab/attachment/script/state-load transitions. Raw batch command strings are split on literal ASCII spaces exactly like upstream and inspected recursively just like batch stdin arrays; Electron launch handoffs, probes, and later capture share the same boundary: snapshot/tabs handoff and probes verify the live URL before tab/title/content helpers, and cancellation during handoff closes the managed session plus process/profile. The wrapper rejects enabled `--allow-file-access` argv/env plus file-access-enabling or protected-path `--args` / `AGENT_BROWSER_ARGS` values, removes caller file-access occurrences, clears raw-args env, and adds canonical `--args "" --allow-file-access false` defaults on local-browser spawns so project/user config cannot silently preserve local-page filesystem access; attached-session follow-ups omit those launch-only flags; an explicit safe CLI `--args` value may still override the empty default. Post-transition summaries, including after arbitrary `eval`, verify the live URL before title and fail implicit transitions to local file pages; failed navigation attempts remain unverified, and stale concurrent completions cannot overwrite newer unknown page state. `get url`, `tab list`, non-content `tab <id>` selection, explicit safe navigation away, and session/tab close remain available for recovery; tab selection stays unverified until `get url` succeeds. The wrapper repeats checkout, storage, environment, managed-session ownership, and managed-state access validation after async config/socket setup immediately before spawn. On POSIX the selected daemon socket directory must be absolute, current-user-owned, mode `0700`, under trusted ancestry, and free of symlink, foreign-owner, or special planted entries. Pre-existing unsafe modes are rejected rather than repaired, and the check is repeated immediately before spawn. On native Windows, command-first launcher reordering moves only syntactically valid leading globals, rewrites a valued `--restore <name>` as `--restore=<name>` to preserve upstream optional-value semantics, and leaves invalid or command-scoped leading tokens untouched. Upstream periodically saves restore-enabled cookies/localStorage while the browser is open; `AGENT_BROWSER_AUTOSAVE_INTERVAL_MS` defaults to `30000`, `0` disables periodic saves but keeps save-on-close, and the `never` value for `--restore-save` disables automatic saves for that restore session.
515
515
  - Caller-owned explicit sessions are live-checked with `get url` before content-bearing reads or interactions. Missing or stale transcript page state is not treated as proof of a safe target; if the live URL cannot be verified, the requested content command does not run. Calls to the same effective canonical namespace/session are serialized inside one extension instance; explicit namespace argv overrides `AGENT_BROWSER_NAMESPACE`, including an explicit empty default from that probe through any semantic-action snapshot and the requested command, while different caller-owned sessions remain independent. Raw non-bail batches are rejected when a failed navigation could expose prior local or unverified page content; use exact `batch --bail` or split navigation from content. Protected Windows paths include drive-relative forms such as `C:.agent-browser\\state\\...`. Nested `batch` steps are rejected, and raw batch command strings mirror upstream's ASCII-space tokenizer, including its single/double-quote and backslash handling, without splitting on other Unicode whitespace.
516
516
  - Prefer page actions and storage checks over cookie dumps. `cookies get` can expose real profile cookies.
517
517
  - Prefer `auth save --password-stdin` over putting passwords in `args`; the wrapper only accepts caller `stdin` for `batch`, `eval --stdin`, and `auth save --password-stdin` (top-level `job` and `qa` compile to `batch` and supply their own stdin).
@@ -17,6 +17,7 @@ import { cleanupManagedSessionRestoreConfig, ManagedSessionRestoreState } from "
17
17
  import { isRecord } from "./lib/parsing.js";
18
18
  import { buildPromptPolicy, getLatestUserPrompt, shouldAppendBrowserSystemPrompt } from "./lib/prompt-policy.js";
19
19
  import { isCloseCommand } from "./lib/command-taxonomy.js";
20
+ import { hasLaunchScopedFlagToken } from "./lib/launch-scoped-flags.js";
20
21
  import { cleanupSecureTempArtifacts } from "./lib/temp.js";
21
22
  import { AGENT_BROWSER_PARAMS, } from "./lib/input-modes.js";
22
23
  import { parseAllowedDomainsPolicyFromArgs } from "./lib/navigation-policy.js";
@@ -118,6 +119,42 @@ function getToolResultArgs(details) {
118
119
  return details.effectiveArgs;
119
120
  return [];
120
121
  }
122
+ function isAttachedBrowserInvocation(args, env = process.env) {
123
+ const autoConnectEnv = env.AGENT_BROWSER_AUTO_CONNECT;
124
+ return extractCommandTokens(args)[0] === "connect"
125
+ || hasLaunchScopedFlagToken(args, "--cdp")
126
+ || hasLaunchScopedFlagToken(args, "--auto-connect")
127
+ || env.AGENT_BROWSER_CDP !== undefined
128
+ || (autoConnectEnv !== undefined && !["", "0", "false", "no"].includes(autoConnectEnv.toLowerCase()));
129
+ }
130
+ function restoreAttachedSessionKeysFromBranch(branch) {
131
+ const attachedSessionKeys = new Set();
132
+ for (const entry of branch) {
133
+ if (!isRecord(entry) || entry.type !== "message")
134
+ continue;
135
+ const message = isRecord(entry.message) ? entry.message : undefined;
136
+ if (!message || message.toolName !== "agent_browser")
137
+ continue;
138
+ const details = isRecord(message.details) ? message.details : undefined;
139
+ if (!details)
140
+ continue;
141
+ const managedSessionOutcome = isRecord(details.managedSessionOutcome) ? details.managedSessionOutcome : undefined;
142
+ const retainedFailedAttachment = details.attachedBrowserSession === true && managedSessionOutcome?.activeAfter === true;
143
+ if (!getSuccessfulToolResult(details, message) && !retainedFailedAttachment)
144
+ continue;
145
+ const args = getToolResultArgs(details);
146
+ const sessionName = typeof details.sessionName === "string" ? details.sessionName : extractExplicitSessionName(args);
147
+ if (!sessionName)
148
+ continue;
149
+ const namespace = typeof details.namespace === "string" ? details.namespace : extractExplicitNamespace(args);
150
+ const sessionKey = getSessionContextKey(sessionName, namespace) ?? sessionName;
151
+ if (isCloseCommand(extractCommandTokens(args)[0]))
152
+ attachedSessionKeys.delete(sessionKey);
153
+ else if (details.attachedBrowserSession === true || isAttachedBrowserInvocation(args, {}))
154
+ attachedSessionKeys.add(sessionKey);
155
+ }
156
+ return attachedSessionKeys;
157
+ }
121
158
  function restoreAllowedDomainsBySessionFromBranch(branch) {
122
159
  const restoredPolicies = new Map();
123
160
  for (const entry of branch) {
@@ -433,18 +470,18 @@ function syncElectronCleanupManagedSessions(sessions, cleanupResults) {
433
470
  untrackOwnedManagedSession(sessions, sessionName);
434
471
  }
435
472
  }
436
- async function closeOwnedManagedSessionsExcept(sessions, restoreState, keepSessionName, timeoutMs, keepNamespace) {
473
+ async function closeOwnedManagedSessionsExcept(sessions, restoreState, keepSessionName, timeoutMs, attachedSessionKeys, keepNamespace) {
437
474
  const keepKey = getSessionContextKey(keepSessionName, keepNamespace);
438
475
  for (const [key, owner] of [...sessions]) {
439
476
  if (key === keepKey)
440
477
  continue;
441
- const error = await closeManagedSession({ cwd: owner.cwd, namespace: owner.namespace, restoreState, sessionName: owner.sessionName, timeoutMs });
478
+ const error = await closeManagedSession({ cwd: owner.cwd, namespace: owner.namespace, preserveAttachedBrowserSession: attachedSessionKeys.has(key), restoreState, sessionName: owner.sessionName, timeoutMs });
442
479
  if (!error)
443
480
  sessions.delete(key);
444
481
  }
445
482
  }
446
- async function closeOwnedManagedSessions(sessions, restoreState, timeoutMs) {
447
- await closeOwnedManagedSessionsExcept(sessions, restoreState, undefined, timeoutMs);
483
+ async function closeOwnedManagedSessions(sessions, restoreState, timeoutMs, attachedSessionKeys) {
484
+ await closeOwnedManagedSessionsExcept(sessions, restoreState, undefined, timeoutMs, attachedSessionKeys);
448
485
  }
449
486
  function getOffBranchOwnedElectronLaunchRecords(ownedRecords, branchRecords) {
450
487
  const activeBranchLaunchIds = new Set(getActiveElectronRecords(branchRecords).map((record) => record.launchId));
@@ -585,6 +622,7 @@ export default function agentBrowserExtension(pi) {
585
622
  let traceOwners = new Map();
586
623
  let artifactManifest;
587
624
  let allowedDomainsBySession = new Map();
625
+ let attachedSessionKeys = new Set();
588
626
  let networkRoutesBySession = new Map();
589
627
  let electronLaunchRecords = new Map();
590
628
  let ownedElectronLaunchRecords = new Map();
@@ -600,6 +638,7 @@ export default function agentBrowserExtension(pi) {
600
638
  const key = getSessionContextKey(sessionName, namespace) ?? sessionName;
601
639
  allowedDomainsBySession = new Map(allowedDomainsBySession);
602
640
  allowedDomainsBySession.delete(key);
641
+ attachedSessionKeys.delete(key);
603
642
  networkRoutesBySession = new Map(networkRoutesBySession);
604
643
  networkRoutesBySession.delete(key);
605
644
  sessionPageState.clearSession(key);
@@ -610,6 +649,7 @@ export default function agentBrowserExtension(pi) {
610
649
  const previousManagedSessionActive = managedSessionActive;
611
650
  const previousManagedSessionName = managedSessionName;
612
651
  const previousFreshSessionOrdinal = freshSessionOrdinal;
652
+ const previousAttachedSessionKeys = attachedSessionKeys;
613
653
  managedSessionBaseName = createImplicitSessionName(ctx.sessionManager.getSessionId(), ctx.cwd, ephemeralSessionSeed);
614
654
  const branch = ctx.sessionManager.getBranch();
615
655
  const branchResourceEvents = collectBranchManagedResourceEvents(branch);
@@ -646,8 +686,13 @@ export default function agentBrowserExtension(pi) {
646
686
  traceOwners = new Map();
647
687
  artifactManifest = restoreArtifactManifestFromBranch(branch);
648
688
  allowedDomainsBySession = restoreAllowedDomainsBySessionFromBranch(branch);
689
+ attachedSessionKeys = restoreAttachedSessionKeysFromBranch(branch);
649
690
  networkRoutesBySession = new Map();
650
691
  electronLaunchRecords = restoreElectronLaunchRecordsFromBranch(branch);
692
+ for (const record of getActiveElectronRecords(electronLaunchRecords)) {
693
+ if (record.sessionName)
694
+ attachedSessionKeys.add(getSessionContextKey(record.sessionName) ?? record.sessionName);
695
+ }
651
696
  if (options.resetRuntimeOwnership) {
652
697
  ownedManagedSessions.clear();
653
698
  ownedElectronLaunchRecords = new Map();
@@ -666,6 +711,17 @@ export default function agentBrowserExtension(pi) {
666
711
  branchOwnedLaunchIds: branchOwnedElectronLaunchIds,
667
712
  markBranchOwned: true,
668
713
  });
714
+ if (!options.resetRuntimeOwnership) {
715
+ for (const sessionKey of previousAttachedSessionKeys) {
716
+ if (ownedManagedSessions.has(sessionKey))
717
+ attachedSessionKeys.add(sessionKey);
718
+ }
719
+ for (const record of ownedElectronLaunchRecords.values()) {
720
+ const sessionKey = getSessionContextKey(record.sessionName);
721
+ if (sessionKey && previousAttachedSessionKeys.has(sessionKey))
722
+ attachedSessionKeys.add(sessionKey);
723
+ }
724
+ }
669
725
  };
670
726
  const registerWebSearchToolIfAvailable = (configState) => {
671
727
  if (webSearchToolRegistered || !canRegisterWebSearchTool(configState))
@@ -707,6 +763,7 @@ export default function agentBrowserExtension(pi) {
707
763
  ? ownedElectronLaunchRecords
708
764
  : getOffBranchOwnedElectronLaunchRecords(ownedElectronLaunchRecords, electronLaunchRecords);
709
765
  const electronCleanupResults = await cleanupActiveElectronHostLaunches({
766
+ attachedSessionKeys,
710
767
  cwd: shutdownCwd,
711
768
  electronChildProcesses,
712
769
  electronLaunchRecords: electronRecordsToCleanup,
@@ -719,10 +776,10 @@ export default function agentBrowserExtension(pi) {
719
776
  ])];
720
777
  syncElectronCleanupManagedSessions(ownedManagedSessions, electronCleanupResults);
721
778
  if (quitting) {
722
- await closeOwnedManagedSessions(ownedManagedSessions, managedSessionRestoreState, implicitSessionCloseTimeoutMs);
779
+ await closeOwnedManagedSessions(ownedManagedSessions, managedSessionRestoreState, implicitSessionCloseTimeoutMs, attachedSessionKeys);
723
780
  }
724
781
  else {
725
- await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs, managedSessionActive ? managedSessionNamespace : undefined);
782
+ await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, managedSessionActive ? managedSessionNamespace : undefined);
726
783
  }
727
784
  });
728
785
  managedSessionActive = false;
@@ -732,6 +789,7 @@ export default function agentBrowserExtension(pi) {
732
789
  traceOwners = new Map();
733
790
  artifactManifest = undefined;
734
791
  allowedDomainsBySession = new Map();
792
+ attachedSessionKeys = new Set();
735
793
  networkRoutesBySession = new Map();
736
794
  electronLaunchRecords = new Map();
737
795
  ownedElectronLaunchRecords = new Map();
@@ -822,6 +880,7 @@ export default function agentBrowserExtension(pi) {
822
880
  ownedRecords: ownedElectronLaunchRecords,
823
881
  });
824
882
  const electronHostResult = await handleElectronHostInput({
883
+ attachedSessionKeys,
825
884
  compiledElectron,
826
885
  cwd: ctx.cwd,
827
886
  electronChildProcesses,
@@ -891,6 +950,7 @@ export default function agentBrowserExtension(pi) {
891
950
  const browserRunState = {
892
951
  allowedDomainsBySession,
893
952
  artifactManifest,
953
+ attachedSessionKeys,
894
954
  closedManagedSessionNames: new Set(),
895
955
  electronChildProcesses,
896
956
  electronLaunchRecords,
@@ -911,6 +971,14 @@ export default function agentBrowserExtension(pi) {
911
971
  const initialAllowedDomainsBySession = browserRunState.allowedDomainsBySession;
912
972
  const initialArtifactManifest = browserRunState.artifactManifest;
913
973
  const initialNetworkRoutesBySession = browserRunState.networkRoutesBySession;
974
+ const attachedSessionRequested = isAttachedBrowserInvocation(toolArgs)
975
+ || (resolvedInput.kind === "electron" && resolvedInput.compiledElectron.action === "launch");
976
+ const allocatesFreshManagedSession = explicitSessionName === undefined
977
+ && (params.sessionMode === "fresh" || (resolvedInput.kind === "electron" && resolvedInput.compiledElectron.action === "launch"));
978
+ const reusableSessionKey = allocatesFreshManagedSession
979
+ ? undefined
980
+ : callerOwnedSessionQueueKey ?? getSessionContextKey(browserRunState.managedSessionName, browserRunState.managedSessionNamespace);
981
+ const attachedSessionKnown = reusableSessionKey !== undefined && attachedSessionKeys.has(reusableSessionKey);
914
982
  let result = await runAgentBrowserTool({
915
983
  ctx,
916
984
  cwd: ctx.cwd,
@@ -921,12 +989,34 @@ export default function agentBrowserExtension(pi) {
921
989
  input: resolvedInput,
922
990
  onUpdate,
923
991
  params,
992
+ establishAttachedBrowserSession: attachedSessionRequested && !attachedSessionKnown,
993
+ preserveAttachedBrowserSession: attachedSessionRequested || attachedSessionKnown,
924
994
  promptPolicy,
925
995
  sessionPageStateUpdate,
926
996
  signal,
927
997
  state: browserRunState,
928
998
  });
929
999
  const branchRestoreStillCurrent = branchRestoreGenerationAtStart === branchRestoreGeneration;
1000
+ if (branchRestoreStillCurrent) {
1001
+ const resultDetails = isRecord(result.details) ? result.details : undefined;
1002
+ const resultSessionName = typeof resultDetails?.sessionName === "string"
1003
+ ? resultDetails.sessionName
1004
+ : extractExplicitSessionName(toolArgs);
1005
+ const resultNamespace = typeof resultDetails?.namespace === "string"
1006
+ ? resultDetails.namespace
1007
+ : resolveAgentBrowserNamespace(toolArgs, process.env.AGENT_BROWSER_NAMESPACE);
1008
+ const resultSessionKey = getSessionContextKey(resultSessionName, resultNamespace) ?? resultSessionName;
1009
+ const managedSessionOutcome = isRecord(resultDetails?.managedSessionOutcome) ? resultDetails.managedSessionOutcome : undefined;
1010
+ const attachedSessionRemainsActive = result.isError !== true
1011
+ || (attachedSessionRequested && managedSessionOutcome?.activeAfter === true);
1012
+ const closesAttachedSession = result.isError !== true && isCloseCommand(extractCommandTokens(toolArgs)[0]);
1013
+ if (resultSessionKey && closesAttachedSession)
1014
+ attachedSessionKeys.delete(resultSessionKey);
1015
+ else if (resultSessionKey && attachedSessionRemainsActive && (attachedSessionRequested || attachedSessionKnown)) {
1016
+ attachedSessionKeys.add(resultSessionKey);
1017
+ result = { ...result, details: { ...(resultDetails ?? {}), attachedBrowserSession: true } };
1018
+ }
1019
+ }
930
1020
  if (branchRestoreStillCurrent) {
931
1021
  allowedDomainsBySession = mergeBrowserRunMap(allowedDomainsBySession, initialAllowedDomainsBySession, browserRunState.allowedDomainsBySession);
932
1022
  networkRoutesBySession = mergeBrowserRunMap(networkRoutesBySession, initialNetworkRoutesBySession, browserRunState.networkRoutesBySession);
@@ -1,4 +1,4 @@
1
- import { runAgentBrowserProcess } from "../../process.js";
1
+ import { runAgentBrowserProcess, withAttachedBrowserSessionContext } from "../../process.js";
2
2
  import { withOwnedManagedSessionContext } from "../../managed-session-restore.js";
3
3
  import { cleanupClickDispatchProbe } from "./click-dispatch.js";
4
4
  import { applyBrowserRunStatePatch } from "./session-state.js";
@@ -8,6 +8,9 @@ import { processBrowserOutput } from "./process-output.js";
8
8
  export { closeManagedSession } from "./managed-session-daemon-policy.js";
9
9
  export { getSessionContextKey } from "./session-state.js";
10
10
  export async function runAgentBrowserTool(options) {
11
+ return await withAttachedBrowserSessionContext(options.preserveAttachedBrowserSession === true, () => runAgentBrowserToolInContext(options));
12
+ }
13
+ async function runAgentBrowserToolInContext(options) {
11
14
  const preparedResult = await prepareBrowserRun(options);
12
15
  applyBrowserRunStatePatch(options.state, preparedResult.kind === "ready" ? preparedResult.prepared.statePatch : preparedResult.statePatch);
13
16
  if (preparedResult.kind === "early-result") {
@@ -19,6 +19,7 @@ export async function inspectManagedSessionDaemon(options) {
19
19
  allowManagedSessionTarget: options.allowManagedSessionTarget,
20
20
  args: ["--json", "--namespace", options.namespace ?? "", "--session", options.sessionName, "session", "info"],
21
21
  cwd: options.cwd,
22
+ preserveAttachedBrowserSession: options.preserveAttachedBrowserSession,
22
23
  signal: options.signal,
23
24
  timeoutMs: options.timeoutMs ?? MANAGED_SESSION_DAEMON_INSPECTION_TIMEOUT_MS,
24
25
  });
@@ -128,6 +129,7 @@ export async function closeManagedSession(options) {
128
129
  allowManagedSessionTarget: true,
129
130
  cwd: options.cwd,
130
131
  namespace: options.namespace,
132
+ preserveAttachedBrowserSession: options.preserveAttachedBrowserSession,
131
133
  sessionName: options.sessionName,
132
134
  signal: controller.signal,
133
135
  timeoutMs: Math.min(options.timeoutMs, 2_000),
@@ -143,6 +145,7 @@ export async function closeManagedSession(options) {
143
145
  env: { AGENT_BROWSER_JSON: "1" },
144
146
  managedSessionRestoreState: options.restoreState,
145
147
  ownedManagedSession: true,
148
+ preserveAttachedBrowserSession: options.preserveAttachedBrowserSession,
146
149
  signal: controller.signal,
147
150
  });
148
151
  stdoutSpillPath = processResult.stdoutSpillPath;
@@ -451,13 +451,18 @@ export async function prepareBrowserRun(options) {
451
451
  const priorRefSnapshotState = priorSessionPageState.refSnapshot;
452
452
  const priorRefSnapshotInvalidation = priorSessionPageState.refSnapshotInvalidation;
453
453
  let semanticActionVisibleRefResolution;
454
- let callerOwnedLivePageVerified = false;
454
+ let livePageVerified = false;
455
455
  const isCallerOwnedExplicitSession = () => executionPlan.sessionName !== undefined
456
456
  && executionPlan.usedImplicitSession === false
457
457
  && ownedManagedSession === undefined;
458
- const verifyCallerOwnedLivePage = async (options) => {
459
- if (!options.requirement || !executionPlan.sessionName)
458
+ const requiresLivePageVerification = () => isCallerOwnedExplicitSession() || options.preserveAttachedBrowserSession === true;
459
+ const verifyLivePage = async (request) => {
460
+ if (!request.requirement || !executionPlan.sessionName)
460
461
  return;
462
+ if (options.establishAttachedBrowserSession) {
463
+ executionPlan = { ...executionPlan, recoveryHint: undefined, validationError: request.requirement };
464
+ return;
465
+ }
461
466
  let liveUrl;
462
467
  try {
463
468
  const liveUrlData = await runSessionCommandData({
@@ -475,27 +480,27 @@ export async function prepareBrowserRun(options) {
475
480
  throw signal.reason ?? error;
476
481
  }
477
482
  if (liveUrl === undefined) {
478
- executionPlan = { ...executionPlan, recoveryHint: undefined, validationError: options.requirement };
483
+ executionPlan = { ...executionPlan, recoveryHint: undefined, validationError: request.requirement };
479
484
  return;
480
485
  }
481
486
  const livePageValidationError = getManagedSessionStateAccessValidationError({
482
- args: options.args,
487
+ args: request.args,
483
488
  currentPageUrl: liveUrl,
484
489
  cwd,
485
490
  pageUrlUnknown: false,
486
- stdin: options.stdin,
491
+ stdin: request.stdin,
487
492
  });
488
493
  if (livePageValidationError) {
489
494
  executionPlan = { ...executionPlan, recoveryHint: undefined, validationError: livePageValidationError };
490
495
  return;
491
496
  }
492
- callerOwnedLivePageVerified = true;
497
+ livePageVerified = true;
493
498
  priorSessionTabTarget ??= { url: liveUrl };
494
499
  priorSessionTabTargetUnknown = undefined;
495
500
  };
496
501
  const mayResolveSemanticVisibleRef = executionPlan.managedSessionName !== freshSessionName && canResolveSemanticVisibleRef(compiledSemanticAction);
497
- if (!executionPlan.validationError && mayResolveSemanticVisibleRef && isCallerOwnedExplicitSession()) {
498
- await verifyCallerOwnedLivePage({
502
+ if (!executionPlan.validationError && mayResolveSemanticVisibleRef && requiresLivePageVerification()) {
503
+ await verifyLivePage({
499
504
  args: ["snapshot", "-i"],
500
505
  requirement: getCallerOwnedSessionLivePageVerificationRequirement({ args: ["snapshot", "-i"], cwd }),
501
506
  });
@@ -530,21 +535,21 @@ export async function prepareBrowserRun(options) {
530
535
  refSnapshotInvalidation: resolvedSemanticActionRefSnapshot ? undefined : priorRefSnapshotInvalidation,
531
536
  stdin: runtimeToolStdin,
532
537
  });
533
- const callerOwnedPageAccessEligible = !executionPlan.validationError
538
+ const livePageAccessEligible = !executionPlan.validationError
534
539
  && preLiveStaleRefPreflight === undefined
535
540
  && validateStdinCommandContract({ command: executionPlan.commandInfo.command, commandTokens, stdin: runtimeToolStdin }) === undefined
536
- && isCallerOwnedExplicitSession();
537
- const callerOwnedLivePageRequirement = callerOwnedPageAccessEligible
538
- && !callerOwnedLivePageVerified
541
+ && requiresLivePageVerification();
542
+ const livePageRequirement = livePageAccessEligible
543
+ && !livePageVerified
539
544
  ? getCallerOwnedSessionLivePageVerificationRequirement({
540
545
  args: executionPlan.effectiveArgs,
541
546
  cwd,
542
547
  stdin: runtimeToolStdin,
543
548
  })
544
549
  : undefined;
545
- await verifyCallerOwnedLivePage({
550
+ await verifyLivePage({
546
551
  args: executionPlan.effectiveArgs,
547
- requirement: callerOwnedLivePageRequirement,
552
+ requirement: livePageRequirement,
548
553
  stdin: runtimeToolStdin,
549
554
  });
550
555
  const redactedEffectiveArgs = redactInvocationArgs(executionPlan.effectiveArgs);
@@ -405,9 +405,12 @@ export async function processBrowserOutput(input) {
405
405
  networkRoutesBySession = new Map(networkRoutesBySession);
406
406
  networkRoutesBySession.delete(replacedSessionStateKey ?? replacedManagedSessionName);
407
407
  sessionPageState.clearSession(replacedSessionStateKey ?? replacedManagedSessionName);
408
- const replacedCloseError = await closeManagedSession({ cwd: priorManagedSessionCwd, namespace: priorManagedSessionNamespace, restoreState: state.managedSessionRestoreState, sessionName: replacedManagedSessionName, timeoutMs: implicitSessionCloseTimeoutMs });
409
- if (!replacedCloseError)
410
- state.closedManagedSessionNames.add(replacedSessionStateKey ?? replacedManagedSessionName);
408
+ const replacedSessionKey = replacedSessionStateKey ?? replacedManagedSessionName;
409
+ const replacedCloseError = await closeManagedSession({ cwd: priorManagedSessionCwd, namespace: priorManagedSessionNamespace, preserveAttachedBrowserSession: state.attachedSessionKeys.has(replacedSessionKey), restoreState: state.managedSessionRestoreState, sessionName: replacedManagedSessionName, timeoutMs: implicitSessionCloseTimeoutMs });
410
+ if (!replacedCloseError) {
411
+ state.attachedSessionKeys.delete(replacedSessionKey);
412
+ state.closedManagedSessionNames.add(replacedSessionKey);
413
+ }
411
414
  }
412
415
  let electronLaunchRecord;
413
416
  let electronFailedConnectCleanup = prepared.electronFailedConnectCleanup;
@@ -431,7 +434,7 @@ export async function processBrowserOutput(input) {
431
434
  if (electronHandoff.error) {
432
435
  succeeded = false;
433
436
  presentationEnvelope = { error: electronHandoff.error, success: false };
434
- const closeError = await closeManagedSession({ cwd, namespace: prepared.executionPlan.namespace, policyLock: prepared.managedSessionPolicyLock, restoreState: state.managedSessionRestoreState, sessionName: electronSessionName, timeoutMs: implicitSessionCloseTimeoutMs });
437
+ const closeError = await closeManagedSession({ cwd, namespace: prepared.executionPlan.namespace, policyLock: prepared.managedSessionPolicyLock, preserveAttachedBrowserSession: input.preserveAttachedBrowserSession, restoreState: state.managedSessionRestoreState, sessionName: electronSessionName, timeoutMs: implicitSessionCloseTimeoutMs });
435
438
  electronFailedConnectCleanup = await cleanupElectronLaunchResources({ child: prepared.electronLaunch.child, record: electronLaunchRecord, timeoutMs: implicitSessionCloseTimeoutMs });
436
439
  electronLaunchRecord = electronFailedConnectCleanup.record;
437
440
  if (electronFailedConnectCleanup.partial) {
@@ -9,6 +9,7 @@ import { boundElectronProbeString } from "../../electron/text.js";
9
9
  import { buildOwnedManagedSessionRestoreContext, withOwnedManagedSessionContext, } from "../../managed-session-restore.js";
10
10
  import { getManagedSessionStateAccessValidationError } from "../../managed-session-state-policy.js";
11
11
  import { isRecord } from "../../parsing.js";
12
+ import { withAttachedBrowserSessionContext } from "../../process.js";
12
13
  import { buildAgentBrowserNextActions, buildAgentBrowserResultCategoryDetails } from "../../results.js";
13
14
  import { appendUniqueAgentBrowserNextActions } from "../../results/next-actions.js";
14
15
  import { extractRefSnapshotFromData, getSessionPageStateKey, isAboutBlankUrl, normalizeSessionTabTarget } from "../../session-page-state.js";
@@ -597,7 +598,7 @@ export async function cleanupTrackedElectronHostLaunches(options) {
597
598
  const results = [];
598
599
  for (const record of options.records) {
599
600
  const managedSessionCloseError = record.sessionName
600
- ? await closeManagedSession({ cwd: options.cwd, restoreState: options.managedSessionRestoreState, sessionName: record.sessionName, timeoutMs: options.timeoutMs })
601
+ ? await closeManagedSession({ cwd: options.cwd, preserveAttachedBrowserSession: options.attachedSessionKeys.has(getSessionPageStateKey(record.sessionName) ?? record.sessionName), restoreState: options.managedSessionRestoreState, sessionName: record.sessionName, timeoutMs: options.timeoutMs })
601
602
  : undefined;
602
603
  const managedSessionStep = record.sessionName
603
604
  ? managedSessionCloseError
@@ -640,7 +641,13 @@ export async function cleanupActiveElectronHostLaunches(options) {
640
641
  : [];
641
642
  }
642
643
  export async function handleElectronHostInput(options) {
643
- const { compiledElectron, cwd, electronChildProcesses, electronLaunchRecords, implicitSessionCloseTimeoutMs, managedSessionActive, managedSessionName, managedSessionNamespace, managedSessionRestoreState, redactedCompiledElectron, sessionPageState, signal, } = options;
644
+ const currentSessionKey = getSessionPageStateKey(options.managedSessionName, options.managedSessionNamespace) ?? options.managedSessionName;
645
+ const preserveAttachedBrowserSession = options.attachedSessionKeys.has(currentSessionKey)
646
+ || [...options.electronLaunchRecords.values()].some((record) => record.sessionName !== undefined && options.attachedSessionKeys.has(getSessionPageStateKey(record.sessionName) ?? record.sessionName));
647
+ return await withAttachedBrowserSessionContext(preserveAttachedBrowserSession, () => handleElectronHostInputInContext(options));
648
+ }
649
+ async function handleElectronHostInputInContext(options) {
650
+ const { attachedSessionKeys, compiledElectron, cwd, electronChildProcesses, electronLaunchRecords, implicitSessionCloseTimeoutMs, managedSessionActive, managedSessionName, managedSessionNamespace, managedSessionRestoreState, redactedCompiledElectron, sessionPageState, signal, } = options;
644
651
  if (compiledElectron?.action === "list") {
645
652
  try {
646
653
  const discovery = await discoverElectronApps({ maxResults: compiledElectron.maxResults, query: compiledElectron.query });
@@ -794,7 +801,7 @@ export async function handleElectronHostInput(options) {
794
801
  const selection = selectElectronRecords(compiledElectron, electronLaunchRecords);
795
802
  if (selection.error)
796
803
  return buildElectronHostFailureResult({ compiledElectron: redactedCompiledElectron ?? compiledElectron, errorText: selection.error, failureCategory: "validation-error" });
797
- const cleanupResults = await cleanupTrackedElectronHostLaunches({ cwd, electronChildProcesses, electronLaunchRecords, managedSessionRestoreState, records: selection.records ?? [], timeoutMs: compiledElectron.timeoutMs ?? implicitSessionCloseTimeoutMs });
804
+ const cleanupResults = await cleanupTrackedElectronHostLaunches({ attachedSessionKeys, cwd, electronChildProcesses, electronLaunchRecords, managedSessionRestoreState, records: selection.records ?? [], timeoutMs: compiledElectron.timeoutMs ?? implicitSessionCloseTimeoutMs });
798
805
  return buildElectronCleanupResult(redactedCompiledElectron ?? compiledElectron, cleanupResults);
799
806
  }
800
807
  return undefined;
@@ -37,6 +37,7 @@ export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
37
37
  "Do not invent fixed explicit session names for routine tasks. Use the implicit session unless you truly need multiple isolated browser sessions in the same conversation.",
38
38
  `When using launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}), put them on the first command for that session. If you intentionally use an explicit --session, keep using that same explicit session for follow-ups.`,
39
39
  "Caller-owned explicit sessions are serialized per effective canonical namespace/session inside this extension while live URL checks, semantic-action snapshots, and the requested command run. For raw batches whose later content step depends on navigation, use exact batch --bail or split the calls; unsafe continue-after-navigation-failure shapes are rejected before the batch runs.",
40
+ "After a successful `connect`, `--cdp`, or enabled `--auto-connect` call, verify with get url and keep using the resulting session without repeating the attach flag. The wrapper remembers that attachment across active-branch reload/resume, omits local-launch-only `--args` / `--allow-file-access` defaults from follow-up and cleanup subprocesses so upstream keeps the existing CDP connection, and live-checks the URL before later page reads/interactions because an attached browser can drift externally; a successful close clears the marker. First-use attach plus content calls are blocked until the URL is verified.",
40
41
  `If you already used the implicit session and now need launch-scoped flags (${LAUNCH_SCOPED_FLAG_LABEL}), retry with top-level sessionMode set to fresh or pass an explicit --session for the new launch; never pass --session-mode inside args. After a successful unnamed fresh launch, later auto calls follow that new session.`,
41
42
  "For WebGPU pages, use args [\"--webgpu\", \"open\", \"<url>\"] on a fresh local browser launch; use doctor --webgpu (or --headed on Linux/Windows capture paths) to prove rendering before trusting a non-black screenshot. WebGPU cannot be combined with --cdp, --auto-connect, or provider launches unless --webgpu false overrides an enabled config/environment default.",
42
43
  "For --allowed-domains, use a fresh local Chrome context. Upstream rejects CDP/auto-connect, profiles, restore/state replay, direct-page providers, iOS/Safari, and startup/profile Chrome args because they cannot guarantee containment; Chromium also disables RTCPeerConnection while the allowlist is active.",
@@ -5,6 +5,7 @@
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.
7
7
  */
8
+ import { AsyncLocalStorage } from "node:async_hooks";
8
9
  import { spawn } from "node:child_process";
9
10
  import { lstat, mkdir, readdir } from "node:fs/promises";
10
11
  import { dirname, isAbsolute, join } from "node:path";
@@ -32,6 +33,10 @@ const DEFAULT_AGENT_BROWSER_PROCESS_TIMEOUT_MS = 35_000;
32
33
  /** Grace period after `exit` before resolving when `close` is delayed by inherited stdio handles. */
33
34
  const EXIT_STDIO_GRACE_MS = 100;
34
35
  const WINDOWS_AGENT_BROWSER_MISSING_MARKER = "PI_AGENT_BROWSER_COMMAND_NOT_FOUND:agent-browser.cmd";
36
+ const attachedBrowserSessionContext = new AsyncLocalStorage();
37
+ export function withAttachedBrowserSessionContext(preserve, run) {
38
+ return attachedBrowserSessionContext.run(preserve || attachedBrowserSessionContext.getStore() === true, run);
39
+ }
35
40
  function appendTail(text, addition, maxChars) {
36
41
  const combined = text + addition;
37
42
  return combined.length <= maxChars ? combined : combined.slice(combined.length - maxChars);
@@ -87,7 +92,7 @@ export function reorderWindowsLeadingGlobalArgs(args) {
87
92
  }
88
93
  return args;
89
94
  }
90
- export function pinAgentBrowserFileAccessDisabled(args, wrapperCompatibilityUserAgent) {
95
+ export function pinAgentBrowserFileAccessDisabled(args, wrapperCompatibilityUserAgent, preserveAttachedBrowserSession = false) {
91
96
  const filtered = [];
92
97
  for (let index = 0; index < args.length; index += 1) {
93
98
  const token = args[index];
@@ -100,6 +105,9 @@ export function pinAgentBrowserFileAccessDisabled(args, wrapperCompatibilityUser
100
105
  }
101
106
  filtered.push(token);
102
107
  }
108
+ // These are launch-only controls. Sending them on an attached-session follow-up makes upstream replace the CDP connection with a local browser.
109
+ if (preserveAttachedBrowserSession)
110
+ return filtered;
103
111
  // Upstream's flag overrides only the active CDP target; the Chrome arg covers new tabs. Its --args parser splits commas/newlines.
104
112
  const browserArgs = wrapperCompatibilityUserAgent
105
113
  ? `--user-agent=${wrapperCompatibilityUserAgent.replaceAll(/[\r\n,]/g, "")}`
@@ -333,6 +341,7 @@ function getManagedPreSpawnPolicyError(options, effectiveEnv, allowManagedSessio
333
341
  }
334
342
  export async function runAgentBrowserProcess(options) {
335
343
  const { allowManagedSessionTarget, cwd, env, managedSessionRestoreState, managedStateCurrentPageUrl, managedStatePageUrlUnknown, signal, stdin, trustedFirstBatchTabSelection } = options;
344
+ const preserveAttachedBrowserSession = options.preserveAttachedBrowserSession === true || attachedBrowserSessionContext.getStore() === true;
336
345
  const ownedManagedSession = options.ownedManagedSession === true || isOwnedManagedSessionTarget(options.args);
337
346
  const args = canonicalizeOwnedManagedSessionCloseArgs({
338
347
  args: options.args,
@@ -390,6 +399,7 @@ export async function runAgentBrowserProcess(options) {
390
399
  ...getManagedSessionRestoreProtectedEnv(managedSessionRestoreOptions, managedSessionRestoreEnv),
391
400
  ...getOwnedManagedSessionNamespaceEnv(managedSessionRestoreOptions),
392
401
  ...ownedManagedSessionCompatibilityEnv,
402
+ AGENT_BROWSER_ALLOW_FILE_ACCESS: undefined,
393
403
  [AGENT_BROWSER_ARGS_ENV]: undefined,
394
404
  };
395
405
  const explicitSocketDir = processOverrides[AGENT_BROWSER_SOCKET_DIR_ENV];
@@ -524,7 +534,7 @@ export async function runAgentBrowserProcess(options) {
524
534
  resolve({ aborted: false, agentBrowserStarted: false, exitCode: 1, spawnError: new Error(spawnPolicyError), stderr: "", stdout: "", timedOut: false });
525
535
  return;
526
536
  }
527
- const spawnCommand = buildAgentBrowserSpawnCommand(pinAgentBrowserFileAccessDisabled(args, ownedManagedSessionCompatibilityEnv.AGENT_BROWSER_USER_AGENT));
537
+ const spawnCommand = buildAgentBrowserSpawnCommand(pinAgentBrowserFileAccessDisabled(args, ownedManagedSessionCompatibilityEnv.AGENT_BROWSER_USER_AGENT, preserveAttachedBrowserSession));
528
538
  const child = spawn(spawnCommand.command, spawnCommand.args, {
529
539
  cwd,
530
540
  env: childEnv,
@@ -145,6 +145,7 @@ Practical policy:
145
145
  - keep explicit screenshots, downloads, PDFs, traces, HAR captures, and recordings written to caller-chosen paths on disk after a successful upstream close command (`close`, `quit`, or `exit`); before artifact-producing commands run, create missing parent directories for requested host paths, and for simple loopback HTML anchor downloads with resolvable HTTP(S) hrefs the wrapper may save directly to the requested path before upstream fallback. When the bounded `details.artifactManifest` has entries, successful close commands also surface `details.artifactCleanup` and a compact `Artifact lifecycle` note pointing to structured explicit paths so operators remove files with normal host tools—the native tool does not delete arbitrary user paths (`extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts`, `getArtifactCleanupGuidance`); contract in [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details), checklist `RQ-0079` in [`SUPPORT_MATRIX.md`](SUPPORT_MATRIX.md)
146
146
  - reconstruct the current branch-visible extension-managed session, page-scoped refs, newest-revision aggregate artifact manifest, and Electron launch records from the active transcript branch on `session_start` and `session_tree` so later default calls keep following the active managed browser after resume/reload or branch switching; restore also honors successful explicit `--session <wrapper-owned> close` rows and `electron.cleanup` managed-session steps so closed wrapper-owned sessions are not resurrected
147
147
  - keep process-owned cleanup registries for extension-managed sessions and wrapper-launched Electron records separate from the current branch-visible view; `session_tree` restore and wrapper-owned browser commands are serialized with managed-session work, while caller-owned explicit-session commands are serialized by process-local queues keyed to effective canonical namespace/session across prepare helpers (explicit namespace argv overrides inherited `AGENT_BROWSER_NAMESPACE`, including an explicit empty default) and main execution. macOS and Windows additionally normalize and case-fold namespace and session components to match case-insensitive daemon identity. Different caller-owned identities remain concurrent, nested helpers never re-enter the outer queue, policy/route/artifact deltas merge across unrelated managed-state commits, and a separate branch-restore generation guard prevents stale completions from overwriting newer branch-visible state; aggregate artifact results use monotonic revisions so transcript replay cannot lose a concurrently completed entry. Branch switches still must not drop resources the current Pi process owns and must keep fresh-session allocation monotonic
148
+ - record successful `connect`, `--cdp`, enabled `--auto-connect`, environment-configured CDP/auto-connect, and wrapper Electron attachment identities in branch-visible state, then run their main commands, helper probes, and cleanup inside an attached-browser process context that omits wrapper launch-only `--args` / `--allow-file-access` defaults. Re-sending those flags makes upstream choose local launch instead of its existing CDP connection and can trigger remote-debugging permission on every call. Block first-use attachment/content combinations until URL verification, and apply the existing live `get url` gate before every later content-bearing read or interaction because attached targets can drift outside Pi. Every child clears `AGENT_BROWSER_ALLOW_FILE_ACCESS`, including attached calls that omit the canonical launch flags. Successful close removes the marker; existing config, environment, URL, and protected-path checks still apply
148
149
  - when a successful close targets the current extension-managed session, including an explicit `--session <current> close` or an `electron.cleanup` managed-session step, clear page/ref state, mark that session inactive, untrack cleanup ownership, and rotate the next default auto call to a fresh wrapper-generated session name rather than reusing the closed name
149
150
  - on non-quit shutdown such as `/reload`, close off-branch owned managed sessions and off-branch owned Electron launches before clearing process-local ownership, but preserve the current branch-visible active managed session and Electron launch plus that launch's isolated `userDataDir` so reload continuity still works from the active transcript branch
150
151
  - expose still-owned off-branch Electron launch records to `electron.status { launchId }`, `electron.status { all: true }`, `electron.probe { launchId }`, and `electron.cleanup`, while leaving default `electron.probe` scoped to the current managed session
@@ -903,11 +903,11 @@ Browser default config is conservative: it adds agent guidance for signed-in/acc
903
903
  - `--proxy <server>`: proxy server URL. Environments: `AGENT_BROWSER_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`.
904
904
  - `--proxy-bypass <hosts>`: proxy bypass hosts. Environments: `AGENT_BROWSER_PROXY_BYPASS`, `NO_PROXY`.
905
905
  - `--ignore-https-errors`: ignore HTTPS certificate errors. Environment: `AGENT_BROWSER_IGNORE_HTTPS_ERRORS`.
906
- - `--allow-file-access`: upstream capability, but enabled argv/`AGENT_BROWSER_ALLOW_FILE_ACCESS` forms and file-access-enabling `--args` / `AGENT_BROWSER_ARGS` Chrome switches are rejected by this native wrapper. Every spawn adds canonical `--args "" --allow-file-access false` defaults so project/user config cannot re-enable file access; an explicit validated safe CLI `--args` value remains usable. Every spawn removes all caller occurrences before adding one canonical separated `--allow-file-access false`, so unsupported equals forms cannot preserve an earlier enabled flag and config cannot re-enable local filesystem access. Unknown top-level or batch tab/attachment/script/state-load transitions remain blocked for page inspection until `get url` or explicit safe navigation establishes the target. `tab list` and non-content `tab <id>` selection remain available while unknown, but selection stays unverified until `get url`; post-transition summaries (including after arbitrary `eval`) read the live URL before title and stop if the target is a local file page.
906
+ - `--allow-file-access`: upstream capability, but enabled argv/`AGENT_BROWSER_ALLOW_FILE_ACCESS` forms and file-access-enabling `--args` / `AGENT_BROWSER_ARGS` Chrome switches are rejected by this native wrapper. Local-browser spawns add canonical `--args "" --allow-file-access false` defaults so project/user config cannot re-enable file access; an explicit validated safe CLI `--args` value remains usable. Attached-session follow-ups omit those launch-only defaults. Every spawn removes all caller occurrences before any canonical separated `--allow-file-access false` is added, so unsupported equals forms cannot preserve an earlier enabled flag and config cannot re-enable local filesystem access. Unknown top-level or batch tab/attachment/script/state-load transitions remain blocked for page inspection until `get url` or explicit safe navigation establishes the target. `tab list` and non-content `tab <id>` selection remain available while unknown, but selection stays unverified until `get url`; post-transition summaries (including after arbitrary `eval`) read the live URL before title and stop if the target is a local file page.
907
907
  - `--hide-scrollbars <bool>`: explicitly show or hide native scrollbars in headless Chromium screenshots.
908
908
  - `--headed`: ask upstream to show the browser window. Environment: `AGENT_BROWSER_HEADED`. Use it on the first launch, normally with `sessionMode: "fresh"` when changing an existing managed session; verify visibility with screenshot/tab evidence because the wrapper cannot yet prove the OS window is visible to the user.
909
909
  - `--webgpu`: enable upstream's platform-specific WebGPU launch preset. Environment: `AGENT_BROWSER_WEBGPU`; config: `"webgpu": true`. Use it on a fresh local launch. It is incompatible while enabled with `--cdp`, `--auto-connect`, and provider launches. `AGENT_BROWSER_NO_XVFB=1` disables upstream's automatic Xvfb for displayless headed Linux sessions.
910
- - `--cdp <port>`: connect through Chrome DevTools Protocol.
910
+ - `--cdp <port>`: connect through Chrome DevTools Protocol. Use it, `--auto-connect`, or `connect <port|url>` once on a named/fresh session, verify with `get url`, then reuse that session without repeating the attach flag. After a successful attachment the wrapper omits local-launch-only `--args` / `--allow-file-access` defaults on follow-ups and cleanup so upstream keeps one CDP connection instead of requesting Chrome permission again. Content-bearing first use is blocked until URL verification; later page reads/interactions live-check the URL because the attached browser can drift externally. `close` clears attachment state.
911
911
  - `--color-scheme <scheme>`: `dark`, `light`, or `no-preference`. Environment: `AGENT_BROWSER_COLOR_SCHEME`.
912
912
  - `--download-path <path>`: default browser download directory. Environment: `AGENT_BROWSER_DOWNLOAD_PATH`.
913
913
  - `--engine <name>`: browser engine, `chrome` by default or `lightpanda`. Environment: `AGENT_BROWSER_ENGINE`.
@@ -55,6 +55,7 @@ Current summary:
55
55
  | RQ-0137 | Upstream `agent-browser 0.32.4` rebaseline adds HAR `--content` body modes, the `derive-client` skill, and fixed `find role` implicit-ARIA/name matching with locator-detail misses; wrapper classifier/argv/docs cover those surfaces. | [`docs/COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#upstream-0330-rebaseline) |
56
56
  | RQ-0138 | Upstream `agent-browser 0.33.0` rebaseline adds `a11y` axe-core audits and discarded-tab revival on tab switch; wrapper documents/presents `a11y` and samples the new help surface. | [`docs/COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#upstream-0330-rebaseline) |
57
57
  | RQ-0139 | Upstream `agent-browser 0.33.2` rebaseline documents daemon idle timeout, stream quality/size envs, and tab-recovery fields; wrapper enables Git-checkout-generation-stable managed-session restore for SSO stickiness with canonical namespace identity, replayable pinned managed/Electron probes, nested attachment isolation, check-to-spawn config pinning with native POSIX/Windows stale-root identity, fail-closed restore storage, JSON close-state capture, and lockless convergent ownership-proven snapshot retention plus immutable ticket-claim cross-process daemon-policy locking with mandatory per-winner daemon inspection, a pre-update v2 bridge, absolute POSIX process probes, PID/start-identity dead-claim/artifact recovery, final post-setup spawn revalidation, managed restore-capability redaction/access guards, same-process restore-disabled daemon provenance plus inactive-daemon null-policy restart, owned-policy Electron probes, abort-safe Electron launch, independent daemon-inspection timeout, case-insensitive reserved managed live-session names, all-failed Electron probe classification, fail-closed POSIX socket ancestry/entry validation, pinned-disabled file access (including raw Chrome args/config) plus protected local CLI/environment input/output paths, authoritative persisted unverified/failed page transitions and URL-first Electron handoff cleanup, and local state-file navigation, same-checkout-lineage generation expiry, malformed-output discard, a per-key 256-record churn bound, caller-owned explicit-session live URL gating before content access, process-local canonical per-session serialization through semantic snapshots and main execution, non-bail batch failure-path analysis that prevents prior-page content exposure, semantic live gating before snapshot resolution, Windows drive-relative protected-path detection, nested batch rejection, and upstream ASCII-space batch-tokenizer parity. | [`docs/COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#upstream-0332-rebaseline), [`docs/ARCHITECTURE.md`](ARCHITECTURE.md) |
58
+ | RQ-0140 | Successful CDP/auto-connect and Electron sessions preserve one attached browser across native-tool follow-ups and cleanup by omitting wrapper local-launch defaults, with environment support, branch replay, first-use content blocking, live URL gates against external drift, and close cleanup. | [`docs/ARCHITECTURE.md`](ARCHITECTURE.md), [`docs/TOOL_CONTRACT.md`](TOOL_CONTRACT.md), `test/agent-browser.extension-passthrough-validation.test.ts` |
58
59
 
59
60
  ## Verification evidence
60
61
 
@@ -152,6 +152,7 @@ The extension always plans normal browser commands with `--json` prepended in `e
152
152
  - Do not invent fixed explicit session names for routine tasks. Use the implicit session unless you truly need multiple isolated browser sessions in the same conversation.
153
153
  - When using launch-scoped flags (--auto-connect, --allowed-domains, --namespace, --cdp, --enable, --executable-path, --webgpu, --init-script, --idle-timeout, --device, --profile, --provider, -p, --session-name, --restore, --restore-save, --restore-check-url, --restore-check-text, --restore-check-fn, --state), put them on the first command for that session. If you intentionally use an explicit --session, keep using that same explicit session for follow-ups.
154
154
  - Caller-owned explicit sessions are serialized per effective canonical namespace/session inside this extension while live URL checks, semantic-action snapshots, and the requested command run. For raw batches whose later content step depends on navigation, use exact batch --bail or split the calls; unsafe continue-after-navigation-failure shapes are rejected before the batch runs.
155
+ - After a successful `connect`, `--cdp`, or enabled `--auto-connect` call, verify with get url and keep using the resulting session without repeating the attach flag. The wrapper remembers that attachment across active-branch reload/resume, omits local-launch-only `--args` / `--allow-file-access` defaults from follow-up and cleanup subprocesses so upstream keeps the existing CDP connection, and live-checks the URL before later page reads/interactions because an attached browser can drift externally; a successful close clears the marker. First-use attach plus content calls are blocked until the URL is verified.
155
156
  - If you already used the implicit session and now need launch-scoped flags (--auto-connect, --allowed-domains, --namespace, --cdp, --enable, --executable-path, --webgpu, --init-script, --idle-timeout, --device, --profile, --provider, -p, --session-name, --restore, --restore-save, --restore-check-url, --restore-check-text, --restore-check-fn, --state), retry with top-level sessionMode set to fresh or pass an explicit --session for the new launch; never pass --session-mode inside args. After a successful unnamed fresh launch, later auto calls follow that new session.
156
157
  - For WebGPU pages, use args ["--webgpu", "open", "<url>"] on a fresh local browser launch; use doctor --webgpu (or --headed on Linux/Windows capture paths) to prove rendering before trusting a non-black screenshot. WebGPU cannot be combined with --cdp, --auto-connect, or provider launches unless --webgpu false overrides an enabled config/environment default.
157
158
  - For --allowed-domains, use a fresh local Chrome context. Upstream rejects CDP/auto-connect, profiles, restore/state replay, direct-page providers, iOS/Safari, and startup/profile Chrome args because they cannot guarantee containment; Chromium also disables RTCPeerConnection while the allowlist is active.
@@ -785,6 +786,7 @@ Implementation and precedence:
785
786
  - The main tool implementation merges these fields into Pi-facing `details` from `extensions/agent-browser/index.ts` and from `extensions/agent-browser/lib/results/presentation.ts` for presentation-time failures.
786
787
 
787
788
  Additional structured fields can appear when relevant:
789
+ - `attachedBrowserSession: true` on successful calls that establish or reuse a wrapper-tracked CDP/auto-connect/Electron attachment, and on a failed fresh attachment only when `managedSessionOutcome.activeAfter` proves its daemon remained active for cleanup. The marker restores attachment continuity from the active transcript branch, including that active-after-failure case; successful close/cleanup removes it. It does not relax config, environment, protected-path, or live-URL validation.
788
790
  - `sessionTabTargetUnknown: true` after a spawned `connect`, `state load`, history navigation, or tab-selection/close call changes the active page without a trustworthy observed target. The marker is persisted and restored across branch/reload replay, clears stale refs and tab pinning, and blocks page inspection until `get url` or an explicit safe navigation observes a target; `tab list` and close remain available.
789
791
  - `compiledSemanticAction` when the call used `semanticAction` and the result includes the unified `details` merge: `{ action, locator, args }` for `find` actions or `{ action: "select", selector, values, args }` for `select`, with the same redaction rules as `args` / `effectiveArgs`; omitted for plain `args`/`job` calls and omitted on some early error returns that omit this field (see the `semanticAction` section above)
790
792
  - `compiledJob` when the call used `job` or the job-backed `qa` preset: by default `{ args: ["batch", "--bail"], failFast: true, stdin, steps: [{ action, args }] }`; with `failFast: false`, `{ args: ["batch"], failFast: false, stdin, steps: [{ action, args }] }`. Step args are redacted the same way as other invocation details. Semantic `job` click/fill steps appear here as their compiled upstream `find … click|fill …` argv, not as the input object.
@@ -879,12 +881,12 @@ If `agent-browser` is not on `PATH`, fail with a message that:
879
881
  - reconstruct the current branch-visible extension-managed session, latest page-scoped refs, newest-revision aggregate `artifactManifest`, and wrapper-tracked Electron launch records from the active transcript branch on `session_start` and Pi `session_tree` so later default calls keep following the active managed browser and can continue reporting artifact retention state; successful explicit wrapper-owned close rows and `electron.cleanup` managed-session steps are restore-visible close events
880
882
  - keep runtime cleanup ownership separate from branch-visible state: `session_tree` restore and wrapper-owned browser commands are serialized with managed-session work; caller-owned explicit-session commands use separate process-local queues keyed by effective canonical namespace/session (explicit namespace argv wins over inherited `AGENT_BROWSER_NAMESPACE`, including an explicit empty default), so the live URL probe, preparation helpers, semantic snapshot, main command, and state commit for one identity cannot interleave while different identities remain concurrent. Namespace and session identity components are additionally Unicode-normalized and case-folded on macOS and Windows to match their case-insensitive daemon paths. Only the outer tool execution acquires that key; nested helpers run under it without re-entry. Policy, route, and artifact deltas survive unrelated managed-state commits, while a separate branch-restore generation guard prevents stale completions from overwriting a newer branch. Concurrent artifact-producing results carry a monotonic aggregate manifest revision so transcript replay selects the complete bounded manifest rather than whichever call happened to occupy the last row. Extension-managed sessions and wrapper-launched Electron records owned by the current process remain eligible for quit/cleanup, and fresh-session allocation stays monotonic across branch restores, including auto rows and close rows that reference wrapper-generated fresh names
881
883
  - when a close command or `electron.cleanup` successfully closes the current wrapper-managed session, clear live page/ref state, reserve the next generated fresh-session ordinal, and rotate the next default auto call to a fresh wrapper-generated session name rather than reusing the closed name
882
- - when `/reload` shuts down an extension instance, close off-branch owned managed sessions and off-branch owned Electron launches before clearing process-local ownership; preserve only the current branch-visible active managed session and active Electron launch plus its isolated `userDataDir` for reload continuity, and also persistently protect `userDataDir` paths when partial cleanup intentionally skips or fails profile removal so later temp cleanup, process exit, and stale temp-root pruning after restart do not violate Electron cleanup's safety decision; rebuild active branch state from the active branch on the next `session_start`
884
+ - when `/reload` shuts down an extension instance, close off-branch owned managed sessions and off-branch owned Electron launches before clearing process-local ownership; retain attached-browser context for those still-owned off-branch resources so cleanup cannot resend local-launch defaults. Preserve only the current branch-visible active managed session and active Electron launch plus its isolated `userDataDir` for reload continuity, and also persistently protect `userDataDir` paths when partial cleanup intentionally skips or fails profile removal so later temp cleanup, process exit, and stale temp-root pruning after restart do not violate Electron cleanup's safety decision; rebuild active branch state from the active branch on the next `session_start`
883
885
  - when an unnamed `sessionMode: "fresh"` launch succeeds, make it the new extension-managed session so later default calls keep using it
884
886
  - when an unnamed `sessionMode: "fresh"` launch fails or times out, preserve the previous managed session when one was active or report the attempted fresh session as abandoned when no managed session was active (`details.managedSessionOutcome`; visible `Managed session outcome: …` only when the final tool call used `sessionMode: "fresh"` and failed—see `#details`)
885
887
  - if that unnamed fresh launch replaced an already-active managed session, best-effort close the old managed session after the switch succeeds
886
- - treat explicit caller-provided non-managed `--session` choices outside the reserved `piab-*` prefix as user-managed; those names alone isolate a live browser session but are not a persisted tab/auth restore mechanism after a close command (`close`, `quit`, or `exit`), so use `--session <id> --restore`, `--profile`, or `--state` when persisted auth/tab state is required. Wrapper-owned managed sessions (the current/generated identity or an exact namespace/session in this extension instance's ownership records; foreign `piab-*` targets are rejected) set a Git-checkout-generation-stable `AGENT_BROWSER_RESTORE` env key automatically (disable with `PI_AGENT_BROWSER_MANAGED_SESSION_RESTORE=0`) so SSO cookies/localStorage/sessionStorage can survive idle shutdown and later chats in the same checkout generation without a manual `state save` flow. Any project/user/explicit upstream config discovered while planning blocks browser-backed native calls without parsing caller-selected content; accepted browser-backed subprocesses, including wrapper-owned closes, pin a process-private empty config (`0400` on POSIX) in the marked secure-temp lifecycle so later config creation cannot change the receiving browser. Sessionless local/setup commands retain upstream config behavior, and this restriction is separate from the package's Pi-scoped config. Raw batch argv, batch stdin containing nested `connect`/`batch`, and browser mutation flags/env such as extensions, init scripts, raw launch args, custom user agents/executables, proxies, plugins, WebGPU, profiles, parent env namespaces, CDP, providers, state, and containment also disable automatic managed restore. Wrapper-owned close canonicalizes upstream argv to the known namespace/session, discards caller config/restore globals, and preserves the live daemon's existing restore key rather than injecting one derived from a replacement checkout. A failed fresh call probes its exact daemon identity and remains owned when that daemon is live or uninspectable, so shutdown still closes it. After a wrapper-owned close succeeds, the wrapper persists the returned state path as an atomic record in a lockless convergent per-key ownership directory (`0700`, with `0600` records, on POSIX), retains the two newest proven snapshots for its exact restore key across Pi restarts, self-heals malformed regular records, removes additional proven snapshots older than 30 days, expires stale ownership-proven snapshots and empty manifests from other restore-key generations after 30 days only when a private lineage record proves the same canonical checkout path, and caps young close churn at 256 records per restore key without running namespace-wide `state clean`; unrecorded matching files and the current checkout key are untouched. Automatic restore requires a durable Git checkout generation and an absolute home root. The generation UUID lives in the checkout's Git admin directory and is combined with checkout-root plus Git-admin filesystem identity, so it follows a rename, changes on checkout replacement/copy, and is not migrated from cwd-only keys. On POSIX the wrapper canonicalizes and pins `HOME`, requires trusted non-writable owner ancestry plus stable device/inode/birth-time metadata for checkout and Git-admin directories, enforces owner-only mode `0700` without silently repairing insecure existing directories, and rejects symlinks/non-directories along `~/.agent-browser[/namespaces/<canonical>/state]/sessions` and its `.tmp` write area; Windows requires a valid 64-character hex `AGENT_BROWSER_ENCRYPTION_KEY`. Namespace identity uses upstream's lowercase sanitized form, and wrapper-owned subprocesses pin it (including an explicit empty default) so parent environment cannot redirect helpers or close. `electron.status` target reads and `electron.probe` hold the managed daemon-policy lock and apply the owned restore context to each underlying title/URL/focus/tab/snapshot read. When a managed session was launched with restore suppressed by an incompatible path or the explicit opt-out, `details.managedSessionRestoreDisabled: true` means later bare follow-ups do not inject wrapper restore. Same-process `session_tree` transitions retain the process-owned daemon-policy proof; reload, restart, and `/resume` restore the sticky flag but not that proof, so a still-live restore-disabled daemon fails closed until it is closed or a fresh identity is used. If inspection proves the prior daemon inactive, the next owned no-restore spawn records a null daemon policy so later follow-ups can reuse the replacement. A user-private immutable ticket-claim lock serializes policy inspection through the receiving same-identity spawn and bridges the pre-update v2 lock path; every lock winner re-inspects the live daemon with a fixed bounded timeout independent of shorter caller watchdog overrides, and abandoned v2 locks fail closed for manual repair. Before incompatible reuse, the wrapper inspects the actual same-identity daemon and fails closed when it retains any restore key, cannot be inspected, or reports restore-disabled policy without current-process provenance, including live daemons absent from transcript state and explicit restore-key sessions; close it first, retry without an explicit session using `sessionMode: "fresh"`, or use a distinct explicit session. Managed restore keys and key-bearing paths are redacted from visible text, structured details, JSON-mode output, and persisted tool rows. Browser access to `.agent-browser` local storage is rejected before spawn through command-specific input/output operands (including dash-prefixed values), every path-bearing upstream environment mirror (state/profile/config, executable/extension/init-script, action-policy, artifact, skills, and socket paths), encoded, nested-file-scheme, Windows-aliased, or symlinked paths (including nonexistent descendants of symlinked directories), protected top-level `outputPath`, content-returning local-URL calls, all follow-ups on local file pages, and persisted unverified top-level or batch tab/attachment/script/state-load transitions, plus raw batch command strings recursively tokenized on literal ASCII spaces exactly like upstream. Electron probes and later capture from a wrapper-tracked protected target use the same guard. Raw artifact destinations use the same command parser as preparation and are checked before it creates parent directories. Enabled `--allow-file-access` argv/env and file-access-enabling or protected-path raw Chrome values are rejected. Every subprocess clears `AGENT_BROWSER_ARGS`, removes caller file-access occurrences, and adds canonical `--args "" --allow-file-access false` defaults so unsupported equals forms and project/user config cannot re-enable it; an explicit validated safe CLI `--args` value may follow and override the empty raw-args default. Post-transition summaries, including forced live probes after arbitrary `eval`, verify URL before title and fail implicit transitions to local file pages. Failed or unexecuted navigation attempts remain unverified, and stale concurrent completions serialize only the authoritative page state; transcript replay gives `sessionTabTargetUnknown` precedence over any inconsistent stale target/ref fields. `get url`, `tab list`, non-content `tab <id>` selection, safe explicit navigation away, and session/tab close remain available; tab selection stays unverified until `get url` succeeds. `session list` and `state list` omit wrapper-managed rows; the `piab-*` live-session reservation is case-insensitive, and foreign managed live-session and restore/state references, broad state clearing/cleaning, and managed save/rename targets fail before spawn. Malformed oversized upstream output is discarded rather than persisted as a parse-failure spill. Checkout, storage, environment, managed-session ownership, and state-access policy is checked again after async config/socket setup immediately before spawn. POSIX socket storage is accepted only when the target is current-user-owned mode `0700`, its ancestry is trusted, and existing entries are current-user-owned regular files, sockets, or directories without symlinks; pre-existing unsafe modes are rejected rather than repaired. Identity and config globals use upstream's separated forms (`--session <name>`, `--namespace <name>`, `--config <path>`); the wrapper rejects leading identity equals forms that upstream 0.33.2 does not recognize, while equals-shaped tokens in command payload positions remain ordinary payload rather than silently selecting wrapper ownership or config. Native Windows command-first adaptation relocates only syntactically valid leading globals, rewrites valued `--restore <name>` as `--restore=<name>`, and leaves command-scoped or invalid leading input untouched. Optional global booleans also follow upstream's separated, last-occurrence-wins form: only exact lowercase `false` after the flag disables it, and an unsupported token such as `--auto-connect=false` does not override an earlier bare flag
887
- - before a content-bearing read or interaction against a caller-owned explicit session, run a session-scoped `get url` probe and apply the local-state boundary to that live target. Missing or stale transcript page state is not accepted as proof; a failed or non-URL probe blocks the requested content command. The process-local effective-namespace/session queue keeps that probe atomic with semantic snapshot resolution and the main command inside one extension instance; direct CLI calls and other Pi processes remain outside this guarantee. Non-bail batch validation retains every possible pre-transition page state up to a fixed bound, so later content is rejected when any failed transition could expose local or unverified content and branching beyond that bound fails closed with exact `batch --bail` guidance; exact `batch --bail` or split calls remove that continuation path. Protected Windows paths include drive-relative forms such as `C:.agent-browser\\state\\...`. Nested `batch` steps are rejected, while raw batch command strings mirror upstream's ASCII-space tokenizer, including its single/double-quote and backslash handling, without splitting on other Unicode whitespace.
888
+ - treat explicit caller-provided non-managed `--session` choices outside the reserved `piab-*` prefix as user-managed; those names alone isolate a live browser session but are not a persisted tab/auth restore mechanism after a close command (`close`, `quit`, or `exit`), so use `--session <id> --restore`, `--profile`, or `--state` when persisted auth/tab state is required. Wrapper-owned managed sessions (the current/generated identity or an exact namespace/session in this extension instance's ownership records; foreign `piab-*` targets are rejected) set a Git-checkout-generation-stable `AGENT_BROWSER_RESTORE` env key automatically (disable with `PI_AGENT_BROWSER_MANAGED_SESSION_RESTORE=0`) so SSO cookies/localStorage/sessionStorage can survive idle shutdown and later chats in the same checkout generation without a manual `state save` flow. Any project/user/explicit upstream config discovered while planning blocks browser-backed native calls without parsing caller-selected content; accepted browser-backed subprocesses, including wrapper-owned closes, pin a process-private empty config (`0400` on POSIX) in the marked secure-temp lifecycle so later config creation cannot change the receiving browser. Sessionless local/setup commands retain upstream config behavior, and this restriction is separate from the package's Pi-scoped config. Raw batch argv, batch stdin containing nested `connect`/`batch`, and browser mutation flags/env such as extensions, init scripts, raw launch args, custom user agents/executables, proxies, plugins, WebGPU, profiles, parent env namespaces, CDP, providers, state, and containment also disable automatic managed restore. Wrapper-owned close canonicalizes upstream argv to the known namespace/session, discards caller config/restore globals, and preserves the live daemon's existing restore key rather than injecting one derived from a replacement checkout. A failed fresh call probes its exact daemon identity and remains owned when that daemon is live or uninspectable, so shutdown still closes it. After a wrapper-owned close succeeds, the wrapper persists the returned state path as an atomic record in a lockless convergent per-key ownership directory (`0700`, with `0600` records, on POSIX), retains the two newest proven snapshots for its exact restore key across Pi restarts, self-heals malformed regular records, removes additional proven snapshots older than 30 days, expires stale ownership-proven snapshots and empty manifests from other restore-key generations after 30 days only when a private lineage record proves the same canonical checkout path, and caps young close churn at 256 records per restore key without running namespace-wide `state clean`; unrecorded matching files and the current checkout key are untouched. Automatic restore requires a durable Git checkout generation and an absolute home root. The generation UUID lives in the checkout's Git admin directory and is combined with checkout-root plus Git-admin filesystem identity, so it follows a rename, changes on checkout replacement/copy, and is not migrated from cwd-only keys. On POSIX the wrapper canonicalizes and pins `HOME`, requires trusted non-writable owner ancestry plus stable device/inode/birth-time metadata for checkout and Git-admin directories, enforces owner-only mode `0700` without silently repairing insecure existing directories, and rejects symlinks/non-directories along `~/.agent-browser[/namespaces/<canonical>/state]/sessions` and its `.tmp` write area; Windows requires a valid 64-character hex `AGENT_BROWSER_ENCRYPTION_KEY`. Namespace identity uses upstream's lowercase sanitized form, and wrapper-owned subprocesses pin it (including an explicit empty default) so parent environment cannot redirect helpers or close. `electron.status` target reads and `electron.probe` hold the managed daemon-policy lock and apply the owned restore context to each underlying title/URL/focus/tab/snapshot read. When a managed session was launched with restore suppressed by an incompatible path or the explicit opt-out, `details.managedSessionRestoreDisabled: true` means later bare follow-ups do not inject wrapper restore. Same-process `session_tree` transitions retain the process-owned daemon-policy proof; reload, restart, and `/resume` restore the sticky flag but not that proof, so a still-live restore-disabled daemon fails closed until it is closed or a fresh identity is used. If inspection proves the prior daemon inactive, the next owned no-restore spawn records a null daemon policy so later follow-ups can reuse the replacement. A user-private immutable ticket-claim lock serializes policy inspection through the receiving same-identity spawn and bridges the pre-update v2 lock path; every lock winner re-inspects the live daemon with a fixed bounded timeout independent of shorter caller watchdog overrides, and abandoned v2 locks fail closed for manual repair. Before incompatible reuse, the wrapper inspects the actual same-identity daemon and fails closed when it retains any restore key, cannot be inspected, or reports restore-disabled policy without current-process provenance, including live daemons absent from transcript state and explicit restore-key sessions; close it first, retry without an explicit session using `sessionMode: "fresh"`, or use a distinct explicit session. Managed restore keys and key-bearing paths are redacted from visible text, structured details, JSON-mode output, and persisted tool rows. Browser access to `.agent-browser` local storage is rejected before spawn through command-specific input/output operands (including dash-prefixed values), every path-bearing upstream environment mirror (state/profile/config, executable/extension/init-script, action-policy, artifact, skills, and socket paths), encoded, nested-file-scheme, Windows-aliased, or symlinked paths (including nonexistent descendants of symlinked directories), protected top-level `outputPath`, content-returning local-URL calls, all follow-ups on local file pages, and persisted unverified top-level or batch tab/attachment/script/state-load transitions, plus raw batch command strings recursively tokenized on literal ASCII spaces exactly like upstream. Electron probes and later capture from a wrapper-tracked protected target use the same guard. Raw artifact destinations use the same command parser as preparation and are checked before it creates parent directories. Enabled `--allow-file-access` argv/env and file-access-enabling or protected-path raw Chrome values are rejected. Every subprocess clears `AGENT_BROWSER_ARGS` and removes caller file-access occurrences. Local-browser spawns add canonical `--args "" --allow-file-access false` defaults so unsupported equals forms and project/user config cannot re-enable it, while attached-session follow-ups omit those launch-only defaults; an explicit validated safe CLI `--args` value may follow and override the empty raw-args default. Post-transition summaries, including forced live probes after arbitrary `eval`, verify URL before title and fail implicit transitions to local file pages. Failed or unexecuted navigation attempts remain unverified, and stale concurrent completions serialize only the authoritative page state; transcript replay gives `sessionTabTargetUnknown` precedence over any inconsistent stale target/ref fields. `get url`, `tab list`, non-content `tab <id>` selection, safe explicit navigation away, and session/tab close remain available; tab selection stays unverified until `get url` succeeds. `session list` and `state list` omit wrapper-managed rows; the `piab-*` live-session reservation is case-insensitive, and foreign managed live-session and restore/state references, broad state clearing/cleaning, and managed save/rename targets fail before spawn. Malformed oversized upstream output is discarded rather than persisted as a parse-failure spill. Checkout, storage, environment, managed-session ownership, and state-access policy is checked again after async config/socket setup immediately before spawn. POSIX socket storage is accepted only when the target is current-user-owned mode `0700`, its ancestry is trusted, and existing entries are current-user-owned regular files, sockets, or directories without symlinks; pre-existing unsafe modes are rejected rather than repaired. Identity and config globals use upstream's separated forms (`--session <name>`, `--namespace <name>`, `--config <path>`); the wrapper rejects leading identity equals forms that upstream 0.33.2 does not recognize, while equals-shaped tokens in command payload positions remain ordinary payload rather than silently selecting wrapper ownership or config. Native Windows command-first adaptation relocates only syntactically valid leading globals, rewrites valued `--restore <name>` as `--restore=<name>`, and leaves command-scoped or invalid leading input untouched. Optional global booleans also follow upstream's separated, last-occurrence-wins form: only exact lowercase `false` after the flag disables it, and an unsupported token such as `--auto-connect=false` does not override an earlier bare flag
889
+ - before a content-bearing read or interaction against a caller-owned explicit session or any established attached browser, run a session-scoped `get url` probe and apply the local-state boundary to that live target. A first-use attach/content combination is blocked until the caller establishes the attachment with `connect`, `get url`, `tab list`, or safe navigation. Missing or stale transcript page state is not accepted as proof; a failed or non-URL probe blocks the requested content command. The process-local effective-namespace/session queue keeps that probe atomic with semantic snapshot resolution and the main command inside one extension instance; external attached-browser activity, direct CLI calls, and other Pi processes remain outside this guarantee. Non-bail batch validation retains every possible pre-transition page state up to a fixed bound, so later content is rejected when any failed transition could expose local or unverified content and branching beyond that bound fails closed with exact `batch --bail` guidance; exact `batch --bail` or split calls remove that continuation path. Protected Windows paths include drive-relative forms such as `C:.agent-browser\\state\\...`. Nested `batch` steps are rejected, while raw batch command strings mirror upstream's ASCII-space tokenizer, including its single/double-quote and backslash handling, without splitting on other Unicode whitespace.
888
890
  - pass explicit `--profile` straight through to upstream `agent-browser`; no profile-cloning or isolation layer is added in v1
889
891
  <!-- agent-browser-playbook:start wrapper-tab-recovery -->
890
892
  <!-- Generated from extensions/agent-browser/lib/playbook.ts. Run `npm run docs -- playbook write` to update. -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-browser-native",
3
- "version": "0.2.76",
3
+ "version": "0.2.77",
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)",