Skip to content

feat(supervisor): a supervisor is a component, and the tree has a fifth place - #47

Merged
dimmus merged 5 commits into
mainfrom
iritur/a-supervisor-is-a-component-file
Sep 13, 2026
Merged

dimmus merged 5 commits into
mainfrom
iritur/a-supervisor-is-a-component-file

Conversation

@dimmus

@dimmus dimmus commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

E1-B05 increment 5. One commit on main.

kernel/src/component.rs names three things missing and calls them one thing — there is no supervisor component. This is it.

What landed

user/supervisor/ on user/store's shape: a manifest declaring an account, a heap and its own endpoint, a five-node state schema, f_ring::heap::Heap::COMPONENT as its #[global_allocator], and an entry point the linker script places at the image's first byte. Wired into COMPONENTS, user/generation.toml and the workspace, with PLACES_MAX raised from four to five.

supervisor  5 place(s) from 5 component file(s), one per file …
spawn       place supervisor epoch 0 — manifest 0x59e3b52e2eeee313, 3 need(s) supplied … 25 frame(s)
state mount place supervisor -> frame root slot 1: root supervisor, 5 node(s) …
state tree  52 nodes, snapshot 0x2b0ecee627e69785, stable across a re-read

What it does today is announce itself and end

That is not an unfinished edge. A component spawned into a place is never handed a core (kernel/src/runtime.rs), so nothing in this image has executed or can. What the crate buys before it runs a line is that the declaration exists and is checked on every boot: a manifest the lint refuses when it stops fitting, a schema the frame writes out before the first instruction, and a place the assembler checks routes against.

A submission loop written against a machine state no boot reaches would be a promised layer with no owner — this tree's own phrase for it — so it is not written. It lands in the increment that first makes it run.

Two omissions that are the argument, not gaps

  • No [[device]]: a supervisor drives nothing, and RFC 0067 leaves an empty device list as the list rather than a default nobody chose.
  • No revoke on its own endpoint: revoke is the right op::STOP is checked against, so a supervisor holding it on itself could stop itself — and a place whose occupant ended it holds nobody who can refill it.

The bootstrap circle is placed, not removed

A supervisor is spawned into a place like anything else, and the thing that spawns it is the frame. That cannot be designed away; somebody is first. What this chooses is that the frame keeps exactly one spawn and every other one moves above it — a smaller privileged surface than a frame that spawns all of them.

The tree predicted its own failure

PLACES_MAX was four and the root declared place0place3, so the fifth spawn failed with a component's state tree could not be published or read back. state::node::mount had carried this sentence:

A build that grew a fifth place would mint a fifth id here and find the None arm at the call site rather than silently publishing into the fourth.

Exactly what happened. That paragraph is now a record instead of a forecast.

COMPONENT_TREE_4 is id 51, not 41: ids here are permanent and never reused, so a slot added after 41–50 were spent takes the next free one. Renumbering to keep the mount run contiguous would have changed what every node from 41 up meant, to make one node look tidy.

A correction to my own first attempt

I placed the entry after RESERVED_KIND at offset 51 * WORD, reasoning that 51 was the next free word so nothing above 40 would move. f_abi::state::validate refused it — the published schema does not describe a tree — and the two rules are why:

  • ids must strictly ascend in the array's own order, and
  • every offset must equal its index times WORD, because the words tile the block with no gap and no overlap.

So an offset is a position, not an address a node may pick, and a node's id and its place in the array are one decision rather than two. Appending with the next free id is the only move satisfying both. The commit keeps that reasoning because it was confidently wrong in a way the next person could repeat.

Three refusals found all of this, each from a check doing its job and none needing a debugger: lint-manifests on parent being a name rather than an id and two units outside the closed table; the boot on the missing mount slot; the frame's own schema self-test on the ordering.

Evidence

