Booting
A runtime starts from its own boot section: a read-only image forged into the executable at link time. The OS maps it with the binary, C finds it at a fixed symbol pair, validates its small internal header, and enters pit. No path lookup or file I/O precedes the first actor.
Minimal C is the point
C is the trusted computing base and the surface that has to be audited. Pit is confined by construction: something dangerous can only happen because something holds a powerful endowment. So the rule for the boot path is not “make it fast” or “make it flexible” — it is make it small, because every line of C is a line someone has to read for holes.
Everything that can be pit, is pit. Reading a bundle’s directory, mounting it, resolving a name, choosing what to start: all of that is ordinary pit code doing ordinary work, running after the first actor exists.
What C does
take the boot-section bounds from the linker symbols
read the start declaration and take the role it names
read the fixed header
validate the ranges it names
load the engine pool
create actor 0 and enter it
That is the entire C boot path. The fixed header is the whole C↔pit boot ABI:
| Field | For |
|---|---|
| magic | this is the internal boot image |
| engine pool offset | the code that runs actors |
| boot entry offset | the first actor |
| start flags | what kind of program this is |
The cart declares what the program is
A binary’s start declaration is a flags word in that fixed header, and it is the only answer to “what kind of program am I”. The launcher reads it before anything else — before a shop is resolved, before a daemon is contacted, before there is a runtime at all — because the answer decides which of those steps happen.
| Declared | The program |
|---|---|
| resident | the client/daemon pair. Invoking the binary forwards argv to a daemon over a socket and relays the reply; the daemon outlives the command. This is what ./pit is |
| one-shot | the binary boots pit in its own process, runs its root actor to completion, and exits with that actor’s exit code. No daemon is spawned and no argv is forwarded |
| shop / no shop | whether the launcher resolves and creates a shop. A no-shop program creates nothing on disk it did not ask for: no directory tree, no crash log file, no boot.log. The boot trace still reaches stderr, so nothing is lost but the files |
| daemon / no daemon | whether the program may spawn a daemon or be one |
Every bit subtracts a default, so the zero word is the development program — resident, with a shop, with a daemon — and a cart pressed before the field existed says exactly that. There is no bridge and no compatibility read: the zero is the statement.
Exit is not one of these modes. Stopping a runtime is stopping its root actor;
everything is transitively $coupled to it, and the engine leaves when the last
actor is removed. A one-shot is a program whose root finishes and a resident is one
whose root does not — the same code path, with no flag in the scheduler behind it.
What the declaration changes is what the launcher does before that path is
entered.
The declaration is a fact about the artifact, never about the environment it was
launched in. There is no environment variable behind it, no path convention, and
no launcher heuristic. This matters most where a heuristic could not work at all: a
shipped Linux or Windows binary has no bundle layout to inspect, so under the old
model it had no way to say what it was. A binary that must behave differently
carries a different cart. The producer is pit cement (see the --one-shot /
--resident and --no-shop / --with-shop levers), and pit cart info prints the
declaration back as words.
pit product is the ordinary way to reach that producer: one command discovers an
entrypoint’s closure, presses it as a cart and forges a binary around the result,
declaring one-shot/no-shop/no-daemon unless told otherwise. See
Compilation and Shipping.
The header is an internal binary detail, not an artifact handshake. Forge produces the header, its pools, and the C reader in one candidate, and only promotes that candidate after a clean fixpoint press. A profile/ABI comparison between independently-produced binary and boot file is therefore gone: there is no independently-produced boot file to compare. External bundles still carry profile and ABI identity and are refused by name when mounted. See Target Profiles.
“Load the engine pool” means reading it in place from the already-mapped section. Forge presses those pools for the candidate runtime’s profile. Before the Pit lowering lands, an ABI move uses forge’s private exact-predecessor read bridge: the bridge-enabled candidate presses once, then a clean candidate boots and reproduces the bytes exactly. Bridge output never promotes. The portable snapshot cannot cross alone because the engine that opens it is itself a pool.
So C’s whole share of the artifact world is the interpreter, the pool validator and mapper, and the boot-section symbol pair plus its small internal header reader. C emits none of the boot contents. Directories, catalogs, start plans, and placement rows are Pit’s business.
The boot section is trusted by ruling. It has to be: code that has not run yet cannot authorize the bytes it is about to run, and a check performed by those same bytes proves nothing. Authorization is forge’s candidate gates and atomic promotion. External content does not receive this trust; the clerk admits it later through the ordinary mount decision. See Capabilities.
The boot actor
The first actor is called boot, and it is the same code in every build:
start the clerk, and give it the boot image
ask the clerk for the entry
start it
be the root
What differs between a development runtime and a shipped game is not boot — it is what
the boot section’s entry names, and what the clerk can reach.
- A development binary’s entry is the shop: it mounts the bundle beside it and finds a compiler, a fetcher, and a policy actor from there.
- A game binary’s entry is the game. There is no development shop in the boot section, which is what makes a sealed binary small enough for a console.
Boot residents prove that they are current
Code carried by the binary’s boot section is a resident, not an exemption from the
derivation rules. Every resident executable carries a pit.resident.derivation@1 record
whose source_closure is the hash of its pit.executable.manifest@1. That manifest names
the exact source closure and the mcode hashes selected for it; the mcode hashes in turn
fold the compiler identity, pipeline stages, and target-neutral flags. One hash comparison
therefore answers the boot question: is the resident the realization of the source tree
that is present now?
The comparison deliberately does not use the target-realization identity. A resident pool and a development realization from portable mcode are two payload forms of the same logical executable and have different target-realization keys. Treating that difference as staleness would rebuild an unchanged fleet on every boot.
On a development boot the seeded development coordinator is provisional. It realizes its own locator from the current source tree before starting any other service:
- equal source-closure hashes: keep the resident and continue;
- different source-closure hashes: start the current realization, hand the development role to it, and stop the provisional actor;
- current source exists but cannot be realized: refuse the boot by name. Running the stale copy is not a recovery lane.
The current coordinator applies the same comparison to every later resident start. An equal resident stays mapped; a stale one is replaced by the realization the ordinary shop cache produced. The cache’s dependency fingerprint makes that realization once per edit, not once per boot. Only the root/boot actor is irreducible, because it is the actor that must exist before there is a shop capable of making the comparison.
A sealed or source-less binary has no competing source derivation to compare. It runs the resident admitted when the binary was forged; the absence of a compiler fleet is what makes it sealed, not a switch that disables freshness.
Boot reaches the section through the same linker symbol pair on every target. The target’s loader already mapped those bytes as part of the executable, so reading a range does not copy it. File and ROM providers begin after the first actor and serve external bundles; they do not participate in cold boot.
Boot holds $image
Turning a pool’s bytes into code an actor can call is $image, and boot is the actor
that holds it. C pins the engine’s image itself at cold boot, because there is no pit yet to
ask; every image after that is brought in from pit, by boot and the clerk, following the rows
of the realization being started.
$image authorize with the realization you were handed
open a pool by content hash, validated
pin its placement
bind its declared cross-image imports
resolve an entry function
unpin, close
The division is the same one the rest of this page draws. C keeps the mechanics — mapping,
validation, the pin table, everything that touches address space — and pit holds every
question of which image and when. A pool’s bytes are ordinary store content that anything
holding the store may read; what $image gates is executable placement. See
The Executable as an Array.
The clerk mounts; there is no $mount
Boot is deliberately stupid. It has the boot section C exposed for it, starts the clerk, asks it
for the entry, starts that, and then does nothing that requires knowing what a name is.
Everything that requires understanding — what a package is, where bytes live, how to go and
get them — is the clerk’s. Boot’s own remaining job, placing images through $image, is
mechanical: it is handed a hash by a realization row and never chooses one.
Mounting is not an endowment. There is no $mount capability to hold or withhold. The
clerk simply mounts, and what it can mount is decided by which mount modules it carries:
clerk
├─ mount a directory claims the target's file endowment
├─ mount a .pit store the shop beside the binary
├─ mount a git repository uses the git actor, which claims a socket
└─ mount an archive uses fetch, likewise
So the clerk opens the image it was booted from, mounts its own .pit store beside the
binary, and mounts whatever else its modules know how to reach.
A separate $mount endowment would have been a second gate on top of gates that already
exist. The real gating is underneath, where it belongs: a directory-mount module claims a
file endowment, a git mount reaches the network through an actor that claims a socket. Deny
those and the clerk cannot mount those things, without any new mechanism.
So the containment argument is the one that already governs everything else: a clerk can do what its modules and actors let it do. A clerk with no git module cannot mount a repository; a sealed console binary carries none of them, so its clerk answers only from what it already has. Making a clerk weaker is not a setting — it is shipping a binary without those pieces. One rule, the same as Capabilities: what a build can do is what it carries.
Starting things afterward
$start requests go to the clerk, not to boot. Boot can only see its own image;
the clerk is what can actually find things, so routing starts through boot would make it a
hop rather than a decision.
The clerk holds the shop’s lists of package names — endowments, endowments_whole,
c_native, fallback, bin. A start begins on the same chain an import walks, and then
keeps going where an import stops. fallback and bin are what make those two different:
fallback answers a bare use, bin names the packages that supply top-level programs to
a start, and only a start ever reads it.
$start → clerk
├─ the resolution chain the caller's package, its aliases,
│ the shop's aliases, the fallback packages
│ — an import stops here
├─ the bin packages shop_tools first, as <bin package>::<path>
├─ the text as a locator fetching the package if it is not here yet
├─ the text as a script only if the text is not locator-shaped
└─ nothing worked the whole chain that was walked
The last two steps are decided by the text itself: locator-shaped text never falls through
to the compiler. An argument of identifiers, /, ::, dots and dashes is resolution-only,
so a miss reports the chain rather than a parse error, and only text that could not be a
locator runs as a script. There is no flag either way.
The command line enters at the same door, because it is the same thing: the CLI resolves
nothing on its own — it forwards what you typed to a $start in an actor. pit test is a
start whose calling package is the shell’s cwd, so the shell gets the chain, the bin
packages, the fetch, and the script fallback in that order and for the same reasons. There
is no separate CLI resolver and no command table. See
Shop.
Along the way the clerk starts ordinary actors: compile where there is source but no
mcode, git or fetch where the bytes are not here yet. Those are actors it starts, not
parts of it. So a sealed console binary does not get a different start path — it gets the
same clerk, carrying none of those actors, which can therefore only answer from what it
already has. One protocol everywhere, degrading honestly.
That is the general rule again: what a build can do is which actors it carries. Making a clerk weaker is not a setting; it is shipping a binary without the fetchers.
Logging is a token, handed at start
A system has a logger actor, well known to the actor that starts actors. When a new
actor’s program mentions log, its start carries the logger actor’s token along with the
other supplied facts — $self, $overling — and the log function it binds forwards to
that logger. An actor whose program never mentions log receives neither the token nor the
function: nothing is instantiated on its heap at all, which is the general rule that unused
means absent. There is no per-actor gate, because the claim decision is the gate;
per-channel filtering is the logger’s own business.
log is a claimed global: a bare name, not deniable, present exactly when referenced. A
build that strips log statements also strips the claim, so a stripped actor carries no
logging machinery whatsoever. C code logs through the boot sink — the same fallback that
serves the brief window before the logger actor exists, writing to stderr until there is
somewhere better to send it.
pit log channel <name> [--off] configures the logger, because that is where every
claiming actor’s lines pass. It gates fan-out — subscribers, and so terminal output — and
not the store, so a channel you silenced for the live stream is still in the observe log for
pit log and pit why to find. An actor’s floor keeps a per-channel selector of its own
and keeps it for one reason: the message that sets it is a system letter, so it reaches an
actor listening for no user mail at all, and the floor’s own error and disrupt lines consult
it whether that actor claimed logging or not. That selector only ever governed those floor
lines. Everything a claiming actor says goes to the logger, and the logger decides.
Nothing addresses boot
Boot is a thing that exists, not a thing you talk to. Once you follow where each of its plausible jobs actually went, none of them are left:
| starting things | the clerk |
| stopping an actor, stopping the runtime, setting runtime facts | $runtime |
| listing actors | $inspect |
| being your parent | $overling — you already have it |
| dying when your parent dies | $couple — automatic |
So there is no $boot endowment. Boot is actor 0 and the root overling, which is what
makes stopping it stop everything, and it is what starts the clerk. Afterwards it takes a
turn only for a genuine decision — mount this, fetch that, evict an image — each arriving as
an ordinary message, and each one a use of $image or of what boot mapped. The only fact it
uniquely knows besides those, which binary image this runtime booted from, is a fact and belongs with
the other runtime facts rather than justifying a token and a protocol.
That split is deliberate. Everything about naming belongs to the clerk, so a
different shop supplies a different one and the idea of a name changes with it. The core
knows only that use() takes a text literal and $start takes a literal or a variable.
See Shop.
Engine ownership
The engine — the code that walks a realization and starts an actor — belongs to the runtime, not to any realization. It is loaded from the boot section at cold boot and handed to every actor thereafter, so no realization names it and it can never skew against the VM it runs on. It is found through the header’s offset, which is the only reason it needs no name at all.
engine is its own package, and it is on none of the shop’s lists. It imports nothing,
compiles to one unit, and is found through the boot section’s fixed header — so no chain resolves
it, no alias can repoint it, and no fallback can shadow it. Being unnameable is what makes
it unswappable.
Because every target pays for it, including the smallest, the engine holds only the
realization walk. The per-actor standard library — logging, message send and reply,
requestors, the implementations behind $clock and $start — is ordinary modules an actor
receives only if something claimed them. See
Endowments.
The engine is one pool, pressed ahead of time
The engine ships as one pool, found by the header’s offset. Because every pool is linked when forge produces the boot section, the engine’s pool may fuse more than one source unit, with the import edges between them already turned into direct indices. What the boot path does with it is what it does with any pool: map and enter. No linking, and no resolution, happens at boot.
What keeps the engine small is a different force: every target pays for it, including the smallest. So when the engine shrinks, the shape that works is moving a piece out — so that actors receive it only by claiming it — which is the shape the standard-library split above takes. A claim is what brings the code back, for the actor that made it and no other.
The requestor library is the first piece to go this way, and it shows the shape exactly.
fallback, parallel, race, sequence and time_limit are five modules in a provider
package over one ordinary helper module; the engine does not import that helper, so what
it lost is simply gone from it. A claim is what brings the code back, for the actor that made
it and no other. Nothing in the engine mediates: the name is published by the unit that
answers it, as a row on that unit’s own step in the start walk, exactly as an imported name is.