chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import { workspaceFileBroker } from '@/lib/execution/sandbox/brokers/workspace-file'
|
||||
import { defineSandboxTask } from '@/lib/execution/sandbox/define-task'
|
||||
import type { SandboxTaskInput } from '@/lib/execution/sandbox/types'
|
||||
|
||||
export const docxGenerateTask = defineSandboxTask<SandboxTaskInput>({
|
||||
id: 'docx-generate',
|
||||
timeoutMs: 60_000,
|
||||
bundles: ['docx'],
|
||||
brokers: [workspaceFileBroker],
|
||||
bootstrap: `
|
||||
const docx = globalThis.__bundles['docx'];
|
||||
if (!docx) throw new Error('docx bundle not loaded');
|
||||
globalThis.docx = docx;
|
||||
globalThis.__docxSections = [];
|
||||
globalThis.addSection = (section) => {
|
||||
globalThis.__docxSections.push(section);
|
||||
};
|
||||
// Set globalThis.__docxDocOptions = { styles: {...}, numbering: {...} } in chunk 1
|
||||
// to configure document-wide styles and numbering in chunked (addSection) mode.
|
||||
globalThis.__docxDocOptions = null;
|
||||
|
||||
// Page geometry constants (twips, 1 twip = 1/1440 inch) for US Letter
|
||||
globalThis.PAGE_W = 12240; // 8.5"
|
||||
globalThis.PAGE_H = 15840; // 11"
|
||||
globalThis.MARGIN = 1440; // 1" margins
|
||||
globalThis.CONTENT_W = 9360; // PAGE_W - 2 * MARGIN
|
||||
|
||||
// 6 MB raw ≈ 8 MB base64; reject above this to avoid sandbox OOM.
|
||||
const _MAX_IMG_B64 = 8 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* getFileBase64(fileId) — load a workspace file as a full data URI string.
|
||||
* Returns the complete "data:image/png;base64,..." string.
|
||||
* Use addImage() rather than passing this directly to ImageRun.
|
||||
*/
|
||||
globalThis.getFileBase64 = async function getFileBase64(fileId) {
|
||||
if (!fileId || typeof fileId !== 'string') {
|
||||
throw new Error('getFileBase64: fileId must be a non-empty string');
|
||||
}
|
||||
const res = await globalThis.__brokers.workspaceFile({ fileId });
|
||||
if (!res || !res.dataUri) {
|
||||
throw new Error('getFileBase64: broker returned no data for file ' + fileId);
|
||||
}
|
||||
if (res.dataUri.length > _MAX_IMG_B64) {
|
||||
throw new Error(
|
||||
'getFileBase64: image exceeds the 6 MB embed limit (~8 MB base64). Use a smaller/compressed image.'
|
||||
);
|
||||
}
|
||||
return res.dataUri;
|
||||
};
|
||||
|
||||
/**
|
||||
* addImage(fileId, opts) — fetch a workspace file and return a docx.ImageRun.
|
||||
* Required opts: width, height (pixels or EMUs via transformation option).
|
||||
* Example:
|
||||
* new docx.Paragraph({ children: [await addImage('abc123', { width: 200, height: 100 })] })
|
||||
*/
|
||||
globalThis.addImage = async function addImage(fileId, opts) {
|
||||
if (!opts || opts.width == null || opts.height == null) {
|
||||
throw new Error('addImage: opts must include width and height (in pixels)');
|
||||
}
|
||||
const dataUri = await globalThis.getFileBase64(fileId);
|
||||
const comma = dataUri.indexOf(',');
|
||||
if (comma === -1) throw new Error('addImage: invalid data URI (no comma separator)');
|
||||
const header = dataUri.slice(0, comma);
|
||||
const base64 = dataUri.slice(comma + 1);
|
||||
const mime = header.split(';')[0].replace('data:', '');
|
||||
const extMap = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/jpg': 'jpg', 'image/gif': 'gif', 'image/bmp': 'bmp', 'image/svg+xml': 'svg' };
|
||||
const ext = extMap[mime];
|
||||
if (!ext) throw new Error('addImage: unsupported image type "' + mime + '". Use PNG, JPEG, GIF, BMP, or SVG.');
|
||||
if (!globalThis.Buffer) throw new Error('addImage: Buffer polyfill missing — ensure docx bundle is loaded');
|
||||
const { width, height, type: _t, data: _d, transformation: userTransform, ...passThrough } = opts;
|
||||
return new globalThis.docx.ImageRun(Object.assign(passThrough, {
|
||||
data: globalThis.Buffer.from(base64, 'base64'),
|
||||
type: ext,
|
||||
transformation: Object.assign({ width, height }, userTransform || {}),
|
||||
}));
|
||||
};
|
||||
`,
|
||||
// JSZip's browser build doesn't support nodebuffer output, so we go through
|
||||
// base64 and decode back to bytes inside the isolate (avoids DataURL / Blob).
|
||||
finalize: `
|
||||
let doc = globalThis.doc;
|
||||
if (!doc && globalThis.__docxSections.length > 0) {
|
||||
doc = new globalThis.docx.Document({
|
||||
...(globalThis.__docxDocOptions || {}),
|
||||
sections: globalThis.__docxSections,
|
||||
});
|
||||
}
|
||||
if (!doc) {
|
||||
throw new Error('No document created. Use addSection({ children: [...] }) for chunked writes, or set globalThis.doc = new docx.Document({...}) for a single write.');
|
||||
}
|
||||
const b64 = await globalThis.docx.Packer.toBase64String(doc);
|
||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
const lookup = new Uint8Array(128);
|
||||
for (let i = 0; i < alphabet.length; i++) lookup[alphabet.charCodeAt(i)] = i;
|
||||
const clean = b64.replace(/=+$/, '');
|
||||
const out = new Uint8Array(Math.floor((clean.length * 3) / 4));
|
||||
let pos = 0;
|
||||
for (let i = 0; i < clean.length; i += 4) {
|
||||
const c0 = lookup[clean.charCodeAt(i)];
|
||||
const c1 = lookup[clean.charCodeAt(i + 1)];
|
||||
const c2 = lookup[clean.charCodeAt(i + 2)];
|
||||
const c3 = lookup[clean.charCodeAt(i + 3)];
|
||||
out[pos++] = (c0 << 2) | (c1 >> 4);
|
||||
if (i + 2 < clean.length) out[pos++] = ((c1 & 0x0f) << 4) | (c2 >> 2);
|
||||
if (i + 3 < clean.length) out[pos++] = ((c2 & 0x03) << 6) | c3;
|
||||
}
|
||||
return out.subarray(0, pos);
|
||||
`,
|
||||
toResult: (bytes) => Buffer.from(bytes),
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Everything that runs inside the isolated-vm pool lives in one of two tiers.
|
||||
* This file is the single grep surface for "what user code can execute in the
|
||||
* sandbox" — if you are adding a new place that spawns into the isolate,
|
||||
* register it below and/or extend one of the two tiers.
|
||||
*
|
||||
* Tier 1 — Sandbox tasks (this folder).
|
||||
* Bytes-producing tasks with a fixed input shape (`{ workspaceId, code }`),
|
||||
* pre-loaded library bundles, and a host-side broker set. User code is
|
||||
* trusted to the extent that our bootstrap + finalize wrap every execution.
|
||||
* Invoked via `runSandboxTask(id, input, options?)`.
|
||||
* - `pptx-generate` → `apps/sim/sandbox-tasks/pptx-generate.ts`
|
||||
* - `docx-generate` → `apps/sim/sandbox-tasks/docx-generate.ts`
|
||||
* - `pdf-generate` → `apps/sim/sandbox-tasks/pdf-generate.ts`
|
||||
*
|
||||
* Tier 2 — Raw isolated-vm consumers.
|
||||
* Value-producing executions where the user supplies arbitrary JS and the
|
||||
* host consumes whatever the code returns. Different contract (no finalize,
|
||||
* no bundles, no broker allowlist — just the built-in fetch bridge) so they
|
||||
* call `executeInIsolatedVM` directly rather than going through
|
||||
* `runSandboxTask`. If you add a new Tier 2 caller, record it here so the
|
||||
* set of sandbox entry points stays grep-able from one place.
|
||||
* - `apps/sim/app/api/function/execute/route.ts` — user function blocks
|
||||
* - `apps/sim/executor/orchestrators/loop.ts` — loop-condition eval
|
||||
*
|
||||
* E2B-routed executions (untrusted workflow runs) are a separate runtime
|
||||
* entirely and are not part of this registry.
|
||||
*/
|
||||
|
||||
export { docxGenerateTask } from '@/sandbox-tasks/docx-generate'
|
||||
export { pdfGenerateTask } from '@/sandbox-tasks/pdf-generate'
|
||||
export { pptxGenerateTask } from '@/sandbox-tasks/pptx-generate'
|
||||
export { getSandboxTask, SANDBOX_TASKS, type SandboxTaskId } from '@/sandbox-tasks/registry'
|
||||
@@ -0,0 +1,90 @@
|
||||
import { workspaceFileBroker } from '@/lib/execution/sandbox/brokers/workspace-file'
|
||||
import { defineSandboxTask } from '@/lib/execution/sandbox/define-task'
|
||||
import type { SandboxTaskInput } from '@/lib/execution/sandbox/types'
|
||||
|
||||
export const pdfGenerateTask = defineSandboxTask<SandboxTaskInput>({
|
||||
id: 'pdf-generate',
|
||||
timeoutMs: 60_000,
|
||||
bundles: ['pdf-lib'],
|
||||
brokers: [workspaceFileBroker],
|
||||
bootstrap: `
|
||||
const PDFLib = globalThis.__bundles['pdf-lib'];
|
||||
if (!PDFLib) throw new Error('pdf-lib bundle not loaded');
|
||||
globalThis.PDFLib = PDFLib;
|
||||
globalThis.pdf = await PDFLib.PDFDocument.create();
|
||||
|
||||
// Convenience shortcuts — avoids verbose PDFLib.rgb() / PDFLib.StandardFonts.Helvetica
|
||||
globalThis.rgb = PDFLib.rgb;
|
||||
globalThis.StandardFonts = PDFLib.StandardFonts;
|
||||
|
||||
// Page-size constants in points (1pt = 1/72 inch)
|
||||
globalThis.LETTER = [612, 792]; // 8.5" × 11"
|
||||
globalThis.A4 = [595.28, 841.89]; // 210mm × 297mm
|
||||
|
||||
// 6 MB raw ≈ 8 MB base64; reject above this to avoid sandbox OOM.
|
||||
const _MAX_IMG_B64 = 8 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* embedImage(dataUri) — embed a data-URI image into the active PDF document.
|
||||
* Dispatches to embedPng or embedJpg based on MIME type.
|
||||
*/
|
||||
globalThis.embedImage = async function embedImage(dataUri) {
|
||||
if (!dataUri || typeof dataUri !== 'string') {
|
||||
throw new Error('embedImage: dataUri must be a non-empty string');
|
||||
}
|
||||
const comma = dataUri.indexOf(',');
|
||||
if (comma === -1) throw new Error('embedImage: invalid data URI (no comma separator)');
|
||||
const header = dataUri.slice(0, comma);
|
||||
const base64 = dataUri.slice(comma + 1);
|
||||
if (!globalThis.Buffer) throw new Error('embedImage: Buffer polyfill missing');
|
||||
const binary = globalThis.Buffer.from(base64, 'base64');
|
||||
const mime = header.split(';')[0].split(':')[1] || '';
|
||||
// image/jpg is non-standard but tolerated; the canonical MIME is image/jpeg
|
||||
if (mime === 'image/png') return globalThis.pdf.embedPng(binary);
|
||||
if (mime === 'image/jpeg' || mime === 'image/jpg') return globalThis.pdf.embedJpg(binary);
|
||||
throw new Error('embedImage: only PNG and JPEG are supported (got ' + (mime || 'unknown — check data URI header') + ')');
|
||||
};
|
||||
|
||||
/**
|
||||
* getFileBase64(fileId) — load a workspace file as a data URI string.
|
||||
*/
|
||||
globalThis.getFileBase64 = async function getFileBase64(fileId) {
|
||||
if (!fileId || typeof fileId !== 'string') {
|
||||
throw new Error('getFileBase64: fileId must be a non-empty string');
|
||||
}
|
||||
const res = await globalThis.__brokers.workspaceFile({ fileId });
|
||||
if (!res || !res.dataUri) {
|
||||
throw new Error('getFileBase64: broker returned no data for file ' + fileId);
|
||||
}
|
||||
if (res.dataUri.length > _MAX_IMG_B64) {
|
||||
throw new Error(
|
||||
'getFileBase64: image exceeds the 6 MB embed limit (~8 MB base64). Use a smaller/compressed image.'
|
||||
);
|
||||
}
|
||||
return res.dataUri;
|
||||
};
|
||||
|
||||
/**
|
||||
* drawImage(page, fileId, opts) — fetch a workspace file and draw it on the given page.
|
||||
* Required opts: x, y, width, height (points).
|
||||
* Example: await drawImage(page, 'abc123', { x: 50, y: 700, width: 200, height: 100 });
|
||||
*/
|
||||
globalThis.drawImage = async function drawImage(page, fileId, opts) {
|
||||
if (!opts || opts.x == null || opts.y == null || opts.width == null || opts.height == null) {
|
||||
throw new Error('drawImage: opts must include x, y, width, and height (in points)');
|
||||
}
|
||||
const dataUri = await globalThis.getFileBase64(fileId);
|
||||
const img = await globalThis.embedImage(dataUri);
|
||||
page.drawImage(img, opts);
|
||||
};
|
||||
`,
|
||||
finalize: `
|
||||
const pdf = globalThis.pdf;
|
||||
if (!pdf) {
|
||||
throw new Error('No PDF document. Use the injected pdf object or load one with PDFLib.PDFDocument.load().');
|
||||
}
|
||||
const bytes = await pdf.save();
|
||||
return bytes;
|
||||
`,
|
||||
toResult: (bytes) => Buffer.from(bytes),
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
import { workspaceFileBroker } from '@/lib/execution/sandbox/brokers/workspace-file'
|
||||
import { defineSandboxTask } from '@/lib/execution/sandbox/define-task'
|
||||
import type { SandboxTaskInput } from '@/lib/execution/sandbox/types'
|
||||
|
||||
export const pptxGenerateTask = defineSandboxTask<SandboxTaskInput>({
|
||||
id: 'pptx-generate',
|
||||
timeoutMs: 60_000,
|
||||
bundles: ['pptxgenjs'],
|
||||
brokers: [workspaceFileBroker],
|
||||
bootstrap: `
|
||||
const PptxGenJS = globalThis.__bundles['pptxgenjs'];
|
||||
if (!PptxGenJS) throw new Error('pptxgenjs bundle not loaded');
|
||||
globalThis.pptx = new PptxGenJS();
|
||||
globalThis.pptx.layout = 'LAYOUT_16x9';
|
||||
|
||||
// Slide geometry for LAYOUT_16x9 (inches)
|
||||
globalThis.SLIDE_W = 10;
|
||||
globalThis.SLIDE_H = 5.625;
|
||||
globalThis.MARGIN = 0.5;
|
||||
globalThis.CONTENT_W = 9; // SLIDE_W - 2 * MARGIN
|
||||
globalThis.CONTENT_H = 3.8; // usable body height below a standard title row
|
||||
|
||||
// ── Image helpers ──────────────────────────────────────────────────────────
|
||||
// 6 MB raw ≈ 8 MB base64; reject above this to avoid sandbox OOM.
|
||||
const _MAX_IMG_B64 = 8 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* getFileBase64(fileId) — load a workspace file as a data URI string.
|
||||
* PptxGenJS data format: "image/png;base64,<data>" (no "data:" prefix).
|
||||
* Use as: slide.addImage({ data: await getFileBase64(fileId), x, y, w, h })
|
||||
*/
|
||||
globalThis.getFileBase64 = async function getFileBase64(fileId) {
|
||||
if (!fileId || typeof fileId !== 'string') {
|
||||
throw new Error('getFileBase64: fileId must be a non-empty string');
|
||||
}
|
||||
const res = await globalThis.__brokers.workspaceFile({ fileId });
|
||||
if (!res || !res.dataUri) {
|
||||
throw new Error('getFileBase64: broker returned no data for file ' + fileId);
|
||||
}
|
||||
if (res.dataUri.length > _MAX_IMG_B64) {
|
||||
throw new Error(
|
||||
'getFileBase64: image exceeds the 6 MB embed limit (~8 MB base64). Use a smaller/compressed image.'
|
||||
);
|
||||
}
|
||||
// PptxGenJS expects "image/png;base64,..." — strip the leading "data:" if present
|
||||
return res.dataUri.replace(/^data:/, '');
|
||||
};
|
||||
|
||||
/**
|
||||
* addImage(slide, fileId, opts) — fetch a workspace file and embed it.
|
||||
* Required opts: x, y, w, h (inches).
|
||||
* Example: await addImage(slide, 'abc123', { x: 0.5, y: 1, w: 2, h: 1 });
|
||||
*/
|
||||
globalThis.addImage = async function addImage(slide, fileId, opts) {
|
||||
if (!opts || opts.x == null || opts.y == null || opts.w == null || opts.h == null) {
|
||||
throw new Error('addImage: opts must include x, y, w, and h (in inches)');
|
||||
}
|
||||
const data = await globalThis.getFileBase64(fileId);
|
||||
slide.addImage(Object.assign({}, opts, { data }));
|
||||
};
|
||||
`,
|
||||
finalize: `
|
||||
if (!globalThis.pptx) {
|
||||
throw new Error('No presentation found. globalThis.pptx was overwritten — use the pre-initialized instance and call addSlide() on it to build your presentation.');
|
||||
}
|
||||
const bytes = await globalThis.pptx.write({ outputType: 'uint8array' });
|
||||
return bytes;
|
||||
`,
|
||||
toResult: (bytes) => Buffer.from(bytes),
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { SandboxTask } from '@/lib/execution/sandbox/types'
|
||||
import { docxGenerateTask } from '@/sandbox-tasks/docx-generate'
|
||||
import { pdfGenerateTask } from '@/sandbox-tasks/pdf-generate'
|
||||
import { pptxGenerateTask } from '@/sandbox-tasks/pptx-generate'
|
||||
|
||||
/**
|
||||
* Every piece of user code that runs inside the isolated-vm sandbox is defined
|
||||
* here. Adding a new sandbox task = add one file under `apps/sim/sandbox-tasks/`
|
||||
* and register it below. Mirrors the `apps/sim/background/` pattern.
|
||||
*/
|
||||
export const SANDBOX_TASKS = {
|
||||
'pptx-generate': pptxGenerateTask,
|
||||
'docx-generate': docxGenerateTask,
|
||||
'pdf-generate': pdfGenerateTask,
|
||||
} as const satisfies Record<string, SandboxTask>
|
||||
|
||||
export type SandboxTaskId = keyof typeof SANDBOX_TASKS
|
||||
|
||||
export function getSandboxTask(id: SandboxTaskId): SandboxTask {
|
||||
const task = SANDBOX_TASKS[id]
|
||||
if (!task) {
|
||||
throw new Error(`Unknown sandbox task: "${id}"`)
|
||||
}
|
||||
return task
|
||||
}
|
||||
Reference in New Issue
Block a user