
Flue Daytona 沙箱适配器基于 SandboxDriver 契约接入远程沙箱执行环境【免费下载链接】flueThe sandbox agent framework.项目地址: https://gitcode.com/GitHub_Trending/flue1/flue本文以 Flue 仓库中的 Daytona 沙箱蓝图 blueprints/sandbox--daytona.md 为主体完整讲解如何为 Flue 项目接入 Daytona 远程沙箱从适配器文件的完整实现含沙箱死亡检测机制、SandboxDriver底层契约的源码级解析到依赖安装、DAYTONA_API_KEY认证、Agent 接线与验证步骤。读完本文你可以直接在生产项目中复制出可运行的 Daytona 适配器并理解 Flue 运行时如何在 SDK 无法取消的远程调用上保证中止语义与故障收敛。一、蓝图是什么面向 AI 编码代理的标准化接入指南Flue 的 blueprints/ 目录存放由flue add和flue update命令返回的「事实源」Markdown 实现指南详见 blueprints/README.md。Blueprint 不是 npm 包也不是运行时抽象而是写给 AI 编码代理的完整实施说明书CLI 负责拉取并打印指南编码代理负责按指南编辑用户项目。两个命令返回同一份完整指南因此指南必须同时适用于「新增集成」和「更新已有集成」两种场景。Daytona 蓝图的 JSON frontmatter注意是 JSON 而非 YAML声明了它的元数据{ kind: sandbox, version: 1, website: https://daytona.io, aliases: [daytona/sdk] }各字段含义依据 blueprints/README.md 的 Frontmatter 规范字段类型说明kindstring必为sandbox、channel、database、tooling之一sandbox表示这是一个远程执行提供商的沙箱适配器versioninteger指南契约版本必须单调递增、从 1 开始任何改变生成文件、依赖、配置、接线、验证方式的变更都必须递增且不能回退、复用或跳号websitestring命名蓝图必填提供商主页flue add时会展示aliasesstring[]可选用户可能输入的额外名称如包名daytona/sdk匹配不区分大小写不得与其他 slug 或 alias 冲突文件命名遵循kind--name.md约定双连字符保证含单连字符的提供商名无歧义索引生成器packages/cli/scripts/generate-blueprint-index.ts会将sandbox--daytona.md推导为路由daytona。因此用户执行flue add sandbox daytona或别名flue add sandbox daytona/sdk即可获取这份指南。与指南配套的关键机制是生成文件标记marker。Daytona 蓝图要求写出的主文件第一行必须是// flue-blueprint: sandbox/daytona1该行位于所有 import 之前标识「本文件由蓝图sandbox/daytona的 Version 1 管理」。当用户更新集成时若标记缺失代理必须将现有实现与完整蓝图逐段比对、保留定制地应用变更然后补上或更新标记。指南末尾的## Upgrade Guide是唯一的累积升级说明Version 1 仅含Initial version.且无 diff后续每个版本条目必须附带完整的 unified diff 块作为规范性的机械升级依据。当前 Daytona 蓝图即处于 Version 12026-06-14 初始版本。二、适配器的定位只包装不拥有生命周期Daytona 蓝图的核心设计原则只有一条用户拥有 Daytona 客户端的完整生命周期适配器只负责适配。适配器把「用户已用daytona/sdk自行初始化好的 Daytona 沙箱」包装进 Flue 的SandboxFactory接口。创建沙箱、选择镜像、配置 API Key 等操作全部由用户通过 Daytona SDK 直接完成Flue 侧的daytona(sandbox)只是把一个现成的Sandbox实例转成 Agent 可用的执行环境。这与「沙箱适配器由应用拥有提供商资源生命周期」的通用蓝图约定一致见 blueprints/README.md 的 Sandbox adapter blueprints 小节。这种「懒构造」是SandboxFactory契约的强制要求把工厂对象传给useSandbox(...)只是构造一个廉价的普通对象真正昂贵的 Daytona 沙箱创建只发生在createSandbox()内部、且仅在执行初始化时发生一次——绝不能在 Agent 的每次重渲染时触发。仓库中保留了一份较早的示例实现 examples/hello-world/src/sandboxes/daytona.ts展示了最基础的DaytonaSandboxDriver与daytona()工厂形态蓝图中的当前版本在此基础上增加了完整的沙箱死亡检测下文第四节以蓝图文件为权威依据。三、文件落位规则与完整适配器代码3.1 写在哪里蓝图规定按以下顺序选择第一个已存在的源码目录作为落点root/.flue/root/src/root/适配器写入source-dir/sandboxes/daytona.ts缺失的父目录需自行创建。若项目布局特殊多工作区等导致以上选择都不合适应向用户确认后再写。3.2 完整实现蓝图要求逐字写入蓝图明确要求以下文件逐字写入、不得自行改进因为它严格符合已发布的SandboxDriver契约// flue-blueprint: sandbox/daytona1 /** * Daytona adapter for Flue. * * Wraps an already-initialized Daytona sandbox into Flues SandboxFactory * interface. The user creates and configures the sandbox using the Daytona * SDK directly — Flue just adapts it. * * example * typescript * use agent; * import { Daytona } from daytona/sdk; * import { useModel, useSandbox } from flue/runtime; * import { daytona } from ./sandboxes/daytona; * * export function Assistant() { * useModel(anthropic/claude-sonnet-4-6); * useSandbox({ * // Lazy, per the SandboxFactory contract: constructing this object is * // cheap; the expensive Daytona sandbox creation happens once, inside * // createSandbox(), at initialization — never on a re-render. * async createSandbox(options) { * const client new Daytona({ apiKey: process.env.DAYTONA_API_KEY }); * const sandbox await client.create({ image: ubuntu:latest }); * return daytona(sandbox).createSandbox(options); * }, * }); * return You are a helpful assistant with a full sandbox.; * } * */ import { sandboxFromDriver, SandboxDiedError, SandboxOperationUnsupportedError, } from flue/runtime; import type { SandboxDriver, SandboxFactory, Sandbox, FileStat } from flue/runtime; import { DaytonaNotFoundError } from daytona/sdk; import type { Sandbox as DaytonaSandbox } from daytona/sdk; /** How often the death detector reads sandbox state while a call is pending. */ const STATE_POLL_MS 5_000; /** How long a state probe may go unanswered before the sandbox is presumed dead. */ const PROBE_SILENCE_MS 10_000; /** * Sandbox states that mean the sandbox is authoritatively gone. Everything * else — transitional states (starting, stopping, destroying, …), * unknown, archived, paused, a missing value, and any state added in * future SDK versions — counts as alive, so a legitimately slow command on * a healthy sandbox is never interrupted. */ const DEAD_STATES: ReadonlySetstring new Set([ destroyed, stopped, error, build_failed, ]); /** * Await a Daytona SDK call while watching for sandbox death. The Daytona SDK * routes control-plane and toolbox requests through one HTTP client whose * request timeout is 24 hours — effectively unbounded — so a call that is in * flight when the sandbox dies can hang an agent for hours. While the call * is pending, this polls sandbox.refreshData() (one control-plane GET) and * rejects with {link SandboxDiedError} once sandbox.state reports a dead * state; a probe that itself goes unanswered for the silence bound means the * control plane is unreachable too, and the sandbox is presumed dead with it. * * There is deliberately no deadline: any state outside {link DEAD_STATES} * counts as alive, and a rejecting probe is an answer, not death — a * transient control-plane error must not kill a healthy command. The one * exception is DaytonaNotFoundError: the control plane no longer knows the * sandbox, which the SDK itself maps to destroyed. * * Liveness only: this never races the callers abort signal. Caller-facing * cancellation is owned one layer up, by sandboxFromDrivers exec * abort race — it rejects promptly on abort and consumes this promises * eventual settlement once the caller has already been released. */ function raceSandboxDeathT( sandbox: DaytonaSandbox, operation: string, call: PromiseT, ): PromiseT { return new PromiseT((resolve, reject) { let settled false; let pollTimer: ReturnTypetypeof setTimeout | undefined; let silenceTimer: ReturnTypetypeof setTimeout | undefined; const settle (complete: () void): void { if (settled) return; settled true; clearTimeout(pollTimer); clearTimeout(silenceTimer); complete(); }; const probe (): void { silenceTimer setTimeout(() { settle(() reject(new SandboxDiedError({ operation, reason: probe_silent }))); }, PROBE_SILENCE_MS); sandbox.refreshData().then( () { if (settled) return; clearTimeout(silenceTimer); const state sandbox.state; if (state ! undefined DEAD_STATES.has(state)) { settle(() reject(new SandboxDiedError({ operation, reason: stopped }))); } else { pollTimer setTimeout(probe, STATE_POLL_MS); } }, (error: unknown) { if (settled) return; clearTimeout(silenceTimer); if (error instanceof DaytonaNotFoundError) { settle(() reject(new SandboxDiedError({ operation, reason: stopped }))); } else { // Any other rejecting probe is an answer, not silence — and // not proof of death. Keep polling. pollTimer setTimeout(probe, STATE_POLL_MS); } }, ); }; pollTimer setTimeout(probe, STATE_POLL_MS); // These handlers double as the losing branchs rejection consumer, so a // late settlement after death or abort cant surface as an unhandled // rejection. call.then( (value) settle(() resolve(value)), (error: unknown) settle(() reject(error)), ); }); } /** * Implements SandboxDriver by wrapping Daytonas TypeScript SDK. Every SDK call * goes through the death detector so a call that is in flight when the * sandbox dies settles instead of hanging. */ class DaytonaSandboxDriver implements SandboxDriver { constructor(private sandbox: DaytonaSandbox) {} private guardedT(operation: string, call: PromiseT): PromiseT { return raceSandboxDeath(this.sandbox, operation, call); } async readFile(path: string): Promisestring { const buffer await this.guarded(readFile, this.sandbox.fs.downloadFile(path)); return buffer.toString(utf-8); } async readFileBuffer(path: string): PromiseUint8Array { const buffer await this.guarded(readFile, this.sandbox.fs.downloadFile(path)); return new Uint8Array(buffer); } async writeFile(path: string, content: string | Uint8Array): Promisevoid { const buffer typeof content string ? Buffer.from(content, utf-8) : Buffer.from(content); await this.guarded(writeFile, this.sandbox.fs.uploadFile(buffer, path)); } async stat(path: string): PromiseFileStat { const info await this.guarded(stat, this.sandbox.fs.getFileDetails(path)); return { isFile: !info.isDir, isDirectory: info.isDir, size: info.size, mtime: new Date(info.modTime), }; } async readdir(path: string): Promisestring[] { const entries await this.guarded(readdir, this.sandbox.fs.listFiles(path)); return entries.map((e) e.name).filter((name): name is string !!name); } async exists(path: string): Promiseboolean { try { await this.guarded(exists, this.sandbox.fs.getFileDetails(path)); return true; } catch (error) { // Sandbox death is an infrastructure failure, not a missing path. if (error instanceof SandboxDiedError) throw error; return false; } } async mkdir(path: string, options?: { recursive?: boolean }): Promisevoid { if (options?.recursive) { await this.exec(mkdir -p ${path.replace(//g, \\)}); return; } await this.guarded(mkdir, this.sandbox.fs.createFolder(path, 755)); } async rm(path: string, options?: { recursive?: boolean; force?: boolean }): Promisevoid { if (options?.force) { throw new SandboxOperationUnsupportedError({ operation: rm, provider: Daytona, options: [force], }); } await this.guarded(rm, this.sandbox.fs.deleteFile(path, options?.recursive)); } async exec( command: string, options?: { cwd?: string; env?: Recordstring, string; timeoutMs?: number; signal?: AbortSignal; }, ): Promise{ stdout: string; stderr: string; exitCode: number } { // Daytonas executeCommand does not accept an AbortSignal, so it is // deliberately not forwarded here — sandboxFromDriver (which // this adapter builds on) owns caller-facing abort and rejects // promptly while the sandbox keeps running the command. const response await this.guarded( exec, this.sandbox.process.executeCommand( command, options?.cwd, options?.env, typeof options?.timeoutMs number ? Math.ceil(options.timeoutMs / 1000) : undefined, ), ); return { stdout: response.result ?? , stderr: , exitCode: response.exitCode ?? 0, }; } } /** * Create a Flue sandbox factory from an initialized Daytona sandbox. * The user owns the sandbox lifecycle; Flue wraps it into a Sandbox * for agent use. */ export function daytona(sandbox: DaytonaSandbox): SandboxFactory { return { async createSandbox(): PromiseSandbox { const sandboxCwd (await raceSandboxDeath(sandbox, getWorkDir, sandbox.getWorkDir())) ?? /home/daytona; const driver new DaytonaSandboxDriver(sandbox); return sandboxFromDriver(driver, sandboxCwd); }, }; }3.3 死亡检测机制raceSandboxDeath逐项解析这是 Daytona 适配器最关键的部分直接回应 Daytona SDK 的一个硬伤控制面与工具箱请求共用同一个 HTTP 客户端其请求超时为 24 小时——事实上是无界超时。这意味着沙箱在调用飞行途中死亡时该调用可能让 Agent 挂起数小时。检测器的设计要点轮询间隔STATE_POLL_MS 5_000调用 pending 期间每 5 秒执行一次sandbox.refreshData()一次控制面 GET读取沙箱状态。静默界限PROBE_SILENCE_MS 10_000如果一次探测本身 10 秒无响应说明控制面也不可达沙箱推定随之死亡以reason: probe_silent拒绝。死亡状态白名单DEAD_STATES只有destroyed、stopped、error、build_failed四种状态被视为「权威地消失」。其余一切——过渡态starting、stopping、destroying、unknown、archived、paused、缺失值、以及未来 SDK 新增的任意状态——都按存活处理。这个「默认存活」策略保证健康沙箱上的合法慢命令永不误杀。DaytonaNotFoundError是唯一例外控制面已不认识该沙箱SDK 本身将其映射为destroyed立即以reason: stopped判定死亡。拒绝的探测 ≠ 死亡除DaytonaNotFoundError外的探测失败只是「一次回答」控制面瞬时错误不能杀死健康命令轮询继续。故意没有总时限契约中不存在 per-command 的基础设施存活界限Agent 命令本身可以合法地无限长命令自己的期限由timeoutMs表达。只做存活检测不抢中止该机制从不与调用方的 abort signal 竞争。面向调用方的取消由上一层sandboxFromDriver的 exec abort race 独占见第四节它在中止时立即拒绝、并在调用方已释放后消费该 Promise 的最终结算——raceSandboxDeath中call.then(...)的两个处理器同时充当「落败分支的拒绝消费者」避免死亡或中止之后的迟到结算浮现为 unhandled rejection。3.4 驱动层DaytonaSandboxDriver的 SDK 映射DaytonaSandboxDriver把SandboxDriver契约的每个操作映射到 Daytona SDK且每个 SDK 调用都经过guarded()即死亡检测器Flue 操作Daytona SDK 调用关键细节readFilefs.downloadFile(path)以 UTF-8 解码readFileBufferfs.downloadFile(path)原样返回Uint8ArraywriteFilefs.uploadFile(buffer, path)字符串与Uint8Array统一转 Buffer 后上传statfs.getFileDetails(path)isFile: !info.isDir返回size与mtimereaddirfs.listFiles(path)过滤掉无name的条目existsfs.getFileDetails(path)成功即存在但SandboxDiedError必须原样抛出——沙箱死亡是基础设施故障不能误报为路径不存在mkdirfs.createFolder(path, 755)recursive: true时改走exec(mkdir -p ...)并对路径做单引号转义→\rmfs.deleteFile(path, recursive)不支持force选项传入即抛SandboxOperationUnsupportedError由运行时向模型如实报告能力缺口execprocess.executeCommand(command, cwd, env, timeout)timeoutMs向上取整换算成秒Daytona 的超时单位是秒向上取整保证提供商期限不会短于请求值stderr恒为exitCode缺失时回退 0关于exec中的一个刻意决定Daytona 的executeCommand不接受AbortSignal因此适配器不转发signal参数。这不是遗漏——SandboxDriver契约允许适配器在 SDK 不支持中途取消时忽略signal面向调用方的中止由sandboxFromDriver包装层统一负责下文第四节。3.5 工厂函数daytona()daytona(sandbox)返回一个SandboxFactory其createSandbox()做两件事确定工作目录调用sandbox.getWorkDir()同样受死亡检测保护取不到时回退到 Daytona 镜像的标准用户目录/home/daytona包装驱动new DaytonaSandboxDriver(sandbox)后经sandboxFromDriver(driver, sandboxCwd)产出最终Sandbox。sandboxFromDriver在此提供了适配器无需自己实现的横切能力相对路径解析以sandboxCwd为基准的resolvePath、writeFile的父目录自动创建重试、以及 exec 的 abort race。四、底层契约SandboxDriver与sandboxFromDriver的运行时实现Daytona 蓝图「逐字写入」的安全性来自 packages/runtime/src/sandbox.ts 中已发布的契约。理解这两段源码才能理解适配器里每一处不做的设计。4.1SandboxDriver接口与取消语义SandboxDriver 定义 包含 8 个文件系统操作加exec。接口文档明确了取消的两条表达路径timeoutMs主要契约适配器应转发到提供商的原生超时选项E2BtimeoutMs、Daytonatimeout、Modaltimeout等粒度更粗的提供商可以向上取整绝不可向下取整。这与 LLM bash 工具总是随请求携带期限提示的行为保持对等。Daytona 适配器里的Math.ceil(options.timeoutMs / 1000)正是这一条的直接落地。signal?: AbortSignal可选仅当 SDK 支持中途取消如 Mirage 执行器、进程内 bash时才有意义不支持的适配器忽略它即可期限仍由timeoutMs保证。文档还给出了存活liveness要求适配器应通过其提供商 SDK 支持的机制保证沙箱死亡时飞行中的操作能结算——原生拒绝在途调用或在调用 pending 时轮询一次廉价的控制面状态读取Flue 第一方 Cloudflare 适配器即采用后者。没有这类机制的适配器承担一个已被接受的局限调用可能挂起直到外层操作被中止。检测到沙箱死亡的适配器应以SandboxDiedError拒绝让 shell 分类把故障报告为基础设施失败而非调用方取消——该错误被刻意设计为不是AbortError因为中止分类器不能误报它。4.2sandboxFromDriverabort race 的唯一所有者sandboxFromDriver 将任意驱动包装为活的Sandbox。其exec分支把调用送入 raceExecAbort实现了统一的中止语义中止前已 abort 的 signal命令根本不执行直接拒绝飞行途中 abort立即以AbortError拒绝附加提示「沙箱命令无法确认已被取消、可能仍在运行」提供商 Promise 成为孤儿命令——其最终结算成功或失败在此被消费永远不会浮现为 unhandled rejection在 workerd 上 unhandled rejection 就是异常并通过可选的onOrphanSettled回调报告供适配器带外记录、计费或收割沙箱继续执行该命令直到自行结束但调用方已被释放。这正是 Daytona 适配器注释中「sandboxFromDriversexecabort race owns caller-facing abort」的含义也解释了为什么契约禁止适配器自行实现第二层 abort race——那会分裂孤儿记账。此外writeFile经由writeFileCreatingParents实现了「先写、失败则mkdir -p父目录后重试一次」的跨模式统一保证Happy path 只有一次远程调用。4.3useSandbox的挂载约束useSandbox 对工厂对象做了运行时校验必须是含createSandbox()的对象daytona(sandbox)的返回值正符合一次渲染只能调用一次——一个 Agent 只有一个环境可选options.cwd用于在已初始化的环境中限定工作目录须为非空字符串useSandbox在子代理subagent渲染中不可用委托者共享父代理的环境。五、依赖与认证5.1 安装依赖适配器从daytona/sdk导入因此用户项目必须直接依赖它。若package.json尚未列出安装npm install daytona/sdk^0.187.0按项目锁文件使用对应包管理器即可pnpm add、yarn add等。5.2 认证DAYTONA_API_KEY运行时必须提供DAYTONA_API_KEY。蓝图对此有一条硬性纪律绝不自造一个值它必须来自用户。放置位置由项目惯例决定——项目的AGENTS.md、已有.env/.dev.vars、密钥管理器、CI 变量等信号通常能给出答案若项目内没有任何明确信号应询问用户而非猜测。环境变量的加载路径以当前仓库为准flue run默认加载项目的.env--env file可指定另一个.env格式文件vite dev与构建后的服务器读取 shell 环境process.env。六、将适配器接入 Agent蓝图给出的接线示例若用户正在开发的 Agent 正是该适配器的目标可直接代为完成接线use agent; import { Daytona } from daytona/sdk; import { useModel, useSandbox } from flue/runtime; import { daytona } from ../sandboxes/daytona; // adjust path to match the users layout export function Assistant() { useModel(anthropic/claude-sonnet-4-6); useSandbox({ // Lazy, per the SandboxFactory contract: constructing this object is // cheap; the expensive Daytona sandbox creation happens once, inside // createSandbox(), at initialization — never on a re-render. async createSandbox(options) { const client new Daytona({ apiKey: process.env.DAYTONA_API_KEY }); const sandbox await client.create(); return daytona(sandbox).createSandbox(options); }, }); return You are a helpful assistant with a full sandbox.; }要点文件顶部的use agent指令是把模块注册进应用的方式只有当 Agent 需要 HTTP 端点时才在app.ts中挂载createAgentRouter(...)来自flue/runtime/routing——flue run与dispatch()不需要挂载即可工作注意懒构造createSandbox内才创建Daytona客户端与沙箱实例对象构造本身保持廉价。七、验证步骤与版本升级7.1 验证蓝图规定的三步验证运行用户项目的类型检查器安全默认值npx tsc --noEmit确认新文件无错误确认导入适配器的路径与实际写入位置一致例如写在src/sandboxes/daytona.ts就不要从.flue/sandboxes/导入告知用户后续步骤安装daytona/sdk若尚未安装、确保运行时DAYTONA_API_KEY可用然后运行flue run path-to-the-agent-module --message ...或vite dev跑完整应用来试用。7.2 更新已有集成更新集成时代理必须检查现有实现并与本蓝图完整比对应用所有相关变更并保留用户的定制然后在主标记文件中添加或更新// flue-blueprint: sandbox/daytona1标记。当标记缺失时这一比对是强制的。后续版本升级时蓝图末尾的## Upgrade Guide会累积### Version N — YYYY-MM-DD条目每个条目附带从上一版本到本版本的完整 unified diff作为可直接机械应用的升级依据。7.3 与仓库示例实现的差异仓库中的示例 examples/hello-world/src/sandboxes/daytona.ts 是一份较早期的基线实现其DaytonaSandboxDriver直接调用 Daytona SDK 而没有死亡检测包裹stat在 SDK 未返回size/modTime时省略字段exec也不含signal参数。blueprints/sandbox--daytona.md 中的 Version 1 是当前权威契约——所有 SDK 调用都经过raceSandboxDeathexists显式区分「沙箱死亡」与「路径不存在」。在真实项目中接入时应以蓝图文件为唯一事实源。小结Daytona 蓝图示范了 Flue 沙箱生态的完整接入范式用户拥有提供商客户端的生命周期适配器只实现SandboxDriver的九个操作timeoutMs向上取整映射到提供商原生超时signal的缺位由sandboxFromDriver的 abort race 统一兜底而 Daytona SDK 24 小时无界请求超时这一特定风险则由STATE_POLL_MS/PROBE_SILENCE_MS双定时器的死亡检测器以「默认存活、白名单判死」的保守策略化解。这套模式可以平移到任何 SDK 超时行为类似、又不支持中途取消的远程沙箱提供商。【免费下载链接】flueThe sandbox agent framework.项目地址: https://gitcode.com/GitHub_Trending/flue1/flue创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考