news 2026/9/16 19:58:53

在 Flue 中使用 Mirage Sandbox 适配器:把应用自有的 Workspace 挂载为 Agent 沙箱

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
在 Flue 中使用 Mirage Sandbox 适配器:把应用自有的 Workspace 挂载为 Agent 沙箱

在 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 完成三件事:

  1. 按构建目标安装对应的 Mirage 运行时包:Node 目标安装@struktoai/mirage-node@^0.0.2,Cloudflare 目标安装@struktoai/mirage-browser@^0.0.2
  2. 在源码根目录(优先<root>/.flue/,其次<root>/src/,再次<root>/)创建sandboxes/mirage.ts
  3. 将适配器接入到你的 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-nodeNode.js提供 Node 兼容的Workspace资源,含 SSH、数据库等 Node 专属资源
@struktoai/mirage-browserCloudflare提供浏览器兼容的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 未提供sizemodified时,直接省略对应字段,而不是伪造占位值——因为调用方无法区分伪造值与真实元数据,这会影响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 的约定:在任何变更之前抛出SandboxOperationUnsupportedErrortype: 'sandbox_operation_unsupported',可从@flue/runtime导入,见 packages/runtime/src/errors.ts),绝不静默忽略选项或留下 provider 自定义行为。unlink失败时回退到rmdir,实现单层删除。

mkdir -preaddir的细节处理

由于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-nodeNode.js 上必需—— 提供 Node 兼容的 Mirage Workspace 资源
@struktoai/mirage-browserCloudflare 上必需—— 仅提供浏览器兼容的 Workspace 资源
应用自有的资源配置必需—— 定义挂载、凭据、可写边界与生命周期
环境变量凭据不需要—— Mirage 资源的凭据由应用自行配置

运行时包与构建目标的匹配

blueprint 会通过vite.config.ts中是否同时存在cloudflare()插件与flue()flue.config.ts中的target: 'cloudflare'、项目根是否存在wrangler.jsonc/.toml/.json来判断目标;无法判断时会向你确认。

值得特别警惕的是:部分 Mirage 资源是 Node 专属的——SSHResourcePostgresResourceMongoDBResourceEmailResource、FUSE 等。从@struktoai/mirage-browser导入它们是构建错误,因此只要用到其中任何一个,就锁定 Node 目标。另外,如果 Mirage 文档中出现@struktoai/mirage-agents不要为 Flue 安装它——那是面向其他 Agent 框架的适配器,与 Flue 无关。

认证方式:无 API Key,凭据按资源配置

Mirage 本身没有 API Key——它是进程内运行的,没有需要认证的远程服务。认证是"按挂载资源"的:每个后端(S3ResourceSlackResourceGitHubResourcePostgresResource等)在构造资源时由你配置各自凭据,适配器完全不接触它们。凭据的存储遵循项目既有约定(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 rundispatch()无需挂载即可工作;
  • 沙箱工厂是惰性的:构造工厂对象很廉价,真正昂贵的createSandbox()只在初始化时调用一次,重渲染时不会重建。

接入后,Agent 会获得一整套基于沙箱的工具:read(带 offset/limit 分页)、writeeditbashgrepglob,以及基于工作目录的 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完整契约、sandboxFromDriverSandboxOperationUnsupportedError
  • 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 19:57:44

产品经理用Cursor自动生成技术文档:规则文件与提示词实战指南

产品经理用Cursor自动生成技术文档&#xff0c;这段时间在我们团队内部已经成了默认流程。你可能听到“Cursor”第一反应是“AI编程工具&#xff0c;跟我产品经理有什么关系”&#xff0c;但实际上&#xff0c;它是目前最合适做“需求语言到工程语言”翻译的AI编辑器。配合一套…

作者头像 李华
网站建设 2026/9/16 19:57:20

ClawHub插件镜像加速方案:智能CDN与存储优化实践

1. 项目背景与核心价值作为一名常年与开发工具打交道的技术从业者&#xff0c;我深刻理解国内开发者在获取插件资源时面临的困境。SkillHub镜像的诞生&#xff0c;正是为了解决这个长期存在的痛点。不同于常规的镜像服务&#xff0c;这个方案专门针对ClawHub插件生态进行了深度…

作者头像 李华
网站建设 2026/9/16 19:55:48

国产电源芯片选型实战指南:从参数对标到系统替代

1. 项目概述&#xff1a;为什么这份电源芯片选型清单值得你花5分钟读完最近半年&#xff0c;我几乎把国内主流电源管理芯片&#xff08;PMIC&#xff09;原厂的官网、产品手册、应用笔记、FAE技术文档翻了个底朝天&#xff0c;不是为了写软文&#xff0c;也不是接了KOL推广&…

作者头像 李华
网站建设 2026/9/16 19:55:42

3个免费降AIGC平台,让你的论文AI率直降个位数[必看]

最近不少同学私信我&#xff0c;说论文明明是自己一个字一个字敲的&#xff0c;用AI辅助整理了一下思路&#xff0c;结果在学校的AIGC检测系统里&#xff0c;相似度直接飙到30%以上&#xff0c;人都傻了。这还真不是个例&#xff0c;随着各大查重平台上线AI检测&#xff0c;&qu…

作者头像 李华
网站建设 2026/9/16 19:55:37

Kimi Chat 连上 TaoToken 后,20 万字长文一次能读完

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华