pi-agent-browser-native 0.2.72 → 0.2.75
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 +33 -0
- package/README.md +14 -12
- package/dist/extensions/agent-browser/index.js +148 -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 +374 -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 +629 -430
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +148 -57
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +30 -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 +271 -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 +114 -107
- 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 +10 -9
- package/docs/COMMAND_REFERENCE.md +43 -23
- package/docs/ELECTRON.md +10 -10
- package/docs/RELEASE.md +3 -2
- package/docs/REQUIREMENTS.md +1 -1
- package/docs/SUPPORT_MATRIX.md +19 -18
- package/docs/TOOL_CONTRACT.md +29 -26
- 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,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Purpose: Canonical launch-scoped agent-browser flag metadata shared by runtime planning and agent-facing guidance.
|
|
3
|
-
* Responsibilities: Define which upstream flags require a fresh launch, explain why,
|
|
4
|
-
* Scope:
|
|
3
|
+
* Responsibilities: Define which upstream flags require a fresh launch, explain why, expose stable guidance labels, and share launch-token detection used by runtime and managed restore policy.
|
|
4
|
+
* Scope: Launch-scoped metadata and token classification; broader execution planning lives in runtime.ts.
|
|
5
5
|
*/
|
|
6
|
+
import { findCommandStartIndex } from "./argv-descriptor.js";
|
|
7
|
+
import { isBooleanFlagEnabled, optionalGlobalValueFlagConsumesNext } from "./argv-grammar.js";
|
|
6
8
|
export const LAUNCH_SCOPED_FLAG_DEFINITIONS = [
|
|
7
9
|
{
|
|
8
10
|
flag: "--auto-connect",
|
|
@@ -88,3 +90,87 @@ export const LAUNCH_SCOPED_FLAG_DEFINITIONS = [
|
|
|
88
90
|
export const LAUNCH_SCOPED_FLAGS = LAUNCH_SCOPED_FLAG_DEFINITIONS.map((definition) => definition.flag);
|
|
89
91
|
export const LAUNCH_SCOPED_FLAG_LABEL = LAUNCH_SCOPED_FLAGS.join(", ");
|
|
90
92
|
export const OPEN_RESULT_TAB_CORRECTION_FLAGS = new Set(["--profile", "--restore", "--session-name", "--state"]);
|
|
93
|
+
/** Launch modes that must never be combined with wrapper-managed restore state. */
|
|
94
|
+
export const MANAGED_RESTORE_INCOMPATIBLE_FLAGS = [
|
|
95
|
+
"--restore",
|
|
96
|
+
"--restore-save",
|
|
97
|
+
"--restore-check-url",
|
|
98
|
+
"--restore-check-text",
|
|
99
|
+
"--restore-check-fn",
|
|
100
|
+
"--allowed-domains",
|
|
101
|
+
"--profile",
|
|
102
|
+
"--state",
|
|
103
|
+
"--cdp",
|
|
104
|
+
"--auto-connect",
|
|
105
|
+
"--session-name",
|
|
106
|
+
"--provider",
|
|
107
|
+
"-p",
|
|
108
|
+
"--executable-path",
|
|
109
|
+
"--extension",
|
|
110
|
+
"--init-script",
|
|
111
|
+
"--enable",
|
|
112
|
+
"--args",
|
|
113
|
+
"--user-agent",
|
|
114
|
+
"--proxy",
|
|
115
|
+
"--proxy-bypass",
|
|
116
|
+
"--ignore-https-errors",
|
|
117
|
+
"--allow-file-access",
|
|
118
|
+
"--webgpu",
|
|
119
|
+
"--device",
|
|
120
|
+
"--engine",
|
|
121
|
+
];
|
|
122
|
+
/** Nonempty upstream env defaults that can replace or mutate the browser receiving restored state. */
|
|
123
|
+
export const MANAGED_RESTORE_INCOMPATIBLE_ENVS = [
|
|
124
|
+
"AGENT_BROWSER_RESTORE",
|
|
125
|
+
"AGENT_BROWSER_RESTORE_SAVE",
|
|
126
|
+
"AGENT_BROWSER_RESTORE_CHECK_URL",
|
|
127
|
+
"AGENT_BROWSER_RESTORE_CHECK_TEXT",
|
|
128
|
+
"AGENT_BROWSER_RESTORE_CHECK_FN",
|
|
129
|
+
"AGENT_BROWSER_ALLOWED_DOMAINS",
|
|
130
|
+
"AGENT_BROWSER_PROFILE",
|
|
131
|
+
"AGENT_BROWSER_STATE",
|
|
132
|
+
"AGENT_BROWSER_CDP",
|
|
133
|
+
"AGENT_BROWSER_NAMESPACE",
|
|
134
|
+
"AGENT_BROWSER_SESSION_NAME",
|
|
135
|
+
"AGENT_BROWSER_PROVIDER",
|
|
136
|
+
"AGENT_BROWSER_EXECUTABLE_PATH",
|
|
137
|
+
"AGENT_BROWSER_EXTENSIONS",
|
|
138
|
+
"AGENT_BROWSER_INIT_SCRIPTS",
|
|
139
|
+
"AGENT_BROWSER_ENABLE",
|
|
140
|
+
"AGENT_BROWSER_ARGS",
|
|
141
|
+
"AGENT_BROWSER_USER_AGENT",
|
|
142
|
+
"AGENT_BROWSER_PROXY",
|
|
143
|
+
"AGENT_BROWSER_PROXY_BYPASS",
|
|
144
|
+
"AGENT_BROWSER_PLUGINS",
|
|
145
|
+
"AGENT_BROWSER_IOS_DEVICE",
|
|
146
|
+
"AGENT_BROWSER_IOS_UDID",
|
|
147
|
+
"AGENT_BROWSER_ENGINE",
|
|
148
|
+
"HTTP_PROXY",
|
|
149
|
+
"HTTPS_PROXY",
|
|
150
|
+
"ALL_PROXY",
|
|
151
|
+
"http_proxy",
|
|
152
|
+
"https_proxy",
|
|
153
|
+
"all_proxy",
|
|
154
|
+
];
|
|
155
|
+
/** Boolean launch mutators block restore only when enabled. */
|
|
156
|
+
export const MANAGED_RESTORE_INCOMPATIBLE_BOOLEAN_ENVS = [
|
|
157
|
+
"AGENT_BROWSER_AUTO_CONNECT",
|
|
158
|
+
"AGENT_BROWSER_IGNORE_HTTPS_ERRORS",
|
|
159
|
+
"AGENT_BROWSER_ALLOW_FILE_ACCESS",
|
|
160
|
+
"AGENT_BROWSER_WEBGPU",
|
|
161
|
+
];
|
|
162
|
+
export function hasLaunchScopedFlagToken(args, flag) {
|
|
163
|
+
const commandStartIndex = findCommandStartIndex(args);
|
|
164
|
+
const command = commandStartIndex === undefined ? undefined : args[commandStartIndex];
|
|
165
|
+
return args.some((token, index) => {
|
|
166
|
+
if (token !== flag && !token.startsWith(`${flag}=`))
|
|
167
|
+
return false;
|
|
168
|
+
if (flag === "--auto-connect")
|
|
169
|
+
return isBooleanFlagEnabled(args, flag);
|
|
170
|
+
if (flag === "--restore" && token === "--restore" && optionalGlobalValueFlagConsumesNext(flag, args[index + 1]))
|
|
171
|
+
return true;
|
|
172
|
+
if (flag === "--state" && command === "wait" && commandStartIndex !== undefined && index > commandStartIndex)
|
|
173
|
+
return false;
|
|
174
|
+
return true;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Purpose: Identify and redact wrapper-owned managed restore capabilities.
|
|
2
|
+
// Scope: Pure string helpers shared by storage, policy, and presentation paths.
|
|
3
|
+
const MANAGED_SESSION_RESTORE_KEY_PATTERN = /piab-r(?:2)?-[a-f\d]{32}/gi;
|
|
4
|
+
const MANAGED_SESSION_RESTORE_KEY_EXACT_PATTERN = /^piab-r2-[a-f\d]{32}$/i;
|
|
5
|
+
const WRAPPER_MANAGED_SESSION_PREFIX = "piab-";
|
|
6
|
+
export function isWrapperManagedSessionName(value) {
|
|
7
|
+
return typeof value === "string" && value.toLowerCase().startsWith(WRAPPER_MANAGED_SESSION_PREFIX);
|
|
8
|
+
}
|
|
9
|
+
export function isManagedSessionRestoreKey(value) {
|
|
10
|
+
return typeof value === "string" && MANAGED_SESSION_RESTORE_KEY_EXACT_PATTERN.test(value);
|
|
11
|
+
}
|
|
12
|
+
export function extractManagedSessionRestoreKeys(value) {
|
|
13
|
+
return [...new Set(value.match(MANAGED_SESSION_RESTORE_KEY_PATTERN)?.map((key) => key.toLowerCase()) ?? [])];
|
|
14
|
+
}
|
|
15
|
+
export function containsManagedSessionRestoreKey(value) {
|
|
16
|
+
MANAGED_SESSION_RESTORE_KEY_PATTERN.lastIndex = 0;
|
|
17
|
+
return MANAGED_SESSION_RESTORE_KEY_PATTERN.test(value);
|
|
18
|
+
}
|
|
19
|
+
export function redactManagedSessionRestoreKeys(value) {
|
|
20
|
+
MANAGED_SESSION_RESTORE_KEY_PATTERN.lastIndex = 0;
|
|
21
|
+
return value.replace(MANAGED_SESSION_RESTORE_KEY_PATTERN, "[REDACTED MANAGED STATE]");
|
|
22
|
+
}
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
// Purpose: Serialize wrapper-owned daemon policy decisions across cooperating Pi processes.
|
|
2
|
+
// Responsibilities: Elect one live claim, bridge the pre-update v2 path, recover proven-dead current-protocol artifacts, and release only the caller's immutable token.
|
|
3
|
+
// Scope: Wrapper coordination only; agent-browser never reads these claims.
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { lstat, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { basename, dirname, join } from "node:path";
|
|
8
|
+
import { canonicalizeAgentBrowserNamespace, getAgentBrowserSessionIdentityKey } from "./argv-grammar.js";
|
|
9
|
+
import { processStartIdentitiesMatch, readProcessStartIdentity } from "./process-identity.js";
|
|
10
|
+
const POLICY_LOCK_WAIT_MS = 1_000;
|
|
11
|
+
const POLICY_LOCK_RETRY_MS = 10;
|
|
12
|
+
const POLICY_LOCK_MAX_BYTES = 4_096;
|
|
13
|
+
const LOCK_OWNER_FILE = "owner.json";
|
|
14
|
+
const LOCK_TICKET_FILE = "ticket.json";
|
|
15
|
+
function getCoordinationDirectory(platform = process.platform) {
|
|
16
|
+
if (platform !== "win32") {
|
|
17
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
18
|
+
return `/tmp/pi-agent-browser-policy${uid === undefined ? "" : `-${uid}`}`;
|
|
19
|
+
}
|
|
20
|
+
const user = process.env.USERNAME ?? process.env.USER ?? "unknown";
|
|
21
|
+
const suffix = createHash("sha256").update(user).digest("hex").slice(0, 12);
|
|
22
|
+
return join(tmpdir(), `pi-agent-browser-policy-${suffix}`);
|
|
23
|
+
}
|
|
24
|
+
async function ensureCoordinationDirectory(path, platform) {
|
|
25
|
+
try {
|
|
26
|
+
try {
|
|
27
|
+
await mkdir(path, { mode: 0o700 });
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code !== "EEXIST")
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const entry = await lstat(path);
|
|
34
|
+
if (entry.isSymbolicLink() || !entry.isDirectory())
|
|
35
|
+
return false;
|
|
36
|
+
if (platform !== "win32") {
|
|
37
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
38
|
+
if (uid === undefined || entry.uid !== uid || (entry.mode & 0o077) !== 0)
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function getPolicyLockDigest(sessionName, namespace) {
|
|
48
|
+
const identity = getAgentBrowserSessionIdentityKey(sessionName, canonicalizeAgentBrowserNamespace(namespace));
|
|
49
|
+
return createHash("sha256").update(identity).digest("hex");
|
|
50
|
+
}
|
|
51
|
+
export function getManagedSessionPolicyLockPath(sessionName, namespace) {
|
|
52
|
+
return join(getCoordinationDirectory(), `.pi-agent-browser-policy-${getPolicyLockDigest(sessionName, namespace)}.lock-v3`);
|
|
53
|
+
}
|
|
54
|
+
export function getLegacyManagedSessionPolicyLockPath(sessionName, namespace) {
|
|
55
|
+
return join(getCoordinationDirectory(), `.pi-agent-browser-policy-${getPolicyLockDigest(sessionName, namespace)}.lock-v2`);
|
|
56
|
+
}
|
|
57
|
+
function parseOwner(content) {
|
|
58
|
+
if (Buffer.byteLength(content) > POLICY_LOCK_MAX_BYTES)
|
|
59
|
+
return undefined;
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(content);
|
|
62
|
+
return parsed.version === 3
|
|
63
|
+
&& Number.isSafeInteger(parsed.pid) && (parsed.pid ?? 0) > 0
|
|
64
|
+
&& typeof parsed.startIdentity === "string" && parsed.startIdentity.length > 0
|
|
65
|
+
&& typeof parsed.token === "string" && parsed.token.length > 0
|
|
66
|
+
? parsed
|
|
67
|
+
: undefined;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function parseLegacyBridgeOwner(content) {
|
|
74
|
+
if (Buffer.byteLength(content) > POLICY_LOCK_MAX_BYTES)
|
|
75
|
+
return undefined;
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(content);
|
|
78
|
+
return (parsed.version === 2 || parsed.version === 3)
|
|
79
|
+
&& Number.isSafeInteger(parsed.pid) && (parsed.pid ?? 0) > 0
|
|
80
|
+
&& typeof parsed.startIdentity === "string" && parsed.startIdentity.length > 0
|
|
81
|
+
&& typeof parsed.token === "string" && parsed.token.length > 0
|
|
82
|
+
? parsed
|
|
83
|
+
: undefined;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function parseTicket(content, token) {
|
|
90
|
+
if (Buffer.byteLength(content) > POLICY_LOCK_MAX_BYTES)
|
|
91
|
+
return undefined;
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(content);
|
|
94
|
+
return parsed.version === 3
|
|
95
|
+
&& parsed.token === token
|
|
96
|
+
&& Number.isSafeInteger(parsed.ticket)
|
|
97
|
+
&& (parsed.ticket ?? 0) > 0
|
|
98
|
+
? parsed.ticket
|
|
99
|
+
: undefined;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function readClaim(path) {
|
|
106
|
+
try {
|
|
107
|
+
const directory = await lstat(path);
|
|
108
|
+
const ownerPath = join(path, LOCK_OWNER_FILE);
|
|
109
|
+
const ownerEntry = await lstat(ownerPath);
|
|
110
|
+
if (!directory.isDirectory() || directory.isSymbolicLink() || !ownerEntry.isFile() || ownerEntry.isSymbolicLink())
|
|
111
|
+
return undefined;
|
|
112
|
+
if (ownerEntry.size > POLICY_LOCK_MAX_BYTES)
|
|
113
|
+
return undefined;
|
|
114
|
+
if (process.platform !== "win32") {
|
|
115
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
116
|
+
if (uid === undefined || directory.uid !== uid || ownerEntry.uid !== uid || (directory.mode & 0o077) !== 0 || (ownerEntry.mode & 0o177) !== 0)
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
const owner = parseOwner(await readFile(ownerPath, "utf8"));
|
|
120
|
+
if (!owner)
|
|
121
|
+
return undefined;
|
|
122
|
+
const ticketPath = join(path, LOCK_TICKET_FILE);
|
|
123
|
+
try {
|
|
124
|
+
const ticketEntry = await lstat(ticketPath);
|
|
125
|
+
if (!ticketEntry.isFile() || ticketEntry.isSymbolicLink() || ticketEntry.size > POLICY_LOCK_MAX_BYTES)
|
|
126
|
+
return undefined;
|
|
127
|
+
if (process.platform !== "win32") {
|
|
128
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
129
|
+
if (uid === undefined || ticketEntry.uid !== uid || (ticketEntry.mode & 0o177) !== 0)
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
const ticket = parseTicket(await readFile(ticketPath, "utf8"), owner.token);
|
|
133
|
+
return ticket === undefined ? undefined : { owner, path, ticket };
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
return error.code === "ENOENT" ? { owner, path, ticket: null } : undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function readLegacyBridgeOwner(path) {
|
|
144
|
+
try {
|
|
145
|
+
const directory = await lstat(path);
|
|
146
|
+
const ownerPath = join(path, LOCK_OWNER_FILE);
|
|
147
|
+
const ownerEntry = await lstat(ownerPath);
|
|
148
|
+
if (!directory.isDirectory() || directory.isSymbolicLink() || !ownerEntry.isFile() || ownerEntry.isSymbolicLink())
|
|
149
|
+
return undefined;
|
|
150
|
+
if (ownerEntry.size > POLICY_LOCK_MAX_BYTES)
|
|
151
|
+
return undefined;
|
|
152
|
+
if (process.platform !== "win32") {
|
|
153
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
154
|
+
if (uid === undefined || directory.uid !== uid || ownerEntry.uid !== uid || (directory.mode & 0o077) !== 0 || (ownerEntry.mode & 0o177) !== 0)
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
return parseLegacyBridgeOwner(await readFile(ownerPath, "utf8"));
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function readClaims(basePath) {
|
|
164
|
+
const directory = dirname(basePath);
|
|
165
|
+
const prefix = `${basename(basePath)}.claim-`;
|
|
166
|
+
let names;
|
|
167
|
+
try {
|
|
168
|
+
names = await readdir(directory);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
const claims = [];
|
|
174
|
+
for (const name of names.filter((candidate) => candidate.startsWith(prefix))) {
|
|
175
|
+
const path = join(directory, name);
|
|
176
|
+
const claim = await readClaim(path);
|
|
177
|
+
if (!claim) {
|
|
178
|
+
try {
|
|
179
|
+
await lstat(path);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (error.code === "ENOENT")
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
if (name !== `${prefix}${claim.owner.token}`)
|
|
188
|
+
return undefined;
|
|
189
|
+
claims.push(claim);
|
|
190
|
+
}
|
|
191
|
+
return claims;
|
|
192
|
+
}
|
|
193
|
+
async function ownerAlive(owner) {
|
|
194
|
+
try {
|
|
195
|
+
process.kill(owner.pid, 0);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
const code = error.code;
|
|
199
|
+
if (code === "ESRCH")
|
|
200
|
+
return false;
|
|
201
|
+
if (code !== "EPERM")
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
const current = await readProcessStartIdentity(owner.pid);
|
|
205
|
+
return current === undefined ? undefined : processStartIdentitiesMatch(owner.startIdentity, current);
|
|
206
|
+
}
|
|
207
|
+
async function removeClaimOwnedBy(path, token) {
|
|
208
|
+
const current = await readClaim(path);
|
|
209
|
+
if (current?.owner.token !== token)
|
|
210
|
+
return false;
|
|
211
|
+
const movedPath = join(dirname(path), `.pi-agent-browser-policy-remove-${token}-${randomUUID()}`);
|
|
212
|
+
try {
|
|
213
|
+
await rename(path, movedPath);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
return error.code === "ENOENT";
|
|
217
|
+
}
|
|
218
|
+
const moved = await readClaim(movedPath);
|
|
219
|
+
if (moved?.owner.token !== token) {
|
|
220
|
+
try {
|
|
221
|
+
await rename(movedPath, path);
|
|
222
|
+
}
|
|
223
|
+
catch { }
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
await rm(movedPath, { force: true, recursive: true });
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
async function removeLegacyBridgeOwnedBy(path, token) {
|
|
230
|
+
const current = await readLegacyBridgeOwner(path);
|
|
231
|
+
if (current?.version !== 3 || current.token !== token)
|
|
232
|
+
return false;
|
|
233
|
+
const movedPath = join(dirname(path), `.pi-agent-browser-policy-bridge-remove-${token}-${randomUUID()}`);
|
|
234
|
+
try {
|
|
235
|
+
await rename(path, movedPath);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
return error.code === "ENOENT";
|
|
239
|
+
}
|
|
240
|
+
const moved = await readLegacyBridgeOwner(movedPath);
|
|
241
|
+
if (moved?.version !== 3 || moved.token !== token) {
|
|
242
|
+
try {
|
|
243
|
+
await rename(movedPath, path);
|
|
244
|
+
}
|
|
245
|
+
catch { }
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
await rm(movedPath, { force: true, recursive: true });
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
async function cleanDeadPolicyArtifacts(directory) {
|
|
252
|
+
let names;
|
|
253
|
+
try {
|
|
254
|
+
names = await readdir(directory);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
for (const name of names.filter((candidate) => candidate.startsWith(".pi-agent-browser-policy-remove-")
|
|
260
|
+
|| candidate.startsWith(".pi-agent-browser-policy-bridge-remove-")
|
|
261
|
+
|| candidate.includes(".lock-v2.bridge-candidate-")
|
|
262
|
+
|| candidate.includes(".lock-v2.candidate-")
|
|
263
|
+
|| candidate.includes(".lock-v3.candidate-"))) {
|
|
264
|
+
const path = join(directory, name);
|
|
265
|
+
const owner = await readLegacyBridgeOwner(path);
|
|
266
|
+
if (owner && await ownerAlive(owner) === false)
|
|
267
|
+
await rm(path, { force: true, recursive: true }).catch(() => undefined);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async function hasLegacyV2Contender(path) {
|
|
271
|
+
try {
|
|
272
|
+
const prefix = `${basename(path)}.candidate-`;
|
|
273
|
+
return (await readdir(dirname(path))).some((name) => name.startsWith(prefix));
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function acquireLegacyPolicyBridge(options) {
|
|
280
|
+
const path = getLegacyManagedSessionPolicyLockPath(options.sessionName, options.namespace);
|
|
281
|
+
const candidatePath = `${path}.bridge-candidate-${options.owner.token}`;
|
|
282
|
+
try {
|
|
283
|
+
await mkdir(candidatePath, { mode: 0o700 });
|
|
284
|
+
await writeFile(join(candidatePath, LOCK_OWNER_FILE), JSON.stringify(options.owner), { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
285
|
+
while (!options.signal?.aborted) {
|
|
286
|
+
const legacyContender = await hasLegacyV2Contender(path);
|
|
287
|
+
if (legacyContender === undefined)
|
|
288
|
+
return undefined;
|
|
289
|
+
if (legacyContender) {
|
|
290
|
+
if (Date.now() >= options.deadline)
|
|
291
|
+
return undefined;
|
|
292
|
+
await waitForRetry(options.signal);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
await rename(candidatePath, path);
|
|
297
|
+
const installed = await readLegacyBridgeOwner(path);
|
|
298
|
+
return installed?.version === 3 && installed.token === options.owner.token
|
|
299
|
+
? { release: async () => { await removeLegacyBridgeOwnedBy(path, options.owner.token); } }
|
|
300
|
+
: undefined;
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
if (!["EACCES", "EEXIST", "ENOTEMPTY", "EPERM"].includes(error.code ?? ""))
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
const observed = await readLegacyBridgeOwner(path);
|
|
307
|
+
if (!observed)
|
|
308
|
+
return undefined;
|
|
309
|
+
if (observed.version === 3 && await ownerAlive(observed) === false) {
|
|
310
|
+
if (await removeLegacyBridgeOwnedBy(path, observed.token))
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (Date.now() >= options.deadline)
|
|
314
|
+
return undefined;
|
|
315
|
+
await waitForRetry(options.signal);
|
|
316
|
+
}
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
await rm(candidatePath, { force: true, recursive: true }).catch(() => undefined);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function claimPrecedes(left, right) {
|
|
327
|
+
if (left.ticket === null)
|
|
328
|
+
return true;
|
|
329
|
+
if (right.ticket === null)
|
|
330
|
+
return false;
|
|
331
|
+
return left.ticket < right.ticket || (left.ticket === right.ticket && left.owner.token < right.owner.token);
|
|
332
|
+
}
|
|
333
|
+
function waitForRetry(signal) {
|
|
334
|
+
return new Promise((resolve) => {
|
|
335
|
+
if (signal?.aborted)
|
|
336
|
+
return resolve();
|
|
337
|
+
const timer = setTimeout(done, POLICY_LOCK_RETRY_MS);
|
|
338
|
+
function done() {
|
|
339
|
+
clearTimeout(timer);
|
|
340
|
+
signal?.removeEventListener("abort", done);
|
|
341
|
+
resolve();
|
|
342
|
+
}
|
|
343
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
export async function acquireManagedSessionPolicyLock(options) {
|
|
347
|
+
if (options.signal?.aborted)
|
|
348
|
+
return undefined;
|
|
349
|
+
const platform = process.platform;
|
|
350
|
+
const directory = getCoordinationDirectory(platform);
|
|
351
|
+
if (!await ensureCoordinationDirectory(directory, platform))
|
|
352
|
+
return undefined;
|
|
353
|
+
const basePath = getManagedSessionPolicyLockPath(options.sessionName, options.namespace);
|
|
354
|
+
const token = randomUUID();
|
|
355
|
+
const startIdentity = await readProcessStartIdentity(process.pid);
|
|
356
|
+
if (!startIdentity)
|
|
357
|
+
return undefined;
|
|
358
|
+
const owner = { pid: process.pid, startIdentity, token, version: 3 };
|
|
359
|
+
const candidatePath = `${basePath}.candidate-${token}`;
|
|
360
|
+
const claimPath = `${basePath}.claim-${token}`;
|
|
361
|
+
let claimPublished = false;
|
|
362
|
+
let legacyBridge;
|
|
363
|
+
let lockAcquired = false;
|
|
364
|
+
try {
|
|
365
|
+
await mkdir(candidatePath, { mode: 0o700 });
|
|
366
|
+
await writeFile(join(candidatePath, LOCK_OWNER_FILE), JSON.stringify(owner), { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
367
|
+
await rename(candidatePath, claimPath);
|
|
368
|
+
claimPublished = true;
|
|
369
|
+
const initialClaims = await readClaims(basePath);
|
|
370
|
+
if (!initialClaims)
|
|
371
|
+
return undefined;
|
|
372
|
+
const maxTicket = initialClaims.reduce((max, claim) => claim.ticket === null ? max : Math.max(max, claim.ticket), 0);
|
|
373
|
+
if (!Number.isSafeInteger(maxTicket + 1))
|
|
374
|
+
return undefined;
|
|
375
|
+
const ticket = { ticket: maxTicket + 1, token, version: 3 };
|
|
376
|
+
const ticketCandidatePath = join(claimPath, `.ticket-${token}.tmp`);
|
|
377
|
+
await writeFile(ticketCandidatePath, JSON.stringify(ticket), { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
378
|
+
await rename(ticketCandidatePath, join(claimPath, LOCK_TICKET_FILE));
|
|
379
|
+
const deadline = Date.now() + (options.timeoutMs ?? POLICY_LOCK_WAIT_MS);
|
|
380
|
+
while (!options.signal?.aborted) {
|
|
381
|
+
const claims = await readClaims(basePath);
|
|
382
|
+
if (!claims)
|
|
383
|
+
return undefined;
|
|
384
|
+
const ownClaim = claims.find((claim) => claim.owner.token === token);
|
|
385
|
+
if (!ownClaim || ownClaim.ticket !== ticket.ticket)
|
|
386
|
+
return undefined;
|
|
387
|
+
let blocked = false;
|
|
388
|
+
for (const claim of claims) {
|
|
389
|
+
if (claim.owner.token === token || !claimPrecedes(claim, ownClaim))
|
|
390
|
+
continue;
|
|
391
|
+
const alive = await ownerAlive(claim.owner);
|
|
392
|
+
if (alive === false) {
|
|
393
|
+
await removeClaimOwnedBy(claim.path, claim.owner.token);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
blocked = true;
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
if (!blocked) {
|
|
400
|
+
await cleanDeadPolicyArtifacts(directory);
|
|
401
|
+
legacyBridge = await acquireLegacyPolicyBridge({
|
|
402
|
+
deadline,
|
|
403
|
+
owner,
|
|
404
|
+
signal: options.signal,
|
|
405
|
+
sessionName: options.sessionName,
|
|
406
|
+
namespace: options.namespace,
|
|
407
|
+
});
|
|
408
|
+
if (!legacyBridge)
|
|
409
|
+
return undefined;
|
|
410
|
+
lockAcquired = true;
|
|
411
|
+
return { release: async () => {
|
|
412
|
+
await legacyBridge?.release();
|
|
413
|
+
await removeClaimOwnedBy(claimPath, token);
|
|
414
|
+
} };
|
|
415
|
+
}
|
|
416
|
+
if (Date.now() >= deadline)
|
|
417
|
+
return undefined;
|
|
418
|
+
await waitForRetry(options.signal);
|
|
419
|
+
}
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
finally {
|
|
426
|
+
await rm(candidatePath, { force: true, recursive: true }).catch(() => undefined);
|
|
427
|
+
if (!lockAcquired)
|
|
428
|
+
await legacyBridge?.release();
|
|
429
|
+
if (claimPublished && !lockAcquired)
|
|
430
|
+
await removeClaimOwnedBy(claimPath, token);
|
|
431
|
+
}
|
|
432
|
+
}
|