mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 00:53:38 +02:00
fix(ui): address sandboxed parser review feedback
This commit is contained in:
1 parent
e23fabf5f3
commit
b554542c75
3 files changed
+53
-24
No files matched your search
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user