docs/fuse-mount.md

FUSE Mount

Mount an agent-fs drive as a Linux filesystem with open-to-close consistency.

Markdown

agent-fs mount exposes your drives as a Linux FUSE filesystem so agents and humans can use plain shell verbs (cat, grep, mv, rm) against agent-fs content. It's a *shell adapter for agents*, not a replacement for the CLI / MCP / HTTP surfaces — they remain the universal path for sandboxes that block FUSE.

Platform: Linux only in v1. macOS host mount is on the v2 roadmap. See `fuse-compat.md` for which container runtimes support FUSE.

Quickstart

bash
# 1. Install the CLI (Linux x64 or arm64 — the FUSE helper ships in an
# optionalDependencies sub-package). Prefer Bun's installer; stock
# Ubuntu/Debian Node 18.x aborts on a transitive dep's postinstall.
bun install -g @desplega.ai/agent-fs
# 2. Start a daemon (uses your local agent-fs config).
agent-fs daemon start
# 3. Make a mount point and mount.
mkdir -p /mnt/agent-fs
agent-fs mount /mnt/agent-fs
# 4. List drives — each drive shows up as a top-level directory; `current`
# is a symlink to your default drive.
ls /mnt/agent-fs/
# personal/ team-docs/ current -> ./personal
# 5. Use plain shell verbs against the mount.
echo "hello from FUSE" > /mnt/agent-fs/current/scratch.md
cat /mnt/agent-fs/current/scratch.md
grep -r scratch /mnt/agent-fs/current/
mv /mnt/agent-fs/current/scratch.md /mnt/agent-fs/current/notes/scratch.md
# 6. Unmount when you're done.
agent-fs umount /mnt/agent-fs

If agent-fs mount complains it can't find the helper (agent-fs-fuse: command not found), see `fuse-troubleshooting.md`.

Architecture

+--------------------+           +----------------+
|     agent / shell  |           | other agents / |
| (cat, grep, mv, …) |           |  CLI / MCP     |
+----------+---------+           +-------+--------+
           |                             |
           v                             v
+--------------------+           +----------------+
|  FUSE kernel layer |           |   Bun daemon   |
|  /mnt/agent-fs     |           |  (HTTP server) |
+----------+---------+           +-------+--------+
           |                             ^
           | FUSE protocol               |
           v                             | HTTP /raw + /ops
+--------------------+    Unix socket    |
|  agent-fs-fuse     +-------------------+
|  (Rust helper)     |
+----------+---------+
           |
           v
+--------------------+
|       S3           |
|    (or MinIO)      |
+--------------------+
  • The mount is a FUSE filesystem implemented by `agent-fs-fuse`, a small statically-linked Rust binary that the daemon spawns on agent-fs mount.
  • The helper talks to the daemon over a Unix socket (~/.agent-fs/agent-fs.sock) using msgpack frames. All read/write requests on the mount turn into IPC calls to the daemon.
  • The daemon owns the HTTP API and the S3 client. It runs PUT/GET against S3 (or MinIO) directly, with optimistic concurrency via the expectedVersion parameter on writes.
  • There is no separate cache. Each FUSE open() fetches the head version from the daemon and buffers it; each close() after a content change pushes a new version to S3. We call this *open-to-close consistency*.

Open-to-close consistency

The mount does not stream byte-by-byte to S3. Instead:

  1. open(path, O_WRONLY) — the helper allocates a scratch buffer.
  2. write(...) — bytes accumulate in the scratch buffer; nothing hits S3 yet.
  3. close(...) — the helper:

- Computes the SHA-256 of the buffer. - If the hash matches the current head version, no new version is written (idempotent dedup — touch and echo > same-content are free). - Otherwise, PUT /raw to the daemon with If-Match: <expected-version> for optimistic concurrency. - On 409, the helper records a conflict in <mount>/.agent-fs/conflicts.ndjson and returns EIO to the caller.

This means:

  • A line-buffered echo "x" > foo produces exactly one version per close().
  • Two concurrent writers produce exactly one new version + exactly one conflicts.ndjson record + exactly one EIO to the loser. No silent overwrites.
  • Programs that fsync() between writes still only get one version per close(). There's no per-byte commit.

Feedback surfaces

agent-fs exposes operational signal at well-known paths so agents can self-diagnose. All paths are *virtual* — they live inside the mount but are served by the helper, not S3.

PathContents
<mount>/.agent-fs/conflicts.ndjsonOne JSON line per conflict: { ts, path, drive, op, head_version, your_version, outcome: "rejected" }
<mount>/.agent-fs/conflicts.latest.jsonThe most recent conflict record (for quick reads, no tail -1 needed)
<mount>/.agent-fs/errors.ndjsonOne JSON line per error surfaced as EIO/EACCES to the caller: { ts, path, op, http_status, error }
<mount>/.agent-fs/statusCurrent daemon-link status: { daemon: "ok" \| "down" \| "auth_error", last_seen, drive_count }
~/.agent-fs/mount.logOne line per FUSE callback (debug-only; rotated at 10 MB). Set via --log-level=debug.

Recommended agent recipe: after a write that returned non-zero, cat <mount>/.agent-fs/status first, then tail -1 <mount>/.agent-fs/errors.ndjson — the combination almost always explains the failure.

Mount layout

/mnt/agent-fs/
  current -> ./personal         # symlink to your default drive
  personal/                     # drive root (read-write for files; read-only for the directory itself)
    docs/
      readme.md
    scratch.md
  team-docs/                    # other drives you have access to
    onboarding.md
  .agent-fs/                    # operational surfaces (read-only)
    conflicts.ndjson
    conflicts.latest.json
    errors.ndjson
    status
  • The drive root is read-only: mkdir /mnt/agent-fs/new-drive returns EROFS. Create drives via agent-fs drive create (CLI) and they'll show up automatically on next readdir.
  • The current symlink updates dynamically when you switch default drive (agent-fs drive switch <name>) — no remount needed.
  • POSIX locks (flock) return ENOSYS in v1; agents that rely on them must use the expectedVersion parameter on the CLI / MCP / HTTP surfaces for optimistic concurrency.

What you can and can't do

OpWorks?Notes
cat, head, tail, lessYesBuffered read; daemon serves the head version
echo >, cat <<<, printf >>YesOpen-to-close consistency; one version per close()
grep -r, rg, findYesReads only
mv, renameYesOne op per directory entry
rm, unlinkYesSoft-deletes; previous versions stay accessible via agent-fs log
cp (within mount)YesTwo ops: read + write
mkdir, rmdir (inside a drive)YesVersions still tracked per file
mkdir at mount rootNo (EROFS)Use agent-fs drive create
Writes on a drive where you're a viewerNo (EACCES)All FUSE writes require the editor role or better — see below
chmod, chownNo (ENOSYS / no-op)agent-fs has no POSIX permissions model
flock, fcntl locksNo (ENOSYS)Use --expected-version for optimistic concurrency
Streaming large files (>50 MB)PartialThe 50 MB Hono body limit caps a single PUT; streaming-into-S3 is v1.x
Extended attributes (xattr)NoRead-only xattr window is v1.1 (user.agent-fs.{version, content-hash, ...})

Write permissions

FUSE writes enforce the same RBAC as the JSON write op and PUT /raw: opening a file for write, creating a file, or truncating requires the editor role (or better) on the drive. On a drive where you're a viewer, the mount is effectively read-only for file writes — write attempts fail with EACCES, and the denial is recorded in <mount>/.agent-fs/errors.ndjson with http_status: 403 and error: "PERMISSION_DENIED". Reads (cat, grep, find, ...) work with any role.

See also