ADR-0001: srv-isolation exposure model
Status: Proposed Date: 2026-04-24 Tags: #container #namespace #exports #plan9
Context
The container runtime needs to support two namespace shapes for /srv:
- Shared: child sees the parent’s
/srvFileserver by reference (current behavior, but only by virtue of every container getting its own freshsrvFS()— there is no actual sharing wired today) - Isolated: child sees a fresh empty
srvFS()plus a filtered view of the parent’s/srv(theexportsFS(parentSrv, allowList)primitive that exists inruntime/exports/exports-fs.tsbut has no callers)
The original M4-S4 design left the exposure model for the filtered view unspecified. Three approaches surfaced during grooming on 2026-04-24:
- A. Auto-mount
exportsFSat/dev/container/exports/— the path under the container-introspection tree - B. Union-mount
srvFS()(writable, empty) overexportsFS(parent, allow)(read-only) at/srv— child sees one discovery root via the Plan-9 union-bind primitive - C. Post-only — post
exportsFSto the child’s/srv/parent-exportsand let the child mount it on demand (purest Plan 9:/srvas discovery namespace, child controls its own mount tree)
Surrounding considerations:
- The codebase has a
bindprimitive that already implements union-mount semantics (fileservers/namespaces.md) - Shell scripts that consume
/srvfollow the conventioncat /srv/db,ls /srv— Plan 9 lineage - Container introspection lives at
/dev/container/{status,spec,ctl}— a different concept (state about the container itself, not its discovery namespace)
Decision
Option B — union-mount at /srv. When isolateSrv: true, the child’s /srv is the union of srvFS() (fresh, writable; child’s own posts go here) over exportsFS(parent, allow) (read-only filtered view of parent’s posts). One discovery root in the child. No /dev/container/exports/ mount.
Alternatives Considered
A. Auto-mount at /dev/container/exports/
Rejected: introduces a second discovery root. ls /srv would not show inherited names — surprising to anyone with Plan 9 or even Unix muscle memory. Also conflates “the container’s own introspection surface” (/dev/container/) with “what the parent decided to share” — different concerns.
C. Post-only — child mounts on demand
Rejected: imposes a mounting step on every isolated child for the common case, which is “see what parent shares, like Plan 9 default child behavior.” Pure-Plan-9 but unergonomic for the auto-isolation case unshare --no-srv is asking for. Worth re-considering if/when we add proper rfork namespace forking — but until then, the auto-mount is the right ergonomic default.
Consequences
Positive
- One discovery root in the child.
ls /srvshows everything the child can see (own posts + filtered inheritance) — Plan-9-idiomatic, matches shell muscle memory. - Reuses the existing
bind/union-mount primitive — no new mount-composition machinery. exportsFSlifecycle (subscribe/dispose) hangs off a normal/srvmount-point — bind/unmount semantics apply uniformly.
Negative / Trade-offs
- “Is this name inherited or local?” requires a debug ctl or specialized bin to distinguish — readdir/stat won’t tell you which side of the union it came from.
- Couples srv isolation to the bind primitive’s correctness. If union semantics regress, srv-isolation regresses.
- Slightly higher resolution cost per
/srv/<name>lookup (two-tier fall-through) vs single-tier.
Open follow-ups
feat-m4-s4-exports-multi-level.md—parentEffectivethreading for grandchild narrowing. RequiresContainerManagerImplto track parent/child; out of scope for the initial wiring PR.design-rfork-primitive-vs-flag-accretion.md— broader RFC: shouldNamespaceSpecuse rfork-style flag strings ('s','n','m') instead of growing N booleans (isolateSrv,isolateEnv, …)? If accepted, this ADR’s exposure-model decision is unchanged; only the spec field name changes.feat-m4-s4-exports-rfork-gate-wiring.md— the implementation ticket for this decision.