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
|
@@ -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
|
+
}
|