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
| Component | Type | Description |
|---|---|---|
SrvCapable interface | types.ts | getServer, postServer, removeServer |
isSrvCapable type guard | types.ts | Runtime detection, parallel to isExecCapable |
srvFS fileserver | kernel/fileservers/srv.ts | Shared registry at /srv |
postServer/getServer/removeServer | ProcContext | Optional callbacks, registerExec pattern |
unmount | Kernel + ProcContext | Remove a mount point from the process namespace |
| Union bind | namespace.ts + kernel bind() | before/after/replace flags on bind |
mount bin | bins/system/mount.ts | List mounts, mount from /srv, bind fallback |
umount bin | bins/system/umount.ts | Remove a mount point |
bind bin | bins/system/bind.ts | -b (before), -a (after) union flags |
srv preset | presets/srv.ts | Provides mount/umount/bind bins, added to stdSystem(). srvFS created in image.ts |
End-to-end flow after Phase A
pkg install webcam-device # installs webcam-srv binwebcam-srv & # creates Fileserver, posts to /srv/webcammount webcam ~/cam # mount bin → getServer → proc.mountcat ~/cam/frame.jpg > pic.jpg # direct I/O to webcam Fileserverumount ~/cam # removes namespace entrykill $! # stop the server process
# Union bind examplebind -b /usr/local/bin /bin # local bins searched before system binsWhat Phase A does NOT include
- No service integration for auto-mount/unmount
- No
nscommand 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
| Component | Type | Description |
|---|---|---|
postMount on ServiceDef | types.ts | Record<string, string> — auto-mount after service start |
| Init mount/unmount hooks | runtime/init | Auto-mount on service start, auto-unmount on stop, re-mount on restart |
srv bin | bins/system/srv.ts | Convenience: start server + post to /srv in one command |
ns bin | bins/system/ns.ts | Print current process namespace (mount table) |
| MountUnit implementation | runtime/init | Already designed in architecture/init — implement the mount unit lifecycle |
Service-managed devices after Phase B
// Package definitionexport 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 }], }}pkg install webcam-devicesvc start webcam-srv # init starts daemon, auto-mounts /dev/webcamcat /dev/webcam/frame.jpg # just workssvc stop webcam-srv # init stops daemon, auto-unmounts /dev/webcamWhat Phase B adds to restart semantics
When a supervised service restarts:
- Init calls
removeServer(name)for old entry - Service process starts, posts new Fileserver to
/srv - Init detects post (via
postMountconfig) and re-mounts at declared paths - Existing processes that had direct mounts see errors on old server — this is correct (matches Plan 9)
- New operations through the re-mounted path hit the fresh server
Phase C: Full Plan 9 namespace
Complete namespace isolation, introspection, and sandboxing.
Deliverables
| Component | Type | Description |
|---|---|---|
/proc/<pid>/ns | procFS | Expose per-process namespace as readable file |
| Namespace clone flags | kernel spawn | RFNOMNT (forbid mount in child), RFCNAMEG (clean namespace) |
rfork bin | bins/system/rfork.ts | Shell access to namespace clone flags |
| Namespace groups | kernel | Named namespace groups that processes can join |
Use cases unlocked by Phase C
Sandboxing:
# Spawn a process with no mount permissionrfork -m sh # child shell cannot call mount/bindNamespace introspection:
ns # print own namespacecat /proc/42/ns # inspect another process's namespaceClean namespaces:
# Spawn with minimal namespace (only / and /dev)rfork -n sh # child gets clean namespace, must mount what it needsRelationship 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
-
Kernel stays micro. New primitives (
unmount, union bind) are minimal. srvFS knowledge lives in image.ts via closures, not in the kernel. -
Filesystem is the API.
ls /srvshows available servers.mountlists the namespace. Device state is readable via files, not special APIs. -
Everything composes via Extension. Device packages are Extensions. They provide bins, services, mounts — the same composition unit as everything else.
-
Plan 9 semantics, JS pragmatics.
/srvis 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. -
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 binconst 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 packageexport default function(): Extension { return { bins: { 'myfs-srv': myServerBin }, services: [{ name: 'myfs-srv', bin: 'myfs-srv', restart: { policy: 'on-failure' }, }], }}Users interact with it:
# Manualmyfs-srv --option value &mount myfs /mnt/data
# Or as a servicesvc start myfs-srvmount myfs /mnt/data
# Or in profile for persistenceecho 'svc start myfs-srv && mount myfs /mnt/data' >> ~/.profilePackages that provide simple, stateless devices should skip the daemon and use Extension.mounts directly:
export default function(): Extension { return { mounts: { '/dev/mydevice': myStatelessFS() }, }}