cargo xtask lint is 0 and the boot reaches M0 ok with 5 places, 6 spawns, six trees carrying 30 nodes. cargo xtask verify is running as this opens; CI is the gate that covers it.

heap … peak 0 is unchanged and stays that way until an occupant is scheduled. That is increment 6, and HEAP_GAP is waiting for it.

What increment 6 actually requires — larger than the plan assumed

Worth stating for whoever picks it up, because I located the seam and it is not where the plan guessed. There are two independent construction paths and the join means one of them stops building and starts adopting:

  • component::spawn builds an Instance from an account — address space, text, stack, control ring, state tree, heap, all retyped from a supplied Untyped, with admission and needs checked.
  • process::prepare_runtime builds its own address space, text and stack directly from the frame allocator, from a flat image, with no account and no place.

runtime::demonstrate drives the second and never touches the first. Making a place's occupant the thing that runs means Prepared comes from an Instance rather than from raw frames — which is a change to process.rs's prepare/execute/reap shape, not an addition beside it. That is the decision increment 6 has to make and record, and it deserves its own RFC section rather than being folded in silently.

🤖 Generated with Claude Code

dimmus and others added 5 commits September 12, 2026 14:21
…th place

E1-B05 increment 5. `kernel/src/component.rs` names three things missing and
calls them one thing — *there is no supervisor component*. This is it.

`user/supervisor/` is a component crate on `user/store`'s shape: a manifest
declaring an account, a heap and its own endpoint, a state schema of five nodes,
`f_ring::heap::Heap::COMPONENT` as its `#[global_allocator]`, and an entry point
the linker script places at the image's first byte. It is in `COMPONENTS`, in
`user/generation.toml`, and in the workspace.

**What it does today is announce itself and end, and that is not an unfinished
edge.** A component spawned into a place is never handed a core
(`kernel/src/runtime.rs`), so nothing in this image has executed or can. What the
crate buys before it runs a line is that the *declaration* exists and is checked
on every boot: a manifest the lint refuses when it stops fitting, a schema the
frame writes out before the first instruction, and a place the assembler checks
its routes against. A submission loop written against a machine state no boot
reaches would be a promised layer with no owner, so it is not written; it lands
in the increment that first makes it run.

**Two omissions in the manifest are the safety argument rather than gaps.** No
`[[device]]`: a supervisor drives nothing, and RFC 0067 leaves an empty device
list as the list. No `revoke` on its own endpoint: `revoke` is the right
`op::STOP` is checked against, so a supervisor holding it on itself could stop
itself — and a place whose occupant ended it holds nobody who can refill it.

**The bootstrap circle is placed rather than removed.** A supervisor is spawned
into a place like anything else and the thing that spawns it is the frame. That
cannot be designed away; somebody is first. What this arrangement chooses is that
the frame keeps exactly one spawn and every other one moves above it, which is a
smaller privileged surface than a frame that spawns all of them.

**The fifth place needed a fifth mount, and the tree predicted its own failure.**
`PLACES_MAX` was four and the frame's root declared `place0` through `place3`, so
the fifth spawn failed with *a component's state tree could not be published or
read back*. `state::node::mount` carried this sentence: *a build that grew a
fifth place would mint a fifth id here and find the `None` arm at the call site
rather than silently publishing into the fourth*. That is exactly what happened.
The paragraph is now a record instead of a forecast.

`COMPONENT_TREE_4` is id **51** and not 41. Ids here are permanent and never
reused, so a slot added after 41 through 50 were spent takes the next free one;
renumbering to keep the mount run contiguous would have changed what every node
from 41 up meant in order to make one node look tidy.

**A correction to my own first attempt, kept because the reasoning was
confidently wrong.** I placed the entry after `RESERVED_KIND` at offset
`51 * WORD`, reasoning that 51 was the next free word and nothing above 40 would
move. `f_abi::state::validate` refused it — *the published schema does not
describe a tree* — and the two rules are why. Ids must strictly ascend **in the
array's own order**, and every offset must equal its index times `WORD`, because
the words tile the block with no gap and no overlap. So an offset is a position
and not an address a node may pick, and a node's id and its place in the array
are one decision rather than two. Appending with the next free id is the only
move that satisfies both.

Three refusals found this, each from a check doing its job and none needing a
debugger: `lint-manifests` on `parent` being a name rather than an id and on two
units outside the closed table, the boot on the missing mount slot, and the
frame's own schema self-test on the ordering.

`cargo xtask lint` is 0. The boot reaches `M0 ok` with 5 place(s), 6 spawn(s),
six trees mounted carrying 30 nodes, and `state tree 52 nodes`. `heap … peak 0`
is unchanged and stays that way until an occupant is scheduled, which is the next
increment and is what `HEAP_GAP` is waiting for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ith no row is refused

`f-supervisor` was added to the workspace and not to `PORTABILITY`, so every job
that runs an xtask verb refused with *the workspace and the portability table are
not about the same set of crates*. Four CI checks, one cause.

That is RFC 0045 working exactly as written: the architecture list is derived
from the workspace, a crate added to it is checked on AArch64 **by default**, and
leaving one out costs a sentence saying why. A member with no row is refused
rather than skipped.

Both answers are `None`, matching `f-store` one row up: this crate genuinely does
build for AArch64. The only architecture-specific half is the door and it is
gated, so what is left is a manifest's worth of declaration and an entry point.

The row carries a reason rather than being bare, because the AArch64 compile is
what keeps that true — a supervisor that acquired something x86-64 in it would be
a supervisor that had stopped being a component and started being part of the
frame, which is the whole thing E1-B05 exists to prevent.

Caught locally by `cargo xtask verify` after `cargo xtask lint` and the boot were
both green, which is the distinction worth keeping: lint and a boot are not
verify, and increment 5 was reported finished on the strength of the two that had
run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nt-file' into iritur/a-supervisor-is-a-component-file
… and the simulator has a peer for it

Two refusals from the deployment scenario, in order, each one a check that had
already written down what it would do when somebody did exactly this.

**`supervisor` declares no data ring, so there is nothing for a client to submit
to.** The manifest declared capabilities, a restart policy, a reservation, a
transfer mode and a state schema, and no `[[ring]]`. The refusal is right and the
answer is the ring rather than an exemption: a supervisor is a thing you *ask*.
`op::SPAWN` and `op::STOP` are what it submits to the frame on its control ring;
this is the other direction, the ring a client submits a request on, so that
"please start me one of those" is an entry rather than a privilege. A supervisor
with no ring is not one with nothing to say — it is one nobody can reach.

`inline` and not `registered`, for `store`'s reason: a payload path would be a
claim about a datapath that does not exist here, and what crosses this ring is a
request naming a manifest and an account, which is words rather than bytes. Four
clients, and the number is deliberately not larger — the set of things entitled
to ask a supervisor to make a component is small by design.

**`supervisor` serves the `supervisor` protocol and this simulator has no model
for it.** `MODELS` in `sim/src/deploy.rs` fails closed and asks the diff to say
which of two things it is. It is a **mapping**, not a model. A supervisor has no
device under it for the same reason `store` has none — it answers requests rather
than driving hardware — so `Peer::Native`, which models a registration table and
a service time and nothing below, is the accurate peer rather than an
approximation. What a supervisor does that `store` does not is submit upward on
its control ring, and that direction is not what a deployment scenario drives.

That table's own comment predicted this: *adding a component to `user/` puts a
red scenario in front of whoever adds it until they say what it is. That is the
seam being load-bearing rather than decorative.* It was.

`cargo xtask verify` is 0 — `verify: all green`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dimmus
dimmus merged commit 9d21e04 into main Sep 13, 2026
26 checks passed
@dimmus
dimmus deleted the iritur/a-supervisor-is-a-component-file branch September 13, 2026 11:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant