docs/fuse-mount.md
FUSE Mount
Mount an agent-fs drive as a Linux filesystem with open-to-close consistency.
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
# 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-fsagent-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.mdcat /mnt/agent-fs/current/scratch.mdgrep -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-fsIf 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
expectedVersionparameter on writes. - There is no separate cache. Each FUSE
open()fetches the head version from the daemon and buffers it; eachclose()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:
open(path, O_WRONLY)— the helper allocates a scratch buffer.write(...)— bytes accumulate in the scratch buffer; nothing hits S3 yet.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" > fooproduces exactly one version perclose(). - Two concurrent writers produce exactly one new version + exactly one
conflicts.ndjsonrecord + exactly oneEIOto the loser. No silent overwrites. - Programs that
fsync()between writes still only get one version perclose(). 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.
| Path | Contents |
|---|---|
<mount>/.agent-fs/conflicts.ndjson | One JSON line per conflict: { ts, path, drive, op, head_version, your_version, outcome: "rejected" } |
<mount>/.agent-fs/conflicts.latest.json | The most recent conflict record (for quick reads, no tail -1 needed) |
<mount>/.agent-fs/errors.ndjson | One JSON line per error surfaced as EIO/EACCES to the caller: { ts, path, op, http_status, error } |
<mount>/.agent-fs/status | Current daemon-link status: { daemon: "ok" \| "down" \| "auth_error", last_seen, drive_count } |
~/.agent-fs/mount.log | One 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-drivereturnsEROFS. Create drives viaagent-fs drive create(CLI) and they'll show up automatically on nextreaddir. - The
currentsymlink updates dynamically when you switch default drive (agent-fs drive switch <name>) — no remount needed. - POSIX locks (
flock) returnENOSYSin v1; agents that rely on them must use theexpectedVersionparameter on the CLI / MCP / HTTP surfaces for optimistic concurrency.
What you can and can't do
| Op | Works? | Notes |
|---|---|---|
cat, head, tail, less | Yes | Buffered read; daemon serves the head version |
echo >, cat <<<, printf >> | Yes | Open-to-close consistency; one version per close() |
grep -r, rg, find | Yes | Reads only |
mv, rename | Yes | One op per directory entry |
rm, unlink | Yes | Soft-deletes; previous versions stay accessible via agent-fs log |
cp (within mount) | Yes | Two ops: read + write |
mkdir, rmdir (inside a drive) | Yes | Versions still tracked per file |
mkdir at mount root | No (EROFS) | Use agent-fs drive create |
Writes on a drive where you're a viewer | No (EACCES) | All FUSE writes require the editor role or better — see below |
chmod, chown | No (ENOSYS / no-op) | agent-fs has no POSIX permissions model |
flock, fcntl locks | No (ENOSYS) | Use --expected-version for optimistic concurrency |
| Streaming large files (>50 MB) | Partial | The 50 MB Hono body limit caps a single PUT; streaming-into-S3 is v1.x |
Extended attributes (xattr) | No | Read-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
- `fuse-compat.md` — sandbox compatibility matrix and runtime-specific incantations
- `fuse-troubleshooting.md` —
EIO,Transport endpoint is not connected, helper-not-found, etc. - `api-reference.md` — CLI / MCP / HTTP surfaces, including the
expectedVersionparameter - Implementation plan: `thoughts/taras/plans/2026-05-15-agent-fs-fuse-mount-v1.md`