pi-agent-browser-native 0.1.5 → 0.2.0
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 +23 -0
- package/README.md +89 -2
- package/docs/ARCHITECTURE.md +7 -3
- package/docs/RELEASE.md +1 -1
- package/docs/TOOL_CONTRACT.md +18 -10
- package/extensions/agent-browser/index.ts +185 -62
- package/extensions/agent-browser/lib/process.ts +91 -6
- package/extensions/agent-browser/lib/results/envelope.ts +102 -0
- package/extensions/agent-browser/lib/results/presentation.ts +461 -0
- package/extensions/agent-browser/lib/results/shared.ts +91 -0
- package/extensions/agent-browser/lib/results/snapshot.ts +648 -0
- package/extensions/agent-browser/lib/results.ts +8 -934
- package/extensions/agent-browser/lib/runtime.ts +66 -24
- package/extensions/agent-browser/lib/temp.ts +159 -16
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
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.
|
|
3
|
+
* Responsibilities: Validate raw tool arguments, derive implicit session names from the pi session identity, resolve implicit-session timeout/state helpers, detect explicit session usage, and build the effective CLI argument list passed to the upstream agent-browser binary.
|
|
4
4
|
* Scope: Pure runtime-planning helpers only; no subprocess execution or filesystem access lives here.
|
|
5
5
|
* Usage: Imported by the extension entrypoint and unit tests before spawning the upstream CLI.
|
|
6
6
|
* Invariants/Assumptions: The wrapper stays thin, preserves upstream command vocabulary, and only injects `--json` plus an implicit `--session` when appropriate.
|
|
@@ -11,13 +11,11 @@ import { basename } from "node:path";
|
|
|
11
11
|
|
|
12
12
|
const STARTUP_SCOPED_FLAGS = ["--cdp", "--profile", "--session-name"] as const;
|
|
13
13
|
const BRAVE_API_KEY_ENV = "BRAVE_API_KEY";
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
/\bwhy\s+(?:isn't|is not|doesn't|does not)\b.*\b(?:agent[_ -]?browser|agent_browser)\b/i,
|
|
20
|
-
];
|
|
14
|
+
const AGENT_BROWSER_IDLE_TIMEOUT_ENV = "AGENT_BROWSER_IDLE_TIMEOUT_MS";
|
|
15
|
+
const IMPLICIT_SESSION_IDLE_TIMEOUT_ENV = "PI_AGENT_BROWSER_IMPLICIT_SESSION_IDLE_TIMEOUT_MS";
|
|
16
|
+
const IMPLICIT_SESSION_CLOSE_TIMEOUT_ENV = "PI_AGENT_BROWSER_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS";
|
|
17
|
+
const DEFAULT_IMPLICIT_SESSION_IDLE_TIMEOUT_MS = 15 * 60 * 1000;
|
|
18
|
+
const DEFAULT_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS = 5_000;
|
|
21
19
|
const LEGACY_BASH_ALLOW_PATTERNS = [
|
|
22
20
|
/\b(?:bash-oriented workflow|bash workflow)\b/i,
|
|
23
21
|
/\b(?:use|via|through|with)\s+bash\b/i,
|
|
@@ -50,13 +48,6 @@ const GLOBAL_FLAGS_WITH_VALUES = new Set([
|
|
|
50
48
|
"--port",
|
|
51
49
|
]);
|
|
52
50
|
const SHELL_OPERATOR_TOKENS = new Set(["&&", "||", "|", ";", ">", ">>", "<"]);
|
|
53
|
-
const IMAGE_EXTENSION_TO_MIME_TYPE: Record<string, string> = {
|
|
54
|
-
".gif": "image/gif",
|
|
55
|
-
".jpeg": "image/jpeg",
|
|
56
|
-
".jpg": "image/jpeg",
|
|
57
|
-
".png": "image/png",
|
|
58
|
-
".webp": "image/webp",
|
|
59
|
-
};
|
|
60
51
|
const MAX_PROJECT_SLUG_LENGTH = 24;
|
|
61
52
|
|
|
62
53
|
export interface CommandInfo {
|
|
@@ -64,9 +55,19 @@ export interface CommandInfo {
|
|
|
64
55
|
subcommand?: string;
|
|
65
56
|
}
|
|
66
57
|
|
|
58
|
+
export type SessionMode = "auto" | "fresh";
|
|
59
|
+
|
|
60
|
+
export interface SessionRecoveryHint {
|
|
61
|
+
exampleArgs: string[];
|
|
62
|
+
exampleParams: { args: string[]; sessionMode: "fresh" };
|
|
63
|
+
reason: string;
|
|
64
|
+
recommendedSessionMode: "fresh";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
67
|
export interface ExecutionPlan {
|
|
68
68
|
commandInfo: CommandInfo;
|
|
69
69
|
effectiveArgs: string[];
|
|
70
|
+
recoveryHint?: SessionRecoveryHint;
|
|
70
71
|
sessionName?: string;
|
|
71
72
|
startupScopedFlags: string[];
|
|
72
73
|
usedImplicitSession: boolean;
|
|
@@ -74,7 +75,6 @@ export interface ExecutionPlan {
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
export interface PromptPolicy {
|
|
77
|
-
allowAgentBrowserInspection: boolean;
|
|
78
78
|
allowLegacyAgentBrowserBash: boolean;
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -82,6 +82,44 @@ export function hasUsableBraveApiKey(apiKey: string | null | undefined = process
|
|
|
82
82
|
return typeof apiKey === "string" && apiKey.trim().length > 0;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
function parseTimeoutMs(rawValue: string | undefined, minimumValue: number): number | undefined {
|
|
86
|
+
if (typeof rawValue !== "string") return undefined;
|
|
87
|
+
const normalizedValue = rawValue.trim();
|
|
88
|
+
if (!/^\d+$/.test(normalizedValue)) return undefined;
|
|
89
|
+
const parsedValue = Number(normalizedValue);
|
|
90
|
+
if (!Number.isSafeInteger(parsedValue) || parsedValue < minimumValue) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
return parsedValue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getImplicitSessionIdleTimeoutMs(env: NodeJS.ProcessEnv = process.env): string {
|
|
97
|
+
return String(
|
|
98
|
+
parseTimeoutMs(env[IMPLICIT_SESSION_IDLE_TIMEOUT_ENV], 0) ??
|
|
99
|
+
parseTimeoutMs(env[AGENT_BROWSER_IDLE_TIMEOUT_ENV], 0) ??
|
|
100
|
+
DEFAULT_IMPLICIT_SESSION_IDLE_TIMEOUT_MS,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getImplicitSessionCloseTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
|
|
105
|
+
return parseTimeoutMs(env[IMPLICIT_SESSION_CLOSE_TIMEOUT_ENV], 0) ?? DEFAULT_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function resolveImplicitSessionActiveState(options: {
|
|
109
|
+
command?: string;
|
|
110
|
+
priorActive: boolean;
|
|
111
|
+
succeeded: boolean;
|
|
112
|
+
usedImplicitSession: boolean;
|
|
113
|
+
}): boolean {
|
|
114
|
+
const { command, priorActive, succeeded, usedImplicitSession } = options;
|
|
115
|
+
if (!usedImplicitSession) return priorActive;
|
|
116
|
+
if (command === "close") {
|
|
117
|
+
return succeeded ? false : priorActive;
|
|
118
|
+
}
|
|
119
|
+
if (!command) return priorActive;
|
|
120
|
+
return priorActive || succeeded;
|
|
121
|
+
}
|
|
122
|
+
|
|
85
123
|
export function createEphemeralSessionSeed(): string {
|
|
86
124
|
return randomUUID();
|
|
87
125
|
}
|
|
@@ -141,7 +179,6 @@ export function getStartupScopedFlags(args: string[]): string[] {
|
|
|
141
179
|
|
|
142
180
|
export function buildPromptPolicy(prompt: string): PromptPolicy {
|
|
143
181
|
return {
|
|
144
|
-
allowAgentBrowserInspection: INSPECTION_ALLOW_PATTERNS.some((pattern) => pattern.test(prompt)),
|
|
145
182
|
allowLegacyAgentBrowserBash: LEGACY_BASH_ALLOW_PATTERNS.some((pattern) => pattern.test(prompt)),
|
|
146
183
|
};
|
|
147
184
|
}
|
|
@@ -176,21 +213,29 @@ export function getLatestUserPrompt(branch: unknown[]): string {
|
|
|
176
213
|
|
|
177
214
|
export function buildExecutionPlan(
|
|
178
215
|
args: string[],
|
|
179
|
-
options: { implicitSessionActive: boolean; implicitSessionName: string;
|
|
216
|
+
options: { implicitSessionActive: boolean; implicitSessionName: string; sessionMode: SessionMode },
|
|
180
217
|
): ExecutionPlan {
|
|
181
218
|
const commandInfo = parseCommandInfo(args);
|
|
182
219
|
const explicitSessionName = extractExplicitSessionName(args);
|
|
183
220
|
const startupScopedFlags = getStartupScopedFlags(args);
|
|
184
221
|
const effectiveArgs = args.includes("--json") ? [] : ["--json"];
|
|
222
|
+
let recoveryHint: SessionRecoveryHint | undefined;
|
|
185
223
|
let sessionName = explicitSessionName;
|
|
186
224
|
let usedImplicitSession = false;
|
|
187
225
|
let validationError: string | undefined;
|
|
188
226
|
|
|
189
|
-
if (!explicitSessionName && options.
|
|
227
|
+
if (!explicitSessionName && options.sessionMode === "auto") {
|
|
190
228
|
if (options.implicitSessionActive && startupScopedFlags.length > 0) {
|
|
229
|
+
recoveryHint = {
|
|
230
|
+
exampleArgs: args,
|
|
231
|
+
exampleParams: { args, sessionMode: "fresh" },
|
|
232
|
+
reason:
|
|
233
|
+
"Startup-scoped flags like --profile, --session-name, and --cdp need a fresh upstream launch once the implicit session is already active.",
|
|
234
|
+
recommendedSessionMode: "fresh",
|
|
235
|
+
};
|
|
191
236
|
validationError = [
|
|
192
237
|
`The current implicit agent-browser session is already running, so startup-scoped flags ${startupScopedFlags.join(", ")} would be ignored by upstream agent-browser.`,
|
|
193
|
-
"
|
|
238
|
+
"Retry this call with `sessionMode: \"fresh\"` to force a fresh upstream launch, or pass an explicit `--session ...` if you want to name the new session yourself.",
|
|
194
239
|
].join(" ");
|
|
195
240
|
} else {
|
|
196
241
|
effectiveArgs.push("--session", options.implicitSessionName);
|
|
@@ -204,6 +249,7 @@ export function buildExecutionPlan(
|
|
|
204
249
|
return {
|
|
205
250
|
commandInfo,
|
|
206
251
|
effectiveArgs,
|
|
252
|
+
recoveryHint,
|
|
207
253
|
sessionName,
|
|
208
254
|
startupScopedFlags,
|
|
209
255
|
usedImplicitSession,
|
|
@@ -235,7 +281,3 @@ export function parseCommandInfo(args: string[]): CommandInfo {
|
|
|
235
281
|
return { command: commands[0], subcommand: commands[1] };
|
|
236
282
|
}
|
|
237
283
|
|
|
238
|
-
export function getImageMimeType(filePath: string): string | undefined {
|
|
239
|
-
const extension = filePath.toLowerCase().slice(filePath.lastIndexOf("."));
|
|
240
|
-
return IMAGE_EXTENSION_TO_MIME_TYPE[extension];
|
|
241
|
-
}
|
|
@@ -1,26 +1,115 @@
|
|
|
1
1
|
/**
|
|
2
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,
|
|
3
|
+
* Responsibilities: Maintain a process-private temp root, stamp explicit ownership markers, enforce an aggregate temp-artifact disk budget, create securely permissioned temp files, prune explicitly owned stale temp roots from prior runs, and best-effort clean all owned roots on process exit.
|
|
4
4
|
* Scope: Temporary artifact lifecycle only; callers decide what data to write and when to delete 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
|
-
* Invariants/Assumptions: Temp artifacts live under the OS temp directory,
|
|
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, and stale pruning only touches roots with an explicit pi-agent-browser ownership marker.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { randomBytes } from "node:crypto";
|
|
10
|
-
import { chmod, mkdtemp, open, readdir, rm, stat } from "node:fs/promises";
|
|
11
10
|
import { rmSync } from "node:fs";
|
|
11
|
+
import { chmod, mkdtemp, open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
|
-
import { join } from "node:path";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
14
|
|
|
15
15
|
const TEMP_ROOT_PREFIX = "pi-agent-browser-";
|
|
16
|
+
const TEMP_ROOT_MARKER_FILE_NAME = ".pi-agent-browser-owner.json";
|
|
17
|
+
const TEMP_ROOT_MARKER_KIND = "pi-agent-browser-temp-root";
|
|
18
|
+
const TEMP_ROOT_MARKER_VERSION = 1;
|
|
16
19
|
const STALE_TEMP_ROOT_MAX_AGE_MS = 24 * 60 * 60 * 1_000;
|
|
20
|
+
const TEMP_ROOT_MAX_BYTES_ENV = "PI_AGENT_BROWSER_TEMP_ROOT_MAX_BYTES";
|
|
21
|
+
const DEFAULT_TEMP_ROOT_MAX_BYTES = 32 * 1_024 * 1_024;
|
|
22
|
+
|
|
23
|
+
interface TempRootOwnershipRecord {
|
|
24
|
+
createdAtMs: number;
|
|
25
|
+
kind: string;
|
|
26
|
+
ownerUid?: number;
|
|
27
|
+
version: number;
|
|
28
|
+
}
|
|
17
29
|
|
|
18
30
|
let sessionTempRootPromise: Promise<string> | undefined;
|
|
19
31
|
let exitCleanupRegistered = false;
|
|
32
|
+
let tempMutationQueue = Promise.resolve();
|
|
33
|
+
const ownedTempRoots = new Set<string>();
|
|
34
|
+
|
|
35
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
36
|
+
return typeof value === "object" && value !== null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getCurrentProcessUid(): number | undefined {
|
|
40
|
+
return typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parsePositiveInteger(rawValue: string | undefined): number | undefined {
|
|
44
|
+
if (typeof rawValue !== "string") return undefined;
|
|
45
|
+
const normalizedValue = rawValue.trim();
|
|
46
|
+
if (!/^\d+$/.test(normalizedValue)) return undefined;
|
|
47
|
+
const parsedValue = Number(normalizedValue);
|
|
48
|
+
if (!Number.isSafeInteger(parsedValue) || parsedValue <= 0) return undefined;
|
|
49
|
+
return parsedValue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isTempRootOwnershipRecord(value: unknown): value is TempRootOwnershipRecord {
|
|
53
|
+
if (!isRecord(value)) return false;
|
|
54
|
+
if (value.kind !== TEMP_ROOT_MARKER_KIND || value.version !== TEMP_ROOT_MARKER_VERSION) return false;
|
|
55
|
+
if (typeof value.createdAtMs !== "number" || !Number.isFinite(value.createdAtMs) || value.createdAtMs <= 0) return false;
|
|
56
|
+
return value.ownerUid === undefined || typeof value.ownerUid === "number";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getTempArtifactByteLength(content: string | Uint8Array): number {
|
|
60
|
+
return typeof content === "string" ? Buffer.byteLength(content) : content.byteLength;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function enqueueTempMutation<T>(task: () => Promise<T>): Promise<T> {
|
|
64
|
+
const nextTask = tempMutationQueue.then(task, task);
|
|
65
|
+
tempMutationQueue = nextTask.then(
|
|
66
|
+
() => undefined,
|
|
67
|
+
() => undefined,
|
|
68
|
+
);
|
|
69
|
+
return nextTask;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function getTempRootArtifactBytes(tempRoot: string): Promise<number> {
|
|
73
|
+
const entries = await readdir(tempRoot, { withFileTypes: true }).catch(() => []);
|
|
74
|
+
let totalBytes = 0;
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
if (!entry.isFile() || entry.name === TEMP_ROOT_MARKER_FILE_NAME) continue;
|
|
77
|
+
const path = join(tempRoot, entry.name);
|
|
78
|
+
const stats = await stat(path).catch(() => undefined);
|
|
79
|
+
if (stats?.isFile()) {
|
|
80
|
+
totalBytes += stats.size;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return totalBytes;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function readTempRootOwnershipMarker(tempRoot: string): Promise<TempRootOwnershipRecord | undefined> {
|
|
87
|
+
try {
|
|
88
|
+
const markerText = await readFile(join(tempRoot, TEMP_ROOT_MARKER_FILE_NAME), "utf8");
|
|
89
|
+
const parsed = JSON.parse(markerText) as unknown;
|
|
90
|
+
return isTempRootOwnershipRecord(parsed) ? parsed : undefined;
|
|
91
|
+
} catch {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function writeSecureTempRootOwnershipMarker(tempRoot: string, createdAtMs = Date.now()): Promise<string> {
|
|
97
|
+
const markerPath = join(tempRoot, TEMP_ROOT_MARKER_FILE_NAME);
|
|
98
|
+
const markerRecord: TempRootOwnershipRecord = {
|
|
99
|
+
createdAtMs,
|
|
100
|
+
kind: TEMP_ROOT_MARKER_KIND,
|
|
101
|
+
ownerUid: getCurrentProcessUid(),
|
|
102
|
+
version: TEMP_ROOT_MARKER_VERSION,
|
|
103
|
+
};
|
|
104
|
+
await writeFile(markerPath, JSON.stringify(markerRecord, null, 2), { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
105
|
+
await chmod(markerPath, 0o600).catch(() => undefined);
|
|
106
|
+
return markerPath;
|
|
107
|
+
}
|
|
20
108
|
|
|
21
109
|
async function pruneStaleTempRoots(currentTempRoot: string | undefined): Promise<void> {
|
|
22
110
|
const entries = await readdir(tmpdir(), { withFileTypes: true }).catch(() => []);
|
|
23
111
|
const cutoffTime = Date.now() - STALE_TEMP_ROOT_MAX_AGE_MS;
|
|
112
|
+
const currentUid = getCurrentProcessUid();
|
|
24
113
|
|
|
25
114
|
await Promise.all(
|
|
26
115
|
entries
|
|
@@ -28,30 +117,60 @@ async function pruneStaleTempRoots(currentTempRoot: string | undefined): Promise
|
|
|
28
117
|
.map(async (entry) => {
|
|
29
118
|
const path = join(tmpdir(), entry.name);
|
|
30
119
|
if (path === currentTempRoot) return;
|
|
120
|
+
|
|
121
|
+
const ownershipMarker = await readTempRootOwnershipMarker(path);
|
|
122
|
+
if (!ownershipMarker) return;
|
|
123
|
+
if (
|
|
124
|
+
currentUid !== undefined &&
|
|
125
|
+
ownershipMarker.ownerUid !== undefined &&
|
|
126
|
+
ownershipMarker.ownerUid !== currentUid
|
|
127
|
+
) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
31
131
|
const stats = await stat(path).catch(() => undefined);
|
|
32
|
-
if (!stats || stats.mtimeMs >= cutoffTime) return;
|
|
132
|
+
if (!stats?.isDirectory() || stats.mtimeMs >= cutoffTime) return;
|
|
33
133
|
await rm(path, { force: true, recursive: true }).catch(() => undefined);
|
|
34
134
|
}),
|
|
35
135
|
);
|
|
36
136
|
}
|
|
37
137
|
|
|
38
|
-
function registerExitCleanup(
|
|
138
|
+
function registerExitCleanup(): void {
|
|
39
139
|
if (exitCleanupRegistered) return;
|
|
40
140
|
exitCleanupRegistered = true;
|
|
41
141
|
process.once("exit", () => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
142
|
+
for (const tempRoot of ownedTempRoots) {
|
|
143
|
+
try {
|
|
144
|
+
rmSync(tempRoot, { force: true, recursive: true });
|
|
145
|
+
} catch {
|
|
146
|
+
// Best-effort cleanup only.
|
|
147
|
+
}
|
|
46
148
|
}
|
|
47
149
|
});
|
|
48
150
|
}
|
|
49
151
|
|
|
152
|
+
export function getSecureTempRootMaxBytes(env: NodeJS.ProcessEnv = process.env): number {
|
|
153
|
+
return parsePositiveInteger(env[TEMP_ROOT_MAX_BYTES_ENV]) ?? DEFAULT_TEMP_ROOT_MAX_BYTES;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function assertSecureTempRootBudget(tempRoot: string, additionalBytes: number): Promise<void> {
|
|
157
|
+
if (additionalBytes <= 0) return;
|
|
158
|
+
const currentBytes = await getTempRootArtifactBytes(tempRoot);
|
|
159
|
+
const maxBytes = getSecureTempRootMaxBytes();
|
|
160
|
+
const nextBytes = currentBytes + additionalBytes;
|
|
161
|
+
if (nextBytes > maxBytes) {
|
|
162
|
+
throw new Error(`pi-agent-browser temp spill budget exceeded (${nextBytes} bytes > ${maxBytes} byte limit).`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
50
166
|
export async function cleanupSecureTempArtifacts(): Promise<void> {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
167
|
+
await enqueueTempMutation(async () => {
|
|
168
|
+
const tempRoot = await sessionTempRootPromise?.catch(() => undefined);
|
|
169
|
+
sessionTempRootPromise = undefined;
|
|
170
|
+
if (!tempRoot) return;
|
|
171
|
+
ownedTempRoots.delete(tempRoot);
|
|
172
|
+
await rm(tempRoot, { force: true, recursive: true }).catch(() => undefined);
|
|
173
|
+
});
|
|
55
174
|
}
|
|
56
175
|
|
|
57
176
|
async function getSessionTempRoot(): Promise<string> {
|
|
@@ -60,7 +179,9 @@ async function getSessionTempRoot(): Promise<string> {
|
|
|
60
179
|
await pruneStaleTempRoots(undefined);
|
|
61
180
|
const tempRoot = await mkdtemp(join(tmpdir(), TEMP_ROOT_PREFIX));
|
|
62
181
|
await chmod(tempRoot, 0o700).catch(() => undefined);
|
|
63
|
-
|
|
182
|
+
await writeSecureTempRootOwnershipMarker(tempRoot);
|
|
183
|
+
ownedTempRoots.add(tempRoot);
|
|
184
|
+
registerExitCleanup();
|
|
64
185
|
return tempRoot;
|
|
65
186
|
})();
|
|
66
187
|
}
|
|
@@ -77,6 +198,18 @@ export async function openSecureTempFile(prefix: string, suffix: string): Promis
|
|
|
77
198
|
return { fileHandle, path };
|
|
78
199
|
}
|
|
79
200
|
|
|
201
|
+
export async function writeSecureTempChunk(options: {
|
|
202
|
+
content: string | Uint8Array;
|
|
203
|
+
fileHandle: Awaited<ReturnType<typeof open>>;
|
|
204
|
+
path: string;
|
|
205
|
+
}): Promise<void> {
|
|
206
|
+
const { content, fileHandle, path } = options;
|
|
207
|
+
await enqueueTempMutation(async () => {
|
|
208
|
+
await assertSecureTempRootBudget(dirname(path), getTempArtifactByteLength(content));
|
|
209
|
+
await fileHandle.writeFile(content);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
80
213
|
export async function writeSecureTempFile(options: {
|
|
81
214
|
content: string | Uint8Array;
|
|
82
215
|
prefix: string;
|
|
@@ -85,9 +218,19 @@ export async function writeSecureTempFile(options: {
|
|
|
85
218
|
const { content, prefix, suffix } = options;
|
|
86
219
|
const { fileHandle, path } = await openSecureTempFile(prefix, suffix);
|
|
87
220
|
try {
|
|
88
|
-
await
|
|
221
|
+
await writeSecureTempChunk({ content, fileHandle, path });
|
|
222
|
+
} catch (error) {
|
|
223
|
+
await rm(path, { force: true }).catch(() => undefined);
|
|
224
|
+
throw error;
|
|
89
225
|
} finally {
|
|
90
226
|
await fileHandle.close();
|
|
91
227
|
}
|
|
92
228
|
return path;
|
|
93
229
|
}
|
|
230
|
+
|
|
231
|
+
export async function getSecureTempDebugState(): Promise<{ currentTempRoot?: string; ownedTempRoots: string[] }> {
|
|
232
|
+
return {
|
|
233
|
+
currentTempRoot: await sessionTempRootPromise?.catch(() => undefined),
|
|
234
|
+
ownedTempRoots: [...ownedTempRoots].sort(),
|
|
235
|
+
};
|
|
236
|
+
}
|
package/package.json
CHANGED