Repository · workstreams
Workstream: SimpleFileSystem
Status: In progress — the vNext
SimpleFileSystem/SimpleFileSystemManagercontract (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
FileSystemsemantics mean Kotlin developers already know it, and tests can swap inFakeFileSystem. - 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 —SimpleFileSystemApiholdsSimpleFileSystem/SimpleFileSystemManagerin packagesimplefilesystem— but the other repos remainSimpleFileSystemService{Embedded,Server,Cli,Wui}. - [x] Extract two interfaces —
SimpleFileSystem(single filesystem) andSimpleFileSystemManager(lifecycle) — done (landed in SimpleFileSystemApi #9, published assimplefilesystem:simplefilesystem-api:0.3.0). This instantiates the ecosystem's general single-tenant vs. multi-tenant Api pattern — a single-tenantFoo(SimpleFileSystem) plus a multi-tenantFooManager(SimpleFileSystemManager) that owns and vends it — where that split is now canonically documented. Design (decided): split the formerSimpleFileSystemServiceApiinto two purpose-named contracts rather than one bare interface plus a leftover.SimpleFileSystem(packagesimplefilesystem) is the bare, implementation-neutral file-operations surface for one filesystem — whole-file read/write/append, directory ops, copy/atomic-move, metadata. It takes nofilesystemselector parameter (an instance is one filesystem) and assumes nourl://simple-filesystem/addressing. This is the contract any FileStorage backend implements.SimpleFileSystemManagerowns 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/listFilesystemsreturn plain descriptor data (aFilesystemInfovalue — uuid, description, owner, maxSizeBytes, usedBytes, expiration), not a behavioral handle, and the manager vends theSimpleFileSystemfor a given UUID.- The current
Filesystemresource interface is retired — itsgetExpiration/setExpiration/getMaxSizeBytes/getUsedBytesaccessors becomeSimpleFileSystemManagermethods (itsnamebecomes the descriptor'sdescription). - The ServiceServer implements both. A non-service FileStorage backend implements only
SimpleFileSystemand is never forced to provide filesystem-lifecycle/quota/expiration semantics. - As shipped: exactly this.
simplefilesystem.SimpleFileSystemtakes no filesystem selector;SimpleFileSystemManageris UUID-keyed (createFilesystem/listFilesystems/getFilesystemInfo/openFilesystem/deleteFilesystem,get/setExpiration,getMaxSizeBytes/getUsedBytes);FilesystemInfocarries uuid, description, owner, maxSizeBytes, usedBytes, expiresAtMillis and createdAtMillis; and theFilesystemresource interface is gone, along with theurlproperties that went with it. Two mechanical follow-ons remain from the rename:getFilesystemshipped asgetFilesystemInfo(the behavioral handle isopenFilesystem), 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 /FakeFileSystemtest backend. A CN persistent-auto-start raw-JAR writing to a host directory (themaven-server/root/mavenpattern) 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
descriptionfor human labelling (replacing the globalnamenamespace, so names never collide); theSimpleFileSystemManageraddresses filesystems by UUID. The UUID is the canonical address — URLs becomeurl://simple-filesystem/filesystems/<uuid>/files/<path>(the non-uniquedescriptionnever appears in a URL). Because thedescriptionis 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-admincapability) → 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 onfs-writeto 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(likeClock) 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/Sinkstreaming 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)andappendingSink(path)return aFileSinkwith explicit commit/abort, the whole-file Base64 methods survive under a 4 MiBINLINE_PAYLOAD_MAX_BYTEScap, 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, whosekotlin.build (remote)run is currently red on its loopback streaming test pending the upstream fix in UrlResolver #839 (RemoteInputStreampull 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 aPathWatchPageofPathWatchEvents 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
SimpleFileSystemAPI 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(todaywriteis 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 thatFakeFileSystem, 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.FileMetadataInfoexposes acontentHash(the SHA-256, uppercase-hex to match Blobstore), and the CAS writewrite(path, data, ifMatches: String?)commits only if the current file's content hash equalsifMatches(else a conflict error);ifMatches == nullmeans "must not currently exist," which subsumes the existingmustCreateboolean. The hosted backend implements this as a conditional path→blob rebind (swap only if the path still points at theifMatchesblob); the Okio Embedded andFakeFileSystemcompute 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.contentHashis the whole-file SHA-256 in 64-character uppercase hex,write/writeUtf8/sinktakeifMatches,ifMatches == nullmeans "must not currently exist" (so the CAS methods subsumemustCreate; the unconditional last-writer-wins path is the separateoverwrite/overwriteUtf8), and a mismatch raisesFileContentConflictExceptionnaming 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 aSimpleFileSystemManagerFactory) 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 — neithermainnor 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,mustCreateexclusion, 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).