pi-agent-browser-native 0.3.0 → 0.5.0
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 +127 -0
- package/README.md +63 -20
- package/dist/extensions/agent-browser/index.js +787 -105
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
- package/dist/extensions/agent-browser/lib/argv-grammar.js +44 -2
- package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
- package/dist/extensions/agent-browser/lib/command-policy.js +1 -1
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -2
- package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -4
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
- package/dist/extensions/agent-browser/lib/input-modes/params.js +22 -23
- package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
- package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +8 -0
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -1
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -36
- package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
- package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +47 -29
- package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -24
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +26 -25
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +17 -3
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -4
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +129 -32
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +191 -55
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +57 -29
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +15 -11
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
- package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
- package/dist/extensions/agent-browser/lib/playbook.js +18 -15
- package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
- package/dist/extensions/agent-browser/lib/process-identity.js +4 -4
- package/dist/extensions/agent-browser/lib/process.js +131 -41
- package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +62 -5
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
- package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
- package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +66 -11
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -0
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -1
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -11
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
- package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
- package/dist/extensions/agent-browser/lib/results/presentation.js +6 -3
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -0
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -8
- package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
- package/dist/extensions/agent-browser/lib/runtime.js +116 -39
- package/dist/extensions/agent-browser/lib/session-page-state.js +62 -10
- package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
- package/dist/extensions/agent-browser/script-worker.js +169 -0
- package/dist/scripts/agent-browser-target.mjs +3 -0
- package/docs/ARCHITECTURE.md +40 -21
- package/docs/COMMAND_REFERENCE.md +90 -35
- package/docs/RELEASE.md +3 -3
- package/docs/REQUIREMENTS.md +4 -2
- package/docs/SUPPORT_MATRIX.md +26 -19
- package/docs/TOOL_CONTRACT.md +93 -52
- package/package.json +3 -1
- package/platform-smoke.config.mjs +2 -2
- package/scripts/agent-browser-capability-baseline.mjs +24 -6
- package/scripts/agent-browser-target.mjs +3 -0
- package/scripts/build.mjs +41 -0
- package/scripts/doctor.mjs +7 -6
- package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
- package/scripts/platform-smoke/targets.mjs +12 -6
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { chmodSync, lstatSync, readFileSync, unlinkSync } from "node:fs";
|
|
3
|
-
import {
|
|
4
|
-
import { extractCommandTokens, parseCommandInfo } from "./argv-descriptor.js";
|
|
3
|
+
import { extractUpstreamCommandTokens, parseCommandInfo } from "./argv-descriptor.js";
|
|
5
4
|
import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, extractRequestedRestoreKey, getAgentBrowserSessionIdentityKey, isUpstreamEnvFlagEnabled, scanUpstreamGlobalFlagOccurrences, } from "./argv-grammar.js";
|
|
6
5
|
import { hasLaunchScopedFlagToken, MANAGED_RESTORE_INCOMPATIBLE_BOOLEAN_ENVS, MANAGED_RESTORE_INCOMPATIBLE_ENVS, MANAGED_RESTORE_INCOMPATIBLE_FLAGS, } from "./launch-scoped-flags.js";
|
|
7
|
-
import { createManagedSessionRestoreKey, ensureManagedSessionRestoreStorageIsSecure, getManagedSessionRestoreProtectedStorageEnv, hasManagedSessionRestoreProjectIdentity, resolveManagedSessionRestoreHome, } from "./managed-session-storage.js";
|
|
6
|
+
import { createManagedSessionRestoreKey, ensureManagedSessionRestoreStorageIsSecure, getManagedSessionRestoreScope, getManagedSessionRestoreProtectedStorageEnv, hasManagedSessionRestoreProjectIdentity, resolveManagedSessionRestoreHome, } from "./managed-session-storage.js";
|
|
8
7
|
import { parseUserBatchStdin } from "./orchestration/batch-stdin.js";
|
|
8
|
+
import { getAgentBrowserProcessEnvironment } from "./process-environment.js";
|
|
9
9
|
import { writeSecureTempFile } from "./temp.js";
|
|
10
|
-
export { createManagedSessionRestoreKey, ensureManagedSessionRestoreStorageIsSecure } from "./managed-session-storage.js";
|
|
10
|
+
export { createManagedSessionRestoreKey, ensureManagedSessionRestoreStorageIsSecure, getManagedSessionRestoreScope } from "./managed-session-storage.js";
|
|
11
11
|
export { pruneOwnedManagedSessionRestoreSnapshots } from "./managed-session-snapshots.js";
|
|
12
12
|
const AGENT_BROWSER_CONFIG_ENV = "AGENT_BROWSER_CONFIG";
|
|
13
13
|
const AGENT_BROWSER_RESTORE_ENV = "AGENT_BROWSER_RESTORE";
|
|
@@ -61,7 +61,7 @@ export class ManagedSessionRestoreState {
|
|
|
61
61
|
if (sessionName)
|
|
62
62
|
this.#daemonRestoreKeys.set(getAgentBrowserSessionIdentityKey(sessionName, namespace), restoreKey);
|
|
63
63
|
}
|
|
64
|
-
replace(identities, options = {}) {
|
|
64
|
+
replace(identities = [], options = {}) {
|
|
65
65
|
if (!options.preserveDaemonRestoreKeys)
|
|
66
66
|
this.#daemonRestoreKeys.clear();
|
|
67
67
|
this.#disabled.clear();
|
|
@@ -73,6 +73,9 @@ const ownedManagedSessionStorage = new AsyncLocalStorage();
|
|
|
73
73
|
export async function withOwnedManagedSessionContext(context, run) {
|
|
74
74
|
return await ownedManagedSessionStorage.run(context, run);
|
|
75
75
|
}
|
|
76
|
+
export function getOwnedManagedSessionRestoreKey() {
|
|
77
|
+
return ownedManagedSessionStorage.getStore()?.restoreKey;
|
|
78
|
+
}
|
|
76
79
|
export function resolveOwnedManagedSessionContext(options) {
|
|
77
80
|
const namespace = canonicalizeAgentBrowserNamespace(options.namespace);
|
|
78
81
|
const currentNamespace = canonicalizeAgentBrowserNamespace(options.currentManagedSessionNamespace);
|
|
@@ -96,33 +99,18 @@ function ownedContextMatches(sessionName, namespace) {
|
|
|
96
99
|
export function isOwnedManagedSessionTarget(args) {
|
|
97
100
|
return ownedContextMatches(extractExplicitSessionName(args), extractExplicitNamespace(args)) !== undefined;
|
|
98
101
|
}
|
|
99
|
-
function pathExistsOrIsUnreadable(path) {
|
|
100
|
-
try {
|
|
101
|
-
lstatSync(path);
|
|
102
|
-
return true;
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
return error.code !== "ENOENT";
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
102
|
function hasExplicitConfigArg(args) {
|
|
109
103
|
return scanUpstreamGlobalFlagOccurrences(args, "--config").length > 0;
|
|
110
104
|
}
|
|
111
105
|
function closesBrowserSession(args) {
|
|
112
106
|
return ["close", "exit", "quit"].includes(parseCommandInfo(args).command ?? "");
|
|
113
107
|
}
|
|
114
|
-
export function
|
|
115
|
-
|
|
116
|
-
return true;
|
|
117
|
-
const paths = [join(cwd, "agent-browser.json")];
|
|
118
|
-
const home = resolveManagedSessionRestoreHome(parentEnv, platform);
|
|
119
|
-
if (home)
|
|
120
|
-
paths.push(join(home, ".agent-browser", "config.json"));
|
|
121
|
-
return paths.some(pathExistsOrIsUnreadable);
|
|
108
|
+
export function agentBrowserExplicitConfigIsPresent(parentEnv = getAgentBrowserProcessEnvironment(), args = []) {
|
|
109
|
+
return hasExplicitConfigArg(args) || hasUpstreamEnvValue(parentEnv, AGENT_BROWSER_CONFIG_ENV);
|
|
122
110
|
}
|
|
123
|
-
/**
|
|
124
|
-
export function agentBrowserConfigBlocksManagedRestore(
|
|
125
|
-
return !resolveManagedSessionRestoreHome(parentEnv, platform) ||
|
|
111
|
+
/** Explicit config overrides disable restore; passive files are ignored because every browser-backed subprocess pins a protected empty config. */
|
|
112
|
+
export function agentBrowserConfigBlocksManagedRestore(_cwd, parentEnv = getAgentBrowserProcessEnvironment(), args = [], platform = process.platform) {
|
|
113
|
+
return !resolveManagedSessionRestoreHome(parentEnv, platform) || agentBrowserExplicitConfigIsPresent(parentEnv, args);
|
|
126
114
|
}
|
|
127
115
|
function omitWrapperInjectedUserAgent(args, enabled) {
|
|
128
116
|
if (!enabled)
|
|
@@ -131,7 +119,7 @@ function omitWrapperInjectedUserAgent(args, enabled) {
|
|
|
131
119
|
return index < 0 ? args : [...args.slice(0, index), ...args.slice(index + 2)];
|
|
132
120
|
}
|
|
133
121
|
function batchHasManagedSessionRestoreConflict(args, stdin) {
|
|
134
|
-
const [command, ...commandArgs] =
|
|
122
|
+
const [command, ...commandArgs] = extractUpstreamCommandTokens(args);
|
|
135
123
|
if (command !== "batch")
|
|
136
124
|
return false;
|
|
137
125
|
if (commandArgs.some((token) => token !== "--bail"))
|
|
@@ -142,7 +130,7 @@ function batchHasManagedSessionRestoreConflict(args, stdin) {
|
|
|
142
130
|
return parsed.steps.some((step) => ["connect", "batch"].includes(parseCommandInfo(step).command ?? ""));
|
|
143
131
|
}
|
|
144
132
|
function hasManagedSessionRestoreLaunchConflict(options) {
|
|
145
|
-
const parentEnv = options.parentEnv ??
|
|
133
|
+
const parentEnv = options.parentEnv ?? getAgentBrowserProcessEnvironment();
|
|
146
134
|
const effectiveEnv = { ...parentEnv, ...options.env };
|
|
147
135
|
const args = omitWrapperInjectedUserAgent(options.args, options.wrapperInjectedUserAgent);
|
|
148
136
|
if (MANAGED_RESTORE_INCOMPATIBLE_ENVS.some((name) => hasUpstreamEnvValue(effectiveEnv, name)))
|
|
@@ -156,7 +144,7 @@ function hasManagedSessionRestoreLaunchConflict(options) {
|
|
|
156
144
|
return hasExplicitConfigArg(args) || hasUpstreamEnvValue({ ...parentEnv, ...options.env }, AGENT_BROWSER_CONFIG_ENV);
|
|
157
145
|
}
|
|
158
146
|
function managedSessionRestoreOptedOut(options) {
|
|
159
|
-
const effectiveEnv = { ...(options.parentEnv ??
|
|
147
|
+
const effectiveEnv = { ...(options.parentEnv ?? getAgentBrowserProcessEnvironment()), ...options.env };
|
|
160
148
|
return isDisabledEnvFlag(effectiveEnv[MANAGED_SESSION_RESTORE_ENV]);
|
|
161
149
|
}
|
|
162
150
|
function isManagedSessionRestoreIncompatible(options) {
|
|
@@ -164,7 +152,7 @@ function isManagedSessionRestoreIncompatible(options) {
|
|
|
164
152
|
return true;
|
|
165
153
|
if (managedSessionRestoreOptedOut(options))
|
|
166
154
|
return false;
|
|
167
|
-
const effectiveEnv = { ...(options.parentEnv ??
|
|
155
|
+
const effectiveEnv = { ...(options.parentEnv ?? getAgentBrowserProcessEnvironment()), ...options.env };
|
|
168
156
|
const args = omitWrapperInjectedUserAgent(options.args, options.wrapperInjectedUserAgent);
|
|
169
157
|
if (options.cwd && !hasManagedSessionRestoreProjectIdentity(options.cwd))
|
|
170
158
|
return true;
|
|
@@ -173,7 +161,7 @@ function isManagedSessionRestoreIncompatible(options) {
|
|
|
173
161
|
return !ensureManagedSessionRestoreStorageIsSecure(effectiveEnv, process.platform, extractExplicitNamespace(args));
|
|
174
162
|
}
|
|
175
163
|
function resolveManagedSessionRestorePolicy(options) {
|
|
176
|
-
const parentEnv = options.parentEnv ??
|
|
164
|
+
const parentEnv = options.parentEnv ?? getAgentBrowserProcessEnvironment();
|
|
177
165
|
const sessionName = extractExplicitSessionName(options.args);
|
|
178
166
|
const namespace = extractExplicitNamespace(options.args);
|
|
179
167
|
const ownedContext = ownedContextMatches(sessionName, namespace);
|
|
@@ -204,7 +192,7 @@ export function getOwnedManagedSessionCompatibilityEnv(options) {
|
|
|
204
192
|
const { owned, ownedContext } = resolveManagedSessionRestorePolicy(options);
|
|
205
193
|
if (!owned || !ownedContext)
|
|
206
194
|
return {};
|
|
207
|
-
const parentEnv = options.parentEnv ??
|
|
195
|
+
const parentEnv = options.parentEnv ?? getAgentBrowserProcessEnvironment();
|
|
208
196
|
const callEnv = options.env ?? {};
|
|
209
197
|
const explicitRawInterval = Object.hasOwn(callEnv, "AGENT_BROWSER_AUTOSAVE_INTERVAL_MS")
|
|
210
198
|
? callEnv.AGENT_BROWSER_AUTOSAVE_INTERVAL_MS
|
|
@@ -295,7 +283,7 @@ export function getManagedSessionRestoreProtectedEnv(options, restoreEnv) {
|
|
|
295
283
|
return {};
|
|
296
284
|
if (ownedContext?.protectedStorageEnv)
|
|
297
285
|
return { ...ownedContext.protectedStorageEnv };
|
|
298
|
-
const effectiveEnv = { ...(options.parentEnv ??
|
|
286
|
+
const effectiveEnv = { ...(options.parentEnv ?? getAgentBrowserProcessEnvironment()), ...options.env };
|
|
299
287
|
return getManagedSessionRestoreProtectedStorageEnv(true, effectiveEnv);
|
|
300
288
|
}
|
|
301
289
|
export function validateManagedSessionRestoreContextForSpawn(options) {
|
|
@@ -303,7 +291,7 @@ export function validateManagedSessionRestoreContextForSpawn(options) {
|
|
|
303
291
|
if (closesBrowserSession(options.args) || ownedContext?.restoreDecision !== "enabled")
|
|
304
292
|
return true;
|
|
305
293
|
const ownedCwd = ownedContext.cwd ?? options.cwd;
|
|
306
|
-
if (!ownedContext.restoreKey || createManagedSessionRestoreKey(ownedCwd) !== ownedContext.restoreKey || !hasManagedSessionRestoreProjectIdentity(ownedCwd))
|
|
294
|
+
if (!ownedContext.restoreKey || !ownedContext.restoreScope || createManagedSessionRestoreKey(ownedCwd, ownedContext.restoreScope) !== ownedContext.restoreKey || !hasManagedSessionRestoreProjectIdentity(ownedCwd))
|
|
307
295
|
return false;
|
|
308
296
|
const effectiveEnv = { ...parentEnv, ...options.env };
|
|
309
297
|
if (isDisabledEnvFlag(effectiveEnv[MANAGED_SESSION_RESTORE_ENV]))
|
|
@@ -330,7 +318,7 @@ export function getManagedSessionRestoreEnv(options) {
|
|
|
330
318
|
return {};
|
|
331
319
|
if (restoreState.isDisabled(sessionName, namespace) || !sessionName)
|
|
332
320
|
return {};
|
|
333
|
-
return { [AGENT_BROWSER_RESTORE_ENV]: createManagedSessionRestoreKey(options.cwd) };
|
|
321
|
+
return { [AGENT_BROWSER_RESTORE_ENV]: createManagedSessionRestoreKey(options.cwd, getManagedSessionRestoreScope(sessionName)) };
|
|
334
322
|
}
|
|
335
323
|
/** Commit sticky suppression only after an owned-context subprocess has actually started. */
|
|
336
324
|
export function commitManagedSessionRestoreSuppression(options) {
|
|
@@ -375,8 +363,9 @@ export function buildOwnedManagedSessionRestoreContext(options) {
|
|
|
375
363
|
const projectIdentityAvailable = !optedOut && hasManagedSessionRestoreProjectIdentity(ownedCwd);
|
|
376
364
|
const incompatible = !optedOut && isManagedSessionRestoreIncompatible(policyOptions);
|
|
377
365
|
const enabled = !optedOut && !incompatible;
|
|
378
|
-
const effectiveEnv = { ...(options.parentEnv ??
|
|
379
|
-
const
|
|
366
|
+
const effectiveEnv = { ...(options.parentEnv ?? getAgentBrowserProcessEnvironment()), ...options.env };
|
|
367
|
+
const restoreScope = getManagedSessionRestoreScope(owned.sessionName);
|
|
368
|
+
const restoreKey = projectIdentityAvailable ? createManagedSessionRestoreKey(ownedCwd, restoreScope) : undefined;
|
|
380
369
|
return {
|
|
381
370
|
...owned,
|
|
382
371
|
compatibilityUserAgent: options.compatibilityUserAgent,
|
|
@@ -386,6 +375,7 @@ export function buildOwnedManagedSessionRestoreContext(options) {
|
|
|
386
375
|
protectedStorageEnv: enabled ? getManagedSessionRestoreProtectedStorageEnv(true, effectiveEnv) : undefined,
|
|
387
376
|
restoreDecision: optedOut ? "opted-out" : incompatible ? "incompatible" : "enabled",
|
|
388
377
|
restoreKey,
|
|
378
|
+
restoreScope,
|
|
389
379
|
restoreSuppressed: optedOut || incompatible,
|
|
390
380
|
};
|
|
391
381
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { chmodSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, isAbsolute, join } from "node:path";
|
|
4
|
-
import { createManagedSessionRestoreKey, directoryContainsSymlink, ensureManagedSessionRestoreStorageIsSecure, ensureOwnerOnlyDirectory, getManagedRestoreSessionsDirectory,
|
|
4
|
+
import { createManagedSessionRestoreKey, directoryContainsSymlink, ensureManagedSessionRestoreStorageIsSecure, hasManagedSessionRestoreProjectIdentity, ensureOwnerOnlyDirectory, getManagedRestoreSessionsDirectory, isManagedSessionRestoreKey, resolveManagedSessionRestoreCheckoutRoot, resolveManagedSessionRestoreHome, } from "./managed-session-storage.js";
|
|
5
5
|
const OWNED_RESTORE_SNAPSHOT_FAMILIES_TO_KEEP = 2;
|
|
6
6
|
const OWNED_RESTORE_SNAPSHOT_MAX_RECORDS = 256;
|
|
7
7
|
const OWNED_RESTORE_SNAPSHOT_RECORD_MAX_BYTES = 16 * 1_024;
|
|
@@ -344,9 +344,7 @@ export function pruneOwnedManagedSessionRestoreSnapshots(options) {
|
|
|
344
344
|
if (!changed)
|
|
345
345
|
break;
|
|
346
346
|
}
|
|
347
|
-
const protectedRestoreKey =
|
|
348
|
-
? createManagedSessionRestoreKey(options.cwd)
|
|
349
|
-
: restoreKey;
|
|
347
|
+
const protectedRestoreKey = restoreKey;
|
|
350
348
|
if (!lineage)
|
|
351
349
|
return removed;
|
|
352
350
|
removed += pruneExpiredOtherRestoreKeys({
|
|
@@ -4,18 +4,20 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { parseArgvDescriptor } from "./argv-descriptor.js";
|
|
5
5
|
import { needsManagedSession } from "./command-policy.js";
|
|
6
6
|
import { getScreenshotPathTokenIndex } from "./orchestration/browser-run/artifact-paths.js";
|
|
7
|
-
import { parseBatchCommandArgument, parseUserBatchStdin } from "./orchestration/batch-stdin.js";
|
|
7
|
+
import { getUpstreamEffectiveBatchSteps, parseBatchCommandArgument, parseUserBatchStdin } from "./orchestration/batch-stdin.js";
|
|
8
8
|
import { isUnverifiedPageTransitionCommand } from "./command-taxonomy.js";
|
|
9
9
|
import { GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, VALUE_FLAGS, extractExplicitSessionName, isUpstreamEnvFlagEnabled, optionalGlobalValueFlagConsumesNext, } from "./argv-grammar.js";
|
|
10
10
|
import { extractManagedSessionRestoreKeys, isWrapperManagedSessionName } from "./managed-session-capabilities.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { agentBrowserExplicitConfigIsPresent } from "./managed-session-restore.js";
|
|
12
|
+
import { hasManagedSessionRestoreProjectIdentity } from "./managed-session-storage.js";
|
|
13
|
+
import { getAgentBrowserProcessEnvironment } from "./process-environment.js";
|
|
13
14
|
const BLOCKED_GLOBAL_STATE_MESSAGE = "This operation could read or modify wrapper-owned browser state outside the current checkout. Use a caller-owned state name or path instead.";
|
|
14
15
|
const BLOCKED_MANAGED_BROWSER_FILE_MESSAGE = "Browser access to local .agent-browser storage is blocked because state files can contain authenticated cookies and storage. Use guarded state commands instead.";
|
|
15
16
|
const BLOCKED_MANAGED_SESSION_MESSAGE = "This session name is reserved for a browser managed by this extension instance. Use the current managed session or a caller-owned session name instead.";
|
|
16
17
|
const FILE_ACCESS_FLAG_MESSAGE = "Browser file-access enablement is blocked because a local page could read and exfiltrate authenticated .agent-browser state.";
|
|
17
|
-
const UPSTREAM_CONFIG_MESSAGE = "
|
|
18
|
+
const UPSTREAM_CONFIG_MESSAGE = "Explicit upstream agent-browser config from --config or AGENT_BROWSER_CONFIG is blocked for browser-backed native calls because it can load protected state, profiles, extensions, or file-access settings. Remove that override and pass safe settings explicitly. Passive agent-browser.json files are ignored through the wrapper's protected empty config.";
|
|
18
19
|
const UNVERIFIED_PAGE_MESSAGE = "The active page became unverified after a tab, attachment, history, script, or state-load transition. Run get url or navigate explicitly to a safe URL before page-content inspection.";
|
|
20
|
+
const BATCH_UNVERIFIED_PAGE_MESSAGE = `${UNVERIFIED_PAGE_MESSAGE} In a batch, put get url after the transition before later content steps, or split the batch at that boundary.`;
|
|
19
21
|
const UNSAFE_BATCH_ARGUMENT_MESSAGE = "Batch command arguments could not be safely inspected. Use batch stdin JSON command arrays instead.";
|
|
20
22
|
const NESTED_BATCH_ARGUMENT_MESSAGE = "Nested batch commands are blocked by the wrapper's page-state safety policy. Flatten the batch steps instead.";
|
|
21
23
|
const NON_BAIL_BATCH_NAVIGATION_MESSAGE = "Batches that navigate before page-content access must use exact batch --bail so a failed navigation cannot expose the prior page.";
|
|
@@ -59,7 +61,8 @@ function hasAgentBrowserPathComponent(value) {
|
|
|
59
61
|
function getFileUrlPath(value) {
|
|
60
62
|
if (!value)
|
|
61
63
|
return undefined;
|
|
62
|
-
const
|
|
64
|
+
const trimmedValue = value.replaceAll(/[\t\n\r]/g, "").trim();
|
|
65
|
+
const nestedFileUrl = /^(?:(?:blob|filesystem|view-source):)*(file:.*)$/i.exec(trimmedValue)?.[1];
|
|
63
66
|
if (!nestedFileUrl)
|
|
64
67
|
return undefined;
|
|
65
68
|
try {
|
|
@@ -70,7 +73,7 @@ function getFileUrlPath(value) {
|
|
|
70
73
|
return decodeUrlComponent(new URL(nestedFileUrl).pathname);
|
|
71
74
|
}
|
|
72
75
|
catch {
|
|
73
|
-
return
|
|
76
|
+
return trimmedValue;
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
}
|
|
@@ -148,7 +151,7 @@ function getPositionalOperands(commandTokens) {
|
|
|
148
151
|
}
|
|
149
152
|
function getExplicitNavigationTarget(args) {
|
|
150
153
|
const descriptor = parseArgvDescriptor(args);
|
|
151
|
-
const positionals = getPositionalOperands(descriptor.
|
|
154
|
+
const positionals = getPositionalOperands(descriptor.upstreamCommandTokens);
|
|
152
155
|
if (EXPLICIT_NAVIGATION_COMMANDS.has(descriptor.commandInfo.command ?? ""))
|
|
153
156
|
return positionals[0];
|
|
154
157
|
if (descriptor.commandInfo.command === "tab" && descriptor.commandInfo.subcommand === "new")
|
|
@@ -179,11 +182,11 @@ function getResultingExplicitNavigationTarget(args, currentPageUrl) {
|
|
|
179
182
|
function getBrowserContentUrlOperands(args) {
|
|
180
183
|
const descriptor = parseArgvDescriptor(args);
|
|
181
184
|
const command = descriptor.commandInfo.command;
|
|
182
|
-
const positionals = getPositionalOperands(descriptor.
|
|
185
|
+
const positionals = getPositionalOperands(descriptor.upstreamCommandTokens);
|
|
183
186
|
if (["a11y", "read", "vitals", "web-vitals"].includes(command ?? ""))
|
|
184
187
|
return positionals.slice(0, 1);
|
|
185
188
|
if (command === "auth" && descriptor.commandInfo.subcommand === "login")
|
|
186
|
-
return getFlagValues(descriptor.
|
|
189
|
+
return getFlagValues(descriptor.upstreamCommandTokens, "--url", true);
|
|
187
190
|
if (command === "diff" && descriptor.commandInfo.subcommand === "url")
|
|
188
191
|
return positionals.slice(1, 3);
|
|
189
192
|
if (command === "record" && ["start", "restart"].includes(descriptor.commandInfo.subcommand ?? ""))
|
|
@@ -221,7 +224,7 @@ function getBrowserFileOperands(args, env) {
|
|
|
221
224
|
const descriptor = parseArgvDescriptor(args);
|
|
222
225
|
const command = descriptor.commandInfo.command;
|
|
223
226
|
const subcommand = descriptor.commandInfo.subcommand;
|
|
224
|
-
const positionals = getPositionalOperands(descriptor.
|
|
227
|
+
const positionals = getPositionalOperands(descriptor.upstreamCommandTokens);
|
|
225
228
|
const values = [
|
|
226
229
|
getExplicitNavigationTarget(args),
|
|
227
230
|
...getBrowserContentUrlOperands(args),
|
|
@@ -238,23 +241,23 @@ function getBrowserFileOperands(args, env) {
|
|
|
238
241
|
if (command === "pdf")
|
|
239
242
|
values.push(positionals[0]);
|
|
240
243
|
if (command === "screenshot") {
|
|
241
|
-
const pathIndex = getScreenshotPathTokenIndex(descriptor.
|
|
242
|
-
values.push(pathIndex === undefined ? undefined : descriptor.
|
|
244
|
+
const pathIndex = getScreenshotPathTokenIndex(descriptor.upstreamCommandTokens);
|
|
245
|
+
values.push(pathIndex === undefined ? undefined : descriptor.upstreamCommandTokens[pathIndex]);
|
|
243
246
|
}
|
|
244
247
|
if (["profiler", "trace"].includes(command ?? "") && subcommand === "stop")
|
|
245
248
|
values.push(...positionals.slice(1));
|
|
246
249
|
if (command === "network" && subcommand === "har")
|
|
247
250
|
values.push(...positionals.slice(2));
|
|
248
251
|
if (command === "wait")
|
|
249
|
-
values.push(...getFlagValues(descriptor.
|
|
252
|
+
values.push(...getFlagValues(descriptor.upstreamCommandTokens, "--download", true), ...getFlagValues(descriptor.upstreamCommandTokens, "-d", true));
|
|
250
253
|
if (command === "cookies" && subcommand === "set")
|
|
251
|
-
values.push(...getFlagValues(descriptor.
|
|
254
|
+
values.push(...getFlagValues(descriptor.upstreamCommandTokens, "--curl", true));
|
|
252
255
|
if (command === "state" && ["load", "rename", "save", "show"].includes(subcommand ?? ""))
|
|
253
256
|
values.push(...positionals.slice(1));
|
|
254
257
|
if (command === "diff" && subcommand === "url")
|
|
255
258
|
values.push(...positionals.slice(1));
|
|
256
259
|
if (command === "diff")
|
|
257
|
-
values.push(...getFlagValues(descriptor.
|
|
260
|
+
values.push(...getFlagValues(descriptor.upstreamCommandTokens, "--baseline", true), ...getFlagValues(descriptor.upstreamCommandTokens, "--output", true), ...getFlagValues(descriptor.upstreamCommandTokens, "-o", true));
|
|
258
261
|
if (command === "record" && ["start", "restart"].includes(subcommand ?? ""))
|
|
259
262
|
values.push(...positionals.slice(1));
|
|
260
263
|
return values.filter((value) => typeof value === "string" && value.length > 0);
|
|
@@ -306,13 +309,21 @@ function getManagedBrowserFileAccessValidationError(options) {
|
|
|
306
309
|
return BLOCKED_MANAGED_BROWSER_FILE_MESSAGE;
|
|
307
310
|
return undefined;
|
|
308
311
|
}
|
|
312
|
+
// Deliberately unions raw argv steps AND parseable stdin steps even though
|
|
313
|
+
// upstream executes argv steps exclusively when any exist: this pre-spawn
|
|
314
|
+
// validator must stay a superset of anything upstream could run, so it fails
|
|
315
|
+
// closed on unsafe content in either source. Stdin parse failures are fatal
|
|
316
|
+
// only when upstream would actually read stdin (no argv steps); ignored
|
|
317
|
+
// unparseable stdin cannot execute and must not reject a valid raw-argv call.
|
|
318
|
+
// The upstream-exact selection lives in getUpstreamEffectiveBatchSteps
|
|
319
|
+
// (batch-stdin.ts) for guard/folding paths.
|
|
309
320
|
function getBatchCommandSteps(args, stdin) {
|
|
310
321
|
const descriptor = parseArgvDescriptor(args);
|
|
311
322
|
if (descriptor.commandInfo.command !== "batch")
|
|
312
323
|
return { steps: [] };
|
|
313
324
|
const steps = [];
|
|
314
|
-
for (const command of descriptor.
|
|
315
|
-
if (command === "--bail"
|
|
325
|
+
for (const command of descriptor.upstreamCommandTokens.slice(1)) {
|
|
326
|
+
if (command === "--bail")
|
|
316
327
|
continue;
|
|
317
328
|
if (decodeUrlComponent(command).toLowerCase().includes(".agent-browser"))
|
|
318
329
|
return { error: BLOCKED_MANAGED_BROWSER_FILE_MESSAGE, steps: [] };
|
|
@@ -324,7 +335,7 @@ function getBatchCommandSteps(args, stdin) {
|
|
|
324
335
|
steps.push(parsed.step);
|
|
325
336
|
}
|
|
326
337
|
const parsedStdin = parseUserBatchStdin(stdin);
|
|
327
|
-
if (parsedStdin.error)
|
|
338
|
+
if (parsedStdin.error && steps.length === 0)
|
|
328
339
|
return { error: parsedStdin.error, steps: [] };
|
|
329
340
|
for (const step of parsedStdin.steps ?? []) {
|
|
330
341
|
if (parseArgvDescriptor(step).commandInfo.command === "batch")
|
|
@@ -333,9 +344,16 @@ function getBatchCommandSteps(args, stdin) {
|
|
|
333
344
|
}
|
|
334
345
|
return { steps };
|
|
335
346
|
}
|
|
347
|
+
export function invocationMayNavigateToLocalFile(args, stdin) {
|
|
348
|
+
if (isFileUrl(getResultingExplicitNavigationTarget(args)))
|
|
349
|
+
return true;
|
|
350
|
+
const descriptor = parseArgvDescriptor(args);
|
|
351
|
+
return getUpstreamEffectiveBatchSteps(descriptor.upstreamCommandTokens, stdin)
|
|
352
|
+
.some((step) => isFileUrl(getResultingExplicitNavigationTarget(step)));
|
|
353
|
+
}
|
|
336
354
|
function batchBailsOnFirstError(args) {
|
|
337
355
|
const descriptor = parseArgvDescriptor(args);
|
|
338
|
-
return descriptor.commandInfo.command === "batch" && descriptor.
|
|
356
|
+
return descriptor.commandInfo.command === "batch" && descriptor.upstreamCommandTokens.slice(1).includes("--bail");
|
|
339
357
|
}
|
|
340
358
|
function commandMayChangePageTarget(args, trustedBatchTabSelection) {
|
|
341
359
|
const descriptor = parseArgvDescriptor(args);
|
|
@@ -419,11 +437,11 @@ export function getCallerOwnedSessionLivePageVerificationRequirement(options) {
|
|
|
419
437
|
trustedFirstBatchTabSelection: options.trustedFirstBatchTabSelection,
|
|
420
438
|
trustedPinnedEmptyConfig: true,
|
|
421
439
|
});
|
|
422
|
-
return validationError === UNVERIFIED_PAGE_MESSAGE || validationError === NON_BAIL_BATCH_NAVIGATION_MESSAGE
|
|
440
|
+
return validationError === UNVERIFIED_PAGE_MESSAGE || validationError === BATCH_UNVERIFIED_PAGE_MESSAGE || validationError === NON_BAIL_BATCH_NAVIGATION_MESSAGE
|
|
423
441
|
? UNVERIFIED_PAGE_MESSAGE
|
|
424
442
|
: undefined;
|
|
425
443
|
}
|
|
426
|
-
export function getManagedSessionTargetAccessValidationError(args, ownedManagedSession, env =
|
|
444
|
+
export function getManagedSessionTargetAccessValidationError(args, ownedManagedSession, env = getAgentBrowserProcessEnvironment()) {
|
|
427
445
|
const sessionName = extractExplicitSessionName(args) ?? env.AGENT_BROWSER_SESSION;
|
|
428
446
|
return sessionName && isWrapperManagedSessionName(sessionName) && !ownedManagedSession
|
|
429
447
|
? BLOCKED_MANAGED_SESSION_MESSAGE
|
|
@@ -465,7 +483,7 @@ function getReferencedValues(args, cwd, env) {
|
|
|
465
483
|
env.AGENT_BROWSER_STATE,
|
|
466
484
|
].filter((value) => typeof value === "string" && value.length > 0);
|
|
467
485
|
if (descriptor.commandInfo.command === "state" && ["clear", "load", "rename", "save", "show"].includes(descriptor.commandInfo.subcommand ?? "")) {
|
|
468
|
-
values.push(...descriptor.
|
|
486
|
+
values.push(...descriptor.upstreamCommandTokens.slice(2));
|
|
469
487
|
}
|
|
470
488
|
for (const value of [...values]) {
|
|
471
489
|
const realPath = resolveExistingPath(cwd, value);
|
|
@@ -475,13 +493,13 @@ function getReferencedValues(args, cwd, env) {
|
|
|
475
493
|
return values;
|
|
476
494
|
}
|
|
477
495
|
export function getManagedSessionStateAccessValidationError(options) {
|
|
478
|
-
const effectiveEnv = { ...(options.parentEnv ??
|
|
496
|
+
const effectiveEnv = { ...(options.parentEnv ?? getAgentBrowserProcessEnvironment()), ...options.env };
|
|
479
497
|
const descriptor = parseArgvDescriptor(options.args);
|
|
480
498
|
const command = descriptor.commandInfo.command;
|
|
481
499
|
const subcommand = descriptor.commandInfo.subcommand;
|
|
482
500
|
if (["close", "exit", "quit"].includes(command ?? ""))
|
|
483
501
|
return undefined;
|
|
484
|
-
if (!options.trustedPinnedEmptyConfig && needsManagedSession(descriptor) &&
|
|
502
|
+
if (!options.trustedPinnedEmptyConfig && needsManagedSession(descriptor) && agentBrowserExplicitConfigIsPresent(effectiveEnv, options.args))
|
|
485
503
|
return UPSTREAM_CONFIG_MESSAGE;
|
|
486
504
|
if (command === "batch") {
|
|
487
505
|
const batch = getBatchCommandSteps(options.args, options.stdin);
|
|
@@ -520,7 +538,7 @@ export function getManagedSessionStateAccessValidationError(options) {
|
|
|
520
538
|
}
|
|
521
539
|
}
|
|
522
540
|
if (directError)
|
|
523
|
-
return directError;
|
|
541
|
+
return directError === UNVERIFIED_PAGE_MESSAGE ? BATCH_UNVERIFIED_PAGE_MESSAGE : directError;
|
|
524
542
|
if (failedNavigationHazard)
|
|
525
543
|
return NON_BAIL_BATCH_NAVIGATION_MESSAGE;
|
|
526
544
|
const mayChangePageTarget = commandMayChangePageTarget(step, trustedBatchTabSelection);
|
|
@@ -562,8 +580,8 @@ export function getManagedSessionStateAccessValidationError(options) {
|
|
|
562
580
|
if (command === "state" && subcommand === "clean")
|
|
563
581
|
return BLOCKED_GLOBAL_STATE_MESSAGE;
|
|
564
582
|
if (command === "state" && subcommand === "clear") {
|
|
565
|
-
const target = descriptor.
|
|
566
|
-
if (!target || descriptor.
|
|
583
|
+
const target = descriptor.upstreamCommandTokens.slice(2).find((token) => !token.startsWith("-"));
|
|
584
|
+
if (!target || descriptor.upstreamCommandTokens.includes("--all") || descriptor.upstreamCommandTokens.includes("-a") || isWrapperManagedSessionName(target)) {
|
|
567
585
|
return BLOCKED_GLOBAL_STATE_MESSAGE;
|
|
568
586
|
}
|
|
569
587
|
}
|
|
@@ -576,8 +594,8 @@ export function getManagedSessionStateAccessValidationError(options) {
|
|
|
576
594
|
return undefined;
|
|
577
595
|
if (command === "state" && ["rename", "save"].includes(subcommand ?? ""))
|
|
578
596
|
return BLOCKED_GLOBAL_STATE_MESSAGE;
|
|
579
|
-
if (!hasManagedSessionRestoreProjectIdentity(options.cwd))
|
|
597
|
+
if (!hasManagedSessionRestoreProjectIdentity(options.cwd) || !options.managedSessionRestoreKey)
|
|
580
598
|
return BLOCKED_GLOBAL_STATE_MESSAGE;
|
|
581
|
-
const currentKey =
|
|
599
|
+
const currentKey = options.managedSessionRestoreKey.toLowerCase();
|
|
582
600
|
return referencedKeys.every((key) => key === currentKey) ? undefined : BLOCKED_GLOBAL_STATE_MESSAGE;
|
|
583
601
|
}
|
|
@@ -5,6 +5,7 @@ import { dirname, isAbsolute, join, parse, resolve, win32 } from "node:path";
|
|
|
5
5
|
import { canonicalizeAgentBrowserNamespace } from "./argv-grammar.js";
|
|
6
6
|
export { isManagedSessionRestoreKey } from "./managed-session-capabilities.js";
|
|
7
7
|
const MANAGED_SESSION_NAME_PREFIX = "piab-r2-";
|
|
8
|
+
const MANAGED_SESSION_FRESH_SUFFIX_PATTERN = /-fresh-[a-f\d]{10}$/i;
|
|
8
9
|
const MANAGED_SESSION_RESTORE_KEY_HASH_LENGTH = 32;
|
|
9
10
|
const PROJECT_GENERATION_MARKER_NAME = "pi-agent-browser-project-generation-v1.json";
|
|
10
11
|
const PROJECT_GENERATION_MARKER_MAX_BYTES = 1_024;
|
|
@@ -15,7 +16,7 @@ function isAbsoluteHome(path, platform) {
|
|
|
15
16
|
function currentUid() {
|
|
16
17
|
return typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
17
18
|
}
|
|
18
|
-
function isTrustedPosixDirectory(path, requireCurrentOwner) {
|
|
19
|
+
function isTrustedPosixDirectory(path, requireCurrentOwner, platform = process.platform) {
|
|
19
20
|
const uid = currentUid();
|
|
20
21
|
if (uid === undefined)
|
|
21
22
|
return false;
|
|
@@ -32,11 +33,13 @@ function isTrustedPosixDirectory(path, requireCurrentOwner) {
|
|
|
32
33
|
}
|
|
33
34
|
if (entry.isSymbolicLink() || !entry.isDirectory())
|
|
34
35
|
return false;
|
|
35
|
-
|
|
36
|
+
const androidSystemAncestor = platform === "android" && (cursor === "/data" || cursor === "/data/data") && entry.uid === 1000 && (entry.mode & 0o002) === 0;
|
|
37
|
+
const androidAppDirectory = platform === "android" && entry.uid === uid && entry.gid === uid && (entry.mode & 0o002) === 0;
|
|
38
|
+
if (!androidSystemAncestor && entry.uid !== 0 && entry.uid !== uid)
|
|
36
39
|
return false;
|
|
37
40
|
const writableByOthers = (entry.mode & 0o022) !== 0;
|
|
38
41
|
const rootOwnedStickyDirectory = entry.uid === 0 && (entry.mode & 0o1000) !== 0;
|
|
39
|
-
if (writableByOthers && !rootOwnedStickyDirectory)
|
|
42
|
+
if (writableByOthers && !rootOwnedStickyDirectory && !androidSystemAncestor && !androidAppDirectory)
|
|
40
43
|
return false;
|
|
41
44
|
}
|
|
42
45
|
try {
|
|
@@ -56,7 +59,7 @@ export function resolveManagedSessionRestoreHome(parentEnv, platform = process.p
|
|
|
56
59
|
return candidate;
|
|
57
60
|
try {
|
|
58
61
|
const canonical = realpathSync(candidate);
|
|
59
|
-
return isTrustedPosixDirectory(canonical, true) ? canonical : undefined;
|
|
62
|
+
return isTrustedPosixDirectory(canonical, true, platform) ? canonical : undefined;
|
|
60
63
|
}
|
|
61
64
|
catch {
|
|
62
65
|
return undefined;
|
|
@@ -138,12 +141,15 @@ function readProjectGenerationMarker(path, platform) {
|
|
|
138
141
|
return undefined;
|
|
139
142
|
}
|
|
140
143
|
}
|
|
141
|
-
function getDirectoryFilesystemIdentity(path) {
|
|
144
|
+
function getDirectoryFilesystemIdentity(path, platform) {
|
|
142
145
|
try {
|
|
143
146
|
const entry = statSync(path, { bigint: true });
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
+
if (!entry.isDirectory() || entry.dev <= 0n || entry.ino <= 0n)
|
|
148
|
+
return undefined;
|
|
149
|
+
// ponytail: Android reports mutable ctime as birthtime; use statx birthtime/inode generation when Node exposes either reliably.
|
|
150
|
+
return platform === "android"
|
|
151
|
+
? `${entry.dev}:${entry.ino}`
|
|
152
|
+
: entry.birthtimeNs > 0n ? `${entry.dev}:${entry.ino}:${entry.birthtimeNs}` : undefined;
|
|
147
153
|
}
|
|
148
154
|
catch {
|
|
149
155
|
return undefined;
|
|
@@ -157,13 +163,13 @@ function resolveManagedSessionRestoreProjectCheckout(cwd, platform) {
|
|
|
157
163
|
catch {
|
|
158
164
|
return undefined;
|
|
159
165
|
}
|
|
160
|
-
if (platform !== "win32" && !isTrustedPosixDirectory(canonicalCwd, false))
|
|
166
|
+
if (platform !== "win32" && !isTrustedPosixDirectory(canonicalCwd, false, platform))
|
|
161
167
|
return undefined;
|
|
162
168
|
const checkout = resolveGitCheckout(canonicalCwd, platform);
|
|
163
169
|
if (!checkout)
|
|
164
170
|
return undefined;
|
|
165
|
-
if (platform !== "win32" && (!isTrustedPosixDirectory(checkout.worktreeDirectory, true)
|
|
166
|
-
|| !isTrustedPosixDirectory(checkout.gitDirectory, true)))
|
|
171
|
+
if (platform !== "win32" && (!isTrustedPosixDirectory(checkout.worktreeDirectory, true, platform)
|
|
172
|
+
|| !isTrustedPosixDirectory(checkout.gitDirectory, true, platform)))
|
|
167
173
|
return undefined;
|
|
168
174
|
return { canonicalCwd, ...checkout };
|
|
169
175
|
}
|
|
@@ -175,8 +181,8 @@ function resolveProjectGenerationIdentity(cwd, platform = process.platform) {
|
|
|
175
181
|
if (!checkout)
|
|
176
182
|
return undefined;
|
|
177
183
|
const { canonicalCwd } = checkout;
|
|
178
|
-
const gitFilesystemIdentity = getDirectoryFilesystemIdentity(checkout.gitDirectory);
|
|
179
|
-
const worktreeFilesystemIdentity = getDirectoryFilesystemIdentity(checkout.worktreeDirectory);
|
|
184
|
+
const gitFilesystemIdentity = getDirectoryFilesystemIdentity(checkout.gitDirectory, platform);
|
|
185
|
+
const worktreeFilesystemIdentity = getDirectoryFilesystemIdentity(checkout.worktreeDirectory, platform);
|
|
180
186
|
if (!gitFilesystemIdentity || !worktreeFilesystemIdentity)
|
|
181
187
|
return undefined;
|
|
182
188
|
const markerPath = join(checkout.gitDirectory, PROJECT_GENERATION_MARKER_NAME);
|
|
@@ -192,22 +198,35 @@ function resolveProjectGenerationIdentity(cwd, platform = process.platform) {
|
|
|
192
198
|
projectGenerationCache.delete(canonicalCwd);
|
|
193
199
|
try {
|
|
194
200
|
if (!marker) {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
201
|
+
const content = JSON.stringify({ id: randomUUID(), version: 1 });
|
|
202
|
+
if (platform === "android") {
|
|
203
|
+
// ponytail: Android denies hard links in app storage; use renameat2(RENAME_NOREPLACE) if Node exposes it.
|
|
198
204
|
try {
|
|
199
|
-
|
|
205
|
+
writeFileSync(markerPath, content, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
200
206
|
}
|
|
201
207
|
catch (error) {
|
|
202
208
|
if (error.code !== "EEXIST")
|
|
203
209
|
return undefined;
|
|
204
210
|
}
|
|
205
211
|
}
|
|
206
|
-
|
|
212
|
+
else {
|
|
213
|
+
const candidatePath = `${markerPath}.candidate-${process.pid}-${randomUUID()}`;
|
|
207
214
|
try {
|
|
208
|
-
|
|
215
|
+
writeFileSync(candidatePath, content, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
216
|
+
try {
|
|
217
|
+
linkSync(candidatePath, markerPath);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (error.code !== "EEXIST")
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
try {
|
|
226
|
+
unlinkSync(candidatePath);
|
|
227
|
+
}
|
|
228
|
+
catch { }
|
|
209
229
|
}
|
|
210
|
-
catch { }
|
|
211
230
|
}
|
|
212
231
|
marker = readProjectGenerationMarker(markerPath, platform);
|
|
213
232
|
}
|
|
@@ -231,16 +250,23 @@ function resolveProjectGenerationIdentity(cwd, platform = process.platform) {
|
|
|
231
250
|
export function hasManagedSessionRestoreProjectIdentity(cwd) {
|
|
232
251
|
return resolveProjectGenerationIdentity(cwd) !== undefined;
|
|
233
252
|
}
|
|
234
|
-
/**
|
|
235
|
-
export function
|
|
253
|
+
/** Keep fresh rotations from one Pi transcript in one private upstream restore pool. */
|
|
254
|
+
export function getManagedSessionRestoreScope(sessionName) {
|
|
255
|
+
return sessionName.replace(MANAGED_SESSION_FRESH_SUFFIX_PATTERN, "");
|
|
256
|
+
}
|
|
257
|
+
/** Stable for one Pi transcript and checkout generation; isolated from other concurrent transcripts. */
|
|
258
|
+
export function createManagedSessionRestoreKey(cwd, restoreScope = "", platform = process.platform) {
|
|
236
259
|
let canonicalCwd = resolve(cwd);
|
|
237
260
|
try {
|
|
238
261
|
canonicalCwd = realpathSync(canonicalCwd);
|
|
239
262
|
}
|
|
240
263
|
catch { }
|
|
241
|
-
const identity = resolveProjectGenerationIdentity(canonicalCwd);
|
|
264
|
+
const identity = resolveProjectGenerationIdentity(canonicalCwd, platform);
|
|
242
265
|
const material = identity ?? `unavailable:${canonicalCwd}`;
|
|
243
|
-
const digest = createHash("sha256")
|
|
266
|
+
const digest = createHash("sha256")
|
|
267
|
+
.update(`restore-v3:${material}:scope:${restoreScope}`)
|
|
268
|
+
.digest("hex")
|
|
269
|
+
.slice(0, MANAGED_SESSION_RESTORE_KEY_HASH_LENGTH);
|
|
244
270
|
return `${MANAGED_SESSION_NAME_PREFIX}${digest}`;
|
|
245
271
|
}
|
|
246
272
|
function hasValidEncryptionKey(parentEnv) {
|