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层。这包含以下这些关键的问题:

  1. 何时将文件从 Source 上传​到 Sandbox?
  2. 上传的数据如何确定?
  3. 何时将Sandbox内的更改操作应用回数据源
  4. 多源数据一致性问题:Source的数据被其他Agent编辑了,如何同步到Agent所在的Sandbox
  5. 上下文管理问题:如何实现渐进式披露

Linux FUSE

FUSE的核心思想是: 让文件系统逻辑跑在用户态,但对内核和其他程序看起来像“真实文件系统”。

FUSE 表示“用户态文件系统”。它是个框架,让我们在内核之外实现文件系统,内核对外伪装成真实文件系统。我们只需要写一个​用户态进程​,实现一套接口就可以实现文件系统的逻辑。这些接口包括:lookupopenreadwritereaddircreateunlink。当操作系统访问挂载目录时,所有操作会被转发到我们的用户态实现。

这种抽象能让我们能把任意数据结构伪装成文件,例如将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的架构

结合上面的描述,应该不难理解下面的图(来自博客)

image.png

而如何设计文件夹架构,实际上是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

体验过程

  1. 进入到一个仓库后,启动agentfs,例如: agentfs run --session fix-bug-12 claude ,在里面可以运行Claude Code。人工可以启动 agentfs run --session fix-bug-123 bash 进行检查。

  2. agentfs会将这个文件夹mount到 ~/.agentfs/run/{session_id}/mnt 的目录,里面是原来的代码仓库。对这个mount目录的更改​不会影响原来的文件夹​。所有终端共享同一个这个写时复制文件系统。

  3. 似乎所有对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
    
  4. 底层存储形式

    ┌────────────────────────────────────┐
    │         Merged View (what you see) │
    ├────────────────────────────────────┤
    │  Delta Layer (AgentFS database)    │  ← Writes go here
    ├────────────────────────────────────┤
    │  Base Layer (original directory)   │  ← Read-only
    └────────────────────────────────────┘
    
    • 读取:检查增量层,如果没有找到就从基层读取。
    • 写入:(基层不会被改动)
      1. 将文件复制到增量层
      2. 将更改写入增量层
    • 删除:在增量层中创建”白色标记“(基层不会被改动)
  5. NFS文件服务器

    • 启动NFS服务器: agentfs serve nfs my-agentagentfs 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

体验感受

  1. “能不能看到”的权限问题:默认似乎还是能读到系统内的配置,例如 .ssh 等,只是无法修改而已。那Agent完全可以被提示词注入,读取something后通过curl泄漏数据(不过网络可能被隔离了,但毕竟也泄漏给模型提供商了)
  2. apply change没有相关Api,需要手动操作或者自己写脚本。

相关链接

  1. https://vercel.com/blog/how-to-build-agents-with-filesystems-and-bash
  2. https://jakobemmerling.de/posts/fuse-is-all-you-need/