Skip to content

Mountable Fileservers — Roadmap

Incremental plan for Plan 9-style mountable fileserver packages. Each phase is independently shippable and builds on the previous without breaking changes.

Phase A: srvFS + mount/umount + union bind (Foundation) ✅ IMPLEMENTED

The minimum set of primitives that unlocks “install package → mount device → use it from the shell.”

Deliverables

ComponentTypeDescription
SrvCapable interfacetypes.tsgetServer, postServer, removeServer
isSrvCapable type guardtypes.tsRuntime detection, parallel to isExecCapable
srvFS fileserverkernel/fileservers/srv.tsShared registry at /srv
postServer/getServer/removeServerProcContextOptional callbacks, registerExec pattern
unmountKernel + ProcContextRemove a mount point from the process namespace
Union bindnamespace.ts + kernel bind()before/after/replace flags on bind
mount binbins/system/mount.tsList mounts, mount from /srv, bind fallback
umount binbins/system/umount.tsRemove a mount point
bind binbins/system/bind.ts-b (before), -a (after) union flags
srv presetpresets/srv.tsProvides mount/umount/bind bins, added to stdSystem(). srvFS created in image.ts

End-to-end flow after Phase A

Terminal window
pkg install webcam-device # installs webcam-srv bin
webcam-srv & # creates Fileserver, posts to /srv/webcam
mount webcam ~/cam # mount bin → getServer → proc.mount
cat ~/cam/frame.jpg > pic.jpg # direct I/O to webcam Fileserver
umount ~/cam # removes namespace entry
kill $! # stop the server process
# Union bind example
bind -b /usr/local/bin /bin # local bins searched before system bins

What Phase A does NOT include

  • No service integration for auto-mount/unmount
  • No ns command for namespace introspection
  • No namespace clone flags for sandboxing
  • No /proc/<pid>/ns
  • Server restart requires manual remount

Phase B: Service integration + namespace tooling

Makes device servers first-class in the init system. Server lifecycle becomes declarative.

Deliverables

ComponentTypeDescription
postMount on ServiceDeftypes.tsRecord<string, string> — auto-mount after service start
Init mount/unmount hooksruntime/initAuto-mount on service start, auto-unmount on stop, re-mount on restart
srv binbins/system/srv.tsConvenience: start server + post to /srv in one command
ns binbins/system/ns.tsPrint current process namespace (mount table)
MountUnit implementationruntime/initAlready designed in architecture/init — implement the mount unit lifecycle

Service-managed devices after Phase B

// Package definition
export default function(): Extension {
return {
bins: { 'webcam-srv': webcamServerBin },
services: [{
name: 'webcam-srv',
bin: 'webcam-srv',
restart: { policy: 'on-failure' },
postMount: { 'webcam': '/dev/webcam' },
// After service starts and posts 'webcam' to /srv,
// init auto-mounts it at /dev/webcam
}],
}
}
Terminal window
pkg install webcam-device
svc start webcam-srv # init starts daemon, auto-mounts /dev/webcam
cat /dev/webcam/frame.jpg # just works
svc stop webcam-srv # init stops daemon, auto-unmounts /dev/webcam

What Phase B adds to restart semantics

When a supervised service restarts:

  1. Init calls removeServer(name) for old entry
  2. Service process starts, posts new Fileserver to /srv
  3. Init detects post (via postMount config) and re-mounts at declared paths
  4. Existing processes that had direct mounts see errors on old server — this is correct (matches Plan 9)
  5. New operations through the re-mounted path hit the fresh server

Phase C: Full Plan 9 namespace

Complete namespace isolation, introspection, and sandboxing.

Deliverables

ComponentTypeDescription
/proc/<pid>/nsprocFSExpose per-process namespace as readable file
Namespace clone flagskernel spawnRFNOMNT (forbid mount in child), RFCNAMEG (clean namespace)
rfork binbins/system/rfork.tsShell access to namespace clone flags
Namespace groupskernelNamed namespace groups that processes can join

Use cases unlocked by Phase C

Sandboxing:

Terminal window
# Spawn a process with no mount permission
rfork -m sh # child shell cannot call mount/bind

Namespace introspection:

Terminal window
ns # print own namespace
cat /proc/42/ns # inspect another process's namespace

Clean namespaces:

Terminal window
# Spawn with minimal namespace (only / and /dev)
rfork -n sh # child gets clean namespace, must mount what it needs

Relationship to existing sandbox patterns

Phase C’s namespace flags compose with the existing uid/permission system. A sandboxed process can be given:

  • A restricted namespace (limited mounts)
  • A non-root uid (limited permissions)
  • A read-only view of certain mounts (via middleware wrapping)

This enables multi-tenant isolation without heavyweight virtualization.


Design Principles Across All Phases

  1. Kernel stays micro. New primitives (unmount, union bind) are minimal. srvFS knowledge lives in image.ts via closures, not in the kernel.

  2. Filesystem is the API. ls /srv shows available servers. mount lists the namespace. Device state is readable via files, not special APIs.

  3. Everything composes via Extension. Device packages are Extensions. They provide bins, services, mounts — the same composition unit as everything else.

  4. Plan 9 semantics, JS pragmatics. /srv is a shared registry (Plan 9), but posting is programmatic (JS reality — objects aren’t bytes). Union bind follows Plan 9’s before/after model exactly. No fstab — profile scripts and services handle persistence.

  5. Each phase is independently useful. Phase A enables the core use case. Phase B adds polish. Phase C adds security. No phase depends on a future phase being completed.

Server Daemon Pattern

Packages that provide dynamic, configurable fileservers should follow this pattern:

// The server daemon bin
const myServerBin: BinFunction = async (proc) => {
// 1. Parse configuration from argv/env
const config = parseConfig(proc.argv, proc.env)
// 2. Create the fileserver
const server = createMyFileserver(config)
// 3. Post to /srv
proc.postServer?.('myfs', server)
// 4. Block until terminated
await new Promise<void>(resolve => {
proc.on('SIGTERM', () => {
proc.removeServer?.('myfs')
resolve()
})
})
return exitCode(0)
}
// The package
export default function(): Extension {
return {
bins: { 'myfs-srv': myServerBin },
services: [{
name: 'myfs-srv',
bin: 'myfs-srv',
restart: { policy: 'on-failure' },
}],
}
}

Users interact with it:

Terminal window
# Manual
myfs-srv --option value &
mount myfs /mnt/data
# Or as a service
svc start myfs-srv
mount myfs /mnt/data
# Or in profile for persistence
echo 'svc start myfs-srv && mount myfs /mnt/data' >> ~/.profile

Packages that provide simple, stateless devices should skip the daemon and use Extension.mounts directly:

export default function(): Extension {
return {
mounts: { '/dev/mydevice': myStatelessFS() },
}
}