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.
Files changed (44) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +47 -17
  3. package/docs/ARCHITECTURE.md +25 -13
  4. package/docs/COMMAND_REFERENCE.md +285 -47
  5. package/docs/ELECTRON.md +3 -3
  6. package/docs/RELEASE.md +22 -14
  7. package/docs/REQUIREMENTS.md +5 -5
  8. package/docs/SUPPORT_MATRIX.md +26 -22
  9. package/docs/TOOL_CONTRACT.md +97 -32
  10. package/extensions/agent-browser/index.ts +519 -2402
  11. package/extensions/agent-browser/lib/argv-descriptor.ts +90 -0
  12. package/extensions/agent-browser/lib/argv-grammar.ts +128 -0
  13. package/extensions/agent-browser/lib/command-policy.ts +71 -0
  14. package/extensions/agent-browser/lib/command-taxonomy.ts +336 -0
  15. package/extensions/agent-browser/lib/electron/cleanup.ts +1 -0
  16. package/extensions/agent-browser/lib/executable-path.ts +19 -0
  17. package/extensions/agent-browser/lib/input-modes/job.ts +62 -0
  18. package/extensions/agent-browser/lib/input-modes/params.ts +8 -8
  19. package/extensions/agent-browser/lib/input-modes.ts +3 -0
  20. package/extensions/agent-browser/lib/orchestration/batch-stdin.ts +65 -0
  21. package/extensions/agent-browser/lib/orchestration/browser-run/browser-action-model.ts +154 -0
  22. package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +149 -0
  23. package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +77 -29
  24. package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +6 -2
  25. package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +33 -27
  26. package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +74 -23
  27. package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +67 -17
  28. package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +93 -0
  29. package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +19 -123
  30. package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +32 -1
  31. package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +860 -0
  32. package/extensions/agent-browser/lib/playbook.ts +24 -23
  33. package/extensions/agent-browser/lib/prompt-policy.ts +122 -0
  34. package/extensions/agent-browser/lib/results/action-recommendations.ts +3 -23
  35. package/extensions/agent-browser/lib/results/categories.ts +1 -1
  36. package/extensions/agent-browser/lib/results/presentation/navigation.ts +2 -34
  37. package/extensions/agent-browser/lib/results/presentation/registry.ts +34 -6
  38. package/extensions/agent-browser/lib/results/presentation/semantic-action.ts +133 -0
  39. package/extensions/agent-browser/lib/results/presentation.ts +11 -6
  40. package/extensions/agent-browser/lib/runtime.ts +93 -227
  41. package/extensions/agent-browser/lib/session-page-state.ts +31 -14
  42. package/extensions/agent-browser/lib/temp.ts +148 -23
  43. package/package.json +4 -4
  44. 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 === "close") {
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
- if (!isRecord(details.refSnapshot) || !Array.isArray(details.refSnapshot.refIds)) return undefined;
299
- const refIds = details.refSnapshot.refIds.filter((refId): refId is string => typeof refId === "string" && /^e\d+$/.test(refId));
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
- target: isRecord(details.refSnapshot.target)
318
+ ...(refEntries && Object.keys(refEntries).length > 0 ? { refs: refEntries } : {}),
319
+ target: isRecord(refSnapshot.target)
303
320
  ? normalizeSessionTabTarget({
304
- title: typeof details.refSnapshot.target.title === "string" ? details.refSnapshot.target.title : undefined,
305
- url: typeof details.refSnapshot.target.url === "string" ? details.refSnapshot.target.url : undefined,
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 === "close" && message.isError !== true) {
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 writeFile(markerPath, JSON.stringify(markerRecord, null, 2), { encoding: "utf8", flag: "wx", mode: 0o600 });
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 writeFile(markerPath, JSON.stringify(refreshedMarker, null, 2), { encoding: "utf8", mode: 0o600 });
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
- ownershipMarker.ownerProcessStartIdentity !== undefined &&
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
- if ((await getMarkerOwnerLiveness(ownershipMarker)) === "alive") return;
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
- rmSync(tempRoot, { force: true, recursive: true });
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
- ownedTempRoots.delete(tempRoot);
293
- await rm(tempRoot, { force: true, recursive: true }).catch(() => undefined);
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.33",
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.75.4",
60
- "@earendil-works/pi-coding-agent": "^0.75.4",
61
- "@earendil-works/pi-tui": "^0.75.4",
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",