pi-agent-browser-native 0.2.34 → 0.2.36
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 +44 -0
- package/README.md +25 -15
- package/docs/ARCHITECTURE.md +19 -13
- package/docs/COMMAND_REFERENCE.md +274 -44
- package/docs/ELECTRON.md +3 -3
- package/docs/RELEASE.md +11 -11
- package/docs/REQUIREMENTS.md +5 -5
- package/docs/SUPPORT_MATRIX.md +43 -24
- package/docs/TOOL_CONTRACT.md +50 -30
- package/extensions/agent-browser/index.ts +518 -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/params.ts +6 -6
- 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 +56 -30
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +13 -3
- package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +33 -27
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +48 -22
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +39 -10
- package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +93 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +98 -124
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +40 -1
- package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +860 -0
- package/extensions/agent-browser/lib/playbook.ts +10 -10
- 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/presentation/navigation.ts +2 -34
- 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
|
@@ -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.36",
|
|
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",
|