Files
jb fd63b0ccec
fast-browser scenarios / Offline scenario contract tests (push) Failing after 44s
fast-browser scenarios / Local Chromium headless scenarios (push) Failing after 23s
fast-browser-with-jev
2026-09-20 12:24:08 -04:00

275 lines
11 KiB
TypeScript

import { randomBytes } from "node:crypto";
import { constants as fsConstants } from "node:fs";
import { lstat, link, mkdir, open, realpath, unlink } from "node:fs/promises";
import { dirname, join, relative, resolve, sep } from "node:path";
import { FAST_BROWSER_ARTIFACT_LIMITS } from "./contracts.ts";
import type {
PrivateArtifactKind,
PrivateArtifactMetadata,
PrivateArtifactOwner,
PrivateArtifactStore,
PrivateArtifactWriter,
} from "./contracts.ts";
const MAX_OWNER_FIELD = 200;
const DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
const ROOT_MODE = 0o700;
const FILE_MODE = 0o600;
export interface PrivateArtifactStoreOptions {
/** A dedicated local directory. It is never selected by a caller per artifact. */
root: string;
/** Maximum bytes per artifact; defaults to eight MiB. */
maxArtifactBytes?: number;
}
function safeOwnerField(value: unknown, label: string): string {
if (typeof value !== "string" || value.length === 0 || value.length > MAX_OWNER_FIELD || value.includes("\0")) {
throw new Error(`${label} is invalid`);
}
return value;
}
function validateOwner(owner: PrivateArtifactOwner): PrivateArtifactOwner {
if (!owner || typeof owner !== "object") throw new Error("artifact owner is invalid");
return Object.freeze({
sessionId: safeOwnerField(owner.sessionId, "artifact sessionId"),
runId: safeOwnerField(owner.runId, "artifact runId"),
operationId: safeOwnerField(owner.operationId, "artifact operationId"),
});
}
function validateKind(kind: PrivateArtifactKind): PrivateArtifactKind {
if (kind !== "screenshot" && kind !== "diagnostics") throw new Error("artifact kind is invalid");
return kind;
}
function encodedComponent(value: string): string {
// Owner identifiers are deliberately not included verbatim in filesystem
// paths. This both prevents traversal and keeps credentials/tokens out of
// local artifact metadata and directory names.
return Buffer.from(value, "utf8").toString("base64url").slice(0, 96);
}
function artifactId(): string {
return `a-${randomBytes(18).toString("hex")}`;
}
async function assertRegularDirectory(path: string): Promise<void> {
const stat = await lstat(path);
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("artifact path is not a private directory");
if (stat.mode & 0o077) throw new Error("artifact directory permissions are too broad");
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
if (uid !== undefined && stat.uid !== uid) throw new Error("artifact directory is not owned by this process");
}
async function assertNoSymlinkComponents(path: string): Promise<void> {
const absolute = resolve(path);
const parts = absolute.split(sep);
let current = parts[0] || sep;
for (const part of parts.slice(1)) {
if (!part) continue;
current = current === sep ? join(current, part) : join(current, part);
const stat = await lstat(current).catch(() => undefined);
if (stat?.isSymbolicLink()) throw new Error("artifact path may not contain symlinks");
}
}
async function ensurePrivateDirectory(path: string, create = true, tighten = true): Promise<void> {
await assertNoSymlinkComponents(path);
if (create) {
try {
await mkdir(path, { recursive: true, mode: ROOT_MODE });
} catch (error) {
throw new Error("could not create private artifact directory", { cause: error });
}
}
const stat = await lstat(path).catch(() => undefined);
if (!stat || !stat.isDirectory() || stat.isSymbolicLink()) {
throw new Error("artifact path is not a private directory");
}
// Never chmod a caller-owned parent or weaken a pre-existing directory's
// permissions. Store roots must already be private; only mkdir-created
// children are accepted with the requested mode.
if (tighten) await assertRegularDirectory(path);
}
function contentType(kind: PrivateArtifactKind): PrivateArtifactMetadata["contentType"] {
return kind === "screenshot" ? "image/png" : "application/json";
}
function isWithin(root: string, candidate: string): boolean {
const rel = relative(root, candidate);
return rel !== "" && rel !== ".." && !rel.startsWith(`..${resolve("/")}`) && !rel.startsWith("../") && !rel.startsWith("..\\") && !rel.includes("\0");
}
class FileArtifactWriter implements PrivateArtifactWriter {
readonly owner: PrivateArtifactOwner;
readonly kind: PrivateArtifactKind;
private handle: Awaited<ReturnType<typeof open>> | undefined;
private bytesWritten = 0;
private state: "open" | "committed" | "aborted" = "open";
private readonly tempPath: string;
private readonly finalPath: string;
private readonly id: string;
private readonly maxBytes: number;
private readonly onDone: () => void;
constructor(
owner: PrivateArtifactOwner,
kind: PrivateArtifactKind,
handle: Awaited<ReturnType<typeof open>>,
tempPath: string,
finalPath: string,
id: string,
maxBytes: number,
onDone: () => void,
) {
this.owner = owner;
this.kind = kind;
this.handle = handle;
this.tempPath = tempPath;
this.finalPath = finalPath;
this.id = id;
this.maxBytes = maxBytes;
this.onDone = onDone;
}
async write(chunk: Uint8Array): Promise<void> {
if (this.state !== "open" || !this.handle) throw new Error("artifact writer is closed");
if (!(chunk instanceof Uint8Array)) throw new Error("artifact chunk is invalid");
if (chunk.byteLength > this.maxBytes - this.bytesWritten) {
await this.abort();
throw new Error("artifact exceeds the private size limit");
}
if (chunk.byteLength === 0) return;
try {
await this.handle.write(chunk);
this.bytesWritten += chunk.byteLength;
} catch (error) {
await this.abort();
throw new Error("could not write private artifact", { cause: error });
}
}
async commit(): Promise<PrivateArtifactMetadata> {
if (this.state !== "open" || !this.handle) throw new Error("artifact writer is closed");
try {
await this.handle.sync();
const stat = await this.handle.stat();
if (!stat.isFile() || stat.size !== this.bytesWritten || stat.mode & 0o077) {
throw new Error("private artifact validation failed");
}
await this.handle.chmod(FILE_MODE);
await this.handle.close();
this.handle = undefined;
// link() makes the completed file visible atomically without replacing a
// pre-existing symlink or foreign artifact. The temporary inode is then
// removed; no caller-selected path is ever opened for writing.
await link(this.tempPath, this.finalPath);
await unlink(this.tempPath);
this.state = "committed";
this.onDone();
return Object.freeze({
artifactId: this.id,
kind: this.kind,
contentType: contentType(this.kind),
path: this.finalPath,
bytes: this.bytesWritten,
owner: this.owner,
permissions: "0600",
localOnly: true,
modelForwarded: false,
});
} catch (error) {
await this.abort();
throw error instanceof Error && error.message.startsWith("private artifact")
? error
: new Error("could not commit private artifact", { cause: error });
}
}
async abort(): Promise<void> {
if (this.state !== "open") return;
this.state = "aborted";
const handle = this.handle;
this.handle = undefined;
try { await handle?.close(); } catch { /* best effort cleanup */ }
try { await unlink(this.tempPath); } catch { /* already absent */ }
this.onDone();
}
}
/** Filesystem-backed implementation of the frozen PrivateArtifactStore API. */
export class FilePrivateArtifactStore implements PrivateArtifactStore {
readonly root: string;
private readonly maxBytes: number;
private readonly active = new Set<FileArtifactWriter>();
private closed = false;
private ready: Promise<void>;
constructor(options: PrivateArtifactStoreOptions | string) {
const root = typeof options === "string" ? options : options.root;
const maxBytes = typeof options === "string" ? DEFAULT_MAX_BYTES : options.maxArtifactBytes ?? DEFAULT_MAX_BYTES;
if (typeof root !== "string" || root.length === 0 || root.includes("\0")) throw new Error("artifact root is invalid");
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > DEFAULT_MAX_BYTES) throw new Error("artifact size limit is invalid");
this.root = resolve(root);
this.maxBytes = maxBytes;
this.ready = this.initialize();
}
private async initialize(): Promise<void> {
const parent = dirname(this.root);
await ensurePrivateDirectory(parent, true, false);
await assertNoSymlinkComponents(this.root);
const existing = await lstat(this.root).catch(() => undefined);
if (existing?.isSymbolicLink()) throw new Error("artifact root may not be a symlink");
await ensurePrivateDirectory(this.root);
await assertNoSymlinkComponents(this.root);
const canonical = await realpath(this.root);
if (canonical !== this.root) throw new Error("artifact root may not resolve through a symlink");
}
async open(owner: PrivateArtifactOwner, kind: PrivateArtifactKind): Promise<PrivateArtifactWriter> {
if (this.closed) throw new Error("artifact store is closed");
const checkedOwner = validateOwner(owner);
const checkedKind = validateKind(kind);
await this.ready;
if (this.closed) throw new Error("artifact store is closed");
const sessionDir = join(this.root, encodedComponent(checkedOwner.sessionId));
const runDir = join(sessionDir, encodedComponent(checkedOwner.runId));
await ensurePrivateDirectory(sessionDir);
await ensurePrivateDirectory(runDir);
const id = artifactId();
const extension = checkedKind === "screenshot" ? "png" : "json";
const finalPath = join(runDir, `${id}.${extension}`);
const tempPath = join(runDir, `.${id}.tmp`);
if (!isWithin(this.root, finalPath) || !isWithin(this.root, tempPath)) throw new Error("artifact path escaped private root");
// wx + O_NOFOLLOW ensures a malicious replacement cannot redirect writes.
const flags = fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | (fsConstants.O_NOFOLLOW ?? 0);
const handle = await open(tempPath, flags, FILE_MODE).catch((error) => {
throw new Error("could not create private artifact", { cause: error });
});
await handle.chmod(FILE_MODE);
const kindLimit = checkedKind === "screenshot" ? this.maxBytes : Math.min(this.maxBytes, FAST_BROWSER_ARTIFACT_LIMITS.diagnosticsBytes);
const writer = new FileArtifactWriter(checkedOwner, checkedKind, handle, tempPath, finalPath, id, kindLimit, () => this.active.delete(writer));
this.active.add(writer);
return writer;
}
async close(): Promise<void> {
if (this.closed) return;
this.closed = true;
await this.ready.catch(() => undefined);
await Promise.all([...this.active].map((writer) => writer.abort()));
}
}
export function createPrivateArtifactStore(options: PrivateArtifactStoreOptions | string): PrivateArtifactStore {
return new FilePrivateArtifactStore(options);
}
// A descriptive alias keeps the concrete implementation discoverable without
// widening the frozen contract surface.
export const createArtifactStore = createPrivateArtifactStore;