Build order step 3, first half: `/system/security/users.cfg` exists, `os3dos` reads it, and each session says whose it is. Terminal 1 comes up as `admin` and terminals 2-4 as `guest` — the first thing on this machine that differs between two sessions. **`scope::roles` is the reader**, beside the scope arithmetic rather than in a crate of its own: a role IS a set of scopes, and `parse_list` is what turns its `files` line into them. Three lookups over `key = value` text and no table — a parsed structure would need an allocator or a ceiling on users, for a file read once per session rather than per request. **The file is NOT under `configs/`**, and this page's own §9.3 is why: an administrator who may write every config could otherwise make a guest an admin, and the boundary would be decorative. A prefix grant has no EXCEPT, so what not everyone-with-the-prefix may touch lives in its own subtree. **And the VT-to-user assignment lives in it too — the design said `os3dos.cfg` and the design was wrong.** That file is in `configs/`, so whoever may edit it decides who sits at terminal 1, which is the admin role one indirection later. The same hole §9.3 found, one file over. §5 records the correction. **`os3dos` writes `session::KEY_USER`; `console.sys` stopped.** The console owns that page and every other key in it, but the user is not its fact to know — this root is the only component that may read `users.cfg`. One key, one writer, which is the rule the context has always followed. A terminal nobody is assigned to gets NO key rather than a default: inventing an administrator for an unassigned terminal is the one answer that could be dangerous. **A layout test pins the shipped file to the shipped volume.** `users.cfg` is data on the disk and `HOME`/`DEFAULT_USER` are constants, with no compiler between them; a `vt.1` naming somebody else hands terminal 1 a home directory that is not there, and the symptom is a first write answering "no such file" about a path nobody typed. Proven on a running machine, not inferred: the boot log says `terminal 1: admin` and `terminal 2: guest`, and `set` on terminal 2 draws `user=guest` while terminal 1 draws `admin`. The `disk` lane asserts BOTH lines — one alone would also pass on a machine that gave every terminal the same user. Also written down (docs/namespace-and-authority.md): §6.1 decides that `exec.sys` tells `fs.sys` a pooled channel's scope, since it is the component that hands the channel out and reclaims it; §5.3 states what add, change and remove do, including the two gaps — nothing creates a new user's home directory, and editing `users.cfg` needs a scope no session holds. Host tests 2232/0; `disk`, `screen`, `parallel` and `complete` pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FqjsJEjCB3rSc5RT9u9X7e |
||
|---|---|---|
| abi | ||
| apps | ||
| backlog | ||
| dist | ||
| docs | ||
| issues | ||
| lib | ||
| review | ||
| stage0 | ||
| stage1 | ||
| test | ||
| .gitignore | ||
| BACKLOG.md | ||
| build.sh | ||
| Cargo.lock | ||
| Cargo.toml | ||
| ci.sh | ||
| CLAUDE.md | ||
| deploy.sh | ||
| iss | ||
| ISSUES.md | ||
| README.md | ||
| rust-toolchain.toml | ||
| STAGE0.md | ||
| STAGE1.md | ||
| VISION.md | ||
OS/3
A capability-based microkernel operating system for x86-64, booted from UEFI.
This project is being redesigned greenfield. The current focus is agreeing the architecture from first principles, one stage at a time, on paper before code. Stage 0 is settled — see STAGE0.md.
The idea
Most operating systems fuse two things that don't have to be fused: the low-level machinery that talks to the hardware (privilege, memory, interrupts, IPC) and the personality of the system (what a "process" is, how things are scheduled, what the filesystem looks like, which programs exist). OS/3 splits them cleanly into two layers.
Stage 0 — the microkernel
A tiny, permanent, privileged substrate. It boots from UEFI, holds all authority in the machine, enforces capabilities, and exposes a small set of primitives: address spaces, execution contexts, IPC, and interrupt delivery.
It is deliberately opinionless. It has no scheduler policy, no notion of a "process", no filesystem, and no device drivers. It is mechanism, never policy. It does not run the system — it is the thing the system runs on. It executes only when the machine traps into it, and it stays resident for the entire life of the machine.
Stage 1 — the personality
The actual operating system, built entirely on the microkernel's primitives, in ring 3. This is where a "process", a scheduler, a filesystem, drivers, and programs live. Because the microkernel is opinionless, a personality is free to be anything — a completely different model of tasks and scheduling than any other personality — and still run unchanged on the same stage 0.
The personality lives on its own disk, in its own filesystem. The microkernel never touches it.
The vision
- A microkernel that is a substrate, not a kernel. All authority, minimal mechanism, zero policy. Small enough to reason about; permanent enough to trust.
- Personalities as first-class, swappable OSes. The microkernel boots whichever personality its config points at. Change the config, boot a different world — the substrate does not care and does not change.
- Everything above ring 0 is userland. Drivers, filesystems, schedulers, shells — all ring-3 programs talking to the substrate through capabilities. Nothing privileged that doesn't have to be.
- Clean seams with single owners. Every boundary in the system has exactly one owner. The design is agreed on paper before it is built, so the seams stay clean instead of accreting.
How it boots (the short version)
- UEFI firmware loads
kernel.efioff the FAT EFI System Partition. - The microkernel reads one more file off the ESP through firmware —
bootstrap, a small RAM blob whose only job is to reach the real disk. - The microkernel mints the root authority, starts
bootstrap'sinitin ring 3, and drops firmware — then stays as the permanent substrate. bootstrap'sinitbrings up a disk driver, reads the personality off its own disk, and hands over.bootstrapis spent.- The personality runs the machine, on the microkernel's primitives.
Full detail, invariants, and the boundary rules: STAGE0.md.
Project layout
The tree mirrors the conceptual model, not the build history. Five buckets, each with one owner:
stage0/— the microkernel (kernel/) and its transient reach-the-disk helper (bootstrap/:init+block+fs+archive).stage1/— the personality layer (the userland OS:exec,shell, drivers, programs). It is the layer; today's single DOS-style personality sits flat here. A per-personality subdir arrives only when a second personality does.lib/— pure, host-tested mechanism shared across both stages (FAT, ELF, font, bits, log). The server that wraps a library is policy and lives in its stage.abi/— the syscall-surface contract, kernel-owned, at the top of the tree so nothing depends upward.apps/— applications that run on the personality but are not part of it (a BASIC interpreter, say). An app depends on the OS's public surface (abi,lib/prog); nothing instage0/ stage1/ lib/ abi/may depend onapps/. Each app is a self-contained bundle:/apps/<name>/<name>.exe+ its data.
One Cargo workspace; directories are for humans. Full rules and the "which bucket does this crate go in?" procedure: docs/organization.md.
Building
Two stages, composed by a dist — a named recipe (a personality + a chosen app set + a boot mode), the way a Linux distribution selects packages over a base:
./build.sh dist dev # OS + the BASIC app -> out/dev/{esp,os3.img}
./build.sh dist minimal # the OS alone
./build.sh dist # every recipe in dist/
./build.sh os # just the OS image (out/os3.img)
Recipes are tracked in dist/*.dist; output goes to out/ (gitignored). Recipe
format, boot modes, how an app declares capabilities: docs/dist.md.
To hand a machine to somebody, pack a dist into a bundle that needs only qemu —
the two disks, the UEFI firmware and a run.sh, with no host or path baked in:
./deploy.sh # the `full` dist -> out/bundle + os3-bundle.tar.gz
./deploy.sh --dist minimal # the OS alone
./deploy.sh user@host:dir # ...and ship it there, verified by the image's sha256
Making a new program LAND IN THE IMAGE
A crate that compiles is not automatically on the disk. A program is either a system program (part of the OS) or an app (runs on it), and the two land different ways — this is the step most often missed:
-
A system program (
dir,showfile, … — ships with the OS):- a crate under
stage1/bin/<name>/([[bin]] name = "<name>"); - name its path in
lib/layout/src/paths.rs— aPROGRAMSentry, and bothSHIPPEDandPLACEABLE.mkdiskREFUSES a path the layout does not name, which is exactly why an unwired.exesilently never appears; - a
VOL+=("/system/bin/<name>.exe=stage1/bin/<name>/target/…/<name>")line intest/boot.sh— the boot lanes andbuild.shread the shipped set from there, anddeploy.shships whatbuild.shproduced. The host testsplaceable_covers_every_named_pathanda_program_is_an_exe_under_binfail the build if the wiring is incomplete.
- a crate under
-
An app (
basic— not part of the OS):- a crate under
apps/<name>/prog/([[bin]] name = "<name>", its own[workspace]); - it lands via a dist, not the plain OS image:
./build.sh dist <name>(or a recipe withapps = <name>orapps = *) bundles it at/apps/<name>/<name>.exe. An optionalapps/<name>/prog/capsfile declares its capabilities (e.g.caps.want = fs).
- a crate under
Testing
./ci.sh fast # everything a compiler can check — ~4 min
./ci.sh net # ...plus the eight lanes a network change can break — ~7 min
./ci.sh # the full gate, 33 QEMU boot lanes and all — ~16 min
fast runs the host tests, clippy (the workspace, the kernel across its seven
feature sets, and all 35 ring-3 crates), both compile matrices, every audit and
layering rule, and the image-size budget. It skips the three blocks that own the
wall clock — the boot matrix, the mutation gate and the loom models — and names
them in its last line.
fast boots nothing, which is why the middle tier exists. A change below the
seam is judged by a lane or by nothing: the TLS arc's four defects — a lost TCP
event, a socket held past its use, an ARP-dropped SYN, a session key from a
guessable generator — were every one of them invisible to fast. ./ci.sh net
is fast plus the lanes that cover the network, named in one table in ci.sh so
"which lanes cover this?" has an answer that is not somebody's shell history.
A fast run prints CI FAST PASS, never CI PASS — and a group run prints
CI NET PASS. A green that booted nothing, or only part of the machine, must not
read like one that booted all of it; this tree has already shipped a lane that
reported PASS twice with the feature under test switched off.
Run the full gate before landing. Every phase prints its own elapsed time, so what a run costs is measured rather than folklore — the boot matrix is ~450 s of it, mutation ~105 s, loom ~60 s, and everything else is seconds.
One lane on its own: test/boot.sh <lane> — disk, screen, edit, net, …
(the usage line at the top of test/boot.sh lists them all).
Unchanged env knobs: LOOM=0 drops the models from a full run, MIRI=1 adds the
undefined-behaviour check, REVIEW=1 adds the LLM spec review.
Status
- Stage 0 — designed and agreed (STAGE0.md), refinement of details in progress (BootInfo shape, entry-point config, the primitive syscall surface).
- Stage 1 — not yet designed. Next once stage 0's frame is fully settled.
For contributors (and agents)
Read CLAUDE.md first. It enforces the stage-0 invariants and the design-before-code discipline. The point of the reset is first-principles reasoning — do not reconstruct the previous implementation to answer a design question.
Tracking work
Two lists, one file per item, managed by ./iss — see
issues/_README.md for the full workflow:
issues/→ what is wrong right now (defects + decisions). Read ISSUES.md (generated — never hand-edit it).backlog/→ what to build once the issue list shrinks. Read BACKLOG.md (also generated).
./iss ls [--backlog|--all] ./iss add "title" [--backlog] [--section A2]
./iss show <id> ./iss close <id> # fixed: deletes + retires the id
./iss mv <id> --to issues ./iss gen # regenerate the two views
Ids are random (#k7f) so two sessions adding never clash; ./iss lint (a
CI gate) holds the ids unique and the generated views current.