把 dsh 的 ctx.fs 和 ctx.shell 两个能力缝隙替换为 Mirage 工作区,让 DSH Agent 直接读写挂载进来的 S3、Slack、Redis 等数据源。
- 语言
- TypeScript
- License
- Apache-2.0
- 分支
- main
安装
$ dsh plugin --profile web add github:strukto-ai/mirage/typescript/packages/dsh在终端中运行以上命令,通过 dsh CLI 安装此插件。可在右上角切换 Profile。 第一次用 dsh?看这篇新手教程
一句话定位
把 DeepSeek Harness 的文件与 shell 后端替换为 Mirage 工作区,让 dsh 的文件工具和 bash 工具直接读写通过挂载接入的 S3、Slack、Redis、Gmail 等数据源,而不再是宿主机的本地磁盘。
核心能力
- 接管 dsh 的文件系统接口(ctx.fs):read、write、edit、stat、listDir 等都走 Mirage 工作区,所有操作先经过挂载点模式和策略层
- 接管 dsh 的 shell 接口(ctx.shell):bash 工具改为执行 Mirage 的 shell,POSIX 工具(grep、ls、cat、cp 等)跨多个挂载源统一可用
- 提供声明式挂载:在 profile 的 cordis.patch.yml 里列出资源(resource 注册名、mode、config),token 等秘密通过
!!js process.env.X在挂载时解析 - 支持沙箱 Python(monty 等运行时)和约 50 个内置后端(RAM、Disk、S3、Redis、Slack、Gmail、GDrive、Notion、Postgres、SSH 等),脚本可以跑在任意挂载路径下
- 默认挂载一个
/tmp(RAM 内存盘,可执行模式),可用于临时文件和 Python 脚本输出 - 支持持久 shell:给 shell 插件传
sessionId后,cd、export、函数定义在多次调用之间保留
技术实现
- 语言: TypeScript(DSH 集成包)
- 关键依赖:
@struktoai/mirage-node(提供 Workspace、资源注册表、shell 执行器)、@deepseek-ai/cordis(插件编排)、@deepseek-ai/dsh-fs和@deepseek-ai/dsh-shell(DSH 的文件与 shell 能力缝隙) - 架构模式: 通过 dsh 的 bundle 机制(package.json 里
dsh.bundle.patch指向 cordis.patch.yml)加载;patch 关闭 dsh 自带的fs-sandbox、bash-sandbox、pwsh-sandbox、tool-pwsh、tool-fs-search五个插件行,插入 mirage 的三件套(MirageService持有工作区,MirageFileSystem和MirageShellExecutor各声明inject = ['mirage']拿到它) - 入口文件:
typescript/packages/dsh/cordis.patch.yml(DSH bundle 入口)、typescript/packages/dsh/src/plugin/{service,fs,shell}.ts(每个 Cordis 插件的默认导出)
适用场景
你希望 dsh 中的 Agent 操作的不是宿主机原本的文件系统,而是 S3、Slack、Notion、Redis 等外部数据源里现成的对象;或者你希望给一次 Agent 会话一个干净的、跟宿主机隔离的"虚拟工作区",让 Agent 在里面读写、跑 Python 脚本、同步产出文件。也适合把已有的 Mirage 工作区直接嵌入到 DSH 体系里来使用。
前置依赖与兼容性
| 依赖 | 最低版本 | 说明 |
|---|---|---|
| Node.js | >=20.10.0 | 由包的 engines 字段声明 |
| DSH | 0.1.0-rc.6 | peerDependencies 固定该版本,未声明更宽范围 |
| 平台 | macOS / Linux / Windows | 包本身未声明 os/cpu 限制 |
| 原生模块 | 无 | DSH 集成包本身不含原生依赖 |
安装方式
dsh plugin --profile web add github:strukto-ai/mirage/typescript/packages/dsh
配置项
| 配置 | 类型 | 说明 | 默认值 |
|---|---|---|---|
| MirageService.mounts | 对象 | 声明式挂载:每项是一段 resource(注册名)/ mode(read/write/exec)/ config(资源配置);不传则必须传一个 live workspace,二选一 | 不设 |
| MirageService.workspace | Workspace | 由调用方拥有生命周期的 Mirage 工作区;传入后插件不再自行构造 | 不设 |
| MirageService.runtimes | 字符串或对象数组 | 运行时清单(monty、pyodide、quickjs 等),可与 workspaceOptions.runtimes 二选一 | 不设 |
| MirageShellExecutor.workdir | 字符串 | 命令的默认工作目录;在绑定 sessionId 时用作该会话的初始目录 | / |
| MirageShellExecutor.sessionId | 字符串 | 绑定到指定的工作区会话,所有命令在该会话内执行,cd/export/函数定义跨调用保留 | 不绑定(每次调用独立子 shell) |
| MirageShellExecutor.defaultTimeoutMs | 毫秒 | 单条命令的默认超时 | 120000 |
| MirageShellExecutor.maxTimeoutMs | 毫秒 | 调用方请求超时的上限 | 600000 |
| MirageShellExecutor.stdoutMaxBytes | 字节 | stdout 捕获上限 | 200000 |
| MirageShellExecutor.stderrMaxBytes | 字节 | stderr 捕获上限 | 64000 |
| MirageShellExecutor.spillDir | 字符串 | 后台命令超缓冲后的"溢出"文件目录(应为工作区路径,如 /tmp),Agent 后续可通过该路径读完整输出 | 不设(超出会标记 truncated 而不落盘) |
| MirageFileSystem.cwd | 字符串 | 相对路径解析的虚拟基目录 | / |
| MirageFileSystem.diffBasisMaxBytes | 字节 | 写入回执中 before/after 字段的"前文本"上限 | 10485760(10 MiB) |
常见问题
Q: 装上之后 dsh 默认能看到什么内容?
A: 自动挂载了一个 /tmp 是 RAM 临时盘,权限是 exec(可执行脚本)。除此之外没有其它真实数据源;所有读写都发生在这次会话的内存里,关掉 dsh 就消失。需要真实数据源时,在 profile 里覆盖 mirage 那一行即可。
Q: 怎么把 Slack、Redis、S3 等真实数据源接进来?
A: 在 profile 自己的 cordis.patch.yml 里把 mirage 这条配置改成 mounts 块,写出 resource(注册名,如 slack/redis/s3)、mode(read/write/exec)和 config(资源所需字段)。token 这种秘密用 !!js process.env.SLACK_BOT_TOKEN 在加载时解析,避免明文落盘。
Q: 启用之后 dsh 自带的 PowerShell 和 ripgrep 搜索怎么不见了?
A: 这两个工具会拉起宿主进程,工作区里没有它们的位置,所以这个 bundle 显式禁用了 pwsh-sandbox、tool-pwsh 和 tool-fs-search。搜索能力由 bash 工具里的 grep 覆盖,覆盖所有挂载源。
Q: 怎么让 bash 工具在多次调用之间保持目录和变量?
A: 给 MirageShellExecutor 传 sessionId 字段(如 'agent-1'),cd、export、函数定义就会跨调用保留;不传则每次调用都是独立子 shell,状态不持久。sessionId 相同表示接到同一个工作区会话,不同则完全隔离。
Q: 装上之后的工作区是只读还是可写?
A: 取决于每个 mount 的 mode,read < write < exec 三档依次放开。默认的 /tmp 是 exec 档,可执行 bash 脚本。要让 dsh 的文件工具能写某个挂载(如把结果写回 Redis),就把它声明成 mode: write 或 mode: exec。
Q: 大输出量的后台命令会怎样处理?
A: 默认 stdout 缓冲 200KB、stderr 缓冲 64KB,超出会被截掉并标记 truncated。要保留完整输出就把 MirageShellExecutor.spillDir 指向一个工作区路径(如 /tmp),溢出部分会落盘成文件,路径在 readOutput() 里返回,Agent 可通过同一 VFS 读回。
Q: 它是 DSH 官方插件吗?
A: 不是 DSH 仓库自带的。它通过 dsh 的 bundle 机制(cordis.patch.yml)把自己的三个 Cordis 插件插入到 dsh 体系里,替换 dsh 的 ctx.fs 和 ctx.shell 两个能力缝隙。底层使用的是 DSH 0.1.0-rc.6 暴露的 fs/shell 插件接口。
Q: 怎么挂载自己写的后端?
A: 在调用方一侧 registerResourceFactory('my-store', (config) => new MyStore(config)),然后在 mount 块里写 resource: my-store 即可。注册逻辑必须在工作区构造之前完成,因此放在另一个由 profile 加载的插件里或在 MirageService 启动前直接导入。
上手难度
进阶 — 装上即可用默认 /tmp 跑命令,但要让 Agent 真正拿到 Slack/Redis 等数据,就得在 profile 的 cordis.patch.yml 里写 mount 块、搞清楚 mode 三档和 !!js 表达式的语义;启用持久 shell、溢出目录、Python 沙箱等细节也都需要一段配置。
已知问题与限制
- bundle 默认禁用 dsh 自带的 PowerShell 工具和基于 ripgrep 的全文搜索工具,因为它们跑宿主进程而工作区里没有它们的位置;要 search 改用 bash 工具里的
grep - 同一个目标路径的写操作会被串行化(按路径加锁),并发写时只有一个赢家,其它请求会观测到新版本并被旧版本守卫视为过期拒绝
- 当工作区里包含了一个"绕过 VFS"的运行时(如宿主本地 Python 运行时),shell 插件会向 dsh 报告
sandboxMode = undefined(即"没有沙箱"),而不是声明成workspace-write;这种情形下 dsh 的权限预设不会和这个 shell 组合 spillDir不设时,后台命令输出超出 stdout 缓冲只会标记truncated,不会把完整输出写入文件;想"完整可读"必须显式给一个工作区路径- bash 工具默认在每次调用之间是隔离的(state 不持久),想保留 cwd/export/函数必须传
sessionId;同一个 sessionId 上不同 shell 实例共享同一个工作区会话 - mount 模式强度按
read < write < exec排列,模式弱的 mount 上脚本无法执行;让 Agent 跑 Python 脚本需要把/tmp(或对应路径)显式声明为mode: exec - 写操作前置的"陈旧版本"守卫基于后端的指纹或修改时间+大小,没法同时表征二者的后端会报告为
unversioned,写时不做版本比较 - 读取二进制文件(首 8KB 含 NUL 字节)或非 UTF-8 文本会被
decodeStrictText拒绝为FS_NOT_TEXT;读取走 fatal UTF-8 解码,不会做宽容替换
Mirage is a Unified Virtual File System for AI Agents: it mounts services and data sources like S3, Google Drive, Slack, Gmail, and Redis side-by-side as one filesystem. Any LLM that already knows bash can read, grep, and pipe across every backend out of the box, with zero new vocabulary.
ws = Workspace(
{
"/tmp": (RAMResource(), MountMode.EXEC),
"/redis": (RedisResource(url=redis_url), MountMode.WRITE),
"/slack": (SlackResource(SlackConfig(token=slack_bot_token)), MountMode.EXEC),
},
# monty captures python, so scripts run sandboxed inside the workspace
runtimes=[MontyRuntime(captures=["python", "python3"]), "vfs"],
)
# one grep sweeps every source
await ws.execute("grep -rln session /redis /tmp")
# run a script that lives in Slack, file the report into Redis
await ws.execute(
"python3 /slack/channels/general__C0.../files/example__F0....py > /redis/report.txt"
)
# install a typed CLI under a head word: dispatched by name, not by path,
# and discoverable through `man`, `type` and `which` like any other program
ws.register_cli("slack", SLACK, {"token": slack_bot_token})
await ws.execute('slack send-message --channel general --text "report is up"')
About
- One interface instead of N SDKs and M MCPs. Every service speaks the same filesystem semantics, and pipelines compose across services as naturally as on a local disk.
- Around 50 built-in backends: RAM, Disk, Redis, S3 / R2 / OCI / Supabase / GCS, Gmail / GDrive / GDocs / GSheets / GSlides, GitHub / Linear / Notion / Trello, Slack / Discord / Email, MongoDB / GridFS / Postgres / LanceDB / Qdrant, SSH, and more, mounted side-by-side under a single root.
- Portable workspaces: clone, snapshot, and version a workspace; agent runs move between machines without restarting or reconfiguring the system.
- Embeddable: the Python and TypeScript SDKs run in-process inside FastAPI, Express, browser apps, or any async runtime; no separate process required.
- Agent integrations: OpenAI Agents SDK, Vercel AI SDK, LangChain, Pydantic AI, CAMEL, and OpenHands via the SDKs; coding agents through native adapters, installable plugins, MCP, or FUSE.
Architecture
Installation
- Python ≥ 3.11 for the
mirage-aipackage and themirageCLI - Node.js ≥ 20 for the TypeScript SDK
- macOS or Linux (FUSE-based mounts require platform support)
Python
uv add mirage-ai # installs the `mirage` library and the `mirage` CLI binary
TypeScript
npm install @struktoai/mirage-node # Node.js servers and CLIs
npm install @struktoai/mirage-browser # browser / edge runtimes
npm install @struktoai/mirage-agents # OpenAI / Vercel AI / LangChain / Mastra adapters
Both runtime packages pull in @struktoai/mirage-core automatically.
CLI
curl -fsSL https://strukto.ai/mirage/install.sh | sh
# or
npm install -g @struktoai/mirage-cli
# or
uvx mirage-ai
# or
npx @struktoai/mirage-cli
Quickstart
Python
from mirage import Workspace
from mirage.resource.ram import RAMResource
from mirage.resource.s3 import S3Config, S3Resource
ws = Workspace({
"/data": RAMResource(),
"/s3": S3Resource(S3Config(bucket="my-bucket")),
})
await ws.execute("cp /s3/report.csv /data/report.csv")
await ws.execute("grep alert /s3/data/log.jsonl | wc -l")
await ws.snapshot("demo.tar")
TypeScript
import { Workspace, RAMResource, S3Resource } from '@struktoai/mirage-node'
const ws = new Workspace({
'/data': new RAMResource(),
'/s3': new S3Resource({ bucket: 'my-bucket' }),
})
await ws.execute('cp /s3/report.csv /data/report.csv')
await ws.execute('grep alert /s3/data/log.jsonl | wc -l')
await ws.snapshot('demo.tar')
CLI
mirage workspace create ws.yaml --id demo
mirage execute --workspace_id demo --command "cp /s3/report.csv /data/report.csv"
mirage provision --workspace_id demo --command "cat /s3/data/large.jsonl"
mirage workspace snapshot demo demo.tar
mirage workspace load demo.tar --id demo-restored
Agent Frameworks
Mirage plugs into agent frameworks as a sandbox or tool layer. POSIX operations such as read can also be customized per resource and filetype: Mirage ships no filetype renderers, so a format renders however you register it, and a command registered for one resource and extension wins over the generic one.
| Integrations | |
|---|---|
| Python | OpenAI Agents SDK, LangChain, Pydantic AI, CAMEL, OpenHands, Agno |
| TypeScript | Vercel AI SDK, OpenAI Agents SDK, LangChain, Mastra |
| Coding agents | Claude Code, Codex, DeepSeek Harness, Grok Build, OpenCode, Pi |
Cache
Every Workspace has a two-layer cache so repeated work against remote backends hits local state instead of the network:
- Index cache: listings and metadata. The first directory walk hits the API; later ones serve from the index until the TTL expires (default 10 minutes).
- File cache: object bytes. The first read streams from origin; later pipelines read from cache (default 512 MB).
Both layers default to in-process RAM with zero setup. A Redis store shares cache state across workers, processes, and machines:
import { RedisFileCacheStore, S3Resource, Workspace } from '@struktoai/mirage-node'
const ws = new Workspace(
{ '/s3': new S3Resource({ bucket: 'my-bucket' }) },
{
cache: new RedisFileCacheStore({ url: 'redis://localhost:6379/0', cacheLimit: '8GB' }),
index: { type: 'redis', url: 'redis://localhost:6379/0', ttl: 600 },
},
)
See the cache docs for the full miss/hit lifecycle.
Contributors
Thanks to everyone who has contributed to Mirage.