Runtime, cache, and identity
The omnifs host runtime is the layer between the kernel and the provider. It owns the FUSE mount, the cache, inode assignment, and credential forwarding. Providers know none of this; they answer three read ops and return bytes.
The FUSE mount
Section titled “The FUSE mount”omnifs registers a FUSE filesystem in userspace. The kernel routes read() and readdir() calls on the mount path to the runtime process rather than to a local disk. From the shell’s perspective, /omnifs (or whichever path you chose at mount time) is an ordinary directory tree.
# mountomnifs mount /omnifs
# now every standard tool works against itls /omnifs/github/acme/api/_issues/_opencat /omnifs/github/acme/api/_issues/_open/2853/titlegrep -r "auth" /omnifs/linear/teams/ENG/issues/_openNothing about the calling process changes. Editors, scripts, agents, and pipelines all issue the same syscalls they would against any filesystem.
Path resolution
Section titled “Path resolution”When the kernel delivers a lookup or read request, the runtime walks the path left to right:
- The first segment identifies the provider (
github,docker,linear, and so on). - Each subsequent segment becomes an argument to
lookup-childorlist-childrenon that provider. - Leaf reads become
read-file.
The runtime dispatches to exactly one provider per request. There is no cross-provider join inside a single syscall; composition happens in your shell.
The provider boundary
Section titled “The provider boundary”Providers run as wasm32-wasip2 components under wasmtime. The WIT interface they implement is omnifs:provider@1.0.0, which defines the three read ops:
package omnifs:provider@1.0.0;
interface provider { lookup-child: func(parent: node-id, name: string) -> result<node, error>; list-children: func(parent: node-id) -> result<list<node>, error>; read-file: func(node: node-id) -> result<list<u8>, error>;}The provider sandbox has no ambient authority. It cannot open a socket, read environment variables, or access the local filesystem. When a provider needs to reach a remote service, it calls back to the host through a declared capability. The host holds the credentials and makes the HTTPS request; the provider receives the response bytes. A provider that never declares a network capability never touches the network, regardless of what its code attempts.
The cache
Section titled “The cache”The runtime maintains a capacity-bounded in-memory cache of resolved nodes. The capacity limit is set at mount time; once the cache is full, the runtime evicts the least-recently-used entries.
The cache is not TTL-based. Entries are not expired on a clock; they are invalidated when the upstream service emits a relevant event (a webhook, a push notification, a poll signal). This matters for two reasons:
- A stale read caused by a missed invalidation is a correctness bug. A stale read caused by a short TTL that fires before the upstream changed is noise. The two failure modes are different, and conflating them by accepting TTL guesswork trades one for the other.
- Event-driven invalidation means a cache hit on a node that has not changed upstream remains valid indefinitely. You can
tail -fa log file or hold an issue open in vim for hours without triggering redundant remote calls.
On a cache hit the request never leaves your machine. On a miss the runtime asks the provider, which makes one capability-gated callout.
# first read: one calloutcat /omnifs/github/acme/api/_issues/_open/2853/title
# second read (node unchanged upstream): served from cache, no calloutcat /omnifs/github/acme/api/_issues/_open/2853/titleStable identity
Section titled “Stable identity”Each node the runtime resolves receives an inode number. That inode is assigned by the runtime and kept stable for the lifetime of the object, independent of the object’s name or position in the tree.
If a GitHub issue is renamed upstream (the title changes), the node’s inode does not change. If a Linear ticket moves between states and the filter that exposes it changes, the inode does not change. The node is the same object; only its projection into the tree has shifted.
This matters for tools that hold file descriptors:
# open an issue in vimvim /omnifs/github/acme/api/_issues/_open/2853/body
# the issue is renamed upstream while vim is open# vim's file descriptor still points at the same inode; the buffer is not invalidatedThe same applies to tail -f, inotifywait, rsync, and any other tool that tracks files by inode rather than path. The runtime ensures the kernel sees a consistent identity for each object even as the upstream representation changes.
One request, one provider
Section titled “One request, one provider”A single read() or readdir() call on the mount dispatches to at most one provider. The runtime does not fan out, join results, or synthesize virtual nodes that aggregate across providers. What looks like a multi-provider query in the shell is always multiple syscalls, each handled independently:
# three separate syscalls, three separate cache lookups, at most three calloutscat /omnifs/github/acme/api/_actions/runs/9801/statuscat /omnifs/docker/containers/by-name/api-server/statecat /omnifs/linear/teams/ENG/issues/_open/ENG-204/priorityThis keeps the runtime’s behavior predictable. Each path has exactly one provider responsible for it, and the host is the only party that composes the results.