# mirage

> 把 dsh 的 ctx.fs 和 ctx.shell 两个能力缝隙替换为 Mirage 工作区，让 DSH Agent 直接读写挂载进来的 S3、Slack、Redis 等数据源。

## 元数据

- 作者: [@strukto-ai](https://github.com/strukto-ai)
- 仓库: <https://github.com/strukto-ai/mirage.git>
- GitHub: [strukto-ai/mirage](https://github.com/strukto-ai/mirage)
- Star: 3,497
- 主语言: TypeScript
- License: [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)
- 主页: <https://www.strukto.ai/mirage>
- Topics: `agent-sandbox`, `agent-tools`, `ai-agents`, `bash`, `claude-code`, `dsh`, `dsh-plugin`, `fuse`, `llm-agents`, `openai-agents`, `python`, `typescript`, `vfs`, `virtual-filesystem`
- Fork: 259
- Open Issues: 107
- 最后推送: 2026-08-17T08:46:40.000Z
- 加入目录: 2026-08-16T00:00:00.000Z

## 安装

```bash
dsh plugin --profile web add github:strukto-ai/mirage/typescript/packages/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 集成包本身不含原生依赖 |

## 安装方式
```bash
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 解码，不会做宽容替换

---

本文档由 [deepseek-plugin.org](https://deepseek-plugin.org) 自动生成，对应 HTML 页面: [mirage](https://deepseek-plugin.org/plugins/strukto-ai/mirage/typescript/packages/dsh)
百度百科由 AI 生成 (模型: `MiniMax-M3`)
