pi-agent-browser-native 0.2.74 → 0.2.76
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 +12 -0
- package/dist/extensions/agent-browser/index.js +45 -1
- package/dist/extensions/agent-browser/lib/argv-grammar.js +5 -2
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +7 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +5 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +12 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +2 -0
- package/dist/extensions/agent-browser/lib/process.js +10 -4
- package/dist/extensions/agent-browser/lib/runtime.js +36 -34
- package/docs/ARCHITECTURE.md +2 -2
- package/docs/REQUIREMENTS.md +1 -1
- package/docs/TOOL_CONTRACT.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.76 - 2026-08-04
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Wrapper-managed compatibility sessions now pin the normal Chrome user agent at browser launch as well as on the active page. New tabs and SSO popups therefore inherit it instead of reverting to `HeadlessChrome` and falling back into Cloudflare Turnstile, while caller-selected raw-argument, headed, attached, provider, custom-UA, and non-Chrome modes remain untouched.
|
|
8
|
+
|
|
9
|
+
## 0.2.75 - 2026-08-04
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Headless `dash.cloudflare.com` now uses the same normal-Chrome user-agent compatibility path as OpenAI web properties, bypassing the Turnstile loop caused by `HeadlessChrome`. Wrapper-managed sessions retain that wrapper-owned user agent across follow-up calls and Pi reload/resume while preserving checkout-managed authentication restore.
|
|
14
|
+
|
|
3
15
|
## 0.2.74 - 2026-08-03
|
|
4
16
|
|
|
5
17
|
### Changed
|
|
@@ -11,7 +11,7 @@ import { fileURLToPath } from "node:url";
|
|
|
11
11
|
import { Text } from "@earendil-works/pi-tui";
|
|
12
12
|
import { PROJECT_RULE_PROMPT, buildBrowserDefaultProfileGuideline, buildBrowserExecutablePathGuideline, buildToolPromptGuidelines, } from "./lib/playbook.js";
|
|
13
13
|
import { SessionPageState } from "./lib/session-page-state.js";
|
|
14
|
-
import { createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractCommandTokens, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, restoreManagedSessionStateFromBranch, validateToolArgs, } from "./lib/runtime.js";
|
|
14
|
+
import { canUseHeadlessCompatibilityUserAgent, createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractCommandTokens, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, restoreManagedSessionStateFromBranch, validateToolArgs, } from "./lib/runtime.js";
|
|
15
15
|
import { extractExplicitNamespace, extractExplicitSessionName, resolveAgentBrowserNamespace } from "./lib/argv-grammar.js";
|
|
16
16
|
import { cleanupManagedSessionRestoreConfig, ManagedSessionRestoreState } from "./lib/managed-session-restore.js";
|
|
17
17
|
import { isRecord } from "./lib/parsing.js";
|
|
@@ -75,6 +75,42 @@ function restoreArtifactManifestFromBranch(branch) {
|
|
|
75
75
|
}
|
|
76
76
|
return restoredManifest;
|
|
77
77
|
}
|
|
78
|
+
function restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, sessionName, namespace) {
|
|
79
|
+
let restored;
|
|
80
|
+
const targetKey = getSessionContextKey(sessionName, namespace);
|
|
81
|
+
for (const entry of branch) {
|
|
82
|
+
if (!isRecord(entry) || entry.type !== "message")
|
|
83
|
+
continue;
|
|
84
|
+
const message = isRecord(entry.message) ? entry.message : undefined;
|
|
85
|
+
if (!message || message.toolName !== "agent_browser")
|
|
86
|
+
continue;
|
|
87
|
+
const details = isRecord(message.details) ? message.details : undefined;
|
|
88
|
+
if (!details)
|
|
89
|
+
continue;
|
|
90
|
+
const workaround = isRecord(details.compatibilityWorkaround) ? details.compatibilityWorkaround : undefined;
|
|
91
|
+
if (getSessionContextKey(typeof details.sessionName === "string" ? details.sessionName : undefined, typeof details.namespace === "string" ? details.namespace : undefined) !== targetKey)
|
|
92
|
+
continue;
|
|
93
|
+
const recognizedWorkaround = (workaround?.id === "chatgpt-headless-user-agent" || workaround?.id === "cloudflare-headless-user-agent") && typeof workaround.reason === "string"
|
|
94
|
+
? { id: workaround.id, reason: workaround.reason }
|
|
95
|
+
: undefined;
|
|
96
|
+
const succeeded = getSuccessfulToolResult(details, message);
|
|
97
|
+
const outcome = getManagedSessionOutcome(details);
|
|
98
|
+
const activeAfterFailure = recognizedWorkaround
|
|
99
|
+
&& outcome?.activeAfter === true
|
|
100
|
+
&& typeof outcome.currentSessionName === "string"
|
|
101
|
+
&& getSessionContextKey(outcome.currentSessionName, typeof outcome.currentSessionNamespace === "string" ? outcome.currentSessionNamespace : undefined) === targetKey
|
|
102
|
+
&& (outcome.status === "created" || outcome.status === "replaced" || outcome.status === "unchanged");
|
|
103
|
+
if (!succeeded && !activeAfterFailure)
|
|
104
|
+
continue;
|
|
105
|
+
if (recognizedWorkaround) {
|
|
106
|
+
restored = recognizedWorkaround;
|
|
107
|
+
}
|
|
108
|
+
else if (!canUseHeadlessCompatibilityUserAgent(getToolResultArgs(details))) {
|
|
109
|
+
restored = undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return restored;
|
|
113
|
+
}
|
|
78
114
|
function getToolResultArgs(details) {
|
|
79
115
|
if (Array.isArray(details.args) && details.args.every((arg) => typeof arg === "string"))
|
|
80
116
|
return details.args;
|
|
@@ -540,6 +576,7 @@ export default function agentBrowserExtension(pi) {
|
|
|
540
576
|
let webSearchToolRegistered = false;
|
|
541
577
|
let managedSessionActive = false;
|
|
542
578
|
let managedSessionBaseName = createImplicitSessionName(undefined, process.cwd(), ephemeralSessionSeed);
|
|
579
|
+
let managedSessionCompatibilityWorkaround;
|
|
543
580
|
let managedSessionName = managedSessionBaseName;
|
|
544
581
|
let managedSessionCwd = process.cwd();
|
|
545
582
|
let managedSessionNamespace;
|
|
@@ -600,6 +637,9 @@ export default function agentBrowserExtension(pi) {
|
|
|
600
637
|
: createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, nextFreshSessionOrdinal)
|
|
601
638
|
: restoredState.sessionName;
|
|
602
639
|
managedSessionNamespace = shouldReservePostCloseSession ? undefined : restoredState.namespace;
|
|
640
|
+
managedSessionCompatibilityWorkaround = managedSessionActive
|
|
641
|
+
? restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, managedSessionName, managedSessionNamespace)
|
|
642
|
+
: undefined;
|
|
603
643
|
managedSessionCwd = ctx.cwd;
|
|
604
644
|
freshSessionOrdinal = nextFreshSessionOrdinal;
|
|
605
645
|
sessionPageState = SessionPageState.fromBranch(branch);
|
|
@@ -686,6 +726,7 @@ export default function agentBrowserExtension(pi) {
|
|
|
686
726
|
}
|
|
687
727
|
});
|
|
688
728
|
managedSessionActive = false;
|
|
729
|
+
managedSessionCompatibilityWorkaround = undefined;
|
|
689
730
|
managedSessionNamespace = undefined;
|
|
690
731
|
sessionPageState.reset();
|
|
691
732
|
traceOwners = new Map();
|
|
@@ -816,6 +857,7 @@ export default function agentBrowserExtension(pi) {
|
|
|
816
857
|
clearSessionScopedBrowserState(closedSessionName);
|
|
817
858
|
if (closedSessionName === managedSessionName) {
|
|
818
859
|
managedSessionActive = false;
|
|
860
|
+
managedSessionCompatibilityWorkaround = undefined;
|
|
819
861
|
managedSessionNamespace = undefined;
|
|
820
862
|
freshSessionOrdinal += 1;
|
|
821
863
|
managedSessionName = createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal);
|
|
@@ -856,6 +898,7 @@ export default function agentBrowserExtension(pi) {
|
|
|
856
898
|
freshSessionOrdinal,
|
|
857
899
|
managedSessionActive,
|
|
858
900
|
managedSessionBaseName,
|
|
901
|
+
managedSessionCompatibilityWorkaround,
|
|
859
902
|
managedSessionCwd,
|
|
860
903
|
managedSessionName,
|
|
861
904
|
managedSessionNamespace,
|
|
@@ -903,6 +946,7 @@ export default function agentBrowserExtension(pi) {
|
|
|
903
946
|
if (serializeBrowserCommand || branchStateStillCurrent) {
|
|
904
947
|
freshSessionOrdinal = Math.max(freshSessionOrdinal, browserRunState.freshSessionOrdinal);
|
|
905
948
|
managedSessionActive = browserRunState.managedSessionActive;
|
|
949
|
+
managedSessionCompatibilityWorkaround = browserRunState.managedSessionCompatibilityWorkaround;
|
|
906
950
|
managedSessionCwd = browserRunState.managedSessionCwd;
|
|
907
951
|
managedSessionName = browserRunState.managedSessionName;
|
|
908
952
|
managedSessionNamespace = browserRunState.managedSessionNamespace;
|
|
@@ -103,8 +103,8 @@ export const GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES = new Set([
|
|
|
103
103
|
]);
|
|
104
104
|
const SESSION_COMPONENT_ALPHANUMERIC = /^[\p{Alphabetic}\p{Number}]$/u;
|
|
105
105
|
/** Match upstream's last-wins, case-sensitive boolean semantics; only exact `false` disables a present flag. */
|
|
106
|
-
export function
|
|
107
|
-
let enabled
|
|
106
|
+
export function getBooleanFlagValue(args, flag) {
|
|
107
|
+
let enabled;
|
|
108
108
|
for (let index = 0; index < args.length; index += 1) {
|
|
109
109
|
const token = args[index];
|
|
110
110
|
if (token === flag) {
|
|
@@ -122,6 +122,9 @@ export function isBooleanFlagEnabled(args, flag) {
|
|
|
122
122
|
}
|
|
123
123
|
return enabled;
|
|
124
124
|
}
|
|
125
|
+
export function isBooleanFlagEnabled(args, flag) {
|
|
126
|
+
return getBooleanFlagValue(args, flag) ?? false;
|
|
127
|
+
}
|
|
125
128
|
/** Mirror upstream sanitize_session_component for namespace/socket/state identity. */
|
|
126
129
|
export function canonicalizeAgentBrowserNamespace(value) {
|
|
127
130
|
if (value === undefined)
|
|
@@ -194,6 +194,12 @@ export function getOwnedManagedSessionNamespaceEnv(options) {
|
|
|
194
194
|
const { namespace, owned, ownedContext } = resolveManagedSessionRestorePolicy(options);
|
|
195
195
|
return owned ? { AGENT_BROWSER_NAMESPACE: ownedContext?.namespace ?? namespace ?? "" } : {};
|
|
196
196
|
}
|
|
197
|
+
export function getOwnedManagedSessionCompatibilityEnv(options) {
|
|
198
|
+
const { owned, ownedContext } = resolveManagedSessionRestorePolicy(options);
|
|
199
|
+
return owned && ownedContext?.compatibilityUserAgent
|
|
200
|
+
? { AGENT_BROWSER_USER_AGENT: ownedContext.compatibilityUserAgent }
|
|
201
|
+
: {};
|
|
202
|
+
}
|
|
197
203
|
export function shouldOmitOwnedManagedSessionRestoreEnv(options) {
|
|
198
204
|
return resolveManagedSessionRestorePolicy(options).owned && closesBrowserSession(options.args);
|
|
199
205
|
}
|
|
@@ -358,6 +364,7 @@ export function buildOwnedManagedSessionRestoreContext(options) {
|
|
|
358
364
|
const restoreKey = projectIdentityAvailable ? createManagedSessionRestoreKey(ownedCwd) : undefined;
|
|
359
365
|
return {
|
|
360
366
|
...owned,
|
|
367
|
+
compatibilityUserAgent: options.compatibilityUserAgent,
|
|
361
368
|
expectedDaemonRestoreKey: enabled ? restoreKey : extractRequestedRestoreKey(options.args, owned.sessionName, effectiveEnv[AGENT_BROWSER_RESTORE_ENV]),
|
|
362
369
|
protectedStorageEnv: enabled ? getManagedSessionRestoreProtectedStorageEnv(true, effectiveEnv) : undefined,
|
|
363
370
|
restoreDecision: optedOut ? "opted-out" : incompatible ? "incompatible" : "enabled",
|
|
@@ -16,7 +16,7 @@ import { applyNamespaceToNextActions } from "../../results/next-actions.js";
|
|
|
16
16
|
import { buildSessionAwareStaleRefNextActions, buildSessionTabRecoveryNextActions } from "../../results/recovery-next-actions.js";
|
|
17
17
|
import { resolveVisibleRefActionFromSnapshot } from "../../results/selector-recovery.js";
|
|
18
18
|
import { extractRefSnapshotFromData } from "../../session-page-state.js";
|
|
19
|
-
import { buildExecutionPlan, createFreshSessionName, extractCommandTokens, redactInvocationArgs, } from "../../runtime.js";
|
|
19
|
+
import { buildExecutionPlan, createFreshSessionName, extractCommandTokens, getDefaultHeadlessCompatUserAgent, redactInvocationArgs, } from "../../runtime.js";
|
|
20
20
|
import { buildOwnedManagedSessionRestoreContext, canonicalizeOwnedManagedSessionCloseArgs, withOwnedManagedSessionContext, } from "../../managed-session-restore.js";
|
|
21
21
|
import { getCallerOwnedSessionLivePageVerificationRequirement, getManagedSessionStateAccessValidationError, getManagedSessionTargetAccessValidationError, } from "../../managed-session-state-policy.js";
|
|
22
22
|
import { acquireOwnedManagedSessionDaemonPolicy } from "./managed-session-daemon-policy.js";
|
|
@@ -377,6 +377,7 @@ export async function prepareBrowserRun(options) {
|
|
|
377
377
|
let executionPlan = buildExecutionPlan(preparedArgs.args, {
|
|
378
378
|
freshSessionName,
|
|
379
379
|
managedSessionActive: state.managedSessionActive,
|
|
380
|
+
managedSessionCompatibilityWorkaround: state.managedSessionCompatibilityWorkaround,
|
|
380
381
|
managedSessionName: state.managedSessionName,
|
|
381
382
|
managedSessionNamespace: state.managedSessionNamespace,
|
|
382
383
|
sessionMode,
|
|
@@ -407,7 +408,8 @@ export async function prepareBrowserRun(options) {
|
|
|
407
408
|
restoreState: state.managedSessionRestoreState,
|
|
408
409
|
sessionName: executionPlan.sessionName,
|
|
409
410
|
stdin: runtimeToolStdin,
|
|
410
|
-
|
|
411
|
+
compatibilityUserAgent: executionPlan.compatibilityWorkaround ? getDefaultHeadlessCompatUserAgent() : undefined,
|
|
412
|
+
wrapperInjectedUserAgent: executionPlan.compatibilityWorkaround !== undefined,
|
|
411
413
|
});
|
|
412
414
|
const managedSessionTargetError = getManagedSessionTargetAccessValidationError(executionPlan.effectiveArgs, ownedManagedSession !== undefined);
|
|
413
415
|
if (!executionPlan.validationError && managedSessionTargetError)
|
|
@@ -511,6 +513,7 @@ export async function prepareBrowserRun(options) {
|
|
|
511
513
|
executionPlan = buildExecutionPlan(semanticActionVisibleRefResolution.args, {
|
|
512
514
|
freshSessionName,
|
|
513
515
|
managedSessionActive: state.managedSessionActive,
|
|
516
|
+
managedSessionCompatibilityWorkaround: state.managedSessionCompatibilityWorkaround,
|
|
514
517
|
managedSessionName: state.managedSessionName,
|
|
515
518
|
managedSessionNamespace: state.managedSessionNamespace,
|
|
516
519
|
sessionMode,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { rm } from "node:fs/promises";
|
|
2
|
+
import { getAgentBrowserSessionIdentityKey } from "../../argv-grammar.js";
|
|
2
3
|
import { isCloseCommand, isNavigationObservableCommandName, isOpenNavigationCommand } from "../../command-taxonomy.js";
|
|
3
4
|
import { OPEN_RESULT_TAB_CORRECTION_FLAGS } from "../../launch-scoped-flags.js";
|
|
4
5
|
import { cleanupElectronLaunchResources, inspectElectronLaunchStatus } from "../../electron/cleanup.js";
|
|
@@ -103,6 +104,7 @@ export async function processBrowserOutput(input) {
|
|
|
103
104
|
let artifactManifest = state.artifactManifest;
|
|
104
105
|
let freshSessionOrdinal = state.freshSessionOrdinal;
|
|
105
106
|
let managedSessionActive = state.managedSessionActive;
|
|
107
|
+
let managedSessionCompatibilityWorkaround = state.managedSessionCompatibilityWorkaround;
|
|
106
108
|
let managedSessionCwd = state.managedSessionCwd;
|
|
107
109
|
let managedSessionName = state.managedSessionName;
|
|
108
110
|
let managedSessionNamespace = state.managedSessionNamespace;
|
|
@@ -361,6 +363,15 @@ export async function processBrowserOutput(input) {
|
|
|
361
363
|
managedSessionActive = managedSessionState.active;
|
|
362
364
|
managedSessionName = managedSessionState.sessionName;
|
|
363
365
|
managedSessionNamespace = managedSessionState.namespace;
|
|
366
|
+
const executionTargetsManagedSession = prepared.executionPlan.sessionName
|
|
367
|
+
&& getAgentBrowserSessionIdentityKey(prepared.executionPlan.sessionName, prepared.executionPlan.namespace)
|
|
368
|
+
=== getAgentBrowserSessionIdentityKey(managedSessionName, managedSessionNamespace);
|
|
369
|
+
if (!managedSessionActive) {
|
|
370
|
+
managedSessionCompatibilityWorkaround = undefined;
|
|
371
|
+
}
|
|
372
|
+
else if (managedTransitionSucceeded && executionTargetsManagedSession) {
|
|
373
|
+
managedSessionCompatibilityWorkaround = prepared.compatibilityWorkaround;
|
|
374
|
+
}
|
|
364
375
|
if (commandClosesSession && succeeded && managedCloseSessionName === priorManagedSessionName && !managedSessionActive) {
|
|
365
376
|
const daemonRestoreKey = state.managedSessionRestoreState.getDaemonRestoreKey(managedCloseSessionName, priorManagedSessionNamespace);
|
|
366
377
|
const ownedRestoreKey = !state.managedSessionRestoreState.isDisabled(managedCloseSessionName, priorManagedSessionNamespace)
|
|
@@ -575,7 +586,7 @@ export async function processBrowserOutput(input) {
|
|
|
575
586
|
currentSessionTabTarget = authoritativePageState?.tabTarget;
|
|
576
587
|
const currentSessionTabTargetUnknown = authoritativePageState?.tabTargetUnknown === true ? true : undefined;
|
|
577
588
|
const result = buildFinalAgentBrowserToolResult({ aboutBlankSessionMismatch, artifactCleanup, categoryDetails: finalRecoveryState.categoryDetails, clickDispatchDiagnostic, commandTokens: prepared.commandTokens, comboboxFocusDiagnostic, compiledNetworkSourceLookup: prepared.compiledNetworkSourceLookup, compiledSemanticAction: prepared.compiledSemanticAction, compatibilityWorkaround: prepared.compatibilityWorkaround, currentRefSnapshot, currentRefSnapshotInvalidation, currentSessionTabTarget, currentSessionTabTargetUnknown, electronBroadGetTextScopeDiagnostics, electronFailedConnectCleanup, electronHandoff, electronLaunch: prepared.electronLaunch, electronLaunchRecord, electronLaunchRecords, electronPostCommandHealth, electronProfileIsolationDetails: input.electronProfileIsolationDetails, electronRefFreshnessDiagnostic, electronSessionMismatch, errorText, evalResultWarning, evalStdinHint, exactSensitiveValues: prepared.exactSensitiveValues, executionPlan: prepared.executionPlan, fillVerificationDiagnostic, inspectionText, managedSessionOutcome, managedSessionRestoreDisabled: state.managedSessionRestoreState.isDisabled(prepared.executionPlan.sessionName, prepared.executionPlan.namespace), navigationSummary, networkSourceLookup, noActivePageSnapshotFailure: finalRecoveryState.noActivePageSnapshotFailure, openResultTabCorrection, overlayBlockerDiagnostic, parseError, parseFailureOutput, parseSucceeded, plainTextInspection, presentation, presentationEnvelope, priorSessionTabTarget: prepared.priorSessionTabTarget, processResult, qaAttachedTarget, qaPreset, recordingDependencyWarning, redactedArgs: prepared.redactedArgs, redactedCompiledElectron: prepared.redactedCompiledElectron, redactedCompiledJob: prepared.redactedCompiledJob, redactedCompiledNetworkSourceLookup: prepared.redactedCompiledNetworkSourceLookup, redactedCompiledQaPreset: prepared.redactedCompiledQaPreset, redactedCompiledSemanticAction: prepared.redactedCompiledSemanticAction, redactedCompiledSourceLookup: prepared.redactedCompiledSourceLookup, redactedContent, redactedProcessArgs: prepared.redactedProcessArgs, redactedRecoveryHint: prepared.redactedRecoveryHint, resultArtifactManifest, richInputRecoveryDiagnostic: finalRecoveryState.richInputRecoveryDiagnostic, scrollNoopDiagnostic, selectorTextVisibilityDiagnostics, sessionMode: prepared.sessionMode, sessionTabCorrection, sourceLookup, succeeded, timeoutPartialProgress, userRequestedJson: prepared.userRequestedJson, visibleRefFallbackDiagnostic: finalRecoveryState.visibleRefFallbackDiagnostic, visibleRefFallbackSessionName: finalRecoveryState.visibleRefFallbackSessionName });
|
|
578
|
-
const statePatch = { allowedDomainsBySession, artifactManifest, freshSessionOrdinal, managedSessionActive, managedSessionCwd, managedSessionName, managedSessionNamespace, networkRoutesBySession };
|
|
589
|
+
const statePatch = { allowedDomainsBySession, artifactManifest, freshSessionOrdinal, managedSessionActive, managedSessionCompatibilityWorkaround, managedSessionCwd, managedSessionName, managedSessionNamespace, networkRoutesBySession };
|
|
579
590
|
return { result, statePatch };
|
|
580
591
|
}
|
|
581
592
|
finally {
|
|
@@ -19,6 +19,8 @@ export function applyBrowserRunStatePatch(state, patch) {
|
|
|
19
19
|
state.freshSessionOrdinal = patch.freshSessionOrdinal;
|
|
20
20
|
if (patch.managedSessionActive !== undefined)
|
|
21
21
|
state.managedSessionActive = patch.managedSessionActive;
|
|
22
|
+
if ("managedSessionCompatibilityWorkaround" in patch)
|
|
23
|
+
state.managedSessionCompatibilityWorkaround = patch.managedSessionCompatibilityWorkaround;
|
|
22
24
|
if (patch.managedSessionCwd !== undefined)
|
|
23
25
|
state.managedSessionCwd = patch.managedSessionCwd;
|
|
24
26
|
if (patch.managedSessionName !== undefined)
|
|
@@ -13,7 +13,7 @@ import { parseArgvDescriptor } from "./argv-descriptor.js";
|
|
|
13
13
|
import { needsManagedSession } from "./command-policy.js";
|
|
14
14
|
import { isKnownCommandToken } from "./command-taxonomy.js";
|
|
15
15
|
import { getFlagName, GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS, optionalGlobalValueFlagConsumesNext, } from "./argv-grammar.js";
|
|
16
|
-
import { canonicalizeOwnedManagedSessionCloseArgs, commitManagedSessionRestoreSuppression, getManagedSessionRestoreConfigEnv, getManagedSessionRestoreEnv, getManagedSessionRestoreProtectedEnv, getOwnedManagedSessionNamespaceEnv, isOwnedManagedSessionTarget, shouldOmitOwnedManagedSessionRestoreEnv, validateManagedSessionRestoreContextForSpawn, } from "./managed-session-restore.js";
|
|
16
|
+
import { canonicalizeOwnedManagedSessionCloseArgs, commitManagedSessionRestoreSuppression, getManagedSessionRestoreConfigEnv, getManagedSessionRestoreEnv, getManagedSessionRestoreProtectedEnv, getOwnedManagedSessionCompatibilityEnv, getOwnedManagedSessionNamespaceEnv, isOwnedManagedSessionTarget, shouldOmitOwnedManagedSessionRestoreEnv, validateManagedSessionRestoreContextForSpawn, } from "./managed-session-restore.js";
|
|
17
17
|
import { getManagedSessionStateAccessValidationError, getManagedSessionTargetAccessValidationError, } from "./managed-session-state-policy.js";
|
|
18
18
|
import { getImplicitSessionIdleTimeoutMs, isPlainTextInspectionArgs } from "./runtime.js";
|
|
19
19
|
import { openSecureTempFile, writeSecureTempChunk } from "./temp.js";
|
|
@@ -87,7 +87,7 @@ export function reorderWindowsLeadingGlobalArgs(args) {
|
|
|
87
87
|
}
|
|
88
88
|
return args;
|
|
89
89
|
}
|
|
90
|
-
export function pinAgentBrowserFileAccessDisabled(args) {
|
|
90
|
+
export function pinAgentBrowserFileAccessDisabled(args, wrapperCompatibilityUserAgent) {
|
|
91
91
|
const filtered = [];
|
|
92
92
|
for (let index = 0; index < args.length; index += 1) {
|
|
93
93
|
const token = args[index];
|
|
@@ -100,7 +100,11 @@ export function pinAgentBrowserFileAccessDisabled(args) {
|
|
|
100
100
|
}
|
|
101
101
|
filtered.push(token);
|
|
102
102
|
}
|
|
103
|
-
|
|
103
|
+
// Upstream's flag overrides only the active CDP target; the Chrome arg covers new tabs. Its --args parser splits commas/newlines.
|
|
104
|
+
const browserArgs = wrapperCompatibilityUserAgent
|
|
105
|
+
? `--user-agent=${wrapperCompatibilityUserAgent.replaceAll(/[\r\n,]/g, "")}`
|
|
106
|
+
: "";
|
|
107
|
+
return ["--args", browserArgs, "--allow-file-access", "false", ...filtered];
|
|
104
108
|
}
|
|
105
109
|
export function buildAgentBrowserSpawnCommand(args, platform = processPlatform) {
|
|
106
110
|
if (platform !== "win32") {
|
|
@@ -377,6 +381,7 @@ export async function runAgentBrowserProcess(options) {
|
|
|
377
381
|
timedOut: false,
|
|
378
382
|
};
|
|
379
383
|
}
|
|
384
|
+
const ownedManagedSessionCompatibilityEnv = getOwnedManagedSessionCompatibilityEnv(managedSessionRestoreOptions);
|
|
380
385
|
const processOverrides = {
|
|
381
386
|
[AGENT_BROWSER_IDLE_TIMEOUT_ENV]: String(getImplicitSessionIdleTimeoutMs()),
|
|
382
387
|
...managedSessionRestoreEnv,
|
|
@@ -384,6 +389,7 @@ export async function runAgentBrowserProcess(options) {
|
|
|
384
389
|
...managedSessionRestoreConfigEnv,
|
|
385
390
|
...getManagedSessionRestoreProtectedEnv(managedSessionRestoreOptions, managedSessionRestoreEnv),
|
|
386
391
|
...getOwnedManagedSessionNamespaceEnv(managedSessionRestoreOptions),
|
|
392
|
+
...ownedManagedSessionCompatibilityEnv,
|
|
387
393
|
[AGENT_BROWSER_ARGS_ENV]: undefined,
|
|
388
394
|
};
|
|
389
395
|
const explicitSocketDir = processOverrides[AGENT_BROWSER_SOCKET_DIR_ENV];
|
|
@@ -518,7 +524,7 @@ export async function runAgentBrowserProcess(options) {
|
|
|
518
524
|
resolve({ aborted: false, agentBrowserStarted: false, exitCode: 1, spawnError: new Error(spawnPolicyError), stderr: "", stdout: "", timedOut: false });
|
|
519
525
|
return;
|
|
520
526
|
}
|
|
521
|
-
const spawnCommand = buildAgentBrowserSpawnCommand(pinAgentBrowserFileAccessDisabled(args));
|
|
527
|
+
const spawnCommand = buildAgentBrowserSpawnCommand(pinAgentBrowserFileAccessDisabled(args, ownedManagedSessionCompatibilityEnv.AGENT_BROWSER_USER_AGENT));
|
|
522
528
|
const child = spawn(spawnCommand.command, spawnCommand.args, {
|
|
523
529
|
cwd,
|
|
524
530
|
env: childEnv,
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* Usage: Imported by the extension entrypoint and unit tests before spawning the upstream CLI.
|
|
6
6
|
* Invariants/Assumptions: The wrapper stays thin, preserves upstream command vocabulary, keeps plain-text inspection stateless,
|
|
7
7
|
* and only injects wrapper-owned flags: `--json`, an extension-managed `--session` when appropriate, the narrow
|
|
8
|
-
*
|
|
8
|
+
* site-specific headless compatibility `--user-agent` when that workaround applies.
|
|
9
9
|
*/
|
|
10
10
|
import { createHash, randomUUID } from "node:crypto";
|
|
11
11
|
import { basename } from "node:path";
|
|
12
12
|
import { findCommandStartIndex, parseArgvDescriptor, parseCommandInfo, } from "./argv-descriptor.js";
|
|
13
|
-
import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey,
|
|
13
|
+
import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, getBooleanFlagValue, GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE, PREVALIDATED_VALUE_FLAGS, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences, } from "./argv-grammar.js";
|
|
14
14
|
import { needsManagedSession } from "./command-policy.js";
|
|
15
15
|
import { isWrapperManagedSessionName, redactManagedSessionRestoreKeys } from "./managed-session-capabilities.js";
|
|
16
16
|
import { isCloseCommand, isOpenNavigationCommand } from "./command-taxonomy.js";
|
|
@@ -19,6 +19,7 @@ import { MANAGED_SESSION_NAME_PREFIX, } from "./managed-session-restore.js";
|
|
|
19
19
|
export { extractCommandTokens, findCommandStartIndex, parseArgvDescriptor, parseCommandInfo } from "./argv-descriptor.js";
|
|
20
20
|
import { isRecord } from "./parsing.js";
|
|
21
21
|
const OPENAI_HEADLESS_COMPAT_HOSTS = new Set(["chat.com", "chat.openai.com", "chatgpt.com"]);
|
|
22
|
+
const CLOUDFLARE_HEADLESS_COMPAT_HOST = "dash.cloudflare.com";
|
|
22
23
|
const AGENT_BROWSER_IDLE_TIMEOUT_ENV = "AGENT_BROWSER_IDLE_TIMEOUT_MS";
|
|
23
24
|
const IMPLICIT_SESSION_IDLE_TIMEOUT_ENV = "PI_AGENT_BROWSER_IMPLICIT_SESSION_IDLE_TIMEOUT_MS";
|
|
24
25
|
const IMPLICIT_SESSION_CLOSE_TIMEOUT_ENV = "PI_AGENT_BROWSER_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS";
|
|
@@ -643,17 +644,6 @@ function formatInvalidValueFlagError(details) {
|
|
|
643
644
|
function hasFlagToken(args, flag) {
|
|
644
645
|
return args.some((token) => token === flag || token.startsWith(`${flag}=`));
|
|
645
646
|
}
|
|
646
|
-
function getFlagValue(args, flag) {
|
|
647
|
-
for (const [index, token] of args.entries()) {
|
|
648
|
-
if (token === flag) {
|
|
649
|
-
return args[index + 1];
|
|
650
|
-
}
|
|
651
|
-
if (token.startsWith(`${flag}=`)) {
|
|
652
|
-
return token.slice(flag.length + 1);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
return undefined;
|
|
656
|
-
}
|
|
657
647
|
function normalizeComparableUrl(url) {
|
|
658
648
|
const normalizedUrl = url.trim();
|
|
659
649
|
if (normalizedUrl.length === 0) {
|
|
@@ -699,34 +689,38 @@ function parseComparableNavigationUrl(url) {
|
|
|
699
689
|
}
|
|
700
690
|
}
|
|
701
691
|
}
|
|
702
|
-
function getDefaultHeadlessCompatUserAgent(platform = process.platform) {
|
|
692
|
+
export function getDefaultHeadlessCompatUserAgent(platform = process.platform) {
|
|
703
693
|
return DEFAULT_HEADLESS_COMPAT_USER_AGENT_BY_PLATFORM[platform] ?? FALLBACK_HEADLESS_COMPAT_USER_AGENT;
|
|
704
694
|
}
|
|
695
|
+
export function canUseHeadlessCompatibilityUserAgent(args, env = process.env) {
|
|
696
|
+
if (hasFlagToken(args, "--user-agent") || hasFlagToken(args, "--args"))
|
|
697
|
+
return false;
|
|
698
|
+
if (hasFlagToken(args, "--cdp") || hasFlagToken(args, "--provider") || hasFlagToken(args, "-p"))
|
|
699
|
+
return false;
|
|
700
|
+
if (env.AGENT_BROWSER_USER_AGENT !== undefined || env.AGENT_BROWSER_ARGS !== undefined || env.AGENT_BROWSER_CDP !== undefined || env.AGENT_BROWSER_PROVIDER !== undefined)
|
|
701
|
+
return false;
|
|
702
|
+
const envFlagEnabled = (value) => value !== undefined && !["", "0", "false", "no"].includes(value.toLowerCase());
|
|
703
|
+
if ((getBooleanFlagValue(args, "--headed") ?? envFlagEnabled(env.AGENT_BROWSER_HEADED))
|
|
704
|
+
|| (getBooleanFlagValue(args, "--auto-connect") ?? envFlagEnabled(env.AGENT_BROWSER_AUTO_CONNECT)))
|
|
705
|
+
return false;
|
|
706
|
+
const engine = scanUpstreamGlobalFlagOccurrences(args, "--engine").at(-1)?.value ?? env.AGENT_BROWSER_ENGINE;
|
|
707
|
+
return !engine || engine === "chrome";
|
|
708
|
+
}
|
|
705
709
|
function getCompatibilityWorkaround(args, commandInfo) {
|
|
706
|
-
if (!commandInfo.command || !isOpenNavigationCommand(commandInfo.command) || !commandInfo.subcommand)
|
|
710
|
+
if (!commandInfo.command || !isOpenNavigationCommand(commandInfo.command) || !commandInfo.subcommand || !canUseHeadlessCompatibilityUserAgent(args))
|
|
707
711
|
return undefined;
|
|
708
|
-
}
|
|
709
|
-
if (hasFlagToken(args, "--user-agent")) {
|
|
710
|
-
return undefined;
|
|
711
|
-
}
|
|
712
|
-
if (isBooleanFlagEnabled(args, "--headed")) {
|
|
713
|
-
return undefined;
|
|
714
|
-
}
|
|
715
|
-
if (hasFlagToken(args, "--cdp") || hasFlagToken(args, "--provider") || hasFlagToken(args, "-p") || isBooleanFlagEnabled(args, "--auto-connect")) {
|
|
716
|
-
return undefined;
|
|
717
|
-
}
|
|
718
|
-
const engine = getFlagValue(args, "--engine");
|
|
719
|
-
if (engine && engine !== "chrome") {
|
|
720
|
-
return undefined;
|
|
721
|
-
}
|
|
722
712
|
const parsedTargetUrl = parseComparableNavigationUrl(commandInfo.subcommand);
|
|
723
|
-
if (!parsedTargetUrl || !["http:", "https:"].includes(parsedTargetUrl.protocol))
|
|
713
|
+
if (!parsedTargetUrl || !["http:", "https:"].includes(parsedTargetUrl.protocol))
|
|
724
714
|
return undefined;
|
|
725
|
-
}
|
|
726
715
|
const hostname = parsedTargetUrl.hostname.toLowerCase();
|
|
727
|
-
if (
|
|
728
|
-
return
|
|
716
|
+
if (hostname === CLOUDFLARE_HEADLESS_COMPAT_HOST) {
|
|
717
|
+
return {
|
|
718
|
+
id: "cloudflare-headless-user-agent",
|
|
719
|
+
reason: "Cloudflare Dashboard challenges the default headless Chrome user agent; inject a normal Chrome user agent so authenticated headless browsing reaches the dashboard instead of Turnstile.",
|
|
720
|
+
};
|
|
729
721
|
}
|
|
722
|
+
if (!OPENAI_HEADLESS_COMPAT_HOSTS.has(hostname))
|
|
723
|
+
return undefined;
|
|
730
724
|
return {
|
|
731
725
|
id: "chatgpt-headless-user-agent",
|
|
732
726
|
reason: "OpenAI web properties currently challenge the default headless Chrome user agent; inject a normal Chrome user agent to preserve the default headless workflow without requiring headed mode or auto-connect.",
|
|
@@ -807,7 +801,8 @@ export function buildExecutionPlan(args, options) {
|
|
|
807
801
|
}
|
|
808
802
|
const shouldCreateFreshManagedSession = !explicitSessionName && options.sessionMode === "fresh" && commandInfo.command !== undefined && !isCloseCommand(commandInfo.command);
|
|
809
803
|
let argsToAppend = args;
|
|
810
|
-
const
|
|
804
|
+
const requestedCompatibilityWorkaround = getCompatibilityWorkaround(args, commandInfo);
|
|
805
|
+
let compatibilityWorkaround = requestedCompatibilityWorkaround;
|
|
811
806
|
if (explicitSessionName && explicitNamespacePresent) {
|
|
812
807
|
effectiveArgs.push("--namespace", explicitNamespace ?? "");
|
|
813
808
|
argsToAppend = stripExplicitNamespaceArgs(args);
|
|
@@ -851,6 +846,13 @@ export function buildExecutionPlan(args, options) {
|
|
|
851
846
|
managedSessionName = options.freshSessionName;
|
|
852
847
|
sessionName = options.freshSessionName;
|
|
853
848
|
}
|
|
849
|
+
if (!compatibilityWorkaround
|
|
850
|
+
&& canUseHeadlessCompatibilityUserAgent(args)
|
|
851
|
+
&& options.managedSessionActive
|
|
852
|
+
&& sessionName
|
|
853
|
+
&& getAgentBrowserSessionIdentityKey(sessionName, namespace) === getAgentBrowserSessionIdentityKey(options.managedSessionName, options.managedSessionNamespace)) {
|
|
854
|
+
compatibilityWorkaround = options.managedSessionCompatibilityWorkaround;
|
|
855
|
+
}
|
|
854
856
|
if (compatibilityWorkaround) {
|
|
855
857
|
effectiveArgs.push("--user-agent", getDefaultHeadlessCompatUserAgent());
|
|
856
858
|
}
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -140,7 +140,7 @@ Practical policy:
|
|
|
140
140
|
- preserve the current branch-visible extension-managed session across `/reload`, exact-session relaunch, `/resume`, and Pi 0.79 `session_tree` branch transitions so persisted sessions can keep following the live browser after lifecycle changes
|
|
141
141
|
- close the active extension-managed session when the originating `pi` process quits, while leaving explicit caller-provided sessions alone
|
|
142
142
|
- set an idle timeout on extension-managed sessions as a backstop for abnormal exits or cleanup failures, and apply that same `AGENT_BROWSER_IDLE_TIMEOUT_MS` value to every upstream subprocess (including wrapper helper snapshots, tab lists, and navigation-summary reads) because changing the launch environment between calls can make upstream restart the background browser, discard the active tab, and invalidate fresh refs
|
|
143
|
-
- for wrapper-owned managed sessions only, also set a Git-checkout-generation-stable `AGENT_BROWSER_RESTORE` key on every compatible non-close upstream subprocess so cookies, localStorage, and sessionStorage autosave/restore across idle shutdowns and later Pi chats in the same checkout generation. The wrapper stores a UUID in the resolved Git admin directory and combines it with the checkout root and Git-admin directory filesystem identities: renames preserve the key, copied or path-replacement checkouts get a new key, non-Git directories fail closed, and cwd-only keys are not adopted. Policy lives in `extensions/agent-browser/lib/managed-session-restore.ts`; ownership is resolved by `resolveOwnedManagedSessionContext` (injected managed session, or explicit `--session` equal to the current managed name and namespace) and applied through `AsyncLocalStorage` `withOwnedManagedSessionContext` for prepare helpers plus main process/output, with typed `ownedManagedSession` process options for owned main/close spawns rather than an internal marker leaked into the child environment. `buildOwnedManagedSessionRestoreContext` sets call-scoped `restoreSuppressed` from main-plan argv so helper probes skip restore on incompatible plans without sticky-disabling when prepare returns early; sticky disable commits only after an owned-context subprocess actually starts with suppressed restore policy: POSIX commits on child `spawn`, while PowerShell-backed Windows commits after completion unless command-not-found stderr proves `agent-browser.cmd` never started. No-spawn preflight and missing-binary failures never commit an identity. Duplicate `--session` or `--namespace` flags are rejected, as are leading equals forms that upstream 0.33.2 does not recognize; global identity/config scanning follows upstream across the full argv rather than treating `--` as a sentinel. Native Windows command-first launcher adaptation relocates only valid leading global syntax, canonicalizes a valued optional `--restore <name>` to `--restore=<name>`, consumes only exact lowercase boolean literals, and leaves command-scoped, unknown, or unsupported equals-form input untouched so invalid calls cannot become valid browser activity. Namespace values are canonicalized with upstream's lowercase `sanitize_session_component` algorithm before ownership, sticky/page state, details, socket, or restore-directory identity comparisons; every wrapper-owned subprocess also pins that canonical namespace, including an empty default namespace, so parent environment cannot redirect helpers or close. Electron status target reads and current-managed probes acquire the same daemon-policy lock and owned restore context as ordinary commands for their underlying reads. Probe results then persist the same namespace plus top-level tab/ref state, keeping branch replay keyed to the probed identity. Ownership is typed rather than inferred from a name prefix, and `piab-*` live-session names are reserved: an explicit target is accepted only when it is the current/generated managed session or appears in this extension instance's ownership records. `session list` hides those rows, and the same reservation is rechecked at the final process boundary so another Pi process cannot attach to a managed authenticated browser through the shared per-user daemon socket. Skip when the caller already set restore/profile/state/CDP/provider/auto-connect/containment/session-name or a browser mutation surface (custom executable, extension, init script, raw launch args, proxy, plugin, WebGPU, or related engine/device controls) via argv or matching parent env, when the command is `connect`, when raw batch argv is used, when batch stdin contains nested `connect`/`batch`, or when `PI_AGENT_BROWSER_MANAGED_SESSION_RESTORE=0`. The wrapper's own ChatGPT headless user-agent compatibility injection is excluded from caller-mutation policy. A user-private immutable ticket-claim lock keyed by canonical namespace/session serializes this inspect-through-spawn decision across cooperating Pi processes; every contender publishes a unique claim, deterministic tickets elect one owner, and the winner also holds the legacy v2 path as a bridge. The bridge is transitional for pre-release branch processes and is scheduled for removal after v0.2.74 in [#93](https://github.com/fitchmultz/pi-agent-browser-native/issues/93). Live pre-update processes and their in-flight candidate gaps therefore block new acquisition; an abandoned v2 owner fails closed for manual repair, while current-protocol recovery removes only unique claims and artifacts with proven-dead PID/start identity. Waits are asynchronous and bounded. Every policy-lock winner re-runs `session info` even when this process previously recorded the applied/observed restore key, because another process can restart the same daemon identity between calls. That inspection uses a fixed bounded timeout independent of a caller's shorter `PI_AGENT_BROWSER_PROCESS_TIMEOUT_MS` override. Before an incompatible call, the wrapper reads `session info` for the actual same-identity daemon and fails before the requested spawn when that daemon retains any restore key or cannot be inspected. This covers restore-enabled daemons missing from transcript state after a crash and managed sessions launched with an explicit caller restore key; a confirmed inactive daemon remains reusable; a restore-disabled daemon is reusable only when this process recorded its expected null/custom restore policy after an owned spawn or successful policy match. After reload clears process-only provenance, an inactive old daemon may be restarted without restore and that started subprocess records a null daemon policy for its next follow-up. Same-process `session_tree` branch changes retain that process-owned provenance; a new extension instance after reload, restart, or `/resume` intentionally starts without it and fails closed on a still-live restore-disabled daemon even when the transcript restores sticky-disable state. Close the retained-key daemon first, use a fresh wrapper session, or choose a distinct explicit session. Once a managed session hits any allowed incompatible launch path, restore stays disabled for later bare follow-ups on that same session identity. Sticky identities live in the extension-owned `ManagedSessionRestoreState` instance, persist as `details.managedSessionRestoreDisabled`, and are replaced from current-branch rows during branch restore rather than stored in module-global process state. The opt-out returns before config/storage probes and sticky-records a successfully spawned identity as restore-disabled, allowing later calls to reuse that non-restore daemon without tripping the active restore-enabled conflict gate. This is env-based persistence, not a hidden argv relaunch. Upstream still owns restore file paths/modes under `~/.agent-browser/`; set `AGENT_BROWSER_ENCRYPTION_KEY` on multi-user hosts if plaintext session files are unacceptable. Any project `./agent-browser.json`, explicit `--config` / `AGENT_BROWSER_CONFIG`, or `~/.agent-browser/config.json` discovered while planning disables managed restore without reading caller-selected content in the Pi host; owned spawns sticky-disable that session identity. Each subprocess that receives the wrapper restore key, plus every wrapper-owned close, overrides config discovery with a process-private empty `AGENT_BROWSER_CONFIG` (`0400` on POSIX) inside the canonical marked `0700` secure-temp root, closing the check-to-spawn race without trusting project or user config while retaining normal shutdown cleanup and PID/start-identity stale-root recovery after abnormal exit on POSIX and native Windows; versioned Windows identities treat legacy cross-format markers as unknown instead of incorrectly proving PID reuse, and temp ownership marker schema v2 makes older readers ignore new-format markers. Spawn-time revalidation rejects changed checkout identity, restore storage, unpinned launch-mutator environment, foreign managed-session targets, or forbidden managed-state access before agent-browser starts; the same check runs again after protected-config and socket-directory awaits immediately adjacent to the synchronous spawn. A failed fresh command that started agent-browser triggers an exact-identity daemon probe; an active or uninspectable daemon remains current and wrapper-owned so shutdown cleanup can close it, while pre-aborted and missing-binary calls remain unowned. Wrapper-owned close commands canonicalize upstream argv to JSON plus the known namespace/session and `close`, discarding caller config/restore globals, and do not inject a newly derived restore key into an existing daemon, so checkout replacement cannot make old auth save under the replacement generation; the close path retains the observed wrapper key long enough to record the returned old-generation snapshot safely. Because upstream writes a snapshot per daemon session, a successful wrapper-owned close requests JSON output and persists only the returned state path as an atomic record in a lockless convergent per-key ownership directory beside the snapshots (`0700`, with `0600` records, on POSIX). Cleanup carries that ownership proof across Pi restarts, self-heals malformed or stale regular records without claiming their snapshots, uses immutable atomic record names plus rescan-after-delete convergence so concurrent closers cannot skip ownership recording or exceed the aggregate cap, removes proven snapshots older than 30 days for the exact restore key while retaining the two newest, 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, caps young close churn at 256 records per key, and never deletes matching unrecorded files or the current checkout key. Upstream restore files under `~/.agent-browser/` remain plaintext unless `AGENT_BROWSER_ENCRYPTION_KEY` is set; before automatic managed restore the wrapper requires a durable Git generation and absolute platform home root; it pins the planned encryption-key value after caller env merging; on POSIX it also resolves `HOME` once, validates owner-trusted non-writable ancestry plus stable device/inode/birth-time metadata for both checkout and Git-admin directories, and pins that canonical value, enforces mode `0700` without silently repairing unsafe existing paths, and rejects symlinks/non-directories along the exact `~/.agent-browser[/namespaces/<canonical>/state]/sessions` path and its `.tmp` transactional-write area, while Windows requires an absolute `USERPROFILE` and the documented 64-character hex encryption key because POSIX mode checks cannot verify profile ACLs; malformed keys fail closed on every platform. POSIX process-start probes use absolute `/bin/ps` then `/usr/bin/ps`; if neither is available, managed policy locking fails closed with an actionable validation message. Managed `piab-r2-*` keys and key-bearing paths are redacted from visible/structured/JSON transcript surfaces. Malformed oversized upstream output is discarded after parsing rather than copied into a persistent parse-failure spill, and raw parse-failure stdout is omitted from result details. `session list` and `state list` filter wrapper-managed rows, and the pre-spawn policy blocks foreign managed restore/state references, broad clear/clean operations, and managed save/rename targets while preserving targeted caller-owned state workflows.
|
|
143
|
+
- for wrapper-owned managed sessions only, also set a Git-checkout-generation-stable `AGENT_BROWSER_RESTORE` key on every compatible non-close upstream subprocess so cookies, localStorage, and sessionStorage autosave/restore across idle shutdowns and later Pi chats in the same checkout generation. The wrapper stores a UUID in the resolved Git admin directory and combines it with the checkout root and Git-admin directory filesystem identities: renames preserve the key, copied or path-replacement checkouts get a new key, non-Git directories fail closed, and cwd-only keys are not adopted. Policy lives in `extensions/agent-browser/lib/managed-session-restore.ts`; ownership is resolved by `resolveOwnedManagedSessionContext` (injected managed session, or explicit `--session` equal to the current managed name and namespace) and applied through `AsyncLocalStorage` `withOwnedManagedSessionContext` for prepare helpers plus main process/output, with typed `ownedManagedSession` process options for owned main/close spawns rather than an internal marker leaked into the child environment. `buildOwnedManagedSessionRestoreContext` sets call-scoped `restoreSuppressed` from main-plan argv so helper probes skip restore on incompatible plans without sticky-disabling when prepare returns early; sticky disable commits only after an owned-context subprocess actually starts with suppressed restore policy: POSIX commits on child `spawn`, while PowerShell-backed Windows commits after completion unless command-not-found stderr proves `agent-browser.cmd` never started. No-spawn preflight and missing-binary failures never commit an identity. Duplicate `--session` or `--namespace` flags are rejected, as are leading equals forms that upstream 0.33.2 does not recognize; global identity/config scanning follows upstream across the full argv rather than treating `--` as a sentinel. Native Windows command-first launcher adaptation relocates only valid leading global syntax, canonicalizes a valued optional `--restore <name>` to `--restore=<name>`, consumes only exact lowercase boolean literals, and leaves command-scoped, unknown, or unsupported equals-form input untouched so invalid calls cannot become valid browser activity. Namespace values are canonicalized with upstream's lowercase `sanitize_session_component` algorithm before ownership, sticky/page state, details, socket, or restore-directory identity comparisons; every wrapper-owned subprocess also pins that canonical namespace, including an empty default namespace, so parent environment cannot redirect helpers or close. Electron status target reads and current-managed probes acquire the same daemon-policy lock and owned restore context as ordinary commands for their underlying reads. Probe results then persist the same namespace plus top-level tab/ref state, keeping branch replay keyed to the probed identity. Ownership is typed rather than inferred from a name prefix, and `piab-*` live-session names are reserved: an explicit target is accepted only when it is the current/generated managed session or appears in this extension instance's ownership records. `session list` hides those rows, and the same reservation is rechecked at the final process boundary so another Pi process cannot attach to a managed authenticated browser through the shared per-user daemon socket. Skip when the caller already set restore/profile/state/CDP/provider/auto-connect/containment/session-name or a browser mutation surface (custom executable, extension, init script, raw launch args, proxy, plugin, WebGPU, or related engine/device controls) via argv or matching parent env, when the command is `connect`, when raw batch argv is used, when batch stdin contains nested `connect`/`batch`, or when `PI_AGENT_BROWSER_MANAGED_SESSION_RESTORE=0`. The wrapper's own site-specific headless user-agent compatibility injection is excluded from caller-mutation policy and remains pinned across follow-up subprocesses for the active managed session. For those owned sessions only, the process boundary also replaces the otherwise-empty safe `--args` value with a fixed, comma-safe Chrome `--user-agent=...` launch argument so new targets inherit the compatibility value; upstream's normal `--user-agent` flag applies only to the active CDP target. A user-private immutable ticket-claim lock keyed by canonical namespace/session serializes this inspect-through-spawn decision across cooperating Pi processes; every contender publishes a unique claim, deterministic tickets elect one owner, and the winner also holds the legacy v2 path as a bridge. The bridge is transitional for pre-release branch processes and is scheduled for removal after v0.2.74 in [#93](https://github.com/fitchmultz/pi-agent-browser-native/issues/93). Live pre-update processes and their in-flight candidate gaps therefore block new acquisition; an abandoned v2 owner fails closed for manual repair, while current-protocol recovery removes only unique claims and artifacts with proven-dead PID/start identity. Waits are asynchronous and bounded. Every policy-lock winner re-runs `session info` even when this process previously recorded the applied/observed restore key, because another process can restart the same daemon identity between calls. That inspection uses a fixed bounded timeout independent of a caller's shorter `PI_AGENT_BROWSER_PROCESS_TIMEOUT_MS` override. Before an incompatible call, the wrapper reads `session info` for the actual same-identity daemon and fails before the requested spawn when that daemon retains any restore key or cannot be inspected. This covers restore-enabled daemons missing from transcript state after a crash and managed sessions launched with an explicit caller restore key; a confirmed inactive daemon remains reusable; a restore-disabled daemon is reusable only when this process recorded its expected null/custom restore policy after an owned spawn or successful policy match. After reload clears process-only provenance, an inactive old daemon may be restarted without restore and that started subprocess records a null daemon policy for its next follow-up. Same-process `session_tree` branch changes retain that process-owned provenance; a new extension instance after reload, restart, or `/resume` intentionally starts without it and fails closed on a still-live restore-disabled daemon even when the transcript restores sticky-disable state. Close the retained-key daemon first, use a fresh wrapper session, or choose a distinct explicit session. Once a managed session hits any allowed incompatible launch path, restore stays disabled for later bare follow-ups on that same session identity. Sticky identities live in the extension-owned `ManagedSessionRestoreState` instance, persist as `details.managedSessionRestoreDisabled`, and are replaced from current-branch rows during branch restore rather than stored in module-global process state. The opt-out returns before config/storage probes and sticky-records a successfully spawned identity as restore-disabled, allowing later calls to reuse that non-restore daemon without tripping the active restore-enabled conflict gate. This is env-based persistence, not a hidden argv relaunch. Upstream still owns restore file paths/modes under `~/.agent-browser/`; set `AGENT_BROWSER_ENCRYPTION_KEY` on multi-user hosts if plaintext session files are unacceptable. Any project `./agent-browser.json`, explicit `--config` / `AGENT_BROWSER_CONFIG`, or `~/.agent-browser/config.json` discovered while planning disables managed restore without reading caller-selected content in the Pi host; owned spawns sticky-disable that session identity. Each subprocess that receives the wrapper restore key, plus every wrapper-owned close, overrides config discovery with a process-private empty `AGENT_BROWSER_CONFIG` (`0400` on POSIX) inside the canonical marked `0700` secure-temp root, closing the check-to-spawn race without trusting project or user config while retaining normal shutdown cleanup and PID/start-identity stale-root recovery after abnormal exit on POSIX and native Windows; versioned Windows identities treat legacy cross-format markers as unknown instead of incorrectly proving PID reuse, and temp ownership marker schema v2 makes older readers ignore new-format markers. Spawn-time revalidation rejects changed checkout identity, restore storage, unpinned launch-mutator environment, foreign managed-session targets, or forbidden managed-state access before agent-browser starts; the same check runs again after protected-config and socket-directory awaits immediately adjacent to the synchronous spawn. A failed fresh command that started agent-browser triggers an exact-identity daemon probe; an active or uninspectable daemon remains current and wrapper-owned so shutdown cleanup can close it, while pre-aborted and missing-binary calls remain unowned. Wrapper-owned close commands canonicalize upstream argv to JSON plus the known namespace/session and `close`, discarding caller config/restore globals, and do not inject a newly derived restore key into an existing daemon, so checkout replacement cannot make old auth save under the replacement generation; the close path retains the observed wrapper key long enough to record the returned old-generation snapshot safely. Because upstream writes a snapshot per daemon session, a successful wrapper-owned close requests JSON output and persists only the returned state path as an atomic record in a lockless convergent per-key ownership directory beside the snapshots (`0700`, with `0600` records, on POSIX). Cleanup carries that ownership proof across Pi restarts, self-heals malformed or stale regular records without claiming their snapshots, uses immutable atomic record names plus rescan-after-delete convergence so concurrent closers cannot skip ownership recording or exceed the aggregate cap, removes proven snapshots older than 30 days for the exact restore key while retaining the two newest, 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, caps young close churn at 256 records per key, and never deletes matching unrecorded files or the current checkout key. Upstream restore files under `~/.agent-browser/` remain plaintext unless `AGENT_BROWSER_ENCRYPTION_KEY` is set; before automatic managed restore the wrapper requires a durable Git generation and absolute platform home root; it pins the planned encryption-key value after caller env merging; on POSIX it also resolves `HOME` once, validates owner-trusted non-writable ancestry plus stable device/inode/birth-time metadata for both checkout and Git-admin directories, and pins that canonical value, enforces mode `0700` without silently repairing unsafe existing paths, and rejects symlinks/non-directories along the exact `~/.agent-browser[/namespaces/<canonical>/state]/sessions` path and its `.tmp` transactional-write area, while Windows requires an absolute `USERPROFILE` and the documented 64-character hex encryption key because POSIX mode checks cannot verify profile ACLs; malformed keys fail closed on every platform. POSIX process-start probes use absolute `/bin/ps` then `/usr/bin/ps`; if neither is available, managed policy locking fails closed with an actionable validation message. Managed `piab-r2-*` keys and key-bearing paths are redacted from visible/structured/JSON transcript surfaces. Malformed oversized upstream output is discarded after parsing rather than copied into a persistent parse-failure spill, and raw parse-failure stdout is omitted from result details. `session list` and `state list` filter wrapper-managed rows, and the pre-spawn policy blocks foreign managed restore/state references, broad clear/clean operations, and managed save/rename targets while preserving targeted caller-owned state workflows.
|
|
144
144
|
- clean up process-private temp spill artifacts on shutdown, but keep persisted-session snapshot spill files in a private session-scoped artifact directory with a bounded per-session budget so `details.fullOutputPath` stays usable after reload/resume without unbounded growth
|
|
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
|
|
@@ -170,7 +170,7 @@ The extension should surface that clearly and avoid hidden restart behavior in v
|
|
|
170
170
|
|
|
171
171
|
That means explicit startup-scoping flags like `--allowed-domains`, `--auto-connect`, `--cdp`, `--enable`, `--executable-path`, `--webgpu`, `--init-script`, `--device`, `--namespace`, `--profile`, `--provider`, `-p`, `--restore`, `--restore-save`, restore check flags, `--session-name`, and `--state` should remain explicit upstream argv choices instead of being wrapped in extra hidden restart or cloning logic. The one deliberate exception is the env-only managed-session `AGENT_BROWSER_RESTORE` key above, which does not inject `--restore` into argv and therefore does not trip launch-scoped `sessionMode: "fresh"` recovery.
|
|
172
172
|
|
|
173
|
-
The wrapper may still apply narrow compatibility normalizations when observed behavior justifies them and the result remains thin, local, and opt-out. For example,
|
|
173
|
+
The wrapper may still apply narrow compatibility normalizations when observed behavior justifies them and the result remains thin, local, and opt-out. For example, OpenAI web properties and `dash.cloudflare.com` reject the default local `HeadlessChrome` user agent while the same flow works with a normal Chrome UA, so the extension injects a domain-specific fallback only when the caller did not already choose raw Chrome arguments, a custom user agent, headed mode, CDP, auto-connect, a provider-backed launch, or a non-Chrome engine through argv or matching upstream environment. Managed sessions retain the injected value across helper and follow-up subprocesses, including branch reload/resume, because omitting it can make upstream relaunch the browser with a different launch identity. The process boundary also pins the same fixed compatibility value as a comma-safe Chrome launch argument for owned sessions, covering tabs and SSO popups that do not inherit upstream's per-page CDP override.
|
|
174
174
|
|
|
175
175
|
If the implicit session is already active and one of those startup-scoped flags appears again while `sessionMode` is still `"auto"`, the extension should fail clearly instead of silently sending a command shape that upstream would ignore.
|
|
176
176
|
|
package/docs/REQUIREMENTS.md
CHANGED
|
@@ -104,7 +104,7 @@ The design should comfortably support workflows such as:
|
|
|
104
104
|
- web research
|
|
105
105
|
- using browser UIs for other LLMs such as ChatGPT, Grok, Gemini, and Claude
|
|
106
106
|
- isolated authenticated browser sessions
|
|
107
|
-
- headless authenticated `chat.com` / ChatGPT / OpenAI browsing without forcing `--headed` or `--auto-connect`
|
|
107
|
+
- headless authenticated `chat.com` / ChatGPT / OpenAI and Cloudflare Dashboard browsing without forcing `--headed` or `--auto-connect`
|
|
108
108
|
- upstream profile/debug workflows without adding a local profile-cloning layer in this package
|
|
109
109
|
- provider-backed or iOS device launches where upstream owns credentials, env, and setup; the wrapper forwards argv and the parent environment without emulating those backends
|
|
110
110
|
- desktop Electron targets using top-level `electron` for discover → isolated launch → attach → probe/cleanup, or raw `args: ["connect", …]` when the operator launches the real app with a debug port for signed-in state (see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#electron) and [`COMMAND_REFERENCE.md`](COMMAND_REFERENCE.md#electron-desktop-apps))
|
package/docs/TOOL_CONTRACT.md
CHANGED
|
@@ -898,7 +898,7 @@ If `agent-browser` is not on `PATH`, fail with a message that:
|
|
|
898
898
|
- interactive or long-running upstream families such as `chat` without a prompt, `dashboard start`, `stream enable`, `trace start`, `profiler start`, `record start`, `inspect`, `install`, `upgrade`, `doctor --fix`, and `confirm-interactive` are passed through thinly but remain bounded by the same wrapper timeout/session planning rules; prefer explicit arguments, single-shot `chat <message>`, non-interactive flags like `doctor --offline --quick` or `doctor --json`, and cleanup pairs such as `dashboard stop`, `stream disable`, `trace stop`, `profiler stop`, and `record stop`
|
|
899
899
|
- treat successful plain-text inspection commands like `--help` and `--version` as stateless: do not inject the implicit managed session and do not let those calls claim the managed-session slot
|
|
900
900
|
- if startup-scoped flags like `--profile`, `--executable-path`, `--webgpu`, `--restore`, `--restore-save`, restore check flags, `--namespace`, `--session-name`, `--cdp`, `--state`, `--auto-connect`, `--init-script`, `--idle-timeout`, `--enable`, `-p` / `--provider`, or iOS `--device` are supplied after the implicit session is already active while `sessionMode` is `"auto"`, return a validation error with a structured recovery hint that recommends `sessionMode: "fresh"`
|
|
901
|
-
- for direct headless local Chrome launches to `chat.com` / `chatgpt.com` / `chat.openai.com`, allow a narrow compatibility fallback that injects a normal Chrome `--user-agent` only when the caller did not explicitly provide one and did not choose
|
|
901
|
+
- for direct headless local Chrome launches to `chat.com` / `chatgpt.com` / `chat.openai.com` or `dash.cloudflare.com`, allow a narrow compatibility fallback that injects a normal Chrome `--user-agent` only when the caller did not explicitly provide one and did not choose raw Chrome arguments, headed, CDP, auto-connect, provider-backed, custom-UA, or non-Chrome behavior through argv or matching upstream environment. Wrapper-managed sessions retain that wrapper-owned user agent across follow-up calls and branch reload/resume so upstream does not relaunch them with `HeadlessChrome`. The wrapper also pins a fixed, comma-safe Chrome launch argument for those sessions because upstream's per-page override does not propagate to new tabs or SSO popups.
|
|
902
902
|
|
|
903
903
|
## Non-goals
|
|
904
904
|
|
package/package.json
CHANGED