Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions tools/ui/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@ static/favicon*
*storybook.log
storybook-static
*.code-workspace

# Vitest browser mode failure artifacts
.vitest-attachments/
tests/**/__screenshots__/
3 changes: 3 additions & 0 deletions tools/ui/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ build/
/build/
/.svelte-kit/
test-results

# Vendored third party sources, kept byte identical to upstream
src/lib/vendors/
3 changes: 2 additions & 1 deletion tools/ui/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ export default ts.config(
'.svelte-kit/**',
'test-results/**',
'.storybook/**/*',
'src/lib/services/sandbox-worker.js'
'src/lib/services/sandbox-worker.js',
'src/lib/vendors/**'
]
},
storybook.configs['flat/recommended']
Expand Down
49 changes: 49 additions & 0 deletions tools/ui/scripts/vite-plugin-nerdamer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { build } from 'esbuild';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
import type { Plugin } from 'vite';

const __dirname = dirname(fileURLToPath(import.meta.url));

const VENDORS_DIR = resolve(__dirname, '../src/lib/vendors');
const VIRTUAL_ID = 'virtual:nerdamer';
const RESOLVED_ID = '\0' + VIRTUAL_ID;

/**
* Bundle the vendored nerdamer-prime source into a minified IIFE string,
* exposed as the `virtual:nerdamer` module. Flags mirror the upstream
* build (esbuild --bundle --minify --format=iife --global-name=nerdamer),
* so only human readable source lives in the repo and minification is a
* build artifact. Vendored under src/lib/vendors/, upstream snapshot:
* https://github.com/together-science/nerdamer-prime/commit/1936145f8af306ec0d883b9bfd7730aedd175c24
*/
export function nerdamerPlugin(): Plugin {
let bundled: string | null = null;

return {
name: 'llamacpp:nerdamer',
resolveId(id) {
return id === VIRTUAL_ID ? RESOLVED_ID : undefined;
},
async load(id) {
if (id !== RESOLVED_ID) return undefined;
if (bundled === null) {
const result = await build({
entryPoints: [resolve(VENDORS_DIR, 'nerdamer-prime/all.js')],
bundle: true,
minify: true,
format: 'iife',
globalName: 'nerdamer',
alias: {
'big-integer': resolve(VENDORS_DIR, 'big-integer/BigInteger.js'),
'decimal.js': resolve(VENDORS_DIR, 'decimal.js/decimal.js')
},
write: false,
logLevel: 'silent'
});
bundled = result.outputFiles[0].text;
}
return `export default ${JSON.stringify(bundled)};`;
}
};
}
25 changes: 14 additions & 11 deletions tools/ui/src/lib/services/sandbox-harness.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
/**
* nerdamer-prime.js downloaded from https://raw.githubusercontent.com/together-science/nerdamer-prime/refs/heads/main/all.min.js
* Upstream commit for all.min.js: https://github.com/together-science/nerdamer-prime/commit/fffcce3ebd74fefb51fcf90b613f088fcab4fd93
*/
import { NEWLINE } from '$lib/constants';
import WORKER_SHIM from './sandbox-worker.js?raw';
import NERDAMER_JS from '../vendors/nerdamer-prime.js?raw';

/**
* CSP for the harness document, inherited by the blob worker. connect-src
* falls back to default-src, removing network egress for model and vendored
* code. 'unsafe-eval' is required by the worker's AsyncFunction constructor,
* 'unsafe-inline' by the inline script below, worker-src by the blob worker.
*/
const HARNESS_CSP = `default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval'; worker-src blob:`;

