Add support for triggering from the backend

This commit is contained in:
Eric Allam
2026-03-28 21:46:55 +00:00
parent 9e7b2d1ac5
commit 84d430edcd
13 changed files with 1445 additions and 80 deletions
+16 -1
View File
@@ -31,7 +31,8 @@
"./extensions/typescript": "./src/extensions/typescript.ts",
"./extensions/puppeteer": "./src/extensions/puppeteer.ts",
"./extensions/playwright": "./src/extensions/playwright.ts",
"./extensions/lightpanda": "./src/extensions/lightpanda.ts"
"./extensions/lightpanda": "./src/extensions/lightpanda.ts",
"./extensions/secureExec": "./src/extensions/secureExec.ts"
},
"sourceDialects": [
"@triggerdotdev/source"
@@ -65,6 +66,9 @@
],
"extensions/lightpanda": [
"dist/commonjs/extensions/lightpanda.d.ts"
],
"extensions/secureExec": [
"dist/commonjs/extensions/secureExec.d.ts"
]
}
},
@@ -207,6 +211,17 @@
"types": "./dist/commonjs/extensions/lightpanda.d.ts",
"default": "./dist/commonjs/extensions/lightpanda.js"
}
},
"./extensions/secureExec": {
"import": {
"@triggerdotdev/source": "./src/extensions/secureExec.ts",
"types": "./dist/esm/extensions/secureExec.d.ts",
"default": "./dist/esm/extensions/secureExec.js"
},
"require": {
"types": "./dist/commonjs/extensions/secureExec.d.ts",
"default": "./dist/commonjs/extensions/secureExec.js"
}
}
},
"main": "./dist/commonjs/index.js",
+172
View File
@@ -0,0 +1,172 @@
import { BuildTarget } from "@trigger.dev/core/v3";
import { BuildManifest } from "@trigger.dev/core/v3/schemas";
import { BuildContext, BuildExtension } from "@trigger.dev/core/v3/build";
import { dirname, resolve, join } from "node:path";
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { readPackageJSON } from "pkg-types";
export type SecureExecOptions = {
/**
* Packages available inside the sandbox at runtime.
*
* These are `require()`'d inside the V8 isolate at runtime — the bundler
* never sees them statically. They are marked external and installed as
* deploy dependencies.
*
* @example
* ```ts
* secureExec({ packages: ["jszip", "lodash"] })
* ```
*/
packages?: string[];
};
/**
* Build extension for [secure-exec](https://secureexec.dev) — run untrusted
* JavaScript/TypeScript in V8 isolates with configurable permissions.
*
* Handles the esbuild workarounds needed for secure-exec's runtime
* `require.resolve` calls, native binaries, and module-scope resolution.
*
* @example
* ```ts
* import { secureExec } from "@trigger.dev/build/extensions/secureExec";
*
* export default defineConfig({
* build: {
* extensions: [secureExec()],
* },
* });
* ```
*/
export function secureExec(options?: SecureExecOptions): BuildExtension {
return new SecureExecExtension(options ?? {});
}
class SecureExecExtension implements BuildExtension {
public readonly name = "SecureExecExtension";
private userPackages: string[];
constructor(options: SecureExecOptions) {
this.userPackages = options.packages ?? [];
}
externalsForTarget(_target: BuildTarget) {
return [
// esbuild must not be bundled — it locates its native binary via a
// relative path from its JS API entry point. secure-exec uses esbuild
// at runtime to bundle polyfills for sandbox code.
"esbuild",
// User-specified packages are require()'d inside the V8 sandbox at
// runtime — the bundler never sees them statically.
...this.userPackages,
];
}
onBuildStart(context: BuildContext) {
context.logger.debug(`Adding ${this.name} esbuild plugins`);
// Plugin 1: Replace node-stdlib-browser with pre-resolved paths.
//
// Trigger's ESM shim anchors require.resolve() to the chunk path, so
// node-stdlib-browser's runtime require.resolve("./mock/empty.js") breaks.
// Fix: load the real node-stdlib-browser at build time (where require.resolve
// works), capture the resolved path map, and inline it as a static export.
const workingDir = context.workingDir;
context.registerPlugin({
name: "secure-exec-stdlib-resolver",
setup(build) {
build.onResolve({ filter: /^node-stdlib-browser$/ }, () => ({
path: "node-stdlib-browser",
namespace: "secure-exec-nsb-resolved",
}));
build.onLoad({ filter: /.*/, namespace: "secure-exec-nsb-resolved" }, () => {
const buildRequire = createRequire(join(workingDir, "package.json"));
const resolved = buildRequire("node-stdlib-browser");
return {
contents: `export default ${JSON.stringify(resolved)};`,
loader: "js",
};
});
},
});
// Plugin 2: Inline bridge.js at build time.
//
// bridge-loader.js in @secure-exec/node(js) uses __dirname and
// require.resolve("@secure-exec/core") at module scope to locate
// dist/bridge.js on disk. This fails in Trigger's bundled output.
// Fix: read bridge.js content at build time and inline it as a
// string literal so no runtime filesystem resolution is needed.
//
context.registerPlugin({
name: "secure-exec-bridge-inline",
setup(build) {
build.onLoad(
{ filter: /[\\/]@secure-exec[\\/]node[\\/]dist[\\/]bridge-loader\.js$/ },
(args) => {
try {
const buildRequire = createRequire(args.path);
const coreEntry = buildRequire.resolve("@secure-exec/core");
const coreRoot = resolve(dirname(coreEntry), "..");
const bridgeCode = readFileSync(join(coreRoot, "dist", "bridge.js"), "utf8");
return {
contents: [
`import { getIsolateRuntimeSource } from "@secure-exec/core";`,
`const bridgeCodeCache = ${JSON.stringify(bridgeCode)};`,
`export function getRawBridgeCode() { return bridgeCodeCache; }`,
`export function getBridgeAttachCode() { return getIsolateRuntimeSource("bridgeAttach"); }`,
].join("\n"),
loader: "js",
};
} catch {
// If we can't inline the bridge, let the normal loader handle it.
return undefined;
}
}
);
},
});
}
async onBuildComplete(context: BuildContext, _manifest: BuildManifest) {
if (context.target === "dev") {
return;
}
context.logger.debug(`Adding ${this.name} deploy dependencies`);
const dependencies: Record<string, string> = {};
// Resolve versions for user-specified sandbox packages
for (const pkg of this.userPackages) {
try {
const modulePath = await context.resolvePath(pkg);
if (!modulePath) {
dependencies[pkg] = "latest";
continue;
}
const packageJSON = await readPackageJSON(dirname(modulePath));
dependencies[pkg] = packageJSON.version ?? "latest";
} catch {
context.logger.warn(
`Could not resolve version for sandbox package ${pkg}, defaulting to latest`
);
dependencies[pkg] = "latest";
}
}
context.addLayer({
id: "secureExec",
dependencies,
image: {
// isolated-vm requires native compilation tools
pkgs: ["python3", "make", "g++"],
},
});
}
}
+61 -1
View File
@@ -43,7 +43,8 @@ import { locals } from "./locals.js";
import { metadata } from "./metadata.js";
import type { ResolvedPrompt } from "./prompt.js";
import { streams } from "./streams.js";
import { createTask } from "./shared.js";
import { createTask, trigger as triggerTaskInternal } from "./shared.js";
import type { TriggerChatTaskParams, TriggerChatTaskResult } from "./chat.js";
import { tracer } from "./tracer.js";
/** Re-export for typing `ctx` in `chat.task` hooks without importing `@trigger.dev/core`. */
@@ -5125,6 +5126,63 @@ export type InferChatUIMessage<TTask extends AnyTask> = TTask extends Task<
? TUIM
: UIMessage;
/**
* Options for {@link createChatTriggerAction}.
*/
export type CreateChatTriggerActionOptions = {
/** TTL for the run-scoped public access token. @default "1h" */
tokenTTL?: string | number | Date;
};
/**
* Creates a function that triggers a chat task and returns a run-scoped session.
*
* Wrap the returned function in a Next.js server action (or any server-side handler)
* to keep task triggering on the server. The function calls `tasks.trigger()` with
* the secret key and mints a run-scoped PAT for stream subscription + input stream writes.
*
* @example
* ```ts
* // actions.ts
* "use server";
* import { chat } from "@trigger.dev/sdk/ai";
*
* export const triggerChat = chat.createTriggerAction("my-chat");
* ```
*
* Then pass it to the transport:
* ```tsx
* const transport = useTriggerChatTransport({
* task: "my-chat",
* triggerTask: triggerChat,
* });
* ```
*/
function createChatTriggerAction(
taskId: string,
options?: CreateChatTriggerActionOptions
): (params: TriggerChatTaskParams) => Promise<TriggerChatTaskResult> {
return async (params: TriggerChatTaskParams): Promise<TriggerChatTaskResult> => {
const handle = await triggerTaskInternal(taskId, params.payload, {
tags: params.options.tags,
queue: params.options.queue,
maxAttempts: params.options.maxAttempts,
machine: params.options.machine as any,
priority: params.options.priority,
});
const publicAccessToken = await auth.createPublicToken({
scopes: {
read: { runs: handle.id },
write: { inputStreams: handle.id },
},
expirationTime: options?.tokenTTL ?? "1h",
});
return { runId: handle.id, publicAccessToken };
};
}
export const chat = {
/** Create a chat task. See {@link chatTask}. */
task: chatTask,
@@ -5132,6 +5190,8 @@ export const chat = {
withUIMessage,
/** Create a chat task with a fixed client data schema. See {@link withClientData}. */
withClientData,
/** Create a server-side trigger action helper. See {@link createChatTriggerAction}. */
createTriggerAction: createChatTriggerAction,
/** Pipe a stream to the chat transport. See {@link pipeChat}. */
pipe: pipeChat,
/** Create a per-run typed local. See {@link chatLocal}. */
+7 -3
View File
@@ -86,11 +86,11 @@ export function useTriggerChatTransport<TTask extends AnyTask = AnyTask>(
): TriggerChatTransport {
const ref = useRef<TriggerChatTransport | null>(null);
if (ref.current === null) {
ref.current = new TriggerChatTransport(options);
ref.current = new TriggerChatTransport(options as TriggerChatTransportOptions);
}
// Keep onSessionChange up to date without recreating the transport
const { onSessionChange, renewRunAccessToken } = options;
// Keep callbacks up to date without recreating the transport
const { onSessionChange, renewRunAccessToken, triggerTask } = options;
useEffect(() => {
ref.current?.setOnSessionChange(onSessionChange);
}, [onSessionChange]);
@@ -99,6 +99,10 @@ export function useTriggerChatTransport<TTask extends AnyTask = AnyTask>(
ref.current?.setRenewRunAccessToken(renewRunAccessToken);
}, [renewRunAccessToken]);
useEffect(() => {
ref.current?.setTriggerTask(triggerTask);
}, [triggerTask]);
return ref.current;
}
+156 -71
View File
@@ -66,9 +66,42 @@ export type ResolveChatAccessTokenParams = {
};
/**
* Options for creating a TriggerChatTransport.
* Payload passed to the {@link TriggerChatTransportOptions.triggerTask} callback.
*/
export type TriggerChatTransportOptions<TClientData = unknown> = {
export type TriggerChatTaskParams = {
/** The full payload to pass to the task. */
payload: {
messages: UIMessage[];
chatId: string;
trigger: "submit-message" | "regenerate-message" | "preload";
messageId?: string;
metadata?: Record<string, unknown>;
continuation?: boolean;
previousRunId?: string;
idleTimeoutInSeconds?: number;
};
/** Trigger options (tags, queue, etc.) — pre-merged by the transport. */
options: {
tags: string[];
queue?: string;
maxAttempts?: number;
machine?: string;
priority?: number;
};
};
/**
* Return value from the {@link TriggerChatTransportOptions.triggerTask} callback.
*/
export type TriggerChatTaskResult = {
/** The run ID from the triggered task. */
runId: string;
/** A run-scoped public access token for stream subscription and input stream writes. */
publicAccessToken: string;
};
/** Common options shared by all TriggerChatTransport configurations. */
type TriggerChatTransportOptionsBase<TClientData = unknown> = {
/**
* The Trigger.dev task ID to trigger for chat completions.
* This task should be defined using `chatTask()` from `@trigger.dev/sdk/ai`,
@@ -76,19 +109,6 @@ export type TriggerChatTransportOptions<TClientData = unknown> = {
*/
task: string;
/**
* An access token for authenticating with the Trigger.dev API.
*
* This must be a token with permission to trigger the task. You can use:
* - A **trigger public token** created via `auth.createTriggerPublicToken(taskId)` (recommended for frontend use)
* - A **secret API key** (for server-side use only — never expose in the browser)
*
* Can also be a function that returns a token string (sync or async),
* useful for dynamic token refresh or passing a Next.js server action directly.
* The function receives `chatId` and `purpose` (`trigger` vs `preload`) so you can mint or log per conversation.
*/
accessToken: string | ((params: ResolveChatAccessTokenParams) => string | Promise<string>);
/**
* Base URL for the Trigger.dev API.
* @default "https://api.trigger.dev"
@@ -239,6 +259,51 @@ export type TriggerChatTransportOptions<TClientData = unknown> = {
) => string | undefined | null | Promise<string | undefined | null>;
};
/** Access token used for frontend-triggered runs. */
type AccessTokenOption =
| string
| ((params: ResolveChatAccessTokenParams) => string | Promise<string>);
/**
* Options for creating a TriggerChatTransport.
*
* Provide either `accessToken` (frontend triggering) or `triggerTask` (server-side triggering).
* When `triggerTask` is provided, `accessToken` is optional.
*/
export type TriggerChatTransportOptions<TClientData = unknown> =
| (TriggerChatTransportOptionsBase<TClientData> & {
/** Access token for frontend-triggered runs. Required when `triggerTask` is not set. */
accessToken: AccessTokenOption;
triggerTask?: undefined;
})
| (TriggerChatTransportOptionsBase<TClientData> & {
/**
* Delegate run triggering to a server-side callback (e.g. a Next.js server action).
*
* When provided, the transport calls this function instead of triggering the task directly
* from the browser. The callback should trigger the task using the secret key and return
* both the `runId` and a run-scoped `publicAccessToken` for stream subscription.
*
* Use `chat.createTriggerAction(taskId)` to create the callback body.
*
* @example
* ```ts
* // actions.ts ("use server")
* import { chat } from "@trigger.dev/sdk/ai";
* export const triggerChat = chat.createTriggerAction("my-chat");
*
* // component.tsx
* const transport = useTriggerChatTransport({
* task: "my-chat",
* triggerTask: triggerChat,
* });
* ```
*/
triggerTask: (params: TriggerChatTaskParams) => Promise<TriggerChatTaskResult>;
/** Optional when `triggerTask` is set. Only needed if the transport needs to resolve tokens for other purposes. */
accessToken?: AccessTokenOption;
});
/**
* Internal state for tracking active chat sessions.
* @internal
@@ -286,6 +351,9 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
private readonly resolveAccessTokenFn:
| ((params: ResolveChatAccessTokenParams) => string | Promise<string>)
| undefined;
private triggerTaskFn:
| ((params: TriggerChatTaskParams) => Promise<TriggerChatTaskResult>)
| undefined;
private readonly baseURL: string;
private readonly streamKey: string;
private readonly extraHeaders: Record<string, string>;
@@ -306,12 +374,19 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
constructor(options: TriggerChatTransportOptions) {
this.taskId = options.task;
if (typeof options.accessToken === "function") {
this.staticAccessToken = undefined;
this.resolveAccessTokenFn = options.accessToken;
} else {
this.staticAccessToken = options.accessToken;
this.resolveAccessTokenFn = undefined;
this.triggerTaskFn = options.triggerTask;
if (options.accessToken) {
if (typeof options.accessToken === "function") {
this.staticAccessToken = undefined;
this.resolveAccessTokenFn = options.accessToken;
} else {
this.staticAccessToken = options.accessToken;
this.resolveAccessTokenFn = undefined;
}
} else if (!options.triggerTask) {
throw new Error(
"TriggerChatTransport: either `accessToken` or `triggerTask` must be provided."
);
}
this.baseURL = options.baseURL ?? DEFAULT_BASE_URL;
this.streamKey = options.streamKey ?? DEFAULT_STREAM_KEY;
@@ -425,43 +500,18 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
}
// First message or run has ended — trigger a new run
const currentToken = await this.resolveAccessToken({ chatId, purpose: "trigger" });
const apiClient = new ApiClient(this.baseURL, currentToken);
// Auto-tag with chatId; merge with user-provided tags (API limit: 5 tags)
const autoTags = [`chat:${chatId}`];
const userTags = this.triggerOptions?.tags ?? [];
const tags = [...autoTags, ...userTags].slice(0, 5);
const triggerResponse = await apiClient.triggerTask(this.taskId, {
payload: {
...payload,
continuation: isContinuation,
...(previousRunId ? { previousRunId } : {}),
},
options: {
payloadType: "application/json",
tags,
queue: this.triggerOptions?.queue ? { name: this.triggerOptions.queue } : undefined,
maxAttempts: this.triggerOptions?.maxAttempts,
machine: this.triggerOptions?.machine,
priority: this.triggerOptions?.priority,
},
});
const runId = triggerResponse.id;
const publicAccessToken =
"publicAccessToken" in triggerResponse
? (triggerResponse as { publicAccessToken?: string }).publicAccessToken
: undefined;
const newSession: ChatSessionState = {
runId,
publicAccessToken: publicAccessToken ?? currentToken,
const triggerPayload = {
...payload,
continuation: isContinuation,
...(previousRunId ? { previousRunId } : {}),
};
const { runId, publicAccessToken } = await this.triggerNewRun(chatId, triggerPayload, "trigger");
const newSession: ChatSessionState = { runId, publicAccessToken };
this.sessions.set(chatId, newSession);
this.notifySessionChange(chatId, newSession);
return this.subscribeToStream(runId, publicAccessToken ?? currentToken, abortSignal, chatId);
return this.subscribeToStream(runId, publicAccessToken, abortSignal, chatId);
};
/**
@@ -605,6 +655,15 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
this.renewRunAccessToken = fn;
}
/**
* Update the server-side trigger callback without recreating the transport.
*/
setTriggerTask(
fn: ((params: TriggerChatTaskParams) => Promise<TriggerChatTaskResult>) | undefined
): void {
this.triggerTaskFn = fn;
}
/**
* Eagerly trigger a run for a chat before the first message is sent.
* This allows initialization (DB setup, context loading) to happen
@@ -631,13 +690,51 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
: {}),
};
const currentToken = await this.resolveAccessToken({ chatId, purpose: "preload" });
const apiClient = new ApiClient(this.baseURL, currentToken);
const { runId, publicAccessToken } = await this.triggerNewRun(chatId, payload, "preload");
const autoTags = [`chat:${chatId}`, "preload:true"];
const newSession: ChatSessionState = { runId, publicAccessToken };
this.sessions.set(chatId, newSession);
this.notifySessionChange(chatId, newSession);
}
private async resolveAccessToken(params: ResolveChatAccessTokenParams): Promise<string> {
if (this.staticAccessToken !== undefined) {
return this.staticAccessToken;
}
if (this.resolveAccessTokenFn) {
return await this.resolveAccessTokenFn(params);
}
throw new Error(
"TriggerChatTransport: accessToken is required for this operation but was not provided."
);
}
private async triggerNewRun(
chatId: string,
payload: Record<string, unknown>,
purpose: "trigger" | "preload"
): Promise<{ runId: string; publicAccessToken: string }> {
const autoTags =
purpose === "preload" ? [`chat:${chatId}`, "preload:true"] : [`chat:${chatId}`];
const userTags = this.triggerOptions?.tags ?? [];
const tags = [...autoTags, ...userTags].slice(0, 5);
if (this.triggerTaskFn) {
return await this.triggerTaskFn({
payload: payload as TriggerChatTaskParams["payload"],
options: {
tags,
queue: this.triggerOptions?.queue,
maxAttempts: this.triggerOptions?.maxAttempts,
machine: this.triggerOptions?.machine,
priority: this.triggerOptions?.priority,
},
});
}
const currentToken = await this.resolveAccessToken({ chatId, purpose });
const apiClient = new ApiClient(this.baseURL, currentToken);
const triggerResponse = await apiClient.triggerTask(this.taskId, {
payload,
options: {
@@ -656,19 +753,7 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
? (triggerResponse as { publicAccessToken?: string }).publicAccessToken
: undefined;
const newSession: ChatSessionState = {
runId,
publicAccessToken: publicAccessToken ?? currentToken,
};
this.sessions.set(chatId, newSession);
this.notifySessionChange(chatId, newSession);
}
private async resolveAccessToken(params: ResolveChatAccessTokenParams): Promise<string> {
if (this.staticAccessToken !== undefined) {
return this.staticAccessToken;
}
return await this.resolveAccessTokenFn!(params);
return { runId, publicAccessToken: publicAccessToken ?? currentToken };
}
private notifySessionChange(chatId: string, session: ChatSessionState | null): void {
+953 -3
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -12,6 +12,8 @@ minimumReleaseAgeExclude:
- "next"
- "@next/*"
- "agentcrumbs"
- "secure-exec"
- "@secure-exec/*"
preferOffline: true
linkWorkspacePackages: false
+2
View File
@@ -19,6 +19,8 @@
"@prisma/client": "^7.4.2",
"@e2b/code-interpreter": "^2.4.0",
"@trigger.dev/sdk": "workspace:*",
"secure-exec": "0.1.0",
"serialize-error": "^11.0.3",
"ai": "^6.0.0",
"next": "15.3.3",
"pg": "^8.16.3",
+9
View File
@@ -1,6 +1,7 @@
"use server";
import { auth } from "@trigger.dev/sdk";
import { chat } from "@trigger.dev/sdk/ai";
import type { ResolveChatAccessTokenParams } from "@trigger.dev/sdk/chat";
import type { aiChat, aiChatRaw, aiChatSession } from "@/trigger/chat";
import type { ChatUiMessage } from "@/lib/chat-tools";
@@ -29,6 +30,14 @@ export async function getChatToken(
return auth.createTriggerPublicToken(task, { expirationTime: CHAT_EXAMPLE_PAT_TTL });
}
/**
* Server-side trigger action — delegates run creation to the server.
* Pass this to `useTriggerChatTransport({ triggerTask: triggerChat })`.
*/
export const triggerChat = chat.createTriggerAction("ai-chat", {
tokenTTL: CHAT_EXAMPLE_PAT_TTL,
});
/**
* Mint a fresh run-scoped PAT for an existing chat run (same scopes as the tasks turn token).
* Used by TriggerChatTransport when the stored PAT expires (401 on realtime / input stream).
+26
View File
@@ -6,6 +6,7 @@ import { z } from "zod";
import os from "node:os";
import TurndownService from "turndown";
import { codeSandboxRun, runWithCodeSandbox } from "@/lib/code-sandbox";
import { runInSecureSandbox } from "@/lib/secure-sandbox";
const turndown = new TurndownService();
@@ -283,6 +284,30 @@ export const executeCode = tool({
},
});
export const executeJs = tool({
description:
"Run JavaScript code in an isolated V8 sandbox (secure-exec). " +
"Use for calculations, data transformations, or quick JS snippets. " +
"The code runs as a CommonJS module — assign results to module.exports. " +
"Example: module.exports = { sum: 1 + 2 };",
inputSchema: z.object({
code: z.string().describe("JavaScript code to execute. Assign results to module.exports."),
}),
execute: async ({ code }) => {
return runInSecureSandbox(async (runtime) => {
const result = await runtime.run<unknown>(code);
if (result.code !== 0) {
return {
error: result.errorMessage ?? `Exit code ${result.code}`,
};
}
return { result: result.exports };
});
},
});
/** Tool set passed to `streamText` for the main `chat.task` run (includes PostHog). */
export const chatTools = {
inspectEnvironment,
@@ -290,6 +315,7 @@ export const chatTools = {
deepResearch,
posthogQuery,
executeCode,
executeJs,
};
type ChatToolSet = typeof chatTools;
@@ -0,0 +1,39 @@
/**
* secure-exec V8 sandbox — runs JavaScript in-process via V8 isolates.
*
* No external API key needed. ~14ms cold start, ~3MB per isolate.
* A fresh runtime is created per execution — no warm/dispose lifecycle needed.
*/
import {
NodeRuntime,
createNodeDriver,
createNodeRuntimeDriverFactory,
} from "secure-exec";
export async function runInSecureSandbox<T>(
runner: (runtime: NodeRuntime) => Promise<T>
): Promise<T | { error: string }> {
const runtime = new NodeRuntime({
systemDriver: createNodeDriver({
permissions: {
fs: (req) => ({
allow: req.path.startsWith("/root") || req.path.startsWith("/tmp"),
}),
network: (req) => ({
allow: req.hostname === "127.0.0.1" || req.hostname === "localhost",
}),
},
}),
runtimeDriverFactory: createNodeRuntimeDriverFactory(),
memoryLimit: 128,
cpuTimeLimitMs: 60_000,
});
try {
return await runner(runtime);
} catch (err) {
return { error: err instanceof Error ? err.message : String(err) };
} finally {
runtime.dispose();
}
}
-1
View File
@@ -167,7 +167,6 @@ export const aiChat = chat
})
.onChatSuspend(async ({ phase, ctx }) => {
logger.debug("Chat suspending", { phase, runId: ctx.run.id });
await disposeCodeSandboxForRun(ctx.run.id);
})
.onChatResume(async ({ phase, ctx }) => {
+2
View File
@@ -1,5 +1,6 @@
import { defineConfig } from "@trigger.dev/sdk";
import { prismaExtension } from "@trigger.dev/build/extensions/prisma";
import { secureExec } from "@trigger.dev/build/extensions/secureExec";
export default defineConfig({
project: process.env.TRIGGER_PROJECT_REF!,
@@ -10,6 +11,7 @@ export default defineConfig({
prismaExtension({
mode: "modern",
}),
secureExec(),
],
},
});