Skip to content

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.

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.

Terminal window
# mount
omnifs mount /omnifs
# now every standard tool works against it
ls /omnifs/github/acme/api/_issues/_open
cat /omnifs/github/acme/api/_issues/_open/2853/title
grep -r "auth" /omnifs/linear/teams/ENG/issues/_open

Nothing about the calling process changes. Editors, scripts, agents, and pipelines all issue the same syscalls they would against any filesystem.

When the kernel delivers a lookup or read request, the runtime walks the path left to right:

  1. The first segment identifies the provider (github, docker, linear, and so on).
  2. Each subsequent segment becomes an argument to lookup-child or list-children on that provider.
  3. 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.

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 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 -f a 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.

Terminal window
# first read: one callout
cat /omnifs/github/acme/api/_issues/_open/2853/title
# second read (node unchanged upstream): served from cache, no callout
cat /omnifs/github/acme/api/_issues/_open/2853/title

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:

Terminal window
# open an issue in vim
vim /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 invalidated

The 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.

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:

Terminal window
# three separate syscalls, three separate cache lookups, at most three callouts
cat /omnifs/github/acme/api/_actions/runs/9801/status
cat /omnifs/docker/containers/by-name/api-server/state
cat /omnifs/linear/teams/ENG/issues/_open/ENG-204/priority

This 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.