/**
* Harness loaded as srcdoc into a sandboxed iframe (allow-scripts only).
* The opaque origin is the security boundary: no access to the app origin,
* its storage or its API. The harness spawns a worker so model code never
* runs on a main thread, which makes the parent timeout enforceable by
* removing the iframe.
*
* nerdamer is preloaded in the worker, exposing the `nerdamer` global for
* symbolic computation (simplify, derivative, integrate, solve, etc.).
* removing the iframe. The prelude runs in the worker before the shim,
* exposing globals such as `nerdamer` to model code.
*/
export const SANDBOX_HARNESS_HTML = `<!doctype html><script>
const SHIM = ${JSON.stringify(NERDAMER_JS + NEWLINE + WORKER_SHIM)};
export function buildSandboxHarness(preludeJs: string): string {
return `<!doctype html><meta http-equiv="Content-Security-Policy" content="${HARNESS_CSP}"><script>
const SHIM = ${JSON.stringify(preludeJs + NEWLINE + WORKER_SHIM)};
addEventListener('message', (event) => {
const respond = (payload) => parent.postMessage(payload, '*');
let worker;
Expand All @@ -32,3 +34,4 @@ addEventListener('message', (event) => {
worker.postMessage({ code: event.data.code });
});
</script>`;
}
6 changes: 2 additions & 4 deletions tools/ui/src/lib/services/sandbox-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,8 @@ self.onmessage = async (event) => {
const reply = { logs, result: null, error: null };
try {
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
// Inject `nerdamer` (from preloaded nerdamer-prime.js) into the execution context.
// User code can use: nerdamer(expr), nerdamer.solve(), nerdamer.derivative(),
// nerdamer.integrate(), nerdamer.expand(), nerdamer.factor(), nerdamer.simplify(),
// nerdamer.laplace(), nerdamer.limit(), nerdamer.series(), etc.
// The prelude bundled ahead of this shim defines self.nerdamer,
// passed into the execution scope as the `nerdamer` parameter.
const value = await new AsyncFunction('nerdamer', event.data.code)(self.nerdamer);
if (value !== undefined) reply.result = fmt(value);
} catch (err) {
Expand Down
26 changes: 21 additions & 5 deletions tools/ui/src/lib/services/sandbox.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,23 @@ import {
SANDBOX_TOOL_NAME,
SANDBOX_TRUNCATION_NOTICE
} from '$lib/constants';
import { SANDBOX_HARNESS_HTML } from './sandbox-harness';
import { buildSandboxHarness } from './sandbox-harness';
import type { ToolExecutionResult } from '$lib/types';

let harnessHtml: string | null = null;

/**
* The nerdamer prelude lives in its own lazy chunk via `virtual:nerdamer`,
* keeping it out of the eagerly loaded bundle. Built once per session.
*/
async function getHarness(): Promise<string> {
if (harnessHtml === null) {
const { default: nerdamerJs } = await import('virtual:nerdamer');
harnessHtml = buildSandboxHarness(nerdamerJs);
}
return harnessHtml;
}

interface SandboxReply {
logs?: unknown;
result?: unknown;
Expand Down Expand Up @@ -45,20 +59,22 @@ export class SandboxService {
* timeout or abort. Removing the iframe terminates the worker
* at the browser level, so runaway code cannot outlive it.
*/
static executeTool(
static async executeTool(
toolName: string,
params: Record<string, unknown>,
signal?: AbortSignal
): Promise<ToolExecutionResult> {
if (toolName !== SANDBOX_TOOL_NAME) {
return Promise.resolve({ content: `Unknown frontend tool: ${toolName}`, isError: true });
return { content: `Unknown frontend tool: ${toolName}`, isError: true };
}

const code = typeof params.code === 'string' ? params.code : '';
if (!code) {
return Promise.resolve({ content: 'Missing required parameter: code', isError: true });
return { content: 'Missing required parameter: code', isError: true };
}

const harness = await getHarness();

const requested = Number(params.timeout_ms);
const timeoutMs =
Number.isFinite(requested) && requested > 0
Expand All @@ -69,7 +85,7 @@ export class SandboxService {
const iframe = document.createElement('iframe');
iframe.setAttribute('sandbox', 'allow-scripts');
iframe.style.display = 'none';
iframe.srcdoc = SANDBOX_HARNESS_HTML;
iframe.srcdoc = harness;

let settled = false;

Expand Down
Loading