← Workstreams

Workstream: SimpleFileSystem

Status: In progress — the vNext SimpleFileSystem/SimpleFileSystemManager contract (API 0.3.0), a durable CockroachDB + Blobstore backend, and a backend-neutral conformance suite have landed, and Filedrop is migrated; remaining: the hosted ServiceServer vNext cutover and deploy, W3Wallet quota/identity, Observables push, and the Kotlin Build build-cache adopter · Component: Maximize developer productivity

Goal

Give every service and agent a way to create and manage remote filesystems on demand — named, size-limited, path-structured storage reachable over url:// — so that nobody has to provision a disk, mount a volume, or hand-roll a storage layer just to keep some files around. A developer (or an agent) should be able to say "back up my laptop's home directory into a 100 MB filesystem" — or an agent, "spin me up an agent-scratch workspace" — and then list, read, write, copy, move, and restore any file in it by path from anywhere on the network.

This is a reusable primitive for component 1: it removes "where do I put my files?" from the list of things you have to solve when building a service.

Current state

Much of this already exists as the SimpleFileSystemService (documented in the Documentation Repository as the FileStorage project). It is a remote virtual filesystem service with an Okio-inspired API, served over url://simple-filesystem/, following the standard layered architecture:

Layer Repository What it provides
Api SimpleFileSystemApi The backend-neutral contract (currently simplefilesystem:simplefilesystem-api:0.3.0): SimpleFileSystem and SimpleFileSystemManager in package simplefilesystem, plus the data types (FilesystemInfo, FileEntryInfo, FileMetadataInfo, FileSink, PathWatchEvent) and typed exceptions
Embedded SimpleFileSystemServiceEmbedded Local Okio-backed implementation (OkioSimpleFileSystem/OkioSimpleFileSystemManager); one subdirectory per filesystem; size-limit enforcement; expiration purge; Clock-injectable for tests (FakeFileSystem, ManualClock)
Embedded (durable) SimpleFileSystemDurableEmbedded The durable backend the hosted instance is built on: file/directory metadata transactionally in CockroachDB, immutable 4 MiB content blocks in Blobstore keyed by uppercase SHA-256, staged-then-published generations with idempotent block GC
Conformance SimpleFileSystemConformance The backend-neutral conformance suite (SimpleFileSystemConformanceSuite.runAll(factory)) every backend must pass — validation precedence and exact messages, the operation matrix, content CAS permutations, snapshot revisions and cursor pagination, 10 MiB streaming/ranges/append/abort, expiration and stale handles, and the pull watch log
ServiceServer SimpleFileSystemServiceServer Hosts a backend behind url://simple-filesystem/; SJVM client bytecode; ContainerNursery lazy-start or standalone P2P
CLI SimpleFileSystemServiceCli create-fs, ls, mkdir, read/cat, write, append, cp, mv, rm, info, set-expiration, health; --json output
WUI SimpleFileSystemServiceWui Browser file browser/editor with breadcrumb navigation and filesystem size/expiration management
HealthCheck SimpleFileSystemHealthCheck ProductionHealth check for the hosted service — manager reachability, a streamed 9 MiB round-trip verified byte-for-byte against its committed hash, a CAS-conflict probe, and a permanently retained canary. Built, but not yet published or registered (community.kotlin.healthchecks.simplefilesystem:simple-filesystem-health-check:0.0.3 is absent from kotlin.directory), because it pins server artifacts that ship with the vNext cutover

What already works: filesystems with a required maxSizeBytes, optional expiration (auto-purge), full file I/O (UTF-8 and Base64), directory operations, metadata queries, copy/atomicMove, streaming source/sink with byte-range reads, content-hash compare-and-swap, and a pull-based path watch log.

So this workstream is less "build it from scratch" and more "adopt it as the standard remote-filesystem primitive and flesh out the gaps."

For when to reach for FileStorage over another mechanism — and the filesystem-vs-blob boundary it shares with the Blobstore plan — this plan defers to the storage decision guide rather than forking it; STORAGE.md files FileStorage in its specialized tier, so "standard primitive" here means path-structured file data, not storage in general. SimpleFileSystem also holds current state only — point-in-time versioning/snapshots are an explicit non-goal for v1 (keep "Simple" simple); consumers needing history compose it per STORAGE.md (immutable snapshots as a manifest over content-addressed blobs, never byte-replay through the Event Log), and if a concrete consumer later needs it the shape is cheap copy-on-write snapshots reclaimed by the existing expiration mechanism.

Why it accelerates developers

  • No storage boilerplate. A new service stores files in a SimpleFileSystem instead of inventing persistence, picking a disk, or worrying about ephemeral ContainerNursery containers losing data.
  • Familiar API. Okio FileSystem semantics mean Kotlin developers already know it, and tests can swap in FakeFileSystem.
  • Agent-ready. Because it is reachable over url://, an agent swarm can spin up scratch filesystems for work-in-progress and tear them down via expiration — a natural fit for component 2.

