Background:Turn everything into FS+Bash Agent
近期最流行的Agent工作模式当属Claude Code系列莫属——不需要通过代码仓库索引,只需要在一个FileSystem上面组合Linux的各种bash命令,就可以发挥巨大的编程能力,甚至将这种能力迁移到非Coding的任何场景。Claude Agent SDK,Skills,AgentFS都这方面的杰出之作。
关于这套范式的优越性,这里就不过多阐述了,全网有大量博客在说明这件事情。
我们要讨论的是,既然这套范式是work的,如果我们能做到 everything to FS ,再搭配上一个Agent,理论上任何work都可以交给强大的Agent来操作。那么问题就在于——如何将对已有SaaS或数据服务抽象为 “FS + Agent”?
这本质上是如何设计一套在sandbox和数据源之间做数据转移的系统,即一套Connector层。这包含以下这些关键的问题:
- 何时将文件从 Source 上传到 Sandbox?
- 上传的数据量如何确定?
- 何时将Sandbox内的更改操作应用回数据源
- 多源数据一致性问题:Source的数据被其他Agent编辑了,如何同步到Agent所在的Sandbox
- 上下文管理问题:如何实现渐进式披露
Linux FUSE
FUSE的核心思想是: 让文件系统逻辑跑在用户态,但对内核和其他程序看起来像“真实文件系统”。
FUSE 表示“用户态文件系统”。它是个框架,让我们在内核之外实现文件系统,内核对外伪装成真实文件系统。我们只需要写一个用户态进程,实现一套接口就可以实现文件系统的逻辑。这些接口包括:lookup 、 open 、 read 、 write 、 readdir 、 create 、 unlink。当操作系统访问挂载目录时,所有操作会被转发到我们的用户态实现。
这种抽象能让我们能把任意数据结构伪装成文件,例如将HTTP接口、数据库对象存储都映射为文件甚至是可读文件。下面是一个Python脚本,实现了最基本的FUSE文件系统。
- getattr:
- 作用:返回路径上的stat信息。类型/权限/大小/时间戳等。
- 内核什么时候调用:ls、stat、open 等都会先查属性
- 这里的逻辑:
- 如果是目录 → 返回 S_IFDIR + 权限 0o755
- 如果是文件 → 返回 S_IFREG + 权限 0o444 + st_size
- 否则 → ENOENT(不存在)
- readdir:
- 作用:列出目录内容
- 内核什么时候调用:ls 或访问目录时
- 注意点:必须包含 . 和 ..
- 这里的逻辑:从 _dirs / _files 中取出“当前目录的直接子项”
- open
- 作用:打开文件时的校验(可决定是否允许)
- 内核什么时候调用:open() 或程序读取文件前
- 这里的逻辑:
- 不是文件 → ENOENT
- 有写权限请求 → EACCES(只读)
- 否则允许打开
- read
- 作用:读取文件内容
- 内核什么时候调用:程序 read() 时
- 这里的逻辑:
- 不是文件 → ENOENT
- 从内存里的 _files[path] 切片返回 offset:offset+size
#!/usr/bin/env python3
"""
Minimal FUSE filesystem example using fusepy.
This exposes multiple directories and files in memory:
/docs/readme.txt
/docs/tutorial/intro.txt
/data/notes/today.txt
All file contents are generated in memory.
Notes:
- Requires fusepy (pip install fusepy) and FUSE installed on the system.
- Run with: python dev/minimal_fuse.py <mountpoint>
- Unmount with: fusermount -u <mountpoint> (Linux) or umount <mountpoint> (macOS)
"""
import errno
import os
import stat
import time
from fuse import FUSE, FuseOSError, Operations
class MinimalFS(Operations):
"""
Minimal filesystem that exposes a small directory tree.
The file contents are generated in memory.
"""
def __init__(self):
# In-memory file table: path -> bytes content.
# Paths are absolute (start with "/").
self._files = {
"/docs/readme.txt": b"README: This is a virtual file.\\n",
"/docs/tutorial/intro.txt": b"Intro: FUSE forwards operations to userspace.\\n",
"/data/notes/today.txt": b"Today: Everything here is in memory.\\n",
}
# Directory set. We include all parent directories explicitly.
self._dirs = {
"/",
"/docs",
"/docs/tutorial",
"/data",
"/data/notes",
}
# Fixed timestamps for simplicity.
self._now = int(time.time())
def _is_dir(self, path):
return path in self._dirs
def _is_file(self, path):
return path in self._files
def getattr(self, path, fh=None):
"""
Return stat-like info for a path.
The kernel calls this for both files and directories.
"""
if self._is_dir(path):
# Directory: read/execute for all, write for owner.
return {
"st_mode": stat.S_IFDIR | 0o755,
"st_nlink": 2,
"st_ctime": self._now,
"st_mtime": self._now,
"st_atime": self._now,
}
if self._is_file(path):
# Regular file: read-only.
return {
"st_mode": stat.S_IFREG | 0o444,
"st_nlink": 1,
"st_size": len(self._files[path]),
"st_ctime": self._now,
"st_mtime": self._now,
"st_atime": self._now,
}
# Any other path does not exist.
raise FuseOSError(errno.ENOENT)
def readdir(self, path, fh):
"""
List directory contents.
The kernel always expects '.' and '..' in every directory.
"""
if not self._is_dir(path):
raise FuseOSError(errno.ENOENT)
# Collect immediate children of the directory.
entries = [".", ".."]
prefix = "/" if path == "/" else f"{path}/"
for d in self._dirs:
if d.startswith(prefix) and d != path:
name = d[len(prefix) :].split("/", 1)[0]
if name and name not in entries:
entries.append(name)
for fpath in self._files:
if fpath.startswith(prefix):
name = fpath[len(prefix) :].split("/", 1)[0]
if name and name not in entries:
entries.append(name)
return entries
def open(self, path, flags):
"""
Validate file open. Reject writes since file is read-only.
"""
if not self._is_file(path):
raise FuseOSError(errno.ENOENT)
if flags & (os.O_WRONLY | os.O_RDWR):
raise FuseOSError(errno.EACCES)
return 0
def read(self, path, size, offset, fh):
"""
Return a slice of the file contents.
"""
if not self._is_file(path):
raise FuseOSError(errno.ENOENT)
data = self._files[path]
return data[offset : offset + size]
def main():
# Expect a mount point directory as the only argument.
if len(os.sys.argv) != 2:
print("Usage: python dev/minimal_fuse.py <mountpoint>")
raise SystemExit(2)
mountpoint = os.sys.argv[1]
# foreground=True keeps logs visible; ro enforces read-only mount.
FUSE(MinimalFS(), mountpoint, foreground=True, ro=True)
if __name__ == "__main__":
main()
FUSE和Sandbox的关系
- FUSE是“把任意数据伪装成文件系统”的接口层/数据层能力
- 沙盒(e2b、bwrap)是“限制进程权限/视图”的隔离与权限层能力
它们的功能方向不同,组合后能做更强的安全与虚拟化场景。沙盒能限制权限/网络/进程,但**不能凭空“造一个文件系统语义”**。FUSE 补的是“文件系统语义与数据映射”的能力。
1) 把外部数据源安全地暴露为只读目录
- 需求:让沙盒内程序“以文件方式”读取 HTTP/DB/API 数据,但不允许它访问真实磁盘或网络
- 做法:宿主机 FUSE 把外部数据映射成只读目录 → 再 bind-mount 给沙盒
- 沙盒单独做不到,因为它只管权限,不生成数据源
2) 动态生成“虚拟文件”作为输入
- 需求:每次读取都动态生成内容(实时配置、审计快照、按用户生成视图)
- 做法:FUSE 的 read() 实时生成内容 → 沙盒内程序按文件读
- 沙盒本身只能限制访问,并不会生成动态文件
3) 细粒度审计和控制 IO 行为
- 需求:记录每次文件访问、阻止某些路径、实现策略控制
- 做法:FUSE 实现 open/read/readdir 中插入审计逻辑
- 沙盒只能限制访问范围,难做“文件级策略拦截与日志”
4) 统一多源数据为单一目录树
- 需求:把 S3 + 数据库 + 本地缓存“拼成一个目录树”
- 做法:FUSE 聚合并实现目录结构
- 沙盒无法抽象这些数据源,只能隔离
5) 只给沙盒暴露“最小视图”
- 需求:外部数据很大,但沙盒只看到白名单子集
- 做法:FUSE 层过滤路径,只暴露允许内容
- 沙盒只能做“有/无权限”,无法做“数据层过滤重映射”
它们是正交的,组合可以实现“安全地把任意数据变成文件系统视图”。
FS + Bash Agent的架构
结合上面的描述,应该不难理解下面的图(来自博客)

而如何设计文件夹架构,实际上是context engineering的问题,要设计得比较好理解并且避免过度抽象。当设计好一套FUSE接口后,可以通过system prompt向Agent补充相关信息。例如:
You are an email assistant helping the user manage their inbox.
You have access to a filesystem at /workspace representing their email:
- Folders: Inbox, Sent, Orders, Customers, etc.
- Emails: .eml files named "Subject (sender@email.com).eml"
- Filenames contain spaces (from email subjects), so always quote paths in shell commands
- Starred/Needs_Action: contain symlinks to flagged emails
Commands you use internally:
- ls, cat, find → browse emails
- mv → move emails between folders
- ln -s <email> /Starred/ → star an email
- rm /Starred/<email> → unstar an email
- mkdir → create folders
When responding to the user:
- Describe emails by subject and sender, not filenames
- Say "starred" not "created symlink"
- Say "moved to Orders" not "mv to /Orders/"
- Summarize what you found/did in plain language
AgentFS
体验过程
-
进入到一个仓库后,启动agentfs,例如:
agentfs run --session fix-bug-12 claude,在里面可以运行Claude Code。人工可以启动agentfs run --session fix-bug-123 bash进行检查。 -
agentfs会将这个文件夹mount到
~/.agentfs/run/{session_id}/mnt的目录,里面是原来的代码仓库。对这个mount目录的更改不会影响原来的文件夹。所有终端共享同一个这个写时复制文件系统。 -
似乎所有对session的更改都放到了
.agentfs/my-project.db里面。agentfs存储目录的结构如下/Users/ajaxzhan/.agentfs/run/{session_id}drwxr-xr-x 6 ajaxzhan staff 192 1月 16 15:59 . drwxr-xr-x 3 ajaxzhan staff 96 1月 16 15:59 .. -rw-r--r-- 1 ajaxzhan staff 4096 1月 16 15:59 delta.db -rw-r--r-- 1 ajaxzhan staff 399672 1月 16 16:06 delta.db-wal drwxr-xr-x 1 ajaxzhan staff 0 1月 16 15:59 mnt drwxr-xr-x 3 ajaxzhan staff 96 1月 16 15:59 zsh -
底层存储形式
┌────────────────────────────────────┐ │ Merged View (what you see) │ ├────────────────────────────────────┤ │ Delta Layer (AgentFS database) │ ← Writes go here ├────────────────────────────────────┤ │ Base Layer (original directory) │ ← Read-only └────────────────────────────────────┘- 读取:检查增量层,如果没有找到就从基层读取。
- 写入:(基层不会被改动)
- 将文件复制到增量层
- 将更改写入增量层
- 删除:在增量层中创建”白色标记“(基层不会被改动)
-
NFS文件服务器
- 启动NFS服务器:
agentfs serve nfs my-agent,agentfs serve nfs my-agent --bind 0.0.0.0 --port 2049 - 挂载文件系统:
mount -t nfs -o vers=3,tcp,port=2049,mountport=2049,nolock server-ip:/ /mnt/agentfs
- 启动NFS服务器:
体验感受
- “能不能看到”的权限问题:默认似乎还是能读到系统内的配置,例如
.ssh等,只是无法修改而已。那Agent完全可以被提示词注入,读取something后通过curl泄漏数据(不过网络可能被隔离了,但毕竟也泄漏给模型提供商了) - apply change没有相关Api,需要手动操作或者自己写脚本。