The internal data model for file metadata. How fileservers represent ownership, permissions, and native executable references — and what the kernel sees through Stat.
The Problem
MemoryNode currently stores five fields: name, type, content, children, mtime, logicalSize. This is enough for basic file I/O but missing the metadata that Unix commands and the kernel need:
Permissions — chmod is a no-op. ls -l can’t show mode bits. The kernel can’t check +x during PATH resolution.
Ownership — no uid/gid. Not enforced in v1, but the data model should carry it for ls -l, chown, and future permission enforcement.
Change time — no ctime. stat can’t distinguish “content changed” from “metadata changed.”
Native exec — BinFS is being removed. Native function references need a home on the file node itself, not in a separate fileserver.
INode
The INode is the internal representation for file nodes within fileservers that support full metadata. It is not a protocol type — it’s an implementation detail of MemoryFS.
interfaceINode {
type:'file'|'dir'
content:Uint8Array
children:Map<string, INode>
mtime:number// content modification time (ms since epoch)
ctime:number// metadata change time (ms since epoch)
The exec field is our platform extension — it has no Unix equivalent because Unix stores machine code as file bytes. In fishbowl, native executables are live closures that can’t be serialized to bytes. The exec field stores the closure directly on the node. See How exec Gets Set for details.
Decision: INode is an internal concept, not a protocol type. Each fileserver MAY use INode, but the Fileserver protocol doesn’t mandate it. MemoryFS uses INode as its backing store. ProcFS and DevFS return synthetic Stat with sensible defaults — they have no INode tree. This mirrors real Unix: ext4, procfs, and tmpfs all have different on-disk/in-memory representations, but stat(2) returns a common struct stat. The protocol boundary is Stat, not INode.
Decision: name is removed from INode. The name is a property of the directory entry (the parent’s children map key), not of the node itself. This matches Unix inodes — an inode has no name; a directory entry maps a name to an inode number. MemoryNode conflated these; INode separates them.
Stat Interface Changes
The public Stat interface gains four optional fields:
mtime:number// content modification time (unchanged)
mode?:number// permission bits
uid?:number// owner user id
gid?:number// owner group id
ctime?:number// metadata change time
}
All new fields are optional. Consumers apply defaults when absent:
mode defaults to 0o644 for files, 0o755 for dirs
uid and gid default to 0
ctime defaults to mtime
This preserves backward compatibility. Existing fileservers (ProcFS, DevFS, TtyFS) continue returning four-field Stat objects. Consumers that don’t care about permissions work unchanged.
Decision: exec is NOT on Stat. Stat is the public metadata view — the equivalent of struct stat from stat(2). Native function references are a kernel-internal concern. Exposing them through Stat would leak implementation details to user-space bins. The kernel accesses exec through a separate mechanism (see How exec Gets Set).
Decision: new Stat fields are optional, not required. Mandatory fields would force every fileserver to synthesize permissions and ownership, even when meaningless (what’s the uid of /dev/null?). Optional fields let each fileserver provide what it naturally has. The consumer decides what absence means.
Decision: mode holds permission bits only, not file type. POSIX st_mode packs file type (S_IFDIR, S_IFREG, S_IFLNK) in bits 15-12 alongside permission bits. We split these into separate fields: type: NodeType for the file type and mode for permissions only. This gives us TypeScript exhaustiveness checking on type and avoids bitmask gymnastics. The WASI host maps type → WASI filetype separately from mode, so WASI programs see correct values. C/Rust WASM programs that bypass WASI and inspect raw st_mode type bits would get wrong answers — but well-behaved WASI programs use the filetype field instead. Fields not present in our Stat (st_rdev, st_blocks, st_blksize, st_atim) are omitted intentionally: we have no block layer, no device major/minor numbers, and no access-time tracking (matching Plan 9’s design and Linux’s noatime best practice).
Metadata Mutation: wstat
The Fileserver protocol gains a 10th method for metadata changes:
touch -t can call wstat(path, { mtime }) for explicit timestamp setting
Fileservers that don’t support metadata mutation throw EPERM from wstat. This is the same pattern as mkdir/remove/rename — synthetic fileservers reject operations they can’t perform.
Decision: wstat over separate chmod/chown methods. One method for all metadata changes keeps the protocol lean. Adding chmod and chown as separate methods inflates the protocol from 9 to 11 methods, and a future chtime would make it 12. wstat is a single extension point: any new metadata field becomes a new key in StatChanges, not a new protocol method. The name comes from Plan 9, but the semantics are straightforward — it’s Object.assign for file metadata.
Decision: StatChanges is a separate type from Stat.Stat includes name, size, and type, which are not changeable through wstat — name changes go through rename, size through write/truncate, type is immutable. StatChanges contains only the mutable metadata fields. This makes the type system enforce what’s changeable.
How exec Gets Set
When the builder processes Extension.bins, it writes executable nodes into MemoryFS:
Builder, for each bin entry (e.g., "echo" → echoFn):
1. Create INode at /bin/echo in the root MemoryFS:
- type: 'file'
- mode: 0o755
- exec: echoFn
- content: encoder.encode("# native: echo\n") // metadata for cat
At runtime, the kernel resolves /bin/echo through PATH resolution. It finds the file node, checks the executable bit, and accesses the exec field through ExecCapable.getExec(). The filesystem is the single source of truth — there is no separate exec table or registry. See the executables spec for the full PATH resolution flow.
Kernel Access to exec
The kernel needs to get from a Fileserver (protocol-level) to the exec field (implementation-level). The Fileserver protocol returns Uint8Array from read(), not function references.
Options considered:
a) Add exec(path): BinFunction | undefined to Fileserver protocol.
Adds a method that only one fileserver implements. Pollutes the protocol with a concern that’s specific to executable loading.
b) The kernel knows certain fileservers support exec and uses type narrowing.
The kernel checks if the fileserver implements an ExecCapable interface and accesses the exec field through it.
c) Add a generic metadata method for arbitrary typed data.
Over-general. Creates a stringly-typed escape hatch that undermines the protocol’s type safety.
Decision: option (b) — the kernel narrows to an ExecCapable interface. The kernel is the only consumer of exec. It’s the equivalent of the Unix kernel reading ELF headers — a kernel concern, not a filesystem concern. The kernel knows which mount points could contain executables (the ones on PATH) and checks if their fileservers implement ExecCapable:
This keeps exec out of the general Fileserver protocol. MemoryFS implements ExecCapable; ProcFS, DevFS, TtyFS do not. The kernel’s exec path: resolve the path, find the fileserver for that mount, check isExecCapable, call getExec. If the fileserver isn’t exec-capable or returns undefined, fall back to reading the file for a shebang.
No Exec Table
There is no separate exec table or native function registry. The INode exec field on file nodes in the filesystem is the sole source of truth for native executables. The kernel accesses it through ExecCapable.getExec() during PATH resolution.
This means rm /bin/echo removes the function reference. Overwriting /bin/echo with a script replaces the node — exec field gone. The filesystem has full authority over what executables exist. See the executables spec for detailed write semantics and PATH resolution flow.
Impact on Fileservers
MemoryFS
Full INode support. The MemoryNode type is replaced by INode. All metadata fields are stored and mutable. wstat updates fields in place and sets ctime to Date.now(). Implements ExecCapable — getExec walks the node tree and returns node.exec if present.
ProcFS
Returns synthetic Stat: { mode: 0o444, uid: 0, gid: 0, ctime: 0 }. All files are read-only. wstat throws EPERM — process metadata is not user-modifiable through filesystem operations. Not ExecCapable.
DevFS
Returns synthetic Stat: { mode: 0o666, uid: 0, gid: 0, ctime: 0 }. Devices are read-write for all. wstat throws EPERM — device permissions are fixed. Not ExecCapable.
TtyFS
Returns synthetic Stat: { mode: 0o666, uid: 0, gid: 0, ctime: 0 }. Same as DevFS. wstat throws EPERM. Not ExecCapable.
BinFS
Removed. Its responsibilities are absorbed:
BinFS responsibility
New home
Store function references
INode exec field on MemoryFS nodes
resolve(name) → BinFunction
ExecCapable.getExec() on MemoryFS
readdir() listing
Real directory entries in MemoryFS /bin
stat() for inspection
Real Stat from MemoryFS nodes
read() for cat
Stub content in INode content
Cross-references
Executables — the exec model, PATH resolution, shebangs. INode’s exec field is where native function references live on the filesystem. The filesystem is the single source of truth — no separate exec table.
Developer API — Extension.bins is how native functions enter the system. The builder writes INodes with exec fields into MemoryFS. The builder is the only code that sets exec fields in v1; runtime bin creation uses scripts with shebangs.
Fileservers — the 10-method protocol including wstat. INode is the backing model for MemoryFS but is not visible through the protocol. Stat gains optional fields but remains backward-compatible.
Future: permission enforcement — the data model (mode, uid, gid on every INode) supports full Unix permission checking. In v1, permissions are stored but not enforced — any process can read/write any file. Enforcement would be a fileserver middleware wrapper (as sketched in the fileservers spec) that checks the calling process’s uid/gid against the INode’s mode before delegating to the inner fileserver.