Plan / roadmap

Milestones to take SimpleFileSystem from "exists" to "the obvious default for path-structured remote files." These are proposed and need sharpening before work starts.

  • [x] Canonical name — resolved (three-tier model). The platform distinguishes three layers rather than collapsing to one name: FileStorage is the abstract storage mechanism; SimpleFileSystem is the simplified, Okio-inspired API contract for that mechanism that most FileStorage implementations should implement; and SimpleFileSystemService is the reference implementation served over url://simple-filesystem/. This mirrors existing precedent where a mechanism name differs from its implementing project (BlobStorage is implemented by Blobstore). The model is documented in STORAGE.md and the FileStorage project page; the existing FileStorage page suffices (no separate ALL_PROJECTS entry needed). Remaining cleanup (non-urgent): the Api repo and its contents now agree — SimpleFileSystemApi holds SimpleFileSystem/SimpleFileSystemManager in package simplefilesystem — but the other repos remain SimpleFileSystemService{Embedded,Server,Cli,Wui}.
  • [x] Extract two interfaces — SimpleFileSystem (single filesystem) and SimpleFileSystemManager (lifecycle) — done (landed in SimpleFileSystemApi #9, published as simplefilesystem:simplefilesystem-api:0.3.0). This instantiates the ecosystem's general single-tenant vs. multi-tenant Api pattern — a single-tenant Foo (SimpleFileSystem) plus a multi-tenant FooManager (SimpleFileSystemManager) that owns and vends it — where that split is now canonically documented. Design (decided): split the former SimpleFileSystemServiceApi into two purpose-named contracts rather than one bare interface plus a leftover.
    • SimpleFileSystem (package simplefilesystem) is the bare, implementation-neutral file-operations surface for one filesystem — whole-file read/write/append, directory ops, copy/atomic-move, metadata. It takes no filesystem selector parameter (an instance is one filesystem) and assumes no url://simple-filesystem/ addressing. This is the contract any FileStorage backend implements.
    • SimpleFileSystemManager owns everything specific to this multi-tenant hosted service, all keyed by the filesystem's UUID: lifecycle (createFilesystem/listFilesystems/getFilesystem/deleteFilesystem), per-filesystem policy (getExpiration/setExpiration), and quota/usage reporting (getMaxSizeBytes/getUsedBytes). getFilesystem/listFilesystems return plain descriptor data (a FilesystemInfo value — uuid, description, owner, maxSizeBytes, usedBytes, expiration), not a behavioral handle, and the manager vends the SimpleFileSystem for a given UUID.
    • The current Filesystem resource interface is retired — its getExpiration/setExpiration/getMaxSizeBytes/getUsedBytes accessors become SimpleFileSystemManager methods (its name becomes the descriptor's description).
    • The ServiceServer implements both. A non-service FileStorage backend implements only SimpleFileSystem and is never forced to provide filesystem-lifecycle/quota/expiration semantics.
    • As shipped: exactly this. simplefilesystem.SimpleFileSystem takes no filesystem selector; SimpleFileSystemManager is UUID-keyed (createFilesystem/listFilesystems/getFilesystemInfo/openFilesystem/deleteFilesystem, get/setExpiration, getMaxSizeBytes/getUsedBytes); FilesystemInfo carries uuid, description, owner, maxSizeBytes, usedBytes, expiresAtMillis and createdAtMillis; and the Filesystem resource interface is gone, along with the url properties that went with it. Two mechanical follow-ons remain from the rename: getFilesystem shipped as getFilesystemInfo (the behavioral handle is openFilesystem), and the UUID-in-URL addressing described below is a ServiceServer concern that the backend-neutral contract deliberately does not carry.
  • [ ] Deploy a shared hosted instance on ContainerNursery with a documented url://simple-filesystem/ endpoint that other services can depend on. Durability approach (decided): CN offers no persistent volumes, so the ServiceServer must be stateless with the source of truth off the container — exactly as Blobstore does (bytes in S3, metadata in CockroachDB). Concretely, back file contents with BlobStorage (url://blobstore/) and keep the directory tree, path→blob mapping, and per-filesystem metadata (size, used bytes, expiration, owner) in a durable structured store (KeyValueStorage / SQL / CockroachDB). The current Okio-on-disk Embedded remains the in-process / FakeFileSystem test backend. A CN persistent-auto-start raw-JAR writing to a host directory (the maven-server /root/maven pattern) is acceptable only as an interim stopgap, gated by a total-size cap + monitoring — it is single-host and writes unbounded data onto the shared prod host.
  • [ ] Quota & identity integration with W3Wallet. Approach (decided):
    • Identity — a filesystem is referenced by a generated UUID with a non-unique, free-text description for human labelling (replacing the global name namespace, so names never collide); the SimpleFileSystemManager addresses filesystems by UUID. The UUID is the canonical address — URLs become url://simple-filesystem/filesystems/<uuid>/files/<path> (the non-unique description never appears in a URL). Because the description is non-unique, the CLI/WUI resolve it scoped to the caller's accessible set, erroring on ambiguity: create-fs --description … prints the generated UUID (scripts use it), ls <description> resolves when exactly one matches (otherwise lists the candidate UUIDs), and a UUID is always accepted directly. No per-owner unique alias is reintroduced — a usable handle is provided by lookup, not by a uniqueness constraint.

    • Ownership — creating a filesystem requires a verified W3Wallet capability; the creating principal's public-key hash is recorded as owner (mirroring Blobstore's owner model but verified, not caller-supplied). The principal may be a human or an agent — agents are first-class W3Wallet principals over url://.

    • Quota — the Embedded consults W3Wallet to check/decrement a per-owner aggregate size quota across all of that owner's filesystems, layered above the per-filesystem maxSizeBytes (supports caller-pays billing).

    • Access control — each filesystem mints a small set of distinct W3Wallet capabilities, and granting access means giving a principal USE (W3Wallet's "may invoke") on the one matching their role. The three filesystem permission classes are read / write / owner:

      • fs-read → read/readUtf8, list/listRecursively, metadata/metadataOrNull, exists;
      • fs-write → write/append, delete/deleteRecursively, copy, atomicMove, createDirectory/createDirectories;
      • owner (the creator's key / an fs-admin capability) → setExpiration, delete-filesystem, and granting/revoking the above.

      Note these are filesystem permission classes, not W3Wallet's per-token access levels (USE=invoke, READ=view definition, WRITE=modify, READ_WRITE=full), which apply on a separate axis to each capability: a grantee holds USE on fs-write to write files, while WRITE-level on that capability — re-delegation — stays with the owner.

    • Layering — Api declares per-op capability requirements, ServiceServer verifies via the daemon callback, Embedded takes a constructor-injected W3WalletService (like Clock) with a fake in tests. This is the same secure-my-service path the Blobstore plan calls for — the two converge.

  • [ ] Streaming large files. Today binary crosses the RPC boundary Base64-encoded (≈33% inflation; the whole file must fit in one message and in memory). Approach (decided): mirror Blobstore's streaming rather than inventing one. Add Okio Source/Sink streaming methods to the SimpleFileSystem contract (keeping the Base64 whole-file methods as a small-file convenience); the ServiceServer implements them via session-based chunk RPC (start/chunk/finish, ~4 MB chunks, as Blobstore does), enforcing size/quota as chunks arrive. When contents are BlobStorage-backed (per the hosted-instance item above), streaming delegates to BlobStorage's existing chunked transfer — so durability and streaming are one design, not two. Random access (decided): file-level random access is inherent — any file is addressed and fetched directly by its path (e.g. a backup app restoring one file without scanning the whole archive). Within-file random reads (byte-range reads — previewing or partially restoring a large file) are supported, made efficient by a chunked/extent layout (each file is a sequence of fixed-size block-blobs, so a range touches only the relevant blocks instead of re-reading the whole blob). Within-file random writes are not supported — writes are whole-file or append; the content-addressed blob backing is immutable, so an overwrite produces a new blob and atomically rebinds the path (consistent with the per-file-linearizability / "no byte-range writes" decision below). Status — contract done, transport in flight. The API and both backends ship it: source(path), source(path, offset, byteCount), sink(path, ifMatches) and appendingSink(path) return a FileSink with explicit commit/abort, the whole-file Base64 methods survive under a 4 MiB INLINE_PAYLOAD_MAX_BYTES cap, byte-range reads are supported and byte-range writes are not, and the conformance suite exercises 10 MiB streaming with ranges, append, abort and commit-time conflicts. What remains is the hosted hop: the session-based chunk RPC (v2_ surface, replay-safe read offsets and idempotent commit tokens) is in flight in SimpleFileSystemServiceServer #19, whose kotlin.build (remote) run is currently red on its loopback streaming test pending the upstream fix in UrlResolver #839 (RemoteInputStream pull amplification — 256 KiB refills turn a 10 MiB stream into forty serial RPCs).
  • [ ] Change notifications via Observables so consumers can watch a path and react to writes instead of polling. Status — the pull half is done. API 0.3.0 adds a durable pull watch log — SimpleFileSystemManager.watchFilesystem(uuid, path, sinceRevision, limit) returning a PathWatchPage of PathWatchEvents with terminal reasons and retention-resync semantics — and both the Okio Embedded and the durable backend implement it, with conformance coverage for parent membership, terminal events and retention resync. Not started: the Observables push surface that removes the polling loop, and exposing watch over the hosted RPC.
  • [ ] Adoption examples. Migrate at least one existing service whose data is genuinely path-structured to use SimpleFileSystem, and document the pattern so others follow. The headline adopter is the Kotlin Build shared build cache: the action/input hash is a directory path (the path is the lookup — no separate index) and the build rule's output directory tree lives under it as sub-addressable files, so a rule compiled once is reused across every machine and agent (the motivating "100 MB build-cache" made real; the FileStorage backend dedups identical outputs through BlobStorage underneath, invisibly to the consumer). The cache key is an input hash — the path — not the content hash of the output bytes; conflating them is the slip that makes this look like "just BlobStorage" (the general path-subsumes-KV and layering rationale is in STORAGE.md). Further candidates: agent scratch workspaces (per-task WIP file trees, created on demand and reclaimed by expiration — see agent swarms) and a service's config/template trees. Status — first adopter landed, headline not started. Filedrop is migrated to the vNext contract (FiledropEmbedded #5 and FiledropServiceServer #16, both merged 2026-07-23), which proves the contract carries a real consumer — though the migrated Filedrop cannot deploy until the ServiceServer vNext cutover ships. The Kotlin Build shared build cache, the headline adopter above, has not started.
  • [ ] Define & enforce the consistency contract — substantially done; one backend left to certify. Make every single-path operation atomic. The contract belongs to the SimpleFileSystem API contract (the middle tier above), so it binds every backend — but each enforces it by the means its substrate provides: the Okio Embedded guards writes with an in-process per-path lock and commits overwrites via temp-file + atomicMove (today write is unsynchronized and can tear under concurrent overwrite); the BlobStorage-backed hosted backend (per the deploy item above) commits by writing the new blob and then atomically rebinding the path→blob pointer in its structured metadata store. The per-file-linearizability contract itself is canonical in STORAGE.md; this milestone owns making every backend conform, with a conformance suite that FakeFileSystem, the Okio Embedded, and the hosted ServiceServer must all pass. Chunked/streamed writes (above) commit the same way — staged, then a single atomic swap — so a partial upload is never observable. Compare-and-swap token (decided): the opt-in CAS rides on the existing content-addressed substrate rather than adding version bookkeeping — the file's content hash is the ETag. FileMetadataInfo exposes a contentHash (the SHA-256, uppercase-hex to match Blobstore), and the CAS write write(path, data, ifMatches: String?) commits only if the current file's content hash equals ifMatches (else a conflict error); ifMatches == null means "must not currently exist," which subsumes the existing mustCreate boolean. The hosted backend implements this as a conditional path→blob rebind (swap only if the path still points at the ifMatches blob); the Okio Embedded and FakeFileSystem compute the hash on read. This is compare-and-swap on content, so it gives the promised lost-update protection and is immune to ABA — equal content means no update was lost. (The classical alternative — a monotonic per-path version counter — was rejected as redundant bookkeeping given the hash is already computed.) Status. Done: the CAS design shipped verbatim — FileMetadataInfo.contentHash is the whole-file SHA-256 in 64-character uppercase hex, write/writeUtf8/sink take ifMatches, ifMatches == null means "must not currently exist" (so the CAS methods subsume mustCreate; the unconditional last-writer-wins path is the separate overwrite/overwriteUtf8), and a mismatch raises FileContentConflictException naming both the expected and observed hashes. Both backends implement it — the Okio Embedded stages and commits under a per-path guard and computes the hash on read, the durable backend publishes an immutable generation in one CockroachDB transaction. The conformance suite that this milestone called for exists (741 lines, backend-neutral, driven by a SimpleFileSystemManagerFactory) and passes against three real backends: FakeFileSystem, the real-disk Okio Embedded, and the CockroachDB + Blobstore durable backend. Remaining: the hosted ServiceServer does not yet run the suite — neither main nor the vNext PRs #19/#20 carry a conformance run — so the over-RPC backend is the one substrate whose conformance is still unproven. This milestone stays open until it passes.
  • [ ] End-to-end tests following the testing standards: real ServiceServer in-process, FakeFileSystem + ManualClock, exercising size-limit enforcement, expiration purge, and the concurrency conformance suite (atomic overwrite, read-after-write, last-writer-wins, mustCreate exclusion, opt-in CAS).

Graduation

When the hosted instance, quota integration, and streaming land and at least one service depends on it in production, this graduates from a workstream to a first-class project — document it in the Documentation Repository (it already has a FileStorage page to build on).