473 lines
25 KiB
Markdown
473 lines
25 KiB
Markdown
# The protocol namespace
|
||
|
||
*Design, agreed 2026-07-31. Supersedes the `ServiceId` registry. Not yet implemented —
|
||
the migration plan at the end is the work list.*
|
||
|
||
How a program finds, connects to, and is restricted from the things it talks to.
|
||
Three ideas, kept deliberately separate:
|
||
|
||
1. **Naming** — a path under `/protocol` names a *contract*, not a service.
|
||
2. **Access** — resolving that path yields an endpoint *capability*; what a process
|
||
cannot resolve, it cannot reach.
|
||
3. **Transport** — unchanged: packets over channels, moved by whichever
|
||
transport the channel rides (kernel-ipc first).
|
||
This document is layers **L3** (the namespace) and **L2** (the protocol
|
||
and its envelope) of the communication stack;
|
||
[communication.md](communication.md) owns the model and the vocabulary
|
||
(*protocol* the language, *channel* the conversation, *packet* the
|
||
transmitted unit, *signal* the payload-less poke, *transport* the
|
||
replaceable mechanism), and
|
||
[ipc.md](../device-driver-development/ipc.md) is the first transport.
|
||
|
||
## Why ServiceId has to go
|
||
|
||
Today a service calls `ipc_register(service_id, endpoint)` and a client calls
|
||
`ipc_lookup(service_id)`, where `ServiceId` is a compile-time enum in `abi.zig`
|
||
backed by a flat 16-slot table in the kernel. Three defects, in rising order:
|
||
|
||
- **Static.** The id space is baked into the ABI at compile time. A third-party
|
||
program can never introduce a service; the one place danos is *less* dynamic
|
||
than its own design.
|
||
- **Ungated.** `ipc_register` is callable by any process and *replaces* an
|
||
existing registration. Any process can hijack `.fat` or `.display` and
|
||
impersonate it. `ipc_lookup` is equally ambient.
|
||
- **Unrestrictable.** Because lookup is a syscall available to everyone, there is
|
||
no point at which "this process may not talk to the display" can be enforced.
|
||
Any future file-access restriction would be bypassable by speaking to the FAT
|
||
server directly.
|
||
|
||
## Naming: contracts, not services
|
||
|
||
`/protocol/<name>` names a protocol — the contract a conversation follows — and
|
||
resolving it connects you to whatever process currently provides that contract.
|
||
The client never cared *which* binary answers; it cares that its messages are
|
||
understood. Naming the contract makes that explicit, and buys:
|
||
|
||
- **Swappable providers.** Replace the display server; `/protocol/display`
|
||
routes to the new one; clients notice nothing.
|
||
- **Test fakes.** Spawn a program whose namespace wires `/protocol/display` to a
|
||
mock. The name promises the protocol; the mock speaks it.
|
||
- **One vocabulary.** The names mirror `library/protocol/`: a program imports
|
||
the `display-protocol` module, then opens `/protocol/display`. What you
|
||
compiled against and what you ask the namespace for are the same word.
|
||
|
||
A leaf names one contract — kebab-case, full words, matching the
|
||
`library/protocol/` module that defines its wire format — and related
|
||
contracts group into directories: `/protocol/networking/ip`,
|
||
`/protocol/networking/bluetooth`. Directories organize *contracts only*;
|
||
they never encode addressing (see below), so a directory appears because a
|
||
domain has several contracts, never because hardware multiplied. The module
|
||
tree mirrors the namespace (`library/protocol/networking/ip` ↔
|
||
`/protocol/networking/ip`), and registrar grants scope naturally to subtrees
|
||
— an application installed at `/applications/foo` can be granted
|
||
`/protocol/applications/foo/...` and nothing above it. `/protocol` is
|
||
top level, beside `/system` and `/applications`, because the boundary it names
|
||
is spoken on both sides: applications talk to protocols as much as the OS does
|
||
(see [file-system-hierarchy.md](../file-system-development/file-system-hierarchy.md)).
|
||
|
||
**Addressing lives inside the protocol, never in the path.** Which volume, which
|
||
layer, which input device — that is a destination field in the messages, the way
|
||
TCP carries a destination address, and the way danos protocols already work (the
|
||
display protocol multiplexes layer ids; the vfs protocol addresses node ids).
|
||
The namespace answers exactly one question — *may this process speak this
|
||
protocol at all* — so `/protocol/block` is one name no matter how many disks are
|
||
attached. The source address is never in the message either: it is the IPC
|
||
badge, stamped by the kernel per message, unforgeable — a property TCP's source
|
||
address does not have.
|
||
|
||
`/system/devices` (the device inventory) stays purely informational: facts for
|
||
diagnosis, never a routing mechanism. Unix conflated the two in `/dev`; danos
|
||
does not. You *read about* hardware in `/system/devices`; you *talk to* it
|
||
through `/protocol`.
|
||
|
||
## Resolution: a protocol node in the VFS
|
||
|
||
The kernel VFS router already does the hard part: `fs_resolve` matches a mount
|
||
prefix and installs the backend's endpoint capability in the caller's handle
|
||
table. The registry is just a backend mounted at `/protocol` — ring 3, like FAT.
|
||
Connecting is a normal vfs-protocol `open` with one twist in the reply:
|
||
|
||
```
|
||
client kernel router registry backend
|
||
│ fs_resolve("/protocol/display") │
|
||
│──────────────────────────▶│ prefix match: /protocol │
|
||
│◀── registry endpoint ─────│ (capability installed) │
|
||
│ vfs open("display") ──────────────────────────────────────▶│
|
||
│◀───────────────── Reply + capability = provider endpoint ──│
|
||
│ ipc_call(provider, display-protocol messages...) │
|
||
```
|
||
|
||
Both capability moves use machinery the kernel already has: request-direction
|
||
and reply-direction `send_cap` on `call`/`replyWait`. The vfs protocol needs two
|
||
additions, both append-only:
|
||
|
||
- `NodeKind.protocol` — a node that names a contract; its `open` establishes
|
||
a **channel** (delivered as an endpoint capability) instead of returning a
|
||
file id. The node is the protocol, the channel is the conversation, and the
|
||
addressing inside the packets decides where within the provider each one
|
||
lands. `readdir` over `/protocol` lists protocol nodes like any others, so
|
||
the tree stays browsable for diagnosis.
|
||
- The convention that an `open` reply may carry a capability. File backends
|
||
(FAT) never use it; synthetic backends (the registry, later the device
|
||
inventory) do.
|
||
|
||
The path lookup happens once, at connect time. The hot path — `ipc_call` on the
|
||
cached endpoint — is untouched. A provider crash turns the cached endpoint dead
|
||
(`-EPEER`), and the client's recovery is to re-resolve: the restart story falls
|
||
out of the naming layer for free.
|
||
|
||
## Registration: the registrar, held by init
|
||
|
||
The registry backend is **init**. It is already PID 1, already spawns every
|
||
service from its manifest, and already holds the supervision link to each — it
|
||
is the process that *knows* which binary is which. (If init grows
|
||
uncomfortable, the same design lifts into a dedicated registry service that
|
||
init spawns first and delegates to; nothing below changes.)
|
||
|
||
- **Binding.** A service creates its endpoint and sends the registry a `bind`
|
||
request with the protocol name as payload and the endpoint attached as the
|
||
call's capability.
|
||
- **Authorization.** Init's manifest gains a column: the protocols each spawned
|
||
binary may bind. A `bind` from any process not granted that name is refused
|
||
(`-EPERM`) — the badge identifies the caller, the supervision records map
|
||
badge to binary. This is the registrar authority; it never leaves init.
|
||
- **Collision is an error.** A name already bound refuses a second bind — never
|
||
last-writer-wins. When a provider dies, init (its supervisor) unbinds its
|
||
names; the restarted instance binds again.
|
||
- **Provenance.** The registry records name → task id → binary path, so a
|
||
diagnostic listing answers "who serves this?" at a glance:
|
||
|
||
```
|
||
/protocol/display pid 12 /system/services/display
|
||
/protocol/input pid 7 /system/services/input
|
||
```
|
||
|
||
`ipc_register` and `ipc_lookup` retire; the `ServiceId` enum leaves `abi.zig`.
|
||
The kernel keeps one residual rule: `/protocol` becomes a reserved prefix like
|
||
`/system` — `fs_mount` refuses to shadow it, and init's boot-time mount is the
|
||
only one it will ever hold. (Full gating of `fs_mount` is a separate item on
|
||
the security track; the reserved prefix closes the hole for this namespace
|
||
without waiting for it.)
|
||
|
||
## Restriction: per-process namespaces, not ACLs
|
||
|
||
danos has no users and no principals, deliberately. Restriction is therefore
|
||
**delegation**: what a process may open is decided by whoever spawned it, and
|
||
enforcement is absence — a protocol you cannot resolve does not exist for you.
|
||
"Permission denied" and "not found" are the same answer, which is the same
|
||
discipline the device layer already follows: the claim is the capability; here,
|
||
the resolvable name is the capability.
|
||
|
||
Two stages, deliberately ordered so the useful half lands first:
|
||
|
||
**Stage one — the registry filters by badge.** Init is both the spawner and the
|
||
registry, so its manifest already knows which binary may *open* which protocols
|
||
(a second manifest column, beside the bind grants). An `open` from a process
|
||
whose binary is not granted that protocol is refused. No new kernel mechanism
|
||
at all; the display driver's view can be narrowed to nothing, a future
|
||
downloaded application's to `display` and `input`, today.
|
||
|
||
**Stage two — spawn passes the namespace.** `spawn` gains an initial
|
||
capability: the child's connection to *its* registry view, chosen by the
|
||
spawner. A newly spawned process starts with an empty handle table and this one
|
||
handle — its world is whatever its parent wired in. This removes the last
|
||
ambient reach (`fs_resolve` finding `/protocol` globally), lets any supervisor
|
||
— not just init — narrow or fake a child's view (an application launcher
|
||
granting an app only what its manifest declares; a test harness substituting
|
||
every provider), and composes down the supervision tree. Stage one's manifest
|
||
column becomes the *content* of the view init builds, so nothing is thrown
|
||
away.
|
||
|
||
### A worked example: the microphone prompt
|
||
|
||
The scenario stage two exists for: an application opens
|
||
`/protocol/audio-input`, and the user should be asked. The supervisor is an
|
||
ordinary user process — an application launcher — and the flow needs no new
|
||
security concepts:
|
||
|
||
1. The launcher spawned the app with a namespace channel that terminates at
|
||
**the launcher itself**. The app's whole world is a conversation with its
|
||
supervisor.
|
||
2. The app's `open("audio-input")` packet lands in the launcher,
|
||
badge-stamped. The launcher spawned the app, so badge → binary path
|
||
(`/applications/foo`) is its own supervision record — "remember my choice"
|
||
needs no identity system.
|
||
3. Grant unknown → the launcher parks the request and shows a prompt (it is a
|
||
user process with display access; init never does UI). Blocking an open on
|
||
a human is architecturally fine: opens are connect-time, never hot-path.
|
||
4. **Yes** → the launcher opens `/protocol/audio-input` in *its own*
|
||
namespace and attaches the resulting channel to the parked reply. The app
|
||
cannot tell a prompt happened — a consented open is indistinguishable from
|
||
a direct one, merely slower.
|
||
5. **No** → refuse the open, indistinguishable from "no such protocol" — or
|
||
hand the app a **fake**: a silence-generating provider. The test-fake
|
||
mechanism doubles as a privacy feature.
|
||
|
||
The capability discipline holds throughout: the launcher can only grant what
|
||
it holds — if init never gave the launcher `audio-input`, no prompt can
|
||
conjure it. Consent is delegation flowing down the supervision tree, never a
|
||
global ACL edit. And the provider still sees the app's badge on every packet,
|
||
so a coarser second check at the audio service remains possible.
|
||
|
||
Two mechanical requirements this scenario pins on stage two:
|
||
|
||
- **Parked replies.** A prompt takes seconds, and the service loop holds one
|
||
outstanding reply today — the launcher must park request A, keep serving B
|
||
and C, and reply to A later (by badge). The kernel already tracks owed
|
||
replies (that is how death delivers `-EPEER`); multiple parked replies is
|
||
the extension, in the harness and, if needed, the kernel.
|
||
- **Granted channels are dedicated, hence revocable.** Once the app holds a
|
||
channel capability, nobody reaches into its handle table — so a
|
||
prompt-granted channel must be one that can be *killed*: a dedicated
|
||
endpoint pair (or per-client session at the provider) whose death turns
|
||
the app's capability into `-EPEER`. Revoking microphone access is then
|
||
killing that channel, using machinery that already exists.
|
||
|
||
One adjacent problem, named and deferred: **trusted UI**. The prompt is only
|
||
meaningful if the app cannot draw a convincing fake or overlay the real one —
|
||
a display-layer question (a reserved surface for the supervisor chain), owned
|
||
by the display track, not this one.
|
||
|
||
Fine-grained restriction *within* a protocol (this process may use volume A but
|
||
not volume B) is not the namespace's job. The capability-shaped answer, when it
|
||
is needed: the supervisor pre-opens a connection scoped to one target and passes
|
||
that connection to the child, which never opens `/protocol/block` at all.
|
||
Delegation again, not ACLs.
|
||
|
||
## The envelope: one addressing scheme for every protocol
|
||
|
||
Every protocol module today hand-rolls its `Request`/`Reply` with an
|
||
`operation` first field. That convention becomes a library, so addressing is
|
||
uniform and the rules are enforced by construction rather than by review. New
|
||
module: **`library/protocol/envelope`** (the one protocol-layer module that is
|
||
not itself a protocol).
|
||
|
||
```zig
|
||
/// Every packet a danos protocol transmits begins with this header.
|
||
pub const Header = extern struct {
|
||
operation: u32, // the verb; values 0..15 are reserved universal verbs
|
||
_padding: u32 = 0,
|
||
/// Object addressing, never party addressing: which of the peer's
|
||
/// objects this packet operates on — a volume, layer, node, device.
|
||
/// 0 addresses the provider itself. Parties are addressed by the
|
||
/// channel; the protocol defines target's meaning; the field's place
|
||
/// and width are universal.
|
||
target: u64 = 0,
|
||
};
|
||
|
||
/// Reserved verbs, answered by every provider.
|
||
pub const operation_describe: u32 = 0; // -> protocol name, version, target kinds
|
||
pub const operation_enumerate: u32 = 1; // -> the current targets, one per reply page
|
||
pub const operation_subscribe: u32 = 2; // capability = the subscriber's endpoint
|
||
pub const operation_unsubscribe: u32 = 3;
|
||
pub const first_protocol_operation: u32 = 16;
|
||
|
||
/// Every reply begins with this.
|
||
pub const Status = extern struct {
|
||
status: i32, // 0 or a negative errno
|
||
_padding: u32 = 0,
|
||
len: u32 = 0, // payload bytes following the header
|
||
_padding2: u32 = 0,
|
||
};
|
||
```
|
||
|
||
A protocol is then *defined through* the envelope, not beside it:
|
||
|
||
```zig
|
||
pub const Protocol = envelope.Define(.{
|
||
.name = "display",
|
||
.version = 1,
|
||
.operations = &.{
|
||
.{ .name = "configure_layer", .request = ConfigureLayer, .reply = void },
|
||
.{ .name = "blit", .request = Blit, .reply = void },
|
||
...
|
||
},
|
||
});
|
||
```
|
||
|
||
`Define` is comptime and is where the enforcement lives:
|
||
|
||
- verbs are numbered automatically from `first_protocol_operation`, so no
|
||
protocol can collide with the reserved range;
|
||
- every packet is size-checked at compile time against the kernel-ipc floor
|
||
— `packet_maximum` (256) for request/reply, `post_maximum` (64) for event
|
||
packets. Ceilings are transport properties
|
||
([communication.md](communication.md)); the floor is what every protocol
|
||
may assume on any transport. The errors that today surface as runtime
|
||
truncation become compile errors, and packets-never-fragment is enforced
|
||
at the source;
|
||
- the generated type carries encode/decode helpers and a provider-side dispatch
|
||
table, so a provider answers `describe` automatically and unknown operations
|
||
with `-ENOSYS` uniformly;
|
||
- the service harness (`library/kernel/service.zig`) accepts the generated
|
||
dispatch type, which is what makes the envelope *enforced*: a protocol that
|
||
bypasses `Define` does not plug into the harness.
|
||
|
||
Universal conventions that ride on the reserved verbs:
|
||
|
||
- **`describe`** is the version handshake. Version lives in the handshake, not
|
||
in every message — the 256-byte budget is too small to spend per call.
|
||
- **`enumerate`** is how multi-target protocols expose their targets, and the
|
||
standard `targets_changed` notification (a notify bit) tells subscribers to
|
||
re-enumerate — arrival and removal of volumes, layers, devices all take the
|
||
same shape. Hotplug fits the notification ring far better than a filesystem
|
||
tree ever did.
|
||
- **Source is the badge.** No protocol defines a "sender" field; the kernel's
|
||
per-message badge is the only source identity, and providers key per-client
|
||
state on it.
|
||
|
||
### Paths resolve once; integers do the work
|
||
|
||
A rule the envelope makes official: **a path appears in a conversation at most
|
||
once — at resolve or open — and everything after it addresses integers.** The
|
||
namespace resolves `/protocol/display` to an endpoint; a backend's `open`
|
||
resolves a path payload to a node id; from then on every packet carries the
|
||
integer in `target`. Integers compare in one instruction and fit the fixed
|
||
header, and the 256-byte message budget never re-carries path strings on the
|
||
hot path. This is already the system's shape — vfs node ids, display layer ids
|
||
— and the envelope pins it as the required shape for every protocol.
|
||
|
||
Two integer identities, not to be confused:
|
||
|
||
- **An open handle** — what vfs `open` returns today: transient, meaningful
|
||
only within one client's session with one provider, swept when the client
|
||
exits. Cheap, and all a protocol usually needs. Handles must be **scoped per
|
||
client** — validated against the badge, or drawn from a per-client id
|
||
namespace. (Today the FAT server's node ids are guessable small integers
|
||
honoured across clients; that hole closes with this rule.)
|
||
- **A persistent node identity** — a unix inode number, stable across opens
|
||
and renames. danos deliberately does not promise this, because FAT cannot
|
||
deliver it: a FAT file's identity is its directory entry, and rename or
|
||
truncation moves every candidate anchor. If a future filesystem or a cache
|
||
layer needs stable identity, that is the backend's promise to make, never
|
||
the protocol's assumption.
|
||
|
||
The five existing protocol modules (`vfs`, `display`, `input`, `power`,
|
||
`block`, plus `scanout`, `usb-transfer`, `device-manager`) rebase onto the
|
||
envelope during the migration flag-day. `input-protocol`'s subscribe/publish
|
||
split and `vfs-protocol`'s node addressing both map cleanly (`node` and layer
|
||
ids become `target`).
|
||
|
||
## Wiring: how conversations flow
|
||
|
||
The patterns below are channel-layer (L1) shapes; the delivery mechanics are
|
||
the kernel-ipc transport's, described here because it is the transport every
|
||
channel starts on. Kernel-ipc provides exactly three delivery shapes, and
|
||
every one is unicast. An endpoint is a mailbox owned by one process — its
|
||
creator receives; anyone holding its capability sends into it. That direction
|
||
never reverses:
|
||
|
||
1. **Synchronous call** — request/reply. The kernel parks the caller and
|
||
`replyWait` delivers the reply straight back, so the provider answers
|
||
without holding any capability to the client. Badge-stamped, blocking, and
|
||
the *only* shape that carries capabilities (in the request, and in the
|
||
reply — which is how a reverse path is bootstrapped).
|
||
2. **Asynchronous send** — an event packet pushed into the receiver's post
|
||
ring, at most `post_maximum` (64) bytes, no reply owed, never blocks the
|
||
sender. Strictly one-way: to be pushed to, you must first hand the pusher
|
||
your endpoint.
|
||
3. **Signals** — payload-less notification bits, below the packet layer,
|
||
coalescing: "something changed, come look."
|
||
|
||
A bidirectional link is therefore always **a pair of endpoints**, one per
|
||
direction, each delivered by cap-passing. Three conversation patterns are
|
||
built from these, and the envelope names all three:
|
||
|
||
- **Request/response** — the synchronous call. The default, and the only
|
||
place capabilities move.
|
||
- **Event stream** — `subscribe` (a synchronous call whose attached
|
||
capability is the subscriber's own endpoint), after which the provider
|
||
pushes events asynchronously; `unsubscribe` or subscriber exit ends it.
|
||
Listened-to, not blocked-on.
|
||
- **Change signal** — a signal plus re-read: `targets_changed` →
|
||
`enumerate`. For state whose truth lives with the provider.
|
||
|
||
**Broadcast is a provider pattern, never a kernel primitive.** The kernel
|
||
does not know subscriber sets — a service does. The input service is the
|
||
model: sources *publish* (a unicast call to the service), the service
|
||
*broadcasts* (a fan-out loop of asynchronous sends over its subscriber list,
|
||
so one dead subscriber can never stall the rest). One fan-out point per event
|
||
domain, owned by the service that defines the event.
|
||
|
||
The harness owns the machinery: the subscriber table, the dead-subscriber
|
||
sweep (via process-exit notifications), and the fan-out loop — all written by
|
||
hand in `input.zig` today, lifted into the service harness so every protocol
|
||
gets identical semantics. `Define` declares a protocol's events (`.events`),
|
||
and each event type is checked against `post_maximum` at compile time,
|
||
generalizing the assert `input-protocol` already carries.
|
||
|
||
**Event packets are droppable.** A slow subscriber's ring fills, and the
|
||
provider must not block on it — so an event stream is a hint or a coalescing
|
||
signal, never a ledger. Anything that must not be lost is either re-readable
|
||
state (the change-signal pattern) or bulk data in shared memory with a
|
||
packet as the doorbell, which is how the display path already works — the
|
||
packets-never-fragment rule and this one are the same rule seen from two
|
||
sides.
|
||
|
||
**Source direction (open point).** Today event sources are *clients*: an
|
||
input driver resolves `/protocol/input` and delivers each event as a
|
||
synchronous `publish` call — one capability, obtained by resolution, covers
|
||
everything, and the badge tells the service exactly who each event came from.
|
||
The inversion — the service subscribing to each driver — would require every
|
||
driver to be individually discoverable and its endpoint ferried to the
|
||
service, machinery whose payoff (the service choosing its sources) the
|
||
namespace already provides more cheaply: only a process granted open on
|
||
`/protocol/input` can publish into it. Sources stay clients for now;
|
||
revisited at restriction stage two, when a supervisor can wire capabilities
|
||
at spawn time.
|
||
|
||
## What this deliberately does not solve
|
||
|
||
The wider security track, for which this namespace is the foundation, not the
|
||
whole:
|
||
|
||
- **File access restriction** — the point of the exercise. The same stage-two
|
||
namespace mechanism extends from protocol names to file paths: the spawner
|
||
decides which subtrees resolve. Designed separately once this lands.
|
||
- `fs_mount` gating beyond the reserved prefixes; `system_spawn` gating;
|
||
`klog_read` being world-readable; backends checking the badge on per-node
|
||
operations (the FAT server honours node ids across clients today).
|
||
- Kernel hardening items already noted in-tree: SMEP/SMAP and SYSRET
|
||
canonical-RIP, now designed in [smep-smap.md](smep-smap.md).
|
||
- Pipes/FIFOs for the POSIX layer — a byte-stream object *beside* message IPC,
|
||
wanted by the Python track, unrelated to naming.
|
||
- **Trusted UI** — a permission prompt an application cannot fake or overlay
|
||
(see the microphone example). A display-track concern: the supervisor chain
|
||
needs a reserved surface.
|
||
|
||
## Migration plan
|
||
|
||
Flag-day per phase, in the style of the DMA-capability conversion — no
|
||
dual-stack periods, the QEMU suite green at each phase boundary.
|
||
|
||
**P1 — mechanics, no behavior change.** The `envelope` module with its comptime
|
||
`Define`, unit tests; `NodeKind.protocol` and the open-reply-capability
|
||
convention in `vfs-protocol`; existing protocols untouched.
|
||
|
||
**P2 — the registry.** Init serves `/protocol` (bind with manifest
|
||
authorization, collision refusal, unbind on provider death, provenance);
|
||
kernel reserves the `/protocol` prefix; every service converts from
|
||
`ipc_register` to `bind`, every client from `ipc_lookup` to resolve-and-open;
|
||
`ServiceId`, `ipc_register`, `ipc_lookup` deleted. Tests: unauthorized bind
|
||
refused, collision refused, provider restart re-binds and a client re-resolves.
|
||
|
||
**P3 — restriction, stage one.** The open-grant column in init's manifest;
|
||
registry refuses ungranted opens. Test: a fixture process denied a protocol its
|
||
neighbour is granted.
|
||
|
||
**P4 — protocol rebase.** Existing protocol modules re-expressed through
|
||
`Define`; providers move onto the generated dispatch; `describe`/`enumerate`
|
||
answered everywhere; the conformance test fixture exercises the reserved verbs
|
||
against every registered provider.
|
||
|
||
**P5 — restriction, stage two.** Spawn's initial capability; namespace views
|
||
built by the spawner; ambient resolution of `/protocol` retired. Includes the
|
||
two requirements the microphone example pins: **parked replies** (a
|
||
supervisor parks an open, keeps serving, replies later by badge) and
|
||
**dedicated, killable granted channels** (revocation = channel death →
|
||
`-EPEER`). Scoped separately — it touches `spawn`, the loader contract, and
|
||
every supervisor — and lands together with the file-path half of namespacing.
|
||
|
||
The unix-path migration ([file-system-hierarchy.md](../file-system-development/file-system-hierarchy.md#migration))
|
||
is independent of P1–P5 and can land before or after.
|