# Building a Virtual Filesystem for Mintlify's AI Assistant
**作者**: Dens Sumesh
**日期**: 2026-04-02T18:02:13.000Z
**来源**: [https://x.com/densumesh/status/2039765361533637016](https://x.com/densumesh/status/2039765361533637016)
---

RAG is great, until it isn't.
Our assistant could only retrieve chunks of text that matched a query. If the answer lived across multiple pages, or the user needed exact syntax that didn't land in a top-K result, it was stuck. We wanted it to explore docs the way you'd explore a codebase.
Agents are converging on filesystems as their primary interface because grep, cat, ls, and find are all an agent needs. If each doc page is a file and each section is a directory, the agent can search for exact strings, read full pages, and traverse the structure on its own. We just needed a filesystem that mirrored the live docs site.
## The Container Bottleneck
The obvious way to do this is to just give the agent a real filesystem. Most harnesses solve this by spinning up an isolated sandbox and cloning the repo. We already use sandboxes for asynchronous background agents where latency is an afterthought, but for a frontend assistant where a user is staring at a loading spinner, the approach falls apart. Our p90 session creation time (including GitHub clone and other setup) was ~46 seconds.
Beyond latency, dedicated micro-VMs for reading static documentation introduced a serious infrastructure bill.
At 850,000 conversations a month, even a minimal setup (1 vCPU, 2 GiB RAM, 5-minute session lifetime) would put us north of $70,000 a year based on Daytona's per-second sandbox pricing ($0.0504/h per vCPU, $0.0162/h per GiB RAM). Longer session times double that. (This is based on a purely naive approach, a true production workflow would probably have warm pools and container sharing, but the point still stands)
We needed the filesystem workflow to be instant and cheap, which meant rethinking the filesystem itself.
## Faking a Shell
The agent doesn't need a real filesystem; it just needs the illusion of one. Our documentation was already indexed, chunked, and stored in a Chroma database to power our search, so we built ChromaFs: a virtual filesystem that intercepts UNIX commands and translates them into queries against that same database. Session creation dropped from ~46 seconds to ~100 milliseconds, and since ChromaFs reuses infrastructure we already pay for, the marginal per-conversation compute cost is zero.

ChromaFs Architecture
ChromaFs is built on just-bash by Vercel Labs (shoutout Malte!), a TypeScript reimplementation of bash that supports grep, cat, ls, find, cd, and more. just-bash exposes a pluggable IFileSystem interface, so it handles all the parsing, piping, and flag logic while ChromaFs translates every underlying filesystem call into a Chroma query.
```
export class ChromaFs implements IFileSystem {
private files = new Set<string>();
private dirs = new Map<string, string[]>();
async readFile(path: string): Promise<string> {
this.assertInit();
const normalized = normalizePath(path);
// Serve from cache or fetch from Chroma
const slug = normalized.replace(/\\.mdx$/, '').slice(1);
// Pages are chunked in Chroma. Reassemble them on the fly:
const results = await this.collection.get<ChunkMetadata>({
where: { page: slug },
include: [IncludeEnum.documents, IncludeEnum.metadatas],
});
const chunks = results.ids
.map((id, i) => ({
document: results.documents[i] ?? '',
chunkIndex: parseInt(String(results.metadatas[i]?.chunk_index ?? 0), 10),
}))
.sort((a, b) => a.chunkIndex - b.chunkIndex);
return chunks.map((c) => c.document).join('');
}
// Enforce completely stateless, read-only interaction
async writeFile(): Promise<void> { throw erofs(); }
async appendFile(): Promise<void> { throw erofs(); }
async mkdir(): Promise<void> { throw erofs(); }
async rm(): Promise<void> { throw erofs(); }
}
```
## How it works
Bootstrapping the Directory Tree
ChromaFs needs to know what files exist before the agent runs a single command. We store the entire file tree as a gzipped JSON document (__path_tree__) inside the Chroma collection:
```
{
"auth/oauth": { "isPublic": true, "groups": [] },
"auth/api-keys": { "isPublic": true, "groups": [] },
"internal/billing": { "isPublic": false, "groups": ["admin", "billing"] },
"api-reference/endpoints/users": { "isPublic": true, "groups": [] }
}
```
On init, the server fetches and decompresses this document into two in-memory structures: a Set<string> of file paths and a Map<string, string[]> mapping directories to children.
Once built, ls, cd, and find resolve in local memory with no network calls. The tree is cached, so subsequent sessions for the same site skip the Chroma fetch entirely.
Access Control
Notice the isPublic and groups fields in the path tree. Before building the file tree, ChromaFs prunes the file tree based on the current user's permissions and applies a matching filter to all subsequent Chroma queries.
In a real sandbox, this level of per-user access control would require managing Linux user groups, chmod permissions, or maintaining isolated container images per customer tier. In ChromaFs it's a few lines of filtering before buildFileTree runs.
Reassembling Pages from Chunks
Pages in Chroma are split into chunks for embedding, so when the agent runs cat /auth/oauth.mdx, ChromaFs fetches all chunks with a matching page slug, sorts by chunk_index, and joins them into the full page. Results are cached so repeated reads during grep workflows never hit the database twice.
Not every file needs to exist in Chroma. We register lazy file pointers that resolve on access for large OpenAPI specs stored in customers' S3 buckets. The agent sees v2.json in /api-specs/, but the content only fetches when it runs cat.
Every write operation throws an EROFS (Read-Only File System) error. The agent explores freely but can never mutate documentation, which makes the system stateless with no session cleanup and no risk of one agent corrupting another's view.
## Optimizing Grep
cat and ls are straightforward to virtualize, but grep -r would be far too slow if it naively scanned every file over the network. We intercept just-bash’s grep, parse the flags with yargs-parser, and translate them into a Chroma query ($contains for fixed strings, $regex for patterns).
Chroma acts as a coarse filter that identifies which files might contain the hit, and we bulkPrefetch those matching chunks into a Redis cache. From there, we rewrite the grep command to target only the matched files and hand it back to just-bash for fine filter in-memory execution, which means large recursive queries complete in milliseconds.
```
const chromaFilter = toChromaFilter(
scannedArgs.patterns,
scannedArgs.fixedStrings,
scannedArgs.ignoreCase
);
// 1. Coarse Filter: Ask Chroma for slugs matching the string/regex
const matchedSlugs = await chromaFs.findMatchingFiles(chromaFilter, slugsUnderDirs);
if (matchedSlugs.length === 0) return { stdout: ‘’, exitCode: 1 };
// 2. Prefetch: Pull the chunked files into local cache concurrently
await chromaFs.bulkPrefetch(matchedSlugs);
// 3. Fine Filter: Narrow the arguments to ONLY the resolved hits
const matchedPaths = matchedSlugs.map((s) => ‘/’ + s + ‘.mdx’);
const narrowedArgs = [...args, ...matchedPaths]; // e.g. ["-i", "OAuth", "/docs/auth.mdx"]
// 4. Exec: Let the in-memory RegExp engine format the final output
return execBuiltin(narrowedArgs, ctx);
```
## Conclusion
ChromaFs powers the documentation assistant for hundreds of thousands of users across 30,000+ conversations a day. By replacing sandboxes with a virtual filesystem over our existing Chroma database, we got instant session creation, zero marginal compute cost, and built-in RBAC without any new infrastructure.
Try it on any Mintlify docs site, or mintlify.com/docs.
[Read the full article at: https://www.mintlify.com/blog/how-we-built-a-virtual-filesystem-for-our-assistant]
## 相关链接
- [Dens Sumesh](https://x.com/densumesh)
- [@densumesh](https://x.com/densumesh)
- [902K](https://x.com/densumesh/status/2039765361533637016/analytics)
- [converging on filesystems as their primary interface](https://arxiv.org/abs/2601.11672)
- [Daytona's per-second sandbox pricing](https://www.daytona.io/pricing)
- [just-bash](https://github.com/vercel-labs/just-bash)
- [Malte](https://x.com/cramforce)
- [$regex](https://x.com/search?q=%24regex&src=cashtag_click)
- [mintlify.com/docs](https://mintlify.com/docs)
- [https://www.mintlify.com/blog/how-we-built-a-virtual-filesystem-for-our-assistant](https://www.mintlify.com/blog/how-we-built-a-virtual-filesystem-for-our-assistant)
- [Upgrade to Premium](https://x.com/i/premium_sign_up)
- [2:02 AM · Apr 3, 2026](https://x.com/densumesh/status/2039765361533637016)
- [902.4K Views](https://x.com/densumesh/status/2039765361533637016/analytics)
- [View quotes](https://x.com/densumesh/status/2039765361533637016/quotes)
---
*导出时间: 2026/5/6 13:30:21*
---
## 中文翻译
# 为 Mintlify 的 AI 助手构建虚拟文件系统
**作者**: Dens Sumesh
**日期**: 2026-04-02T18:02:13.000Z
**来源**: [https://x.com/densumesh/status/2039765361533637016](https://x.com/densumesh/status/2039765361533637016)
---

RAG(检索增强生成)很棒,直到它不再奏效。
我们的助手只能检索与查询匹配的文本块。如果答案分散在多个页面上,或者用户需要精确的语法但该语法未出现在 Top-K 结果中,它就会陷入困境。我们希望它能像探索代码库那样探索文档。
Agent(智能体)正逐渐将文件系统作为其主要接口,因为 grep、cat、ls 和 find 是 Agent 所需的全部。如果每个文档页面是一个文件,每个章节是一个目录,Agent 就可以搜索精确的字符串、读取完整的页面,并自行遍历结构。我们只需要一个能镜像实时文档站点的文件系统。
## 容器瓶颈
显而易见的方法是直接给 Agent 一个真实的文件系统。大多数工具通过启动一个隔离的沙箱并克隆仓库来解决这个问题。我们已经在异步后台 Agent 中使用了沙箱,因为在那里延迟是次要考虑因素,但对于一个用户盯着加载旋转图标的前端助手来说,这种方法就行不通了。我们的 p90 会话创建时间(包括 GitHub 克隆和其他设置)大约是 46 秒。
除了延迟之外,为了读取静态文档而使用专用的微型虚拟机,带来了不菲的基础设施账单。
按照每月 850,000 次对话计算,即使是最小的配置(1 vCPU,2 GiB RAM,5 分钟会话生命周期),根据 Daytona 的按秒计费沙箱价格(每个 vCPU $0.0504/小时,每 GiB RAM $0.0162/小时),我们每年的花费也将超过 70,000 美元。更长的会话时间会使成本翻倍。(这是基于完全朴素的方法,真正的生产工作流可能会有预热池和容器共享,但观点依然成立)
我们需要文件系统工作流即时且廉价,这意味着要重新思考文件系统本身。
## 伪造 Shell
Agent 并不需要真实的文件系统;它只需要一种拥有文件系统的幻觉。我们的文档已经被索引、分块并存储在 Chroma 数据库中以支持搜索,因此我们构建了 ChromaFs:一个虚拟文件系统,它拦截 UNIX 命令并将其转换为对同一数据库的查询。会话创建时间从 ~46 秒降至 ~100 毫秒,而且由于 ChromaFs 复用了我们已经在付费的基础设施,每次对话的边际计算成本为零。

ChromaFs 架构
ChromaFs 基于由 Vercel Labs 开发的 just-bash(致谢 Malte!)构建,这是一个 TypeScript 实现的 bash,支持 grep、cat、ls、find、cd 等功能。just-bash 暴露了一个可插拔的 IFileSystem 接口,因此它处理所有的解析、管道和标志逻辑,而 ChromaFs 将每个底层的文件系统调用转换为 Chroma 查询。
```
export class ChromaFs implements IFileSystem {
private files = new Set<string>();
private dirs = new Map<string, string[]>();
async readFile(path: string): Promise<string> {
this.assertInit();
const normalized = normalizePath(path);
// 从缓存提供数据或从 Chroma 获取
const slug = normalized.replace(/\\.mdx$/, '').slice(1);
// 页面在 Chroma 中被分块。即时重组它们:
const results = await this.collection.get<ChunkMetadata>({
where: { page: slug },
include: [IncludeEnum.documents, IncludeEnum.metadatas],
});
const chunks = results.ids
.map((id, i) => ({
document: results.documents[i] ?? '',
chunkIndex: parseInt(String(results.metadatas[i]?.chunk_index ?? 0), 10),
}))
.sort((a, b) => a.chunkIndex - b.chunkIndex);
return chunks.map((c) => c.document).join('');
}
// 强制执行完全无状态、只读的交互
async writeFile(): Promise<void> { throw erofs(); }
async appendFile(): Promise<void> { throw erofs(); }
async mkdir(): Promise<void> { throw erofs(); }
async rm(): Promise<void> { throw erofs(); }
}
```
## 工作原理
引导目录树
在 Agent 运行单个命令之前,ChromaFs 需要知道存在哪些文件。我们将整个文件树存储为 Chroma 集合内的一个 gzip 压缩的 JSON 文档(__path_tree__):
```
{
"auth/oauth": { "isPublic": true, "groups": [] },
"auth/api-keys": { "isPublic": true, "groups": [] },
"internal/billing": { "isPublic": false, "groups": ["admin", "billing"] },
"api-reference/endpoints/users": { "isPublic": true, "groups": [] }
}
```
在初始化时,服务器获取并解压此文档,将其放入两个内存结构中:一个文件路径的 Set<string> 和一个将目录映射到子项的 Map<string, string[]>。
构建完成后,ls、cd 和 find 在本地内存中解析,无需网络调用。树结构被缓存,因此同一站点的后续会话会完全跳过 Chroma 的获取过程。
访问控制
注意路径树中的 isPublic 和 groups 字段。在构建文件树之前,ChromaFs 会根据当前用户的权限修剪文件树,并对所有后续的 Chroma 查询应用匹配的过滤器。
在真实的沙箱中,这种级别的每用户访问控制需要管理 Linux 用户组、chmod 权限,或为每个客户层级维护隔离的容器镜像。在 ChromaFs 中,这只是运行 buildFileTree 之前的几行过滤代码。
从分块中重组页面
Chroma 中的页面被拆分为分块以进行嵌入,因此当 Agent 运行 cat /auth/oauth.mdx 时,ChromaFs 会获取所有具有匹配页面 slug 的分块,按 chunk_index 排序,并将它们连接成完整的页面。结果会被缓存,因此在 grep 工作流中的重复读取绝不会两次击中数据库。
并非每个文件都需要存在于 Chroma 中。我们注册了延迟文件指针,在访问时解析存储在客户 S3 存储桶中的大型 OpenAPI 规范。Agent 在 /api-specs/ 中看到 v2.json,但只有当它运行 cat 时才会获取内容。
每个写操作都会抛出 EROFS(只读文件系统)错误。Agent 可以自由探索,但永远无法改变文档,这使得系统无状态,无需会话清理,也无需担心一个 Agent 破坏另一个 Agent 的视图。
## 优化 Grep
cat 和 ls 很容易虚拟化,但如果 grep -r 朴素地通过网络扫描每个文件,那就太慢了。我们拦截 just-bash 的 grep,使用 yargs-parser 解析标志,并将它们转换为 Chroma 查询(固定字符串用 $contains,模式用 $regex)。
Chroma 充当粗粒度过滤器,识别哪些文件可能包含命中结果,然后我们将这些匹配的分块批量预取到 Redis 缓存中。然后,我们重写 grep 命令,使其仅针对匹配的文件,并将其交还给 just-bash 进行内存中的精细过滤,这意味着大型递归查询可在毫秒级完成。
```
const chromaFilter = toChromaFilter(
scannedArgs.patterns,
scannedArgs.fixedStrings,
scannedArgs.ignoreCase
);
// 1. 粗粒度过滤:向 Chroma 请求匹配字符串/正则的 slug
const matchedSlugs = await chromaFs.findMatchingFiles(chromaFilter, slugsUnderDirs);
if (matchedSlugs.length === 0) return { stdout: ‘’, exitCode: 1 };
// 2. 预取:并发将分块文件拉入本地缓存
await chromaFs.bulkPrefetch(matchedSlugs);
// 3. 精细过滤:将参数缩小到仅限解析出的命中项
const matchedPaths = matchedSlugs.map((s) => ‘/’ + s + ‘.mdx’);
const narrowedArgs = [...args, ...matchedPaths]; // e.g. ["-i", "OAuth", "/docs/auth.mdx"]
// 4. 执行:让内存中的 RegExp 引擎格式化最终输出
return execBuiltin(narrowedArgs, ctx);
```
## 结论
ChromaFs 为每天 30,000 多次对话中的数十万用户驱动的文档助手提供支持。通过用基于现有 Chroma 数据库的虚拟文件系统替代沙箱,我们实现了即时的会话创建、零边际计算成本,以及无需任何新基础设施的内置 RBAC(基于角色的访问控制)。
您可以在任何 Mintlify 文档站点或 mintlify.com/docs 上尝试。
[阅读完整文章: https://www.mintlify.com/blog/how-we-built-a-virtual-filesystem-for-our-assistant]
## 相关链接
- [Dens Sumesh](https://x.com/densumesh)
- [@densumesh](https://x.com/densumesh)
- [902K](https://x.com/densumesh/status/2039765361533637016/analytics)
- [converging on filesystems as their primary interface](https://arxiv.org/abs/2601.11672)
- [Daytona's per-second sandbox pricing](https://www.daytona.io/pricing)
- [just-bash](https://github.com/vercel-labs/just-bash)
- [Malte](https://x.com/cramforce)
- [$regex](https://x.com/search?q=%24regex&src=cashtag_click)
- [mintlify.com/docs](https://mintlify.com/docs)
- [https://www.mintlify.com/blog/how-we-built-a-virtual-filesystem-for-our-assistant](https://www.mintlify.com/blog/how-we-built-a-virtual-filesystem-for-our-assistant)
- [Upgrade to Premium](https://x.com/i/premium_sign_up)
- [2:02 AM · Apr 3, 2026](https://x.com/densumesh/status/2039765361533637016)
- [902.4K Views](https://x.com/densumesh/status/2039765361533637016/analytics)
- [View quotes](https://x.com/densumesh/status/2039765361533637016/quotes)
---
*导出时间: 2026/5/6 13:30:21*