智能体越来越多地编写 TypeScript 程序来协调各种工具并处理其结果。一旦这些程序接触到真实应用,有些步骤需要身份验证,有些则需要人工批准。
用 eval 执行这类代码,等于赋予它与周围应用相同的访问权限,包括应用的密钥和内部服务,而且在这些边界上无法持久地暂停。
今天,我们发布 Run SDK,这个包用于执行不受信任的 JavaScript 和 TypeScript,而不让它直接访问你的应用或系统。应用只暴露少量精简的宿主函数,并且可以因身份验证或人工介入(human-in-the-loop)审批而中断执行。程序在得到决定后继续运行,已完成的工作不会重复执行。
pnpm add run
一个通向宿主的精简接口
Run SDK 在工作线程内的全新 QuickJS 上下文中运行 JavaScript 或剥离类型的 TypeScript,且无法直接访问 Node.js 或网络。
应用通过 hostFunctions 暴露选定的操作。这些都是普通函数,在沙箱内会成为可调用的全局函数:
import { run } from 'run';
const result = await run({
source: `
const orders = await store.listOrders("customer_123");
const total = orders.reduce((sum, order) => sum + order.amount, 0);
return { count: orders.length, total };
`,
hostFunctions: {
store: {
listOrders: async (customerId: string) => {
return database.orders.findMany({ customerId });
},
},
},
});
if (result.status === 'completed') {
console.log(result.value);
}
在这里,生成的程序只知道 store.listOrders()。数据库客户端及其凭据仍留在应用中。
调用通过序列化跨越沙箱边界。宿主函数可以返回 promise,因此现有的服务客户端可以放在这个接口之后,而无需传入沙箱。
你可以在演练场中试用。你在那里运行的代码只能访问页面上的宿主函数。
代码模式的实际应用
Run SDK 是驱动 AI SDK 中代码模式工具执行的内部模块。给智能体一段程序,会改变工作的基本单元:一次模型响应就能描述这些调用以及串联它们的逻辑:
const result = await run({
source: `
const accountId = "account_123";
const [account, invoices] = await Promise.all([
crm.getAccount(accountId),
billing.listInvoices(accountId),
]);
const overdue = invoices.filter(invoice => invoice.status === "overdue");
return { account: account.name, overdue };
`,
hostFunctions: {
crm: { getAccount },
billing: { listInvoices },
},
});
两个请求并发执行,发票过滤则留在程序本地完成。只有有用的结果才返回给应用。
这非常适合需要跨多个内部服务工作的智能体。研究智能体可以先合并搜索结果再作答。客服智能体可以查看某个账户,而不必把整个账单响应放回自己的上下文。
同一个包还可以驱动代码解释器,或是接受客户自定义转换的产品功能。在这两种情况下,可用的数据和操作都由应用来决定。
宿主函数最好与产品中的具体动作一一对应,比如退款。暴露 orders.refund(id) 能让应用有一个清晰的位置来检查用户和订单。而通用的请求函数会让这种权限更难理清。
原生支持人工介入与身份验证
读取发票和发放退款是两回事。当生成的代码走到敏感操作时,宿主函数可以中断执行:
import { getHostFunctionContext } from 'run';
const hostFunctions = {
documents: {
publish: async (draftId: string) => {
const context = getHostFunctionContext();
if (context.resume === undefined) {
context.interrupt({
kind: 'approval',
message: `Publish ${draftId}?`,
});
}
if (context.resume.resolution !== true) {
return { published: false };
}
return publishDraft(draftId);
},
},
};
运行被中断时,结果中会包含一个签名令牌,应用可以将它与审批请求一起保存,并在决定到达时用它恢复运行。
恢复时程序会重放,但已完成的宿主函数调用会使用记录下来的结果。中断之前已完成的宿主函数工作不会再次运行。被中断的函数会收到审批结果,并从那里继续。
当工作流必须等待身份验证时,这一机制同样适用。等待期由应用负责,工作线程无需一直保持存活。
在限制范围内运行
沙箱仍然需要应对无限循环或产生超大结果的代码。createRunner() 用于设置共享限制:
import { createRunner } from 'run';
const runner = createRunner({
limits: {
timeoutMs: 10_000,
memoryLimitBytes: 32 * 1024 * 1024,
},
});
限制也可以按每次运行单独设置。默认值涵盖 QuickJS 堆以及跨越宿主边界的值;应用可以根据自身工作负载收紧这些限制。
每次调用都会获得全新的 QuickJS 上下文。动态求值被禁用,内置原型也经过加固。这一边界针对的是生成的程序,而宿主函数仍属于受信任的应用代码,必须执行常规的授权检查。
Run SDK 面向的是应用内部的 JavaScript 计算。需要操作系统、安装软件包或进程级隔离的工作负载,应改用 Vercel Sandbox。
抽取执行层
这个运行时的第一个版本位于 just-bash 内部,名为 js-exec,由 QuickJS 支撑。它让智能体可以针对该 shell 的虚拟文件系统和命令集编写 TypeScript。
我们把这一层抽取出来做成 Run SDK,并用由应用定义的宿主函数取代了类 Node 环境。我们在 eve 中测试了底层机制,用它针对真实工具运行智能体生成的 TypeScript。现在它驱动着 AI SDK 中的代码模式:现有的 AI SDK 工具被映射为宿主函数,而 Run SDK 负责它们的沙箱化执行。
快速上手
Run SDK 支持 Node.js 22.13+ 和 Bun。你需要在本地开发机器上安装 pnpm 或其他包管理器。
pnpm add run
