fix(ui): address sandboxed parser review feedback

This commit is contained in:
Dotta committed 2026-04-21 12:31:32 -05:00
1 parent e23fabf5f3
commit b554542c75
3 files changed
+53 -24

No files matched your search

+27 -8
View File
@@ -47,6 +47,7 @@ interface SandboxedParser {
hasFactory: boolean;
nextId: number;
pendingResolves: Map<number, (entries: TranscriptEntry[]) => void>;
pendingParserCreates: Map<number, (parserId: number | null) => void>;
}
// ── State ───────────────────────────────────────────────────────────────────
@@ -85,6 +86,18 @@ function parseLineAsync(sandbox: SandboxedParser, line: string, ts: string): Pro
});
}
function drainPendingRequests(sandbox: SandboxedParser): void {
for (const resolver of sandbox.pendingResolves.values()) {
resolver([]);
}
sandbox.pendingResolves.clear();
for (const resolver of sandbox.pendingParserCreates.values()) {
resolver(null);
}
sandbox.pendingParserCreates.clear();
}
/**
* Create a sandboxed worker, send the parser source, and wait for init.
*/
@@ -97,10 +110,12 @@ function initSandboxedWorker(source: string): Promise<SandboxedParser> {
hasFactory: false,
nextId: 1,
pendingResolves: new Map(),
pendingParserCreates: new Map(),
};
// Timeout if the worker doesn't respond within 5s
const timeout = setTimeout(() => {
drainPendingRequests(sandbox);
worker.terminate();
reject(new Error("Parser worker init timed out"));
}, 5000);
@@ -123,12 +138,14 @@ function initSandboxedWorker(source: string): Promise<SandboxedParser> {
resolver(resp.entries as TranscriptEntry[]);
}
} else if (resp.type === "parser_created") {
const resolver = sandbox.pendingResolves.get(resp.id);
const resolver = sandbox.pendingParserCreates.get(resp.id);
if (resolver) {
sandbox.pendingResolves.delete(resp.id);
// Encode parserId in a single-element array for the promise chain
resolver([{ kind: "system", ts: "", text: String(resp.parserId) }] as unknown as TranscriptEntry[]);
sandbox.pendingParserCreates.delete(resp.id);
resolver(resp.parserId);
}
} else if (resp.type === "error") {
console.error("[adapter-ui-loader] Worker reported error:", resp.message);
drainPendingRequests(sandbox);
}
};
@@ -138,6 +155,7 @@ function initSandboxedWorker(source: string): Promise<SandboxedParser> {
if (msg.type === "error") {
clearTimeout(timeout);
drainPendingRequests(sandbox);
worker.terminate();
reject(new Error(msg.message));
return;
@@ -146,6 +164,7 @@ function initSandboxedWorker(source: string): Promise<SandboxedParser> {
worker.onerror = (ev) => {
clearTimeout(timeout);
drainPendingRequests(sandbox);
worker.terminate();
reject(new Error(`Worker error: ${ev.message}`));
};
@@ -200,10 +219,9 @@ function buildParserModule(sandbox: SandboxedParser): DynamicParserModule {
// Request a parser instance from the worker.
const id = nextRequestId(sandbox);
sandbox.pendingResolves.set(id, (entries) => {
// The parserId is encoded in the first entry's text field.
if (entries.length > 0 && (entries[0] as TranscriptEntry & { kind: string }).kind === "system") {
parserId = parseInt((entries[0] as TranscriptEntry & { text: string }).text, 10);
sandbox.pendingParserCreates.set(id, (createdParserId) => {
if (createdParserId !== null) {
parserId = createdParserId;
parserReady = true;
}
});
@@ -303,6 +321,7 @@ export function invalidateDynamicParser(adapterType: string): boolean {
// Terminate the worker to free resources.
const sandbox = sandboxedParsers.get(adapterType);
if (sandbox) {
drainPendingRequests(sandbox);
sandbox.worker.terminate();
sandboxedParsers.delete(adapterType);
}
@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { getWorkerBootstrapSource } from "./sandboxed-parser-worker";
describe("sandboxed parser worker bootstrap", () => {
it("disables child worker and object URL escape hatches", () => {
const source = getWorkerBootstrapSource();
expect(source).toContain("self.Worker = _undefined");
expect(source).toContain("self.SharedWorker = _undefined");
expect(source).toContain("self.Blob = _undefined");
expect(source).toContain('"createObjectURL"');
expect(source).toContain('"revokeObjectURL"');
});
it("does not include the unused parse_batch protocol branch", () => {
expect(getWorkerBootstrapSource()).not.toContain("parse_batch");
});
});
+7 -16
View File
@@ -17,7 +17,6 @@
export type SandboxRequest =
| { type: "init"; source: string }
| { type: "parse"; id: number; line: string; ts: string }
| { type: "parse_batch"; id: number; lines: { line: string; ts: string }[] }
| { type: "create_parser"; id: number }
| { type: "parser_parse"; id: number; parserId: number; line: string; ts: string }
| { type: "parser_reset"; parserId: number };
@@ -63,6 +62,13 @@ self.caches = _undefined;
// Import / eval escape hatches
self.importScripts = _undefined;
self.Worker = _undefined;
self.SharedWorker = _undefined;
self.Blob = _undefined;
if (self.URL) {
try { Object.defineProperty(self.URL, "createObjectURL", { value: _undefined, writable: false, configurable: false }); } catch {}
try { Object.defineProperty(self.URL, "revokeObjectURL", { value: _undefined, writable: false, configurable: false }); } catch {}
}
// Beacon / reporting
if (self.navigator) {
@@ -71,7 +77,6 @@ if (self.navigator) {
// Service worker / broadcast channel
self.BroadcastChannel = _undefined;
self.SharedWorker = _undefined;
// IndexedDB (prevents persistent state exfiltration)
self.indexedDB = _undefined;
@@ -145,20 +150,6 @@ self.onmessage = function (e) {
return;
}
if (msg.type === "parse_batch") {
try {
const allEntries = [];
for (const item of msg.lines) {
const entries = parseStdoutLine ? parseStdoutLine(item.line, item.ts) : [];
allEntries.push(...(entries || []));
}
self.postMessage({ type: "result", id: msg.id, entries: allEntries });
} catch (err) {
self.postMessage({ type: "result", id: msg.id, entries: [] });
}
return;
}
if (msg.type === "create_parser") {
try {
if (!createStdoutParser) {