pi-agent-browser-native 0.2.72 → 0.2.74
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 +27 -0
- package/README.md +14 -12
- package/dist/extensions/agent-browser/index.js +104 -16
- package/dist/extensions/agent-browser/lib/argv-grammar.js +122 -0
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +11 -0
- package/dist/extensions/agent-browser/lib/electron/cdp.js +2 -2
- package/dist/extensions/agent-browser/lib/electron/launch.js +48 -12
- package/dist/extensions/agent-browser/lib/input-modes/params.js +96 -98
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +88 -2
- package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +22 -0
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +432 -0
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +367 -0
- package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +367 -0
- package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +589 -0
- package/dist/extensions/agent-browser/lib/managed-session-storage.js +299 -0
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +35 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +9 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +40 -22
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +15 -6
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +54 -33
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +182 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +625 -429
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +136 -56
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +28 -40
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +102 -19
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +13 -1
- package/dist/extensions/agent-browser/lib/playbook.js +9 -8
- package/dist/extensions/agent-browser/lib/process-identity.js +82 -0
- package/dist/extensions/agent-browser/lib/process.js +270 -34
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +5 -3
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +2 -1
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +35 -12
- package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +42 -0
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +7 -0
- package/dist/extensions/agent-browser/lib/runtime.js +85 -85
- package/dist/extensions/agent-browser/lib/session-page-state.js +48 -17
- package/dist/extensions/agent-browser/lib/temp.js +13 -25
- package/docs/ARCHITECTURE.md +9 -8
- package/docs/COMMAND_REFERENCE.md +43 -23
- package/docs/ELECTRON.md +10 -10
- package/docs/RELEASE.md +3 -2
- package/docs/SUPPORT_MATRIX.md +19 -18
- package/docs/TOOL_CONTRACT.md +28 -25
- package/docs/platform-smoke.md +2 -2
- package/package.json +1 -1
- package/platform-smoke.config.mjs +1 -1
- package/scripts/agent-browser-capability-baseline.mjs +11 -3
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Purpose: Build safe, deterministic agent-browser invocations and persisted session state for the pi-agent-browser extension.
|
|
3
3
|
* Responsibilities: Validate raw tool arguments, derive extension-managed session names from the pi session identity, restore managed-session state from persisted tool details, redact sensitive invocation text, classify browser-oriented prompts, and build the effective CLI argument list passed to the upstream agent-browser binary.
|
|
4
|
-
* Scope:
|
|
4
|
+
* Scope: Runtime-planning helpers only; no subprocess execution or filesystem access.
|
|
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
|
-
* and only injects wrapper-owned flags: `--json`, an extension-managed `--session` when appropriate,
|
|
7
|
+
* and only injects wrapper-owned flags: `--json`, an extension-managed `--session` when appropriate, the narrow
|
|
8
8
|
* OpenAI/ChatGPT 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 { GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE, PREVALIDATED_VALUE_FLAGS,
|
|
13
|
+
import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE, isBooleanFlagEnabled, PREVALIDATED_VALUE_FLAGS, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences, } from "./argv-grammar.js";
|
|
14
14
|
import { needsManagedSession } from "./command-policy.js";
|
|
15
|
+
import { isWrapperManagedSessionName, redactManagedSessionRestoreKeys } from "./managed-session-capabilities.js";
|
|
15
16
|
import { isCloseCommand, isOpenNavigationCommand } from "./command-taxonomy.js";
|
|
16
|
-
import { LAUNCH_SCOPED_FLAG_DEFINITIONS, LAUNCH_SCOPED_FLAG_LABEL } from "./launch-scoped-flags.js";
|
|
17
|
+
import { hasLaunchScopedFlagToken, LAUNCH_SCOPED_FLAG_DEFINITIONS, LAUNCH_SCOPED_FLAG_LABEL, } from "./launch-scoped-flags.js";
|
|
18
|
+
import { MANAGED_SESSION_NAME_PREFIX, } from "./managed-session-restore.js";
|
|
17
19
|
export { extractCommandTokens, findCommandStartIndex, parseArgvDescriptor, parseCommandInfo } from "./argv-descriptor.js";
|
|
18
20
|
import { isRecord } from "./parsing.js";
|
|
19
21
|
const OPENAI_HEADLESS_COMPAT_HOSTS = new Set(["chat.com", "chat.openai.com", "chatgpt.com"]);
|
|
@@ -228,7 +230,7 @@ function redactEnvSecretAssignments(text) {
|
|
|
228
230
|
});
|
|
229
231
|
}
|
|
230
232
|
export function redactSensitiveText(text) {
|
|
231
|
-
return redactEmbeddedStructuredText(redactEnvSecretAssignments(redactStandaloneBasicCredential(redactBearerCredentials(redactLooseUrlUserinfo(redactLooseUrlMatches(text)))
|
|
233
|
+
return redactEmbeddedStructuredText(redactEnvSecretAssignments(redactStandaloneBasicCredential(redactBearerCredentials(redactLooseUrlUserinfo(redactLooseUrlMatches(redactManagedSessionRestoreKeys(text))))
|
|
232
234
|
.replace(/\b(Authorization\s*:\s*Basic)\s+[^\s",]+/gi, "$1 [REDACTED]")
|
|
233
235
|
.replace(/\b(Cookie|Set-Cookie)\s*:\s*[^\n\r"]+/gi, "$1: [REDACTED]"))));
|
|
234
236
|
}
|
|
@@ -322,11 +324,30 @@ export function getImplicitSessionIdleTimeoutMs(env = process.env) {
|
|
|
322
324
|
parseTimeoutMs(env[AGENT_BROWSER_IDLE_TIMEOUT_ENV], 0) ??
|
|
323
325
|
DEFAULT_IMPLICIT_SESSION_IDLE_TIMEOUT_MS;
|
|
324
326
|
}
|
|
327
|
+
function countExplicitGlobalFlags(args, targetFlag) {
|
|
328
|
+
return scanUpstreamGlobalFlagOccurrences(args, targetFlag).length;
|
|
329
|
+
}
|
|
330
|
+
function getUnsupportedLeadingIdentityAssignment(args) {
|
|
331
|
+
const commandStartIndex = findCommandStartIndex(args) ?? args.length;
|
|
332
|
+
for (let index = 0; index < commandStartIndex; index += 1) {
|
|
333
|
+
const token = args[index];
|
|
334
|
+
if (token.startsWith("--session="))
|
|
335
|
+
return "--session";
|
|
336
|
+
if (token.startsWith("--namespace="))
|
|
337
|
+
return "--namespace";
|
|
338
|
+
const flag = token.split("=", 1)[0] ?? token;
|
|
339
|
+
if (!token.includes("=") && PREVALIDATED_VALUE_FLAGS.has(flag))
|
|
340
|
+
index += 1;
|
|
341
|
+
}
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
325
344
|
export function getImplicitSessionCloseTimeoutMs(env = process.env) {
|
|
326
345
|
return parseTimeoutMs(env[IMPLICIT_SESSION_CLOSE_TIMEOUT_ENV], 0) ?? DEFAULT_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS;
|
|
327
346
|
}
|
|
328
347
|
export function resolveManagedSessionState(options) {
|
|
329
|
-
const { command, managedSessionName,
|
|
348
|
+
const { command, managedSessionName, priorActive, priorSessionName, succeeded } = options;
|
|
349
|
+
const managedSessionNamespace = canonicalizeAgentBrowserNamespace(options.managedSessionNamespace);
|
|
350
|
+
const priorNamespace = canonicalizeAgentBrowserNamespace(options.priorNamespace);
|
|
330
351
|
if (!managedSessionName) {
|
|
331
352
|
return { active: priorActive, ...(priorNamespace ? { namespace: priorNamespace } : {}), sessionName: priorSessionName };
|
|
332
353
|
}
|
|
@@ -391,6 +412,7 @@ function getElectronCleanupClosedManagedSessionNames(details, fallbackSessionNam
|
|
|
391
412
|
return [...closedSessionNames];
|
|
392
413
|
}
|
|
393
414
|
export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName) {
|
|
415
|
+
const restoreDisabledIdentities = new Map();
|
|
394
416
|
let restoredState = {
|
|
395
417
|
active: false,
|
|
396
418
|
sessionName: fallbackSessionName,
|
|
@@ -400,6 +422,7 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
|
|
|
400
422
|
let freshSessionOrdinal = 0;
|
|
401
423
|
const freshSessionRanks = new Map();
|
|
402
424
|
const applyManagedClose = (sessionName, namespace) => {
|
|
425
|
+
namespace = canonicalizeAgentBrowserNamespace(namespace);
|
|
403
426
|
const restoreRank = getManagedSessionRestoreRank({
|
|
404
427
|
fallbackSessionName,
|
|
405
428
|
freshSessionRanks,
|
|
@@ -431,7 +454,7 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
|
|
|
431
454
|
}
|
|
432
455
|
const explicitSessionName = extractExplicitSessionName(args);
|
|
433
456
|
const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
|
|
434
|
-
const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
|
|
457
|
+
const namespace = canonicalizeAgentBrowserNamespace(typeof details.namespace === "string" ? details.namespace : undefined);
|
|
435
458
|
const sessionMode = details.sessionMode === "fresh" || details.sessionMode === "auto" ? details.sessionMode : undefined;
|
|
436
459
|
const usedImplicitSession = details.usedImplicitSession === true;
|
|
437
460
|
const command = typeof details.command === "string" ? details.command : parseCommandInfo(args).command;
|
|
@@ -447,6 +470,11 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
|
|
|
447
470
|
const explicitCloseSessionName = commandClosesSession && explicitSessionName && restorableDetailSessionName === explicitSessionName
|
|
448
471
|
? restorableDetailSessionName
|
|
449
472
|
: undefined;
|
|
473
|
+
// Sticky restore policy is session-identity state and must apply even for explicit
|
|
474
|
+
// `--session <current-managed>` rows that are not used for managed-session lifecycle replay.
|
|
475
|
+
if (details.managedSessionRestoreDisabled === true && typeof sessionName === "string") {
|
|
476
|
+
restoreDisabledIdentities.set(getAgentBrowserSessionIdentityKey(sessionName, namespace), { namespace, sessionName });
|
|
477
|
+
}
|
|
450
478
|
const managedSessionName = !explicitSessionName &&
|
|
451
479
|
restorableDetailSessionName &&
|
|
452
480
|
(usedImplicitSession || sessionMode === "fresh")
|
|
@@ -472,8 +500,10 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
|
|
|
472
500
|
const outcomeRepresentsActiveCurrentSession = outcomeActiveAfter && outcomeCurrentSessionName === managedSessionName && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged");
|
|
473
501
|
const succeeded = outcomeRepresentsActiveCurrentSession ? true : messageIsError === undefined ? exitCode === undefined || exitCode === 0 : !messageIsError;
|
|
474
502
|
if (commandClosesSession) {
|
|
475
|
-
if (succeeded)
|
|
503
|
+
if (succeeded) {
|
|
504
|
+
restoreDisabledIdentities.delete(getAgentBrowserSessionIdentityKey(managedSessionName, namespace));
|
|
476
505
|
applyManagedClose(managedSessionName, namespace);
|
|
506
|
+
}
|
|
477
507
|
continue;
|
|
478
508
|
}
|
|
479
509
|
const staleCompletion = succeeded && restoreRank < activeRestoreRank;
|
|
@@ -498,6 +528,7 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
|
|
|
498
528
|
...restoredState,
|
|
499
529
|
...(closedSessionName ? { closedSessionName } : {}),
|
|
500
530
|
freshSessionOrdinal,
|
|
531
|
+
managedSessionRestoreDisabledIdentities: [...restoreDisabledIdentities.values()],
|
|
501
532
|
};
|
|
502
533
|
}
|
|
503
534
|
export function createEphemeralSessionSeed() {
|
|
@@ -515,13 +546,13 @@ export function createImplicitSessionName(sessionId, cwd, ephemeralSeed) {
|
|
|
515
546
|
const cwdHash = createCwdHash(cwd);
|
|
516
547
|
const stableSessionId = sessionId?.replace(/-/g, "").slice(0, SESSION_NAME_SESSION_ID_LENGTH);
|
|
517
548
|
if (stableSessionId && stableSessionId.length > 0) {
|
|
518
|
-
return
|
|
549
|
+
return `${MANAGED_SESSION_NAME_PREFIX}${slug}-${stableSessionId}-${cwdHash}`;
|
|
519
550
|
}
|
|
520
551
|
const digest = createHash("sha256")
|
|
521
552
|
.update(`ephemeral:${cwd}:${ephemeralSeed}`)
|
|
522
553
|
.digest("hex")
|
|
523
554
|
.slice(0, SESSION_NAME_SESSION_ID_LENGTH);
|
|
524
|
-
return
|
|
555
|
+
return `${MANAGED_SESSION_NAME_PREFIX}${slug}-${digest}-${cwdHash}`;
|
|
525
556
|
}
|
|
526
557
|
export function createFreshSessionName(baseSessionName, ephemeralSeed, ordinal) {
|
|
527
558
|
const suffix = createHash("sha256")
|
|
@@ -623,21 +654,6 @@ function getFlagValue(args, flag) {
|
|
|
623
654
|
}
|
|
624
655
|
return undefined;
|
|
625
656
|
}
|
|
626
|
-
function isBooleanFlagEnabled(args, flag) {
|
|
627
|
-
for (const [index, token] of args.entries()) {
|
|
628
|
-
if (token === flag) {
|
|
629
|
-
const nextToken = args[index + 1]?.trim().toLowerCase();
|
|
630
|
-
if (nextToken === "false") {
|
|
631
|
-
return false;
|
|
632
|
-
}
|
|
633
|
-
return true;
|
|
634
|
-
}
|
|
635
|
-
if (token.startsWith(`${flag}=`)) {
|
|
636
|
-
return token.slice(flag.length + 1).trim().toLowerCase() !== "false";
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
return false;
|
|
640
|
-
}
|
|
641
657
|
function normalizeComparableUrl(url) {
|
|
642
658
|
const normalizedUrl = url.trim();
|
|
643
659
|
if (normalizedUrl.length === 0) {
|
|
@@ -716,57 +732,13 @@ function getCompatibilityWorkaround(args, commandInfo) {
|
|
|
716
732
|
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.",
|
|
717
733
|
};
|
|
718
734
|
}
|
|
719
|
-
export function extractExplicitSessionName(args) {
|
|
720
|
-
for (const [index, token] of args.entries()) {
|
|
721
|
-
if (token === "--session") {
|
|
722
|
-
return args[index + 1];
|
|
723
|
-
}
|
|
724
|
-
if (token.startsWith("--session=")) {
|
|
725
|
-
return token.slice("--session=".length);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
return undefined;
|
|
729
|
-
}
|
|
730
|
-
export function extractExplicitNamespace(args) {
|
|
731
|
-
for (const [index, token] of args.entries()) {
|
|
732
|
-
if (token === "--namespace") {
|
|
733
|
-
return args[index + 1];
|
|
734
|
-
}
|
|
735
|
-
if (token.startsWith("--namespace=")) {
|
|
736
|
-
return token.slice("--namespace=".length);
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
return undefined;
|
|
740
|
-
}
|
|
741
735
|
function stripExplicitNamespaceArgs(args) {
|
|
742
|
-
const
|
|
743
|
-
for (
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
index += 1;
|
|
747
|
-
continue;
|
|
748
|
-
}
|
|
749
|
-
if (token.startsWith("--namespace="))
|
|
750
|
-
continue;
|
|
751
|
-
stripped.push(token);
|
|
736
|
+
const namespaceTokenIndexes = new Set();
|
|
737
|
+
for (const occurrence of scanUpstreamGlobalFlagOccurrences(args, "--namespace")) {
|
|
738
|
+
namespaceTokenIndexes.add(occurrence.index);
|
|
739
|
+
namespaceTokenIndexes.add(occurrence.index + 1);
|
|
752
740
|
}
|
|
753
|
-
return
|
|
754
|
-
}
|
|
755
|
-
function hasLaunchScopedFlagToken(args, flag) {
|
|
756
|
-
const commandStartIndex = findCommandStartIndex(args);
|
|
757
|
-
const command = commandStartIndex === undefined ? undefined : args[commandStartIndex];
|
|
758
|
-
return args.some((token, index) => {
|
|
759
|
-
if (token !== flag && !token.startsWith(`${flag}=`))
|
|
760
|
-
return false;
|
|
761
|
-
if (flag === "--auto-connect")
|
|
762
|
-
return isBooleanFlagEnabled(args, flag);
|
|
763
|
-
if (flag === "--restore" && token === "--restore" && optionalGlobalValueFlagConsumesNext(flag, args[index + 1]))
|
|
764
|
-
return true;
|
|
765
|
-
if (flag === "--state" && command === "wait" && commandStartIndex !== undefined && index > commandStartIndex) {
|
|
766
|
-
return false;
|
|
767
|
-
}
|
|
768
|
-
return true;
|
|
769
|
-
});
|
|
741
|
+
return args.filter((_token, index) => !namespaceTokenIndexes.has(index));
|
|
770
742
|
}
|
|
771
743
|
export function getStartupScopedFlags(args) {
|
|
772
744
|
return LAUNCH_SCOPED_FLAG_DEFINITIONS
|
|
@@ -775,14 +747,27 @@ export function getStartupScopedFlags(args) {
|
|
|
775
747
|
}
|
|
776
748
|
export function buildExecutionPlan(args, options) {
|
|
777
749
|
const invalidValueFlag = getInvalidValueFlagDetails(args);
|
|
750
|
+
const unsupportedIdentityAssignment = getUnsupportedLeadingIdentityAssignment(args);
|
|
751
|
+
const explicitNamespacePresent = scanUpstreamGlobalFlagOccurrences(args, "--namespace").length > 0;
|
|
778
752
|
const explicitNamespace = extractExplicitNamespace(args);
|
|
779
|
-
const
|
|
753
|
+
const managedSessionNamespace = canonicalizeAgentBrowserNamespace(options.managedSessionNamespace);
|
|
754
|
+
const startupScopedFlags = getStartupScopedFlags(args).filter((flag) => !(flag === "--namespace" && explicitNamespacePresent && explicitNamespace === managedSessionNamespace));
|
|
780
755
|
const plainTextInspection = isPlainTextInspectionArgs(args);
|
|
781
756
|
const argvDescriptor = parseArgvDescriptor(args);
|
|
782
757
|
const commandInfo = argvDescriptor.commandInfo;
|
|
783
758
|
const commandNeedsManagedSession = !plainTextInspection && needsManagedSession(argvDescriptor);
|
|
784
759
|
const effectiveArgs = plainTextInspection ? [...args] : args.includes("--json") ? [] : ["--json"];
|
|
785
760
|
let namespace = explicitNamespace;
|
|
761
|
+
if (plainTextInspection) {
|
|
762
|
+
return {
|
|
763
|
+
commandInfo,
|
|
764
|
+
effectiveArgs,
|
|
765
|
+
namespace,
|
|
766
|
+
plainTextInspection,
|
|
767
|
+
startupScopedFlags,
|
|
768
|
+
usedImplicitSession: false,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
786
771
|
if (invalidValueFlag) {
|
|
787
772
|
return {
|
|
788
773
|
commandInfo: {},
|
|
@@ -794,22 +779,37 @@ export function buildExecutionPlan(args, options) {
|
|
|
794
779
|
validationError: formatInvalidValueFlagError(invalidValueFlag),
|
|
795
780
|
};
|
|
796
781
|
}
|
|
797
|
-
if (
|
|
782
|
+
if (unsupportedIdentityAssignment) {
|
|
798
783
|
return {
|
|
799
|
-
commandInfo,
|
|
784
|
+
commandInfo: {},
|
|
800
785
|
effectiveArgs,
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
startupScopedFlags,
|
|
786
|
+
plainTextInspection: false,
|
|
787
|
+
startupScopedFlags: [],
|
|
804
788
|
usedImplicitSession: false,
|
|
789
|
+
validationError: `${unsupportedIdentityAssignment}=... is not supported by agent-browser 0.33.2. Pass ${unsupportedIdentityAssignment} and its value as separate arguments.`,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
for (const flag of ["--session", "--namespace"]) {
|
|
793
|
+
if (countExplicitGlobalFlags(args, flag) <= 1)
|
|
794
|
+
continue;
|
|
795
|
+
return {
|
|
796
|
+
commandInfo: {},
|
|
797
|
+
effectiveArgs,
|
|
798
|
+
plainTextInspection: false,
|
|
799
|
+
startupScopedFlags: [],
|
|
800
|
+
usedImplicitSession: false,
|
|
801
|
+
validationError: `Multiple ${flag} flags are not supported. Pass a single ${flag} value; upstream uses the last occurrence while this wrapper would otherwise mis-attribute managed-session ownership.`,
|
|
805
802
|
};
|
|
806
803
|
}
|
|
807
804
|
const explicitSessionName = extractExplicitSessionName(args);
|
|
805
|
+
if (explicitSessionName && !isWrapperManagedSessionName(explicitSessionName)) {
|
|
806
|
+
namespace = resolveAgentBrowserNamespace(args, process.env.AGENT_BROWSER_NAMESPACE);
|
|
807
|
+
}
|
|
808
808
|
const shouldCreateFreshManagedSession = !explicitSessionName && options.sessionMode === "fresh" && commandInfo.command !== undefined && !isCloseCommand(commandInfo.command);
|
|
809
809
|
let argsToAppend = args;
|
|
810
810
|
const compatibilityWorkaround = getCompatibilityWorkaround(args, commandInfo);
|
|
811
|
-
if (explicitSessionName &&
|
|
812
|
-
effectiveArgs.push("--namespace", explicitNamespace);
|
|
811
|
+
if (explicitSessionName && explicitNamespacePresent) {
|
|
812
|
+
effectiveArgs.push("--namespace", explicitNamespace ?? "");
|
|
813
813
|
argsToAppend = stripExplicitNamespaceArgs(args);
|
|
814
814
|
}
|
|
815
815
|
let managedSessionName;
|
|
@@ -831,11 +831,11 @@ export function buildExecutionPlan(args, options) {
|
|
|
831
831
|
].join(" ");
|
|
832
832
|
}
|
|
833
833
|
else {
|
|
834
|
-
namespace = explicitNamespace
|
|
834
|
+
namespace = explicitNamespacePresent ? explicitNamespace : managedSessionNamespace;
|
|
835
835
|
if (namespace)
|
|
836
836
|
effectiveArgs.push("--namespace", namespace);
|
|
837
837
|
effectiveArgs.push("--session", options.managedSessionName);
|
|
838
|
-
if (
|
|
838
|
+
if (explicitNamespacePresent)
|
|
839
839
|
argsToAppend = stripExplicitNamespaceArgs(args);
|
|
840
840
|
managedSessionName = options.managedSessionName;
|
|
841
841
|
sessionName = options.managedSessionName;
|
|
@@ -846,7 +846,7 @@ export function buildExecutionPlan(args, options) {
|
|
|
846
846
|
if (namespace)
|
|
847
847
|
effectiveArgs.push("--namespace", namespace);
|
|
848
848
|
effectiveArgs.push("--session", options.freshSessionName);
|
|
849
|
-
if (
|
|
849
|
+
if (explicitNamespacePresent)
|
|
850
850
|
argsToAppend = stripExplicitNamespaceArgs(args);
|
|
851
851
|
managedSessionName = options.freshSessionName;
|
|
852
852
|
sessionName = options.freshSessionName;
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* Usage: `index.ts` creates one store per Pi session lifecycle and records observations through update tokens.
|
|
6
6
|
* Invariants/Assumptions: One tool-call update token must govern all page-state observations from that invocation; stale overlapping updates must not overwrite newer state.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { getAgentBrowserSessionIdentityKey } from "./argv-grammar.js";
|
|
9
|
+
import { isCloseCommand, isReadOnlyDiagnosticSessionTargetCommand, isUnverifiedPageTransitionCommand } from "./command-taxonomy.js";
|
|
9
10
|
import { isRecord } from "./parsing.js";
|
|
10
11
|
import { getEditableRefEvidence } from "./results/editable-ref-evidence.js";
|
|
11
12
|
import { enrichSnapshotRefEntries, getSnapshotRefEntries } from "./results/snapshot-refs.js";
|
|
@@ -82,6 +83,9 @@ function extractBatchResultCommand(item) {
|
|
|
82
83
|
}
|
|
83
84
|
export function extractSessionTabTargetFromCommandData(commandTokens, data) {
|
|
84
85
|
const [command, subcommand] = commandTokens;
|
|
86
|
+
if (command === "get" && subcommand === "url") {
|
|
87
|
+
return normalizeSessionTabTarget({ url: extractStringResultField(data, "url") ?? extractStringResultField(data, "result") });
|
|
88
|
+
}
|
|
85
89
|
return isReadOnlyDiagnosticSessionTargetCommand(command, subcommand) ? undefined : extractSessionTabTargetFromData(data);
|
|
86
90
|
}
|
|
87
91
|
export function extractSessionTabTargetFromBatchResults(data) {
|
|
@@ -124,10 +128,12 @@ export function deriveSessionTabTarget(options) {
|
|
|
124
128
|
const commandDataTarget = isReadOnlyDiagnosticSessionTargetCommand(options.command, options.subcommand)
|
|
125
129
|
? undefined
|
|
126
130
|
: extractSessionTabTargetFromData(options.data);
|
|
127
|
-
|
|
128
|
-
extractSessionTabTargetFromBatchResults(options.data)
|
|
129
|
-
commandDataTarget
|
|
130
|
-
|
|
131
|
+
const observedTarget = normalizeSessionTabTarget(options.navigationSummary)
|
|
132
|
+
?? extractSessionTabTargetFromBatchResults(options.data)
|
|
133
|
+
?? commandDataTarget;
|
|
134
|
+
if (observedTarget || !isUnverifiedPageTransitionCommand(options.command, options.subcommand))
|
|
135
|
+
return observedTarget ?? options.previousTarget;
|
|
136
|
+
return undefined;
|
|
131
137
|
}
|
|
132
138
|
function batchContainsOnlyReadOnlyDiagnosticTargets(data) {
|
|
133
139
|
if (!Array.isArray(data) || data.length === 0) {
|
|
@@ -263,11 +269,12 @@ function getRestoredRefSnapshot(details) {
|
|
|
263
269
|
: undefined,
|
|
264
270
|
};
|
|
265
271
|
}
|
|
266
|
-
function getLatestTabTargetOrder(targets) {
|
|
272
|
+
function getLatestTabTargetOrder(targets, unknownTargets) {
|
|
267
273
|
let latestOrder = 0;
|
|
268
|
-
for (const target of targets.values())
|
|
274
|
+
for (const target of targets.values())
|
|
269
275
|
latestOrder = Math.max(latestOrder, target.order);
|
|
270
|
-
|
|
276
|
+
for (const order of unknownTargets.values())
|
|
277
|
+
latestOrder = Math.max(latestOrder, order);
|
|
271
278
|
return latestOrder;
|
|
272
279
|
}
|
|
273
280
|
function getLatestRefStateOrder(snapshots, invalidations) {
|
|
@@ -278,8 +285,8 @@ function getLatestRefStateOrder(snapshots, invalidations) {
|
|
|
278
285
|
latestOrder = Math.max(latestOrder, invalidation.order);
|
|
279
286
|
return latestOrder;
|
|
280
287
|
}
|
|
281
|
-
function shouldApplyTabTargetUpdate(current, updateOrder) {
|
|
282
|
-
return
|
|
288
|
+
function shouldApplyTabTargetUpdate(current, unknownOrder, updateOrder) {
|
|
289
|
+
return updateOrder >= Math.max(current?.order ?? 0, unknownOrder ?? 0);
|
|
283
290
|
}
|
|
284
291
|
function shouldApplyRefStateUpdate(options) {
|
|
285
292
|
const currentOrder = Math.max(options.currentSnapshot?.order ?? 0, options.currentInvalidation?.order ?? 0);
|
|
@@ -292,14 +299,13 @@ function stripRefSnapshotInvalidationOrder(invalidation) {
|
|
|
292
299
|
return invalidation ? { reason: invalidation.reason, summary: invalidation.summary } : undefined;
|
|
293
300
|
}
|
|
294
301
|
export function getSessionPageStateKey(sessionName, namespace) {
|
|
295
|
-
|
|
296
|
-
return undefined;
|
|
297
|
-
return namespace ? `${namespace}\u0000${sessionName}` : sessionName;
|
|
302
|
+
return sessionName ? getAgentBrowserSessionIdentityKey(sessionName, namespace) : undefined;
|
|
298
303
|
}
|
|
299
304
|
export class SessionPageState {
|
|
300
305
|
refSnapshotInvalidations = new Map();
|
|
301
306
|
refSnapshots = new Map();
|
|
302
307
|
tabPinningReasons = new Map();
|
|
308
|
+
tabTargetUnknownOrders = new Map();
|
|
303
309
|
tabTargets = new Map();
|
|
304
310
|
updateOrder = 0;
|
|
305
311
|
static fromBranch(branch) {
|
|
@@ -327,13 +333,23 @@ export class SessionPageState {
|
|
|
327
333
|
continue;
|
|
328
334
|
}
|
|
329
335
|
const tabTarget = getRestoredSessionTabTarget(details, command, subcommand);
|
|
336
|
+
const tabTargetUnknown = details.sessionTabTargetUnknown === true;
|
|
330
337
|
const refSnapshotInvalidation = getRestoredRefSnapshotInvalidation(details, command);
|
|
331
338
|
const refSnapshot = refSnapshotInvalidation ? undefined : getRestoredRefSnapshot(details);
|
|
332
|
-
if (!tabTarget && !refSnapshotInvalidation && !refSnapshot)
|
|
339
|
+
if (!tabTarget && !tabTargetUnknown && !refSnapshotInvalidation && !refSnapshot)
|
|
333
340
|
continue;
|
|
334
341
|
restoredOrder += 1;
|
|
335
|
-
if (
|
|
342
|
+
if (tabTargetUnknown) {
|
|
343
|
+
state.refSnapshotInvalidations.delete(sessionKey);
|
|
344
|
+
state.refSnapshots.delete(sessionKey);
|
|
345
|
+
state.tabTargets.delete(sessionKey);
|
|
346
|
+
state.tabTargetUnknownOrders.set(sessionKey, restoredOrder);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (tabTarget) {
|
|
350
|
+
state.tabTargetUnknownOrders.delete(sessionKey);
|
|
336
351
|
state.tabTargets.set(sessionKey, { order: restoredOrder, target: tabTarget });
|
|
352
|
+
}
|
|
337
353
|
if (refSnapshotInvalidation) {
|
|
338
354
|
state.refSnapshots.delete(sessionKey);
|
|
339
355
|
state.refSnapshotInvalidations.set(sessionKey, { ...refSnapshotInvalidation, order: restoredOrder });
|
|
@@ -343,7 +359,7 @@ export class SessionPageState {
|
|
|
343
359
|
state.refSnapshots.set(sessionKey, { ...refSnapshot, order: restoredOrder });
|
|
344
360
|
}
|
|
345
361
|
}
|
|
346
|
-
state.updateOrder = Math.max(restoredOrder, getLatestTabTargetOrder(state.tabTargets), getLatestRefStateOrder(state.refSnapshots, state.refSnapshotInvalidations));
|
|
362
|
+
state.updateOrder = Math.max(restoredOrder, getLatestTabTargetOrder(state.tabTargets, state.tabTargetUnknownOrders), getLatestRefStateOrder(state.refSnapshots, state.refSnapshotInvalidations));
|
|
347
363
|
state.tabPinningReasons = new Map([...state.tabTargets.keys()].map((sessionName) => [sessionName, "restore"]));
|
|
348
364
|
return state;
|
|
349
365
|
}
|
|
@@ -355,6 +371,7 @@ export class SessionPageState {
|
|
|
355
371
|
this.refSnapshotInvalidations = new Map();
|
|
356
372
|
this.refSnapshots = new Map();
|
|
357
373
|
this.tabPinningReasons = new Map();
|
|
374
|
+
this.tabTargetUnknownOrders = new Map();
|
|
358
375
|
this.tabTargets = new Map();
|
|
359
376
|
this.updateOrder = 0;
|
|
360
377
|
}
|
|
@@ -365,14 +382,16 @@ export class SessionPageState {
|
|
|
365
382
|
pinningReason: this.tabPinningReasons.get(sessionName),
|
|
366
383
|
refSnapshot: stripRefSnapshotOrder(this.refSnapshots.get(sessionName)),
|
|
367
384
|
refSnapshotInvalidation: stripRefSnapshotInvalidationOrder(this.refSnapshotInvalidations.get(sessionName)),
|
|
385
|
+
...(this.tabTargetUnknownOrders.has(sessionName) ? { tabTargetUnknown: true } : {}),
|
|
368
386
|
tabTarget: this.tabTargets.get(sessionName)?.target,
|
|
369
387
|
};
|
|
370
388
|
}
|
|
371
389
|
applyTabTarget(options) {
|
|
372
390
|
const current = this.tabTargets.get(options.sessionName);
|
|
373
|
-
if (!shouldApplyTabTargetUpdate(current, options.update)) {
|
|
391
|
+
if (!shouldApplyTabTargetUpdate(current, this.tabTargetUnknownOrders.get(options.sessionName), options.update)) {
|
|
374
392
|
return { ...this.get(options.sessionName), applied: false, stale: true };
|
|
375
393
|
}
|
|
394
|
+
this.tabTargetUnknownOrders.delete(options.sessionName);
|
|
376
395
|
this.tabTargets.set(options.sessionName, { order: options.update, target: options.target });
|
|
377
396
|
return { ...this.get(options.sessionName), applied: true };
|
|
378
397
|
}
|
|
@@ -401,10 +420,22 @@ export class SessionPageState {
|
|
|
401
420
|
this.refSnapshotInvalidations.set(options.sessionName, { ...options.invalidation, order: options.update });
|
|
402
421
|
return { ...this.get(options.sessionName), applied: true };
|
|
403
422
|
}
|
|
423
|
+
markTabTargetUnknown(options) {
|
|
424
|
+
const current = this.tabTargets.get(options.sessionName);
|
|
425
|
+
if (!shouldApplyTabTargetUpdate(current, this.tabTargetUnknownOrders.get(options.sessionName), options.update))
|
|
426
|
+
return { ...this.get(options.sessionName), applied: false, stale: true };
|
|
427
|
+
this.refSnapshotInvalidations.delete(options.sessionName);
|
|
428
|
+
this.refSnapshots.delete(options.sessionName);
|
|
429
|
+
this.tabPinningReasons.delete(options.sessionName);
|
|
430
|
+
this.tabTargets.delete(options.sessionName);
|
|
431
|
+
this.tabTargetUnknownOrders.set(options.sessionName, options.update);
|
|
432
|
+
return { ...this.get(options.sessionName), applied: true };
|
|
433
|
+
}
|
|
404
434
|
clearSession(sessionName) {
|
|
405
435
|
this.refSnapshotInvalidations.delete(sessionName);
|
|
406
436
|
this.refSnapshots.delete(sessionName);
|
|
407
437
|
this.tabPinningReasons.delete(sessionName);
|
|
438
|
+
this.tabTargetUnknownOrders.delete(sessionName);
|
|
408
439
|
this.tabTargets.delete(sessionName);
|
|
409
440
|
}
|
|
410
441
|
markPinning(sessionName, reason) {
|
|
@@ -5,26 +5,24 @@
|
|
|
5
5
|
* Usage: Imported by result/process helpers when they need secure spill files instead of world-readable shared tmp paths.
|
|
6
6
|
* Invariants/Assumptions: Temp artifacts live under the OS temp directory, each active run uses a dedicated 0700 directory, files are created with exclusive 0600 permissions, session-scoped persisted artifacts stay under the pi session directory, and stale pruning only touches roots with an explicit pi-agent-browser ownership marker.
|
|
7
7
|
*/
|
|
8
|
-
import { execFile } from "node:child_process";
|
|
9
8
|
import { randomBytes } from "node:crypto";
|
|
10
9
|
import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
11
10
|
import { chmod, mkdir, mkdtemp, open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
12
11
|
import { tmpdir } from "node:os";
|
|
13
12
|
import { basename, dirname, join, resolve } from "node:path";
|
|
14
|
-
import { promisify } from "node:util";
|
|
15
13
|
import { isRecord, parsePositiveInteger } from "./parsing.js";
|
|
14
|
+
import { processStartIdentitiesMatch, readProcessStartIdentity } from "./process-identity.js";
|
|
16
15
|
const TEMP_ROOT_PREFIX = "pi-agent-browser-";
|
|
17
16
|
const TEMP_ROOT_MARKER_FILE_NAME = ".pi-agent-browser-owner.json";
|
|
18
17
|
const TEMP_ROOT_MARKER_KIND = "pi-agent-browser-temp-root";
|
|
19
|
-
const
|
|
18
|
+
const TEMP_ROOT_LEGACY_MARKER_VERSION = 1;
|
|
19
|
+
const TEMP_ROOT_MARKER_VERSION = 2;
|
|
20
20
|
const STALE_TEMP_ROOT_MAX_AGE_MS = 24 * 60 * 60 * 1_000;
|
|
21
21
|
const TEMP_ROOT_MAX_BYTES_ENV = "PI_AGENT_BROWSER_TEMP_ROOT_MAX_BYTES";
|
|
22
22
|
const DEFAULT_TEMP_ROOT_MAX_BYTES = 32 * 1_024 * 1_024;
|
|
23
23
|
const SESSION_ARTIFACT_MAX_BYTES_ENV = "PI_AGENT_BROWSER_SESSION_ARTIFACT_MAX_BYTES";
|
|
24
24
|
const DEFAULT_SESSION_ARTIFACT_MAX_BYTES = 32 * 1_024 * 1_024;
|
|
25
25
|
const SESSION_ARTIFACTS_ROOT_DIR_NAME = ".pi-agent-browser-artifacts";
|
|
26
|
-
const PROCESS_START_IDENTITY_TIMEOUT_MS = 1_000;
|
|
27
|
-
const execFileAsync = promisify(execFile);
|
|
28
26
|
let sessionTempRootPromise;
|
|
29
27
|
let exitCleanupRegistered = false;
|
|
30
28
|
let tempMutationQueue = Promise.resolve();
|
|
@@ -48,7 +46,7 @@ function isProtectedTempChildName(value) {
|
|
|
48
46
|
function isTempRootOwnershipRecord(value) {
|
|
49
47
|
if (!isRecord(value))
|
|
50
48
|
return false;
|
|
51
|
-
if (value.kind !== TEMP_ROOT_MARKER_KIND || value.version
|
|
49
|
+
if (value.kind !== TEMP_ROOT_MARKER_KIND || ![TEMP_ROOT_LEGACY_MARKER_VERSION, TEMP_ROOT_MARKER_VERSION].includes(value.version))
|
|
52
50
|
return false;
|
|
53
51
|
if (!isPositiveFiniteNumber(value.createdAtMs))
|
|
54
52
|
return false;
|
|
@@ -153,6 +151,7 @@ async function persistProtectedTempChildren(tempRoot, protectedChildren) {
|
|
|
153
151
|
...ownershipMarker,
|
|
154
152
|
leaseUpdatedAtMs: Date.now(),
|
|
155
153
|
protectedChildNames: childNames,
|
|
154
|
+
version: TEMP_ROOT_MARKER_VERSION,
|
|
156
155
|
});
|
|
157
156
|
}
|
|
158
157
|
async function getExistingProtectedChildren(tempRoot, protectedChildren) {
|
|
@@ -180,20 +179,7 @@ async function removeTempRootChildrenExcept(tempRoot, protectedChildren) {
|
|
|
180
179
|
}));
|
|
181
180
|
}
|
|
182
181
|
async function getProcessStartIdentity(pid) {
|
|
183
|
-
|
|
184
|
-
return undefined;
|
|
185
|
-
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
186
|
-
return undefined;
|
|
187
|
-
try {
|
|
188
|
-
const { stdout } = await execFileAsync("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
189
|
-
timeout: PROCESS_START_IDENTITY_TIMEOUT_MS,
|
|
190
|
-
});
|
|
191
|
-
const identity = stdout.trim().replace(/\s+/g, " ");
|
|
192
|
-
return identity || undefined;
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
195
|
-
return undefined;
|
|
196
|
-
}
|
|
182
|
+
return pid === undefined ? undefined : await readProcessStartIdentity(pid);
|
|
197
183
|
}
|
|
198
184
|
export async function writeSecureTempRootOwnershipMarker(tempRoot, options = {}) {
|
|
199
185
|
const createdAtMs = options.createdAtMs ?? Date.now();
|
|
@@ -219,10 +205,10 @@ async function refreshSecureTempRootLease(tempRoot) {
|
|
|
219
205
|
if (currentUid !== undefined && ownershipMarker.ownerUid !== undefined && ownershipMarker.ownerUid !== currentUid)
|
|
220
206
|
return;
|
|
221
207
|
const currentProcessStartIdentity = await getProcessStartIdentity(process.pid);
|
|
222
|
-
if (ownershipMarker.ownerProcessStartIdentity !== undefined &&
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
208
|
+
if (ownershipMarker.ownerProcessStartIdentity !== undefined && currentProcessStartIdentity !== undefined) {
|
|
209
|
+
const identitiesMatch = processStartIdentitiesMatch(ownershipMarker.ownerProcessStartIdentity, currentProcessStartIdentity);
|
|
210
|
+
if (identitiesMatch === false)
|
|
211
|
+
return;
|
|
226
212
|
}
|
|
227
213
|
const refreshedMarker = {
|
|
228
214
|
...ownershipMarker,
|
|
@@ -230,6 +216,7 @@ async function refreshSecureTempRootLease(tempRoot) {
|
|
|
230
216
|
ownerPid: process.pid,
|
|
231
217
|
ownerProcessStartIdentity: currentProcessStartIdentity ?? ownershipMarker.ownerProcessStartIdentity,
|
|
232
218
|
ownerUid: currentUid,
|
|
219
|
+
version: TEMP_ROOT_MARKER_VERSION,
|
|
233
220
|
};
|
|
234
221
|
await writeTempRootOwnershipMarkerRecord(tempRoot, refreshedMarker);
|
|
235
222
|
}
|
|
@@ -251,7 +238,8 @@ async function getMarkerOwnerLiveness(ownershipMarker) {
|
|
|
251
238
|
if (ownershipMarker.ownerProcessStartIdentity === undefined || currentProcessStartIdentity === undefined) {
|
|
252
239
|
return "unknown";
|
|
253
240
|
}
|
|
254
|
-
|
|
241
|
+
const identitiesMatch = processStartIdentitiesMatch(ownershipMarker.ownerProcessStartIdentity, currentProcessStartIdentity);
|
|
242
|
+
return identitiesMatch === undefined ? "unknown" : identitiesMatch ? "alive" : "dead";
|
|
255
243
|
}
|
|
256
244
|
async function pruneStaleTempRoots(currentTempRoot) {
|
|
257
245
|
const entries = await readdir(tmpdir(), { withFileTypes: true }).catch(() => []);
|