pi-agent-browser-native 0.1.1

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.
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Purpose: Build safe, deterministic agent-browser invocations for the pi-agent-browser extension.
3
+ * Responsibilities: Validate raw tool arguments, derive implicit session names from the pi session identity, detect explicit session usage, and build the effective CLI argument list passed to the upstream agent-browser binary.
4
+ * Scope: Pure runtime-planning helpers only; no subprocess execution or filesystem access lives here.
5
+ * Usage: Imported by the extension entrypoint and unit tests before spawning the upstream CLI.
6
+ * Invariants/Assumptions: The wrapper stays thin, preserves upstream command vocabulary, and only injects `--json` plus an implicit `--session` when appropriate.
7
+ */
8
+
9
+ import { createHash, randomUUID } from "node:crypto";
10
+ import { basename } from "node:path";
11
+
12
+ const STARTUP_SCOPED_FLAGS = ["--cdp", "--profile", "--session-name"] as const;
13
+ const INSPECTION_ALLOW_PATTERNS = [
14
+ /\bagent[_ -]?browser\s+--(?:help|version)\b/i,
15
+ /\bagent[_ -]?browser\b.*\b(?:help|version|docs?|documentation|tool contract|tool guidance|tool description)\b/i,
16
+ /\b(?:help|version|docs?|documentation|tool contract|tool guidance|tool description)\b.*\bagent[_ -]?browser\b/i,
17
+ /\bdebug(?:ging)?\b.*\b(?:agent[_ -]?browser|agent_browser|browser integration)\b/i,
18
+ /\bwhy\s+(?:isn't|is not|doesn't|does not)\b.*\b(?:agent[_ -]?browser|agent_browser)\b/i,
19
+ ];
20
+ const LEGACY_BASH_ALLOW_PATTERNS = [
21
+ /\b(?:bash-oriented workflow|bash workflow)\b/i,
22
+ /\b(?:use|via|through|with)\s+bash\b/i,
23
+ /\bnpx\s+agent-browser\b/i,
24
+ /\bagent-browser\s+--(?:help|version)\b/i,
25
+ /\bdebug(?:ging)?\b.*\b(?:agent[_ -]?browser|agent_browser|browser integration)\b/i,
26
+ ];
27
+
28
+ const GLOBAL_FLAGS_WITH_VALUES = new Set([
29
+ "--session",
30
+ "--cdp",
31
+ "--config",
32
+ "--profile",
33
+ "--session-name",
34
+ "--proxy",
35
+ "--proxy-bypass",
36
+ "--headers",
37
+ "--executable-path",
38
+ "--extension",
39
+ "--provider",
40
+ "-p",
41
+ "--engine",
42
+ "--state",
43
+ "--download-path",
44
+ "--screenshot-dir",
45
+ "--screenshot-format",
46
+ "--screenshot-quality",
47
+ "--color-scheme",
48
+ "--device",
49
+ "--port",
50
+ ]);
51
+ const SHELL_OPERATOR_TOKENS = new Set(["&&", "||", "|", ";", ">", ">>", "<"]);
52
+ const IMAGE_EXTENSION_TO_MIME_TYPE: Record<string, string> = {
53
+ ".gif": "image/gif",
54
+ ".jpeg": "image/jpeg",
55
+ ".jpg": "image/jpeg",
56
+ ".png": "image/png",
57
+ ".webp": "image/webp",
58
+ };
59
+ const MAX_PROJECT_SLUG_LENGTH = 24;
60
+
61
+ export interface CommandInfo {
62
+ command?: string;
63
+ subcommand?: string;
64
+ }
65
+
66
+ export interface ExecutionPlan {
67
+ commandInfo: CommandInfo;
68
+ effectiveArgs: string[];
69
+ sessionName?: string;
70
+ startupScopedFlags: string[];
71
+ usedImplicitSession: boolean;
72
+ validationError?: string;
73
+ }
74
+
75
+ export interface PromptPolicy {
76
+ allowAgentBrowserInspection: boolean;
77
+ allowLegacyAgentBrowserBash: boolean;
78
+ }
79
+
80
+ export function createEphemeralSessionSeed(): string {
81
+ return randomUUID();
82
+ }
83
+
84
+ export function createImplicitSessionName(
85
+ sessionId: string | undefined,
86
+ cwd: string,
87
+ ephemeralSeed: string,
88
+ ): string {
89
+ const slug =
90
+ basename(cwd)
91
+ .toLowerCase()
92
+ .replace(/[^a-z0-9]+/g, "-")
93
+ .replace(/^-+|-+$/g, "")
94
+ .slice(0, MAX_PROJECT_SLUG_LENGTH) || "project";
95
+ const stableSessionId = sessionId?.replace(/-/g, "").slice(0, 12);
96
+ if (stableSessionId && stableSessionId.length > 0) {
97
+ return `piab-${slug}-${stableSessionId}`;
98
+ }
99
+
100
+ const digest = createHash("sha256").update(`ephemeral:${cwd}:${ephemeralSeed}`).digest("hex").slice(0, 12);
101
+ return `piab-${slug}-${digest}`;
102
+ }
103
+
104
+ export function validateToolArgs(args: string[]): string | undefined {
105
+ if (args.length === 0) {
106
+ return "`args` must contain at least one agent-browser command token.";
107
+ }
108
+
109
+ const shellOperator = args.find((token) => SHELL_OPERATOR_TOKENS.has(token));
110
+ if (shellOperator) {
111
+ return `Do not pass shell operators like \`${shellOperator}\`. Pass exact agent-browser CLI arguments only.`;
112
+ }
113
+
114
+ return undefined;
115
+ }
116
+
117
+ function hasFlagToken(args: string[], flag: string): boolean {
118
+ return args.some((token) => token === flag || token.startsWith(`${flag}=`));
119
+ }
120
+
121
+ export function extractExplicitSessionName(args: string[]): string | undefined {
122
+ for (const [index, token] of args.entries()) {
123
+ if (token === "--session") {
124
+ return args[index + 1];
125
+ }
126
+ if (token.startsWith("--session=")) {
127
+ return token.slice("--session=".length);
128
+ }
129
+ }
130
+ return undefined;
131
+ }
132
+
133
+ export function getStartupScopedFlags(args: string[]): string[] {
134
+ return STARTUP_SCOPED_FLAGS.filter((flag) => hasFlagToken(args, flag));
135
+ }
136
+
137
+ export function buildPromptPolicy(prompt: string): PromptPolicy {
138
+ return {
139
+ allowAgentBrowserInspection: INSPECTION_ALLOW_PATTERNS.some((pattern) => pattern.test(prompt)),
140
+ allowLegacyAgentBrowserBash: LEGACY_BASH_ALLOW_PATTERNS.some((pattern) => pattern.test(prompt)),
141
+ };
142
+ }
143
+
144
+ function getMessageText(content: unknown): string {
145
+ if (typeof content === "string") return content;
146
+ if (!Array.isArray(content)) return "";
147
+
148
+ return content
149
+ .map((item) => {
150
+ if (typeof item !== "object" || item === null) return "";
151
+ return item.type === "text" && typeof item.text === "string" ? item.text : "";
152
+ })
153
+ .filter((text) => text.length > 0)
154
+ .join("\n");
155
+ }
156
+
157
+ export function getLatestUserPrompt(branch: unknown[]): string {
158
+ for (let index = branch.length - 1; index >= 0; index -= 1) {
159
+ const entry = branch[index];
160
+ if (typeof entry !== "object" || entry === null || !("type" in entry) || entry.type !== "message") {
161
+ continue;
162
+ }
163
+ const message = "message" in entry ? entry.message : undefined;
164
+ if (typeof message !== "object" || message === null || !("role" in message) || message.role !== "user") {
165
+ continue;
166
+ }
167
+ return getMessageText("content" in message ? message.content : undefined);
168
+ }
169
+ return "";
170
+ }
171
+
172
+ export function buildExecutionPlan(
173
+ args: string[],
174
+ options: { implicitSessionActive: boolean; implicitSessionName: string; useActiveSession: boolean },
175
+ ): ExecutionPlan {
176
+ const commandInfo = parseCommandInfo(args);
177
+ const explicitSessionName = extractExplicitSessionName(args);
178
+ const startupScopedFlags = getStartupScopedFlags(args);
179
+ const effectiveArgs = args.includes("--json") ? [] : ["--json"];
180
+ let sessionName = explicitSessionName;
181
+ let usedImplicitSession = false;
182
+ let validationError: string | undefined;
183
+
184
+ if (!explicitSessionName && options.useActiveSession) {
185
+ if (options.implicitSessionActive && startupScopedFlags.length > 0) {
186
+ validationError = [
187
+ `The current implicit agent-browser session is already running, so startup-scoped flags ${startupScopedFlags.join(", ")} would be ignored by upstream agent-browser.`,
188
+ "Reuse the existing implicit session without those flags, or start a fresh upstream session explicitly with `--session ...` (or `useActiveSession: false`) for a new launch.",
189
+ ].join(" ");
190
+ } else {
191
+ effectiveArgs.push("--session", options.implicitSessionName);
192
+ sessionName = options.implicitSessionName;
193
+ usedImplicitSession = true;
194
+ }
195
+ }
196
+
197
+ effectiveArgs.push(...args);
198
+
199
+ return {
200
+ commandInfo,
201
+ effectiveArgs,
202
+ sessionName,
203
+ startupScopedFlags,
204
+ usedImplicitSession,
205
+ validationError,
206
+ };
207
+ }
208
+
209
+ export function parseCommandInfo(args: string[]): CommandInfo {
210
+ const commands: string[] = [];
211
+
212
+ for (let index = 0; index < args.length; index += 1) {
213
+ const token = args[index];
214
+ if (token.startsWith("--session=")) {
215
+ continue;
216
+ }
217
+ if (token.startsWith("-")) {
218
+ const normalizedToken = token.split("=", 1)[0] ?? token;
219
+ if (GLOBAL_FLAGS_WITH_VALUES.has(normalizedToken) && !token.includes("=")) {
220
+ index += 1;
221
+ }
222
+ continue;
223
+ }
224
+ commands.push(token);
225
+ if (commands.length === 2) {
226
+ break;
227
+ }
228
+ }
229
+
230
+ return { command: commands[0], subcommand: commands[1] };
231
+ }
232
+
233
+ export function getImageMimeType(filePath: string): string | undefined {
234
+ const extension = filePath.toLowerCase().slice(filePath.lastIndexOf("."));
235
+ return IMAGE_EXTENSION_TO_MIME_TYPE[extension];
236
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Purpose: Create private temporary files for the pi-agent-browser extension without leaking artifacts broadly on disk.
3
+ * Responsibilities: Maintain a process-private temp root, prune stale temp roots from prior runs, create securely permissioned temp files, and best-effort clean the current run's temp root on process exit.
4
+ * Scope: Temporary artifact lifecycle only; callers decide what data to write and when to delete long-lived references.
5
+ * Usage: Imported by result/process helpers when they need secure spill files instead of world-readable shared tmp paths.
6
+ * Invariants/Assumptions: Temp artifacts live under the OS temp directory, the active run uses a dedicated 0700 directory, and files are created with exclusive 0600 permissions.
7
+ */
8
+
9
+ import { randomBytes } from "node:crypto";
10
+ import { chmod, mkdtemp, open, readdir, rm, stat } from "node:fs/promises";
11
+ import { rmSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+
15
+ const TEMP_ROOT_PREFIX = "pi-agent-browser-";
16
+ const STALE_TEMP_ROOT_MAX_AGE_MS = 24 * 60 * 60 * 1_000;
17
+
18
+ let sessionTempRootPromise: Promise<string> | undefined;
19
+ let exitCleanupRegistered = false;
20
+
21
+ async function pruneStaleTempRoots(currentTempRoot: string | undefined): Promise<void> {
22
+ const entries = await readdir(tmpdir(), { withFileTypes: true }).catch(() => []);
23
+ const cutoffTime = Date.now() - STALE_TEMP_ROOT_MAX_AGE_MS;
24
+
25
+ await Promise.all(
26
+ entries
27
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith(TEMP_ROOT_PREFIX))
28
+ .map(async (entry) => {
29
+ const path = join(tmpdir(), entry.name);
30
+ if (path === currentTempRoot) return;
31
+ const stats = await stat(path).catch(() => undefined);
32
+ if (!stats || stats.mtimeMs >= cutoffTime) return;
33
+ await rm(path, { force: true, recursive: true }).catch(() => undefined);
34
+ }),
35
+ );
36
+ }
37
+
38
+ function registerExitCleanup(tempRoot: string): void {
39
+ if (exitCleanupRegistered) return;
40
+ exitCleanupRegistered = true;
41
+ process.once("exit", () => {
42
+ try {
43
+ rmSync(tempRoot, { force: true, recursive: true });
44
+ } catch {
45
+ // Best-effort cleanup only.
46
+ }
47
+ });
48
+ }
49
+
50
+ export async function cleanupSecureTempArtifacts(): Promise<void> {
51
+ const tempRoot = await sessionTempRootPromise?.catch(() => undefined);
52
+ sessionTempRootPromise = undefined;
53
+ if (!tempRoot) return;
54
+ await rm(tempRoot, { force: true, recursive: true }).catch(() => undefined);
55
+ }
56
+
57
+ async function getSessionTempRoot(): Promise<string> {
58
+ if (!sessionTempRootPromise) {
59
+ sessionTempRootPromise = (async () => {
60
+ await pruneStaleTempRoots(undefined);
61
+ const tempRoot = await mkdtemp(join(tmpdir(), TEMP_ROOT_PREFIX));
62
+ await chmod(tempRoot, 0o700).catch(() => undefined);
63
+ registerExitCleanup(tempRoot);
64
+ return tempRoot;
65
+ })();
66
+ }
67
+
68
+ const tempRoot = await sessionTempRootPromise;
69
+ await pruneStaleTempRoots(tempRoot).catch(() => undefined);
70
+ return tempRoot;
71
+ }
72
+
73
+ export async function openSecureTempFile(prefix: string, suffix: string): Promise<{ fileHandle: Awaited<ReturnType<typeof open>>; path: string }> {
74
+ const tempRoot = await getSessionTempRoot();
75
+ const path = join(tempRoot, `${prefix}-${randomBytes(8).toString("hex")}${suffix}`);
76
+ const fileHandle = await open(path, "wx", 0o600);
77
+ return { fileHandle, path };
78
+ }
79
+
80
+ export async function writeSecureTempFile(options: {
81
+ content: string | Uint8Array;
82
+ prefix: string;
83
+ suffix: string;
84
+ }): Promise<string> {
85
+ const { content, prefix, suffix } = options;
86
+ const { fileHandle, path } = await openSecureTempFile(prefix, suffix);
87
+ try {
88
+ await fileHandle.writeFile(content);
89
+ } finally {
90
+ await fileHandle.close();
91
+ }
92
+ return path;
93
+ }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "pi-agent-browser-native",
3
+ "version": "0.1.1",
4
+ "description": "Native pi integration for agent-browser",
5
+ "type": "module",
6
+ "author": "Mitch Fultz (https://github.com/fitchmultz)",
7
+ "keywords": [
8
+ "pi-package",
9
+ "pi",
10
+ "pi-extension",
11
+ "extension",
12
+ "agent-browser",
13
+ "browser-automation"
14
+ ],
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/fitchmultz/pi-agent-browser.git"
19
+ },
20
+ "homepage": "https://github.com/fitchmultz/pi-agent-browser#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/fitchmultz/pi-agent-browser/issues"
23
+ },
24
+ "engines": {
25
+ "node": ">=20.6.0"
26
+ },
27
+ "files": [
28
+ "extensions",
29
+ "README.md",
30
+ "CHANGELOG.md",
31
+ "LICENSE",
32
+ "docs/ARCHITECTURE.md",
33
+ "docs/RELEASE.md",
34
+ "docs/REQUIREMENTS.md",
35
+ "docs/TOOL_CONTRACT.md"
36
+ ],
37
+ "pi": {
38
+ "extensions": [
39
+ "./extensions/agent-browser/index.ts"
40
+ ]
41
+ },
42
+ "peerDependencies": {
43
+ "@mariozechner/pi-coding-agent": "*",
44
+ "@sinclair/typebox": "*"
45
+ },
46
+ "devDependencies": {
47
+ "@mariozechner/pi-coding-agent": "^0.66.1",
48
+ "@sinclair/typebox": "^0.34.49",
49
+ "@types/node": "^25.5.2",
50
+ "tsx": "^4.21.0",
51
+ "typescript": "^6.0.2"
52
+ },
53
+ "scripts": {
54
+ "typecheck": "tsc --noEmit",
55
+ "test": "tsx --test test/**/*.test.ts",
56
+ "pack:dry-run": "npm pack --json --dry-run",
57
+ "verify": "npm run typecheck && npm run test",
58
+ "verify:package": "node ./scripts/verify-package.mjs",
59
+ "verify:release": "npm run verify && npm run verify:package"
60
+ }
61
+ }