feat(supervisor): a supervisor is a component, and the tree has a fifth place - #47
Merged
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
E1-B05increment 5. One commit onmain.kernel/src/component.rsnames three things missing and calls them one thing — there is no supervisor component. This is it.What landed
user/supervisor/onuser/store's shape: a manifest declaring an account, a heap and its own endpoint, a five-node state schema,f_ring::heap::Heap::COMPONENTas its#[global_allocator], and an entry point the linker script places at the image's first byte. Wired intoCOMPONENTS,user/generation.tomland the workspace, withPLACES_MAXraised from four to five.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
[[device]]: a supervisor drives nothing, and RFC 0067 leaves an empty device list as the list rather than a default nobody chose.revokeon its own endpoint:revokeis the rightop::STOPis 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_MAXwas four and the root declaredplace0–place3, so the fifth spawn failed with a component's state tree could not be published or read back.state::node::mounthad carried this sentence:Exactly what happened. That paragraph is now a record instead of a forecast.
COMPONENT_TREE_4is 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_KINDat offset51 * WORD, reasoning that 51 was the next free word so nothing above 40 would move.f_abi::state::validaterefused it — the published schema does not describe a tree — and the two rules are why: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-manifestsonparentbeing 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 lintis 0 and the boot reachesM0 okwith 5 places, 6 spawns, six trees carrying 30 nodes.cargo xtask verifyis running as this opens; CI is the gate that covers it.heap … peak 0is unchanged and stays that way until an occupant is scheduled. That is increment 6, andHEAP_GAPis 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::spawnbuilds anInstancefrom an account — address space, text, stack, control ring, state tree, heap, all retyped from a suppliedUntyped, with admission and needs checked.process::prepare_runtimebuilds its own address space, text and stack directly from the frame allocator, from a flat image, with no account and no place.runtime::demonstratedrives the second and never touches the first. Making a place's occupant the thing that runs meansPreparedcomes from anInstancerather than from raw frames — which is a change toprocess.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