在 Flue 中使用 Mirage Sandbox 适配器:把应用自有的 Workspace 挂载为 Agent 沙箱
【免费下载链接】flueThe sandbox agent framework.项目地址: https://gitcode.com/GitHub_Trending/flue1/flue
导读
本文围绕 Flue 生态中的 Mirage sandbox 适配器展开:它把由应用自行创建并持有生命周期的 MirageWorkspace包装进 Flue 的SandboxFactory接口,从而让 Agent 通过统一的文件与 Shell 工具访问挂载好的资源。读完本文,你将掌握如何通过flue add sandbox mirage一键接入、生成的适配器源码每个关键方法的语义、Node 与 Cloudflare 两种运行时的选型规则,以及 Mirage 相对其他沙箱方案(如 E2B、Daytona)的差异化定位。
快速开始
为一个已有的 Flue 项目添加"挂载式工作区沙箱"能力,只需在终端或你惯用的 coding agent 中执行一条命令:
flue add sandbox mirage该命令背后的 blueprint(flue-blueprint: sandbox/mirage@1)会引导你的编码 Agent 完成三件事:
- 按构建目标安装对应的 Mirage 运行时包:Node 目标安装
@struktoai/mirage-node@^0.0.2,Cloudflare 目标安装@struktoai/mirage-browser@^0.0.2; - 在源码根目录(优先
<root>/.flue/,其次<root>/src/,再次<root>/)创建sandboxes/mirage.ts; - 将适配器接入到你的 Agent 中。
Mirage 适配器也是 Ecosystem 沙箱目录 中登记在册的官方集成项。
适配器设计:应用拥有 Workspace,Flue 只做适配
Mirage 适配器最核心的设计原则是所有权分离:
- 应用负责用 Mirage SDK 构造
Workspace,并直接控制资源挂载(mounts)、凭据(credentials)、可写边界(writable boundaries)与工作区生命周期(lifetime); - Flue只负责把已经初始化好的
Workspace适配进自己的沙箱接口,不创建、不保留、不销毁任何 Mirage 侧的提供商资源。
这从生成文件的顶层注释可以看得一清二楚——它明确写着"Wraps an already-initialized MirageWorkspace… into Flue'sSandboxFactoryinterface. The user owns the root and its mounts; this adapter just adapts the root"(见 blueprints/sandbox--mirage.md)。
运行时选择上,Mirage 提供两个共享同一WorkspaceAPI 的运行时包:
| 运行时包 | 适用目标 | 说明 |
|---|---|---|
@struktoai/mirage-node | Node.js | 提供 Node 兼容的Workspace资源,含 SSH、数据库等 Node 专属资源 |
@struktoai/mirage-browser | Cloudflare | 提供浏览器兼容的Workspace资源(Cloudflare Workers 属于 browser-class 运行时) |
适配器文件本身只从@struktoai/mirage-core(两个包都会再导出)引入类型,因此同一份sandboxes/mirage.ts可以同时服务于两种目标;具体用哪个运行时包,由你在 Agent 代码里按构建目标决定。
生成文件全貌
以下是 blueprint 生成的sandboxes/mirage.ts的完整实现(blueprints/sandbox--mirage.md 中为逐字写入的版本),先给出骨架:
// flue-blueprint: sandbox/mirage@1 import { sandboxFromDriver, SandboxOperationUnsupportedError } from '@flue/runtime'; import type { SandboxDriver, SandboxFactory, Sandbox, FileStat } from '@flue/runtime'; import type { Workspace as MirageWorkspace } from '@struktoai/mirage-core'; export interface MirageAdapterOptions { /** * exec() 未显式传 cwd 时的默认工作目录。 * Mirage 工作区以 / 为根(挂载点都挂在根下),因此 / 是安全默认值; * 想默认落在某个可写挂载(如 /data)可显式指定。 */ cwd?: string; } class MirageSandboxDriver implements SandboxDriver { constructor( private workspace: MirageWorkspace, private flueContextId: string, ) {} /* ... 文件操作走 workspace.fs.*;rm 在变更前拒绝 recursive/force ... */ /* ... exec()/runShell() 走 workspace.execute(),见下文 ... */ } export function mirage(workspace: MirageWorkspace, options?: MirageAdapterOptions): SandboxFactory { return { async createSandbox({ id }: { id: string }): Promise<Sandbox> { try { workspace.createSession(id); } catch { workspace.getSession(id); } const sandboxCwd = options?.cwd ?? '/'; const driver = new MirageSandboxDriver(workspace, id); return sandboxFromDriver(driver, sandboxCwd); }, }; }上下文与会话的映射:以 Flue context id 为键
createSandbox({ id })中的id是 Agent 实例 id(ctx.id)。适配器把它直接当作 Miragesession 的 id:每个 Flue context 对应一个 Mirage session,从而让 cwd、env、命令历史、lastExitCode在不同的 Agent 实例和会话之间保持隔离;同一个 context 内初始化的多个 harness 会有意复用同一个 Mirage session。
实现上先尝试workspace.createSession(id),若抛错(说明 id 已注册,比如同 context 内另一个 harness 或断点续跑后的 context)则回退到workspace.getSession(id)——这正是"同一 context 重复初始化拿到同一 session"的关键。
stat:保留未知元数据的语义
async stat(path: string): Promise<FileStat> { const s = await this.workspace.fs.stat(path); return { isFile: s.type === 'file', isDirectory: s.type === 'directory', ...(s.size === null ? {} : { size: s.size }), ...(s.modified === null ? {} : { mtime: new Date(s.modified) }), }; }Mirage 的FileStat形如{ name, size: number|null, modified: string|null, type: FileType|null }。适配器遵循 Sandbox Adapter API 中的FileStat契约:当 Mirage 未提供size或modified时,直接省略对应字段,而不是伪造占位值——因为调用方无法区分伪造值与真实元数据,这会影响stat的真实性判断。
exec与超时/取消语义
private async runShell(command, options) { const timeoutSignal = typeof options?.timeoutMs === 'number' ? AbortSignal.timeout(options.timeoutMs) : undefined; const callerSignal = options?.signal; const signal = callerSignal && timeoutSignal ? AbortSignal.any([callerSignal, timeoutSignal]) : (callerSignal ?? timeoutSignal); try { const result = await this.workspace.execute(command, { sessionId: this.flueContextId, cwd: options?.cwd, env: options?.env, signal, }); return { stdout: result.stdoutText, stderr: result.stderrText, exitCode: result.exitCode }; } catch (err) { if (callerSignal?.aborted) throw err; // 调用方主动取消优先,直接重抛 const isTimeout = timeoutSignal?.aborted && (err === timeoutSignal.reason || (err instanceof Error && (err.name === 'AbortError' || err.name === 'TimeoutError'))); if (isTimeout) { return { stdout: '', stderr: `[flue:mirage] Command timed out after ${options?.timeoutMs} milliseconds.`, exitCode: 124, }; } throw err; } }这段代码对应了 Sandbox Adapter API 中exec的完整契约:
timeoutMs:用AbortSignal.timeout(timeoutMs)生成毫秒级超时信号,转发给workspace.execute()的signal参数;- 优先级:调用方
signal与超时信号通过AbortSignal.any([...])组合,谁先触发谁生效;若调用方信号先触发,则throw err让宿主取消语义胜出; - 只有超时被转换为
exitCode: 124(遵循timeout(1)惯例)并返回带提示的 stderr;其余异常正常重抛。
Mirage 的执行器会在 LIST/PIPELINE/循环边界协作式地观察signal,因此无需 shell 前缀之类的 workaround——cwd、env、signal 都是直接透传给ExecuteOptions的。
rm:直接文件系统 API 不支持递归/强制删除
async rm(path: string, options?: { recursive?: boolean; force?: boolean }): Promise<void> { const unsupported = [ options?.recursive ? 'recursive' : undefined, options?.force ? 'force' : undefined, ].filter((option): option is string => option !== undefined); if (unsupported.length > 0) { throw new SandboxOperationUnsupportedError({ operation: 'rm', provider: 'Mirage', options: unsupported, }); } try { await this.workspace.fs.unlink(path); } catch { await this.workspace.fs.rmdir(path); } }Mirage 的直接 VFS API(WorkspaceFS)没有递归或 force 删除能力。适配器的处理完全符合 Sandbox Adapter API 的约定:在任何变更之前抛出SandboxOperationUnsupportedError(type: 'sandbox_operation_unsupported',可从@flue/runtime导入,见 packages/runtime/src/errors.ts),绝不静默忽略选项或留下 provider 自定义行为。unlink失败时回退到rmdir,实现单层删除。
mkdir -p与readdir的细节处理
由于WorkspaceFS.mkdir只支持单层创建,递归的mkdir -p通过 shell 转发实现(Mirage 执行器原生支持mkdir -p),路径用shellQuote()做单引号转义以安全嵌入 POSIX 风格命令行。readdir则对 Mirage 返回的绝对路径做处理:用p.slice(p.lastIndexOf('/') + 1)取 basename,并过滤掉目录路径尾随/可能产生的空字符串。
sandboxFromDriver:包装层的职责
mirage()最终把MirageSandboxDriver交给sandboxFromDriver(driver, sandboxCwd)(实现见 packages/runtime/src/sandbox.ts#L379)。这一包装层自动补齐了适配器不需要重复实现的部分:
- 路径解析:相对路径与缺失/相对的
execcwd 统一按cwd解析并做 POSIX 规范化,driver 方法永远收到绝对路径; writeFile父目录保证:首次写入失败后自动重试一次"先mkdir(parent, { recursive: true })再写",满足跨模式约定;- abort 竞态:已中止的信号在调用
driver.exec前就拒绝;中途 abort 也会立即拒绝调用方,不等driver.exec自身的结算。Mirage 支持真实的取消原语,因此转发signal能把"孤儿命令"窗口从命令剩余时长压缩到 SDK 取消延迟。
配置要求
原文档给出的配置要求清单如下:
| 要求 | 用途 |
|---|---|
@struktoai/mirage-node包 | Node.js 上必需—— 提供 Node 兼容的 Mirage Workspace 资源 |
@struktoai/mirage-browser包 | Cloudflare 上必需—— 仅提供浏览器兼容的 Workspace 资源 |
| 应用自有的资源配置 | 必需—— 定义挂载、凭据、可写边界与生命周期 |
| 环境变量凭据 | 不需要—— Mirage 资源的凭据由应用自行配置 |
运行时包与构建目标的匹配
blueprint 会通过vite.config.ts中是否同时存在cloudflare()插件与flue()、flue.config.ts中的target: 'cloudflare'、项目根是否存在wrangler.jsonc/.toml/.json来判断目标;无法判断时会向你确认。
值得特别警惕的是:部分 Mirage 资源是 Node 专属的——SSHResource、PostgresResource、MongoDBResource、EmailResource、FUSE 等。从@struktoai/mirage-browser导入它们是构建错误,因此只要用到其中任何一个,就锁定 Node 目标。另外,如果 Mirage 文档中出现@struktoai/mirage-agents,不要为 Flue 安装它——那是面向其他 Agent 框架的适配器,与 Flue 无关。
认证方式:无 API Key,凭据按资源配置
Mirage 本身没有 API Key——它是进程内运行的,没有需要认证的远程服务。认证是"按挂载资源"的:每个后端(S3Resource、SlackResource、GitHubResource、PostgresResource等)在构造资源时由你配置各自凭据,适配器完全不接触它们。凭据的存储遵循项目既有约定(AGENTS.md、.env、.dev.vars、密钥管理器或 CI 变量)。作为参考:flue run默认加载项目的.env,--env <file>可指定备用文件;vite dev与构建后的服务器读取 shell 环境变量。
将适配器接入 Agent
在 Agent 中接线的方式是标准的useSandbox()模式:
'use agent'; import { Workspace, RAMResource, MountMode } from '@struktoai/mirage-node'; import { useModel, useSandbox } from '@flue/runtime'; import { mirage } from '../sandboxes/mirage'; // 按实际布局调整路径 export function Assistant() { useModel('anthropic/claude-sonnet-4-6'); const ws = new Workspace({ '/data': new RAMResource() }, { mode: MountMode.WRITE }); useSandbox(mirage(ws, { cwd: '/data' })); return 'You are a helpful assistant with a full sandbox.'; }几个要点:
- 顶部的
'use agent'指令负责把模块注册进应用; mirage(ws, { cwd: '/data' })中的cwd让 Agent 默认工作目录落在可写挂载/data上;- 只有当 Agent 需要 HTTP 端点时才需要在
app.ts中挂载createAgentRouter(...)(来自@flue/runtime/routing),flue run与dispatch()无需挂载即可工作; - 沙箱工厂是惰性的:构造工厂对象很廉价,真正昂贵的
createSandbox()只在初始化时调用一次,重渲染时不会重建。
接入后,Agent 会获得一整套基于沙箱的工具:read(带 offset/limit 分页)、write、edit、bash、grep、glob,以及基于工作目录的 workspace 上下文(目录列表、AGENTS.md)与.agents/skills/下的 workspace skills。要验证接入是否成功,可先运行类型检查(npx tsc --noEmit),再确认适配器导入路径与实际文件位置一致,最后用flue run <agent模块路径> --message "..."(或vite dev启动完整应用)实测。
何时选择 Mirage
根据原文档的定位,当你的应用希望"从显式挂载的资源组装出一个工作区,并通过单一沙箱边界把它呈现给 Agent"时,Mirage 是合适的选择。此时资源挂载、凭据、可写边界与工作区生命周期全部由应用掌控,Flue 只负责适配边界。
- 相比 E2B(provider 托管的 Linux 沙箱,需要
E2B_API_KEY)等远程沙箱,Mirage 完全进程内运行,无 API Key,隔离边界来自你对挂载资源的组合与可写边界设定; - Mirage 的 SDK 支持真实取消,因此 abort 时命令会被真正停止(这一点在 Sandboxes 指南 中被明确提及,区别于那些无法中断、只能让孤儿进程在后台继续跑的 provider);
- 适用于"宿主环境本身可信、但你需要给 Agent 一个受控的可写工作区"的场景,例如基于挂载的 S3/Slack/GitHub/数据库资源构建的自托管 Agent。
进一步阅读
- Sandboxes 指南 ——
useSandbox()钩子、沙箱工具集、workspace 上下文与子代理继承关系 - Sandbox Adapter API ——
SandboxFactory/Sandbox/SandboxDriver完整契约、sandboxFromDriver与SandboxOperationUnsupportedError - Deploy on Node.js 与 Deploy on Cloudflare —— 两种运行时的部署指南,对应
@struktoai/mirage-node与@struktoai/mirage-browser的选择 - Mirage blueprint 原文 —— 含逐字可用的完整生成文件与验证步骤
- Ecosystem 沙箱目录 —— Mirage 及其他已支持沙箱提供商的索引
【免费下载链接】flueThe sandbox agent framework.项目地址: https://gitcode.com/GitHub_Trending/flue1/flue
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考