pi-agent-browser-native 0.2.33 → 0.2.35
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 +46 -0
- package/README.md +47 -17
- package/docs/ARCHITECTURE.md +25 -13
- package/docs/COMMAND_REFERENCE.md +285 -47
- package/docs/ELECTRON.md +3 -3
- package/docs/RELEASE.md +22 -14
- package/docs/REQUIREMENTS.md +5 -5
- package/docs/SUPPORT_MATRIX.md +26 -22
- package/docs/TOOL_CONTRACT.md +97 -32
- package/extensions/agent-browser/index.ts +519 -2402
- package/extensions/agent-browser/lib/argv-descriptor.ts +90 -0
- package/extensions/agent-browser/lib/argv-grammar.ts +128 -0
- package/extensions/agent-browser/lib/command-policy.ts +71 -0
- package/extensions/agent-browser/lib/command-taxonomy.ts +336 -0
- package/extensions/agent-browser/lib/electron/cleanup.ts +1 -0
- package/extensions/agent-browser/lib/executable-path.ts +19 -0
- package/extensions/agent-browser/lib/input-modes/job.ts +62 -0
- package/extensions/agent-browser/lib/input-modes/params.ts +8 -8
- package/extensions/agent-browser/lib/input-modes.ts +3 -0
- package/extensions/agent-browser/lib/orchestration/batch-stdin.ts +65 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/browser-action-model.ts +154 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +149 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +77 -29
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +6 -2
- package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +33 -27
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +74 -23
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +67 -17
- package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +93 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +19 -123
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +32 -1
- package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +860 -0
- package/extensions/agent-browser/lib/playbook.ts +24 -23
- package/extensions/agent-browser/lib/prompt-policy.ts +122 -0
- package/extensions/agent-browser/lib/results/action-recommendations.ts +3 -23
- package/extensions/agent-browser/lib/results/categories.ts +1 -1
- package/extensions/agent-browser/lib/results/presentation/navigation.ts +2 -34
- package/extensions/agent-browser/lib/results/presentation/registry.ts +34 -6
- package/extensions/agent-browser/lib/results/presentation/semantic-action.ts +133 -0
- package/extensions/agent-browser/lib/results/presentation.ts +11 -6
- package/extensions/agent-browser/lib/runtime.ts +93 -227
- package/extensions/agent-browser/lib/session-page-state.ts +31 -14
- package/extensions/agent-browser/lib/temp.ts +148 -23
- package/package.json +4 -4
- package/scripts/agent-browser-capability-baseline.mjs +198 -1
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Invariants/Assumptions: One tool-call update token must govern all page-state observations from that invocation; stale overlapping updates must not overwrite newer state.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { isCloseCommand, isReadOnlyDiagnosticSessionTargetCommand } from "./command-taxonomy.js";
|
|
9
10
|
import { isRecord } from "./parsing.js";
|
|
10
11
|
|
|
11
12
|
export interface SessionTabTarget {
|
|
@@ -20,6 +21,7 @@ interface OrderedSessionTabTarget {
|
|
|
20
21
|
|
|
21
22
|
export interface SessionRefSnapshot {
|
|
22
23
|
refIds: string[];
|
|
24
|
+
refs?: Record<string, { name: string; role: string }>;
|
|
23
25
|
target?: SessionTabTarget;
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -132,12 +134,6 @@ function extractBatchResultCommand(item: Record<string, unknown>): string[] {
|
|
|
132
134
|
return Array.isArray(item.command) ? item.command.filter((token): token is string => typeof token === "string") : [];
|
|
133
135
|
}
|
|
134
136
|
|
|
135
|
-
const READ_ONLY_DIAGNOSTIC_SESSION_TARGET_COMMANDS = new Set(["console", "cookies", "errors", "network", "storage"]);
|
|
136
|
-
|
|
137
|
-
function isReadOnlyDiagnosticSessionTargetCommand(command: string | undefined, _subcommand: string | undefined): boolean {
|
|
138
|
-
return command !== undefined && READ_ONLY_DIAGNOSTIC_SESSION_TARGET_COMMANDS.has(command);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
137
|
export function extractSessionTabTargetFromCommandData(commandTokens: string[], data: unknown): SessionTabTarget | undefined {
|
|
142
138
|
const [command, subcommand] = commandTokens;
|
|
143
139
|
return isReadOnlyDiagnosticSessionTargetCommand(command, subcommand) ? undefined : extractSessionTabTargetFromData(data);
|
|
@@ -186,7 +182,7 @@ export function deriveSessionTabTarget(options: {
|
|
|
186
182
|
previousTarget?: SessionTabTarget;
|
|
187
183
|
subcommand?: string;
|
|
188
184
|
}): SessionTabTarget | undefined {
|
|
189
|
-
if (options.command
|
|
185
|
+
if (isCloseCommand(options.command)) {
|
|
190
186
|
return undefined;
|
|
191
187
|
}
|
|
192
188
|
const commandDataTarget = isReadOnlyDiagnosticSessionTargetCommand(options.command, options.subcommand)
|
|
@@ -234,10 +230,21 @@ function getRestoredSessionTabTarget(details: Record<string, unknown>, command:
|
|
|
234
230
|
return storedTarget;
|
|
235
231
|
}
|
|
236
232
|
|
|
233
|
+
function extractRefSnapshotRefs(data: unknown): Record<string, { name: string; role: string }> | undefined {
|
|
234
|
+
if (!isRecord(data) || !isRecord(data.refs)) return undefined;
|
|
235
|
+
const refs = Object.fromEntries(Object.entries(data.refs).flatMap(([refId, entry]) => {
|
|
236
|
+
if (!/^e\d+$/.test(refId) || !isRecord(entry) || typeof entry.name !== "string" || typeof entry.role !== "string") return [];
|
|
237
|
+
return [[refId, { name: entry.name, role: entry.role }] as const];
|
|
238
|
+
}));
|
|
239
|
+
return Object.keys(refs).length > 0 ? refs : undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
237
242
|
export function extractRefSnapshotFromData(data: unknown): SessionRefSnapshot | undefined {
|
|
238
243
|
if (!isRecord(data)) return undefined;
|
|
244
|
+
const refs = extractRefSnapshotRefs(data);
|
|
239
245
|
return {
|
|
240
246
|
refIds: isRecord(data.refs) ? Object.keys(data.refs).filter((refId) => /^e\d+$/.test(refId)) : [],
|
|
247
|
+
...(refs ? { refs } : {}),
|
|
241
248
|
target: extractSessionTabTargetFromData(data),
|
|
242
249
|
};
|
|
243
250
|
}
|
|
@@ -295,14 +302,24 @@ function getRestoredRefSnapshotInvalidation(details: Record<string, unknown>, co
|
|
|
295
302
|
}
|
|
296
303
|
|
|
297
304
|
function getRestoredRefSnapshot(details: Record<string, unknown>): SessionRefSnapshot | undefined {
|
|
298
|
-
|
|
299
|
-
|
|
305
|
+
const refSnapshot = isRecord(details.refSnapshot) ? details.refSnapshot : undefined;
|
|
306
|
+
if (!refSnapshot || !Array.isArray(refSnapshot.refIds)) return undefined;
|
|
307
|
+
const refIds = refSnapshot.refIds.filter((refId): refId is string => typeof refId === "string" && /^e\d+$/.test(refId));
|
|
308
|
+
const refRecord = isRecord(refSnapshot.refs) ? refSnapshot.refs : undefined;
|
|
309
|
+
const refEntries = refRecord
|
|
310
|
+
? Object.fromEntries(refIds.flatMap((refId) => {
|
|
311
|
+
const entry = refRecord[refId];
|
|
312
|
+
if (!isRecord(entry) || typeof entry.name !== "string" || typeof entry.role !== "string") return [];
|
|
313
|
+
return [[refId, { name: entry.name, role: entry.role }] as const];
|
|
314
|
+
}))
|
|
315
|
+
: undefined;
|
|
300
316
|
return {
|
|
301
317
|
refIds,
|
|
302
|
-
|
|
318
|
+
...(refEntries && Object.keys(refEntries).length > 0 ? { refs: refEntries } : {}),
|
|
319
|
+
target: isRecord(refSnapshot.target)
|
|
303
320
|
? normalizeSessionTabTarget({
|
|
304
|
-
title: typeof
|
|
305
|
-
url: typeof
|
|
321
|
+
title: typeof refSnapshot.target.title === "string" ? refSnapshot.target.title : undefined,
|
|
322
|
+
url: typeof refSnapshot.target.url === "string" ? refSnapshot.target.url : undefined,
|
|
306
323
|
})
|
|
307
324
|
: undefined,
|
|
308
325
|
};
|
|
@@ -340,7 +357,7 @@ function shouldApplyRefStateUpdate(options: {
|
|
|
340
357
|
}
|
|
341
358
|
|
|
342
359
|
function stripRefSnapshotOrder(snapshot: OrderedSessionRefSnapshot | SessionRefSnapshot | undefined): SessionRefSnapshot | undefined {
|
|
343
|
-
return snapshot ? { refIds: snapshot.refIds, target: snapshot.target } : undefined;
|
|
360
|
+
return snapshot ? { refIds: snapshot.refIds, ...(snapshot.refs ? { refs: snapshot.refs } : {}), target: snapshot.target } : undefined;
|
|
344
361
|
}
|
|
345
362
|
|
|
346
363
|
function stripRefSnapshotInvalidationOrder(invalidation: OrderedSessionRefSnapshotInvalidation | SessionRefSnapshotInvalidation | undefined): SessionRefSnapshotInvalidation | undefined {
|
|
@@ -367,7 +384,7 @@ export class SessionPageState {
|
|
|
367
384
|
if (!sessionName) continue;
|
|
368
385
|
const command = typeof details.command === "string" ? details.command : undefined;
|
|
369
386
|
const subcommand = typeof details.subcommand === "string" ? details.subcommand : undefined;
|
|
370
|
-
if (command
|
|
387
|
+
if (isCloseCommand(command) && message.isError !== true) {
|
|
371
388
|
restoredOrder += 1;
|
|
372
389
|
state.clearSession(sessionName);
|
|
373
390
|
continue;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Purpose: Create private temporary and persisted spill files for the pi-agent-browser extension without leaking artifacts broadly on disk.
|
|
3
|
-
* Responsibilities: Maintain a process-private temp root, stamp explicit ownership markers, enforce an aggregate temp-artifact disk budget, create securely permissioned temp files, create session-scoped persisted spill files for resumable sessions, prune explicitly owned stale temp roots from prior runs, and best-effort clean all owned roots on process exit.
|
|
3
|
+
* Responsibilities: Maintain a process-private temp root, stamp explicit ownership/protected-child markers, enforce an aggregate temp-artifact disk budget, create securely permissioned temp files, create session-scoped persisted spill files for resumable sessions, prune explicitly owned stale temp roots from prior runs without deleting protected children, and best-effort clean all owned roots on process exit.
|
|
4
4
|
* Scope: Artifact lifecycle helpers only; callers decide what data to write and when to delete or retain long-lived references.
|
|
5
5
|
* Usage: Imported by result/process helpers when they need secure spill files instead of world-readable shared tmp paths.
|
|
6
6
|
* Invariants/Assumptions: Temp artifacts live under the OS temp directory, each active run uses a dedicated 0700 directory, files are created with exclusive 0600 permissions, session-scoped persisted artifacts stay under the pi session directory, and stale pruning only touches roots with an explicit pi-agent-browser ownership marker.
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
import { execFile } from "node:child_process";
|
|
10
10
|
import { randomBytes } from "node:crypto";
|
|
11
|
-
import { rmSync } from "node:fs";
|
|
11
|
+
import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
12
12
|
import { chmod, mkdir, mkdtemp, open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
13
13
|
import { tmpdir } from "node:os";
|
|
14
|
-
import { dirname, join } from "node:path";
|
|
14
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
15
15
|
import { promisify } from "node:util";
|
|
16
16
|
|
|
17
17
|
import { isRecord, parsePositiveInteger } from "./parsing.js";
|
|
@@ -53,6 +53,7 @@ interface TempRootOwnershipRecord {
|
|
|
53
53
|
ownerPid?: number;
|
|
54
54
|
ownerProcessStartIdentity?: string;
|
|
55
55
|
ownerUid?: number;
|
|
56
|
+
protectedChildNames?: readonly string[];
|
|
56
57
|
version: number;
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -69,6 +70,7 @@ let sessionTempRootPromise: Promise<string> | undefined;
|
|
|
69
70
|
let exitCleanupRegistered = false;
|
|
70
71
|
let tempMutationQueue = Promise.resolve();
|
|
71
72
|
const ownedTempRoots = new Set<string>();
|
|
73
|
+
const protectedTempChildren = new Set<string>();
|
|
72
74
|
|
|
73
75
|
function getCurrentProcessUid(): number | undefined {
|
|
74
76
|
return typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
@@ -78,6 +80,13 @@ function isPositiveFiniteNumber(value: unknown): value is number {
|
|
|
78
80
|
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
function isProtectedTempChildName(value: unknown): value is string {
|
|
84
|
+
if (typeof value !== "string") return false;
|
|
85
|
+
if (value === "" || value === "." || value === ".." || value === TEMP_ROOT_MARKER_FILE_NAME) return false;
|
|
86
|
+
if (value.includes("/") || value.includes("\\")) return false;
|
|
87
|
+
return basename(value) === value;
|
|
88
|
+
}
|
|
89
|
+
|
|
81
90
|
function isTempRootOwnershipRecord(value: unknown): value is TempRootOwnershipRecord {
|
|
82
91
|
if (!isRecord(value)) return false;
|
|
83
92
|
if (value.kind !== TEMP_ROOT_MARKER_KIND || value.version !== TEMP_ROOT_MARKER_VERSION) return false;
|
|
@@ -92,6 +101,10 @@ function isTempRootOwnershipRecord(value: unknown): value is TempRootOwnershipRe
|
|
|
92
101
|
if (value.ownerUid !== undefined) {
|
|
93
102
|
if (typeof value.ownerUid !== "number" || !Number.isSafeInteger(value.ownerUid) || value.ownerUid < 0) return false;
|
|
94
103
|
}
|
|
104
|
+
if (value.protectedChildNames !== undefined) {
|
|
105
|
+
if (!Array.isArray(value.protectedChildNames)) return false;
|
|
106
|
+
if (!value.protectedChildNames.every(isProtectedTempChildName)) return false;
|
|
107
|
+
}
|
|
95
108
|
return true;
|
|
96
109
|
}
|
|
97
110
|
|
|
@@ -137,6 +150,82 @@ async function readTempRootOwnershipMarker(tempRoot: string): Promise<TempRootOw
|
|
|
137
150
|
}
|
|
138
151
|
}
|
|
139
152
|
|
|
153
|
+
function getProtectedTempChildName(tempRoot: string, childPath: string): string | undefined {
|
|
154
|
+
const normalizedTempRoot = resolve(tempRoot);
|
|
155
|
+
const normalizedChildPath = resolve(childPath);
|
|
156
|
+
if (dirname(normalizedChildPath) !== normalizedTempRoot) return undefined;
|
|
157
|
+
const childName = basename(normalizedChildPath);
|
|
158
|
+
return isProtectedTempChildName(childName) ? childName : undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function normalizeProtectedChildNames(names: Iterable<string>): string[] {
|
|
162
|
+
return [...new Set([...names].filter(isProtectedTempChildName))].sort();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getPersistedProtectedChildPaths(tempRoot: string, ownershipMarker: TempRootOwnershipRecord | undefined): Set<string> {
|
|
166
|
+
const normalizedTempRoot = resolve(tempRoot);
|
|
167
|
+
return new Set((ownershipMarker?.protectedChildNames ?? []).map((childName) => resolve(join(normalizedTempRoot, childName))));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function writeTempRootOwnershipMarkerRecord(
|
|
171
|
+
tempRoot: string,
|
|
172
|
+
markerRecord: TempRootOwnershipRecord,
|
|
173
|
+
options: { flag?: "wx" } = {},
|
|
174
|
+
): Promise<string> {
|
|
175
|
+
const markerPath = join(tempRoot, TEMP_ROOT_MARKER_FILE_NAME);
|
|
176
|
+
await writeFile(markerPath, JSON.stringify(markerRecord, null, 2), {
|
|
177
|
+
encoding: "utf8",
|
|
178
|
+
flag: options.flag,
|
|
179
|
+
mode: 0o600,
|
|
180
|
+
});
|
|
181
|
+
await chmod(markerPath, 0o600).catch(() => undefined);
|
|
182
|
+
return markerPath;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function persistProtectedTempChildren(tempRoot: string, protectedChildren: ReadonlySet<string>): Promise<void> {
|
|
186
|
+
if (protectedChildren.size === 0) return;
|
|
187
|
+
const ownershipMarker = await readTempRootOwnershipMarker(tempRoot);
|
|
188
|
+
if (!ownershipMarker) return;
|
|
189
|
+
const childNames = normalizeProtectedChildNames([
|
|
190
|
+
...(ownershipMarker.protectedChildNames ?? []),
|
|
191
|
+
...[...protectedChildren]
|
|
192
|
+
.map((path) => getProtectedTempChildName(tempRoot, path))
|
|
193
|
+
.filter((childName): childName is string => childName !== undefined),
|
|
194
|
+
]);
|
|
195
|
+
if (childNames.length === 0) return;
|
|
196
|
+
await writeTempRootOwnershipMarkerRecord(tempRoot, {
|
|
197
|
+
...ownershipMarker,
|
|
198
|
+
leaseUpdatedAtMs: Date.now(),
|
|
199
|
+
protectedChildNames: childNames,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function getExistingProtectedChildren(
|
|
204
|
+
tempRoot: string,
|
|
205
|
+
protectedChildren: ReadonlySet<string>,
|
|
206
|
+
): Promise<Set<string>> {
|
|
207
|
+
const normalizedTempRoot = resolve(tempRoot);
|
|
208
|
+
const existingChildren = new Set<string>();
|
|
209
|
+
for (const path of protectedChildren) {
|
|
210
|
+
const normalizedPath = resolve(path);
|
|
211
|
+
if (dirname(normalizedPath) !== normalizedTempRoot) continue;
|
|
212
|
+
if (await stat(normalizedPath).then((stats) => stats.isDirectory(), () => false)) {
|
|
213
|
+
existingChildren.add(normalizedPath);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return existingChildren;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function removeTempRootChildrenExcept(tempRoot: string, protectedChildren: ReadonlySet<string>): Promise<void> {
|
|
220
|
+
const entries = await readdir(tempRoot, { withFileTypes: true }).catch(() => []);
|
|
221
|
+
await Promise.all(entries.map(async (entry) => {
|
|
222
|
+
if (entry.name === TEMP_ROOT_MARKER_FILE_NAME) return;
|
|
223
|
+
const entryPath = join(tempRoot, entry.name);
|
|
224
|
+
if (protectedChildren.has(resolve(entryPath))) return;
|
|
225
|
+
await rm(entryPath, { force: true, recursive: true }).catch(() => undefined);
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
|
|
140
229
|
async function getProcessStartIdentity(pid: number | undefined): Promise<string | undefined> {
|
|
141
230
|
if (pid === undefined) return undefined;
|
|
142
231
|
if (!Number.isSafeInteger(pid) || pid <= 0) return undefined;
|
|
@@ -155,7 +244,6 @@ export async function writeSecureTempRootOwnershipMarker(
|
|
|
155
244
|
tempRoot: string,
|
|
156
245
|
options: TempRootOwnershipMarkerOptions = {},
|
|
157
246
|
): Promise<string> {
|
|
158
|
-
const markerPath = join(tempRoot, TEMP_ROOT_MARKER_FILE_NAME);
|
|
159
247
|
const createdAtMs = options.createdAtMs ?? Date.now();
|
|
160
248
|
const ownerPid = options.ownerPid ?? process.pid;
|
|
161
249
|
const markerRecord: TempRootOwnershipRecord = {
|
|
@@ -167,13 +255,10 @@ export async function writeSecureTempRootOwnershipMarker(
|
|
|
167
255
|
ownerUid: getCurrentProcessUid(),
|
|
168
256
|
version: TEMP_ROOT_MARKER_VERSION,
|
|
169
257
|
};
|
|
170
|
-
await
|
|
171
|
-
await chmod(markerPath, 0o600).catch(() => undefined);
|
|
172
|
-
return markerPath;
|
|
258
|
+
return await writeTempRootOwnershipMarkerRecord(tempRoot, markerRecord, { flag: "wx" });
|
|
173
259
|
}
|
|
174
260
|
|
|
175
261
|
async function refreshSecureTempRootLease(tempRoot: string): Promise<void> {
|
|
176
|
-
const markerPath = join(tempRoot, TEMP_ROOT_MARKER_FILE_NAME);
|
|
177
262
|
const ownershipMarker = await readTempRootOwnershipMarker(tempRoot);
|
|
178
263
|
if (!ownershipMarker) return;
|
|
179
264
|
if (ownershipMarker.ownerPid !== process.pid) return;
|
|
@@ -194,8 +279,7 @@ async function refreshSecureTempRootLease(tempRoot: string): Promise<void> {
|
|
|
194
279
|
ownerProcessStartIdentity: currentProcessStartIdentity ?? ownershipMarker.ownerProcessStartIdentity,
|
|
195
280
|
ownerUid: currentUid,
|
|
196
281
|
};
|
|
197
|
-
await
|
|
198
|
-
await chmod(markerPath, 0o600).catch(() => undefined);
|
|
282
|
+
await writeTempRootOwnershipMarkerRecord(tempRoot, refreshedMarker);
|
|
199
283
|
}
|
|
200
284
|
|
|
201
285
|
async function getMarkerOwnerLiveness(ownershipMarker: TempRootOwnershipRecord): Promise<ProcessLiveness> {
|
|
@@ -210,14 +294,10 @@ async function getMarkerOwnerLiveness(ownershipMarker: TempRootOwnershipRecord):
|
|
|
210
294
|
}
|
|
211
295
|
|
|
212
296
|
const currentProcessStartIdentity = await getProcessStartIdentity(pid);
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
currentProcessStartIdentity !== undefined &&
|
|
216
|
-
ownershipMarker.ownerProcessStartIdentity === currentProcessStartIdentity
|
|
217
|
-
) {
|
|
218
|
-
return "alive";
|
|
297
|
+
if (ownershipMarker.ownerProcessStartIdentity === undefined || currentProcessStartIdentity === undefined) {
|
|
298
|
+
return "unknown";
|
|
219
299
|
}
|
|
220
|
-
return "dead";
|
|
300
|
+
return ownershipMarker.ownerProcessStartIdentity === currentProcessStartIdentity ? "alive" : "dead";
|
|
221
301
|
}
|
|
222
302
|
|
|
223
303
|
async function pruneStaleTempRoots(currentTempRoot: string | undefined): Promise<void> {
|
|
@@ -243,22 +323,52 @@ async function pruneStaleTempRoots(currentTempRoot: string | undefined): Promise
|
|
|
243
323
|
}
|
|
244
324
|
const staleTimestampMs = ownershipMarker.leaseUpdatedAtMs ?? ownershipMarker.createdAtMs;
|
|
245
325
|
if (staleTimestampMs >= cutoffTime) return;
|
|
246
|
-
|
|
326
|
+
// Preserve roots when owner liveness cannot be proven; safe cleanup beats deleting another live process's files.
|
|
327
|
+
if ((await getMarkerOwnerLiveness(ownershipMarker)) !== "dead") return;
|
|
247
328
|
|
|
248
329
|
const stats = await stat(path).catch(() => undefined);
|
|
249
330
|
if (!stats?.isDirectory()) return;
|
|
331
|
+
const protectedChildren = await getExistingProtectedChildren(
|
|
332
|
+
path,
|
|
333
|
+
getPersistedProtectedChildPaths(path, ownershipMarker),
|
|
334
|
+
);
|
|
335
|
+
if (protectedChildren.size > 0) {
|
|
336
|
+
await removeTempRootChildrenExcept(path, protectedChildren);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
250
339
|
await rm(path, { force: true, recursive: true }).catch(() => undefined);
|
|
251
340
|
}),
|
|
252
341
|
);
|
|
253
342
|
}
|
|
254
343
|
|
|
344
|
+
function getProtectedChildrenForRoot(tempRoot: string): Set<string> {
|
|
345
|
+
const normalizedTempRoot = resolve(tempRoot);
|
|
346
|
+
return new Set(
|
|
347
|
+
[...protectedTempChildren].filter((path) => dirname(path) === normalizedTempRoot && existsSync(path)),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function removeTempRootChildrenExceptSync(tempRoot: string, protectedChildren: ReadonlySet<string>): void {
|
|
352
|
+
for (const entry of readdirSync(tempRoot, { withFileTypes: true })) {
|
|
353
|
+
if (entry.name === TEMP_ROOT_MARKER_FILE_NAME) continue;
|
|
354
|
+
const entryPath = join(tempRoot, entry.name);
|
|
355
|
+
if (protectedChildren.has(resolve(entryPath))) continue;
|
|
356
|
+
rmSync(entryPath, { force: true, recursive: true });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
255
360
|
function registerExitCleanup(): void {
|
|
256
361
|
if (exitCleanupRegistered) return;
|
|
257
362
|
exitCleanupRegistered = true;
|
|
258
363
|
process.once("exit", () => {
|
|
259
364
|
for (const tempRoot of ownedTempRoots) {
|
|
260
365
|
try {
|
|
261
|
-
|
|
366
|
+
const protectedChildren = getProtectedChildrenForRoot(tempRoot);
|
|
367
|
+
if (protectedChildren.size === 0) {
|
|
368
|
+
rmSync(tempRoot, { force: true, recursive: true });
|
|
369
|
+
} else {
|
|
370
|
+
removeTempRootChildrenExceptSync(tempRoot, protectedChildren);
|
|
371
|
+
}
|
|
262
372
|
} catch {
|
|
263
373
|
// Best-effort cleanup only.
|
|
264
374
|
}
|
|
@@ -284,13 +394,28 @@ async function assertSecureTempRootBudget(tempRoot: string, additionalBytes: num
|
|
|
284
394
|
}
|
|
285
395
|
}
|
|
286
396
|
|
|
287
|
-
export async function cleanupSecureTempArtifacts(): Promise<void> {
|
|
397
|
+
export async function cleanupSecureTempArtifacts(options: { preservePaths?: readonly string[] } = {}): Promise<void> {
|
|
288
398
|
await enqueueTempMutation(async () => {
|
|
289
399
|
const tempRoot = await sessionTempRootPromise?.catch(() => undefined);
|
|
290
|
-
sessionTempRootPromise = undefined;
|
|
291
400
|
if (!tempRoot) return;
|
|
292
|
-
|
|
293
|
-
|
|
401
|
+
const normalizedTempRoot = resolve(tempRoot);
|
|
402
|
+
for (const path of options.preservePaths ?? []) {
|
|
403
|
+
const childName = getProtectedTempChildName(normalizedTempRoot, path);
|
|
404
|
+
if (childName) protectedTempChildren.add(resolve(join(normalizedTempRoot, childName)));
|
|
405
|
+
}
|
|
406
|
+
const preservedChildren = await getExistingProtectedChildren(normalizedTempRoot, protectedTempChildren);
|
|
407
|
+
for (const path of protectedTempChildren) {
|
|
408
|
+
if (dirname(path) === normalizedTempRoot && !preservedChildren.has(path)) protectedTempChildren.delete(path);
|
|
409
|
+
}
|
|
410
|
+
if (preservedChildren.size === 0) {
|
|
411
|
+
sessionTempRootPromise = undefined;
|
|
412
|
+
ownedTempRoots.delete(tempRoot);
|
|
413
|
+
await rm(tempRoot, { force: true, recursive: true }).catch(() => undefined);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
await persistProtectedTempChildren(tempRoot, preservedChildren);
|
|
417
|
+
await removeTempRootChildrenExcept(tempRoot, preservedChildren);
|
|
418
|
+
await refreshSecureTempRootLease(tempRoot).catch(() => undefined);
|
|
294
419
|
});
|
|
295
420
|
}
|
|
296
421
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-agent-browser-native",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.35",
|
|
4
4
|
"description": "pi extension that exposes agent-browser as a native tool for browser automation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Mitch Fultz (https://github.com/fitchmultz)",
|
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
"typebox": "*"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@earendil-works/pi-ai": "^0.
|
|
60
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
61
|
-
"@earendil-works/pi-tui": "^0.
|
|
59
|
+
"@earendil-works/pi-ai": "^0.76.0",
|
|
60
|
+
"@earendil-works/pi-coding-agent": "^0.76.0",
|
|
61
|
+
"@earendil-works/pi-tui": "^0.76.0",
|
|
62
62
|
"@types/node": "^25.6.1",
|
|
63
63
|
"tsx": "^4.21.0",
|
|
64
64
|
"typebox": "^1.1.38",
|