Skip to content

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 /srv Fileserver by reference (current behavior, but only by virtue of every container getting its own fresh srvFS() — there is no actual sharing wired today)
  • Isolated: child sees a fresh empty srvFS() plus a filtered view of the parent’s /srv (the exportsFS(parentSrv, allowList) primitive that exists in runtime/exports/exports-fs.ts but 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 exportsFS at /dev/container/exports/ — the path under the container-introspection tree
  • B. Union-mount srvFS() (writable, empty) over exportsFS(parent, allow) (read-only) at /srv — child sees one discovery root via the Plan-9 union-bind primitive
  • C. Post-only — post exportsFS to the child’s /srv/parent-exports and let the child mount it on demand (purest Plan 9: /srv as discovery namespace, child controls its own mount tree)

Surrounding considerations:

  • The codebase has a bind primitive that already implements union-mount semantics (fileservers/namespaces.md)
  • Shell scripts that consume /srv follow the convention cat /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 /srv shows 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.
  • exportsFS lifecycle (subscribe/dispose) hangs off a normal /srv mount-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.mdparentEffective threading for grandchild narrowing. Requires ContainerManagerImpl to track parent/child; out of scope for the initial wiring PR.
  • design-rfork-primitive-vs-flag-accretion.md — broader RFC: should NamespaceSpec use 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.