8.4 KiB
agent-fs FUSE Mount
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.mdfor 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-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 onagent-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 —
touchandecho > same-contentare free). - Otherwise, PUT
/rawto the daemon withIf-Match: <expected-version>for optimistic concurrency. - On 409, the helper records a conflict in
<mount>/.agent-fs/conflicts.ndjsonand returnsEIOto 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 incantationsfuse-troubleshooting.md—EIO,Transport endpoint is not connected, helper-not-found, etc.api-reference.md— CLI / MCP / HTTP surfaces, including theexpectedVersionparameter- Implementation plan:
thoughts/taras/plans/2026-05-15-agent-fs-fuse-mount-v1.md