diff options
Diffstat (limited to 'svc/doc')
| -rw-r--r-- | svc/doc/design.md | 437 | ||||
| -rw-r--r-- | svc/doc/inventory.md | 113 | ||||
| -rw-r--r-- | svc/doc/todo.md | 101 |
3 files changed, 651 insertions, 0 deletions
diff --git a/svc/doc/design.md b/svc/doc/design.md new file mode 100644 index 0000000..343d23a --- /dev/null +++ b/svc/doc/design.md @@ -0,0 +1,437 @@ +# init + +**init is the service manager. It provides svcfs if you want to interact with services.** + +Status: design. Nothing implemented yet. + +## Why + +A machine is a set of running services. That's all it is. 9front instead has +three half-mechanisms that each know a piece of that and never talk to each +other. + +`service=` in `plan9.ini` picks between `/rc/bin/termrc` and `/rc/bin/cpurc`. +One word decides "what kind of machine is this." + +`/rc/bin/service/` is a directory of scripts named by port, where the enable bit +is a `!` prefix on the filename: + + /rc/bin/service/!tcp23 disabled + /rc/bin/service/tcp17019 enabled + +Everything else is a bare line in a shell script — `plumber`, `webfs`, `ndb/cs`, +`ndb/dns`, `aux/timesync` — started once, owned by nobody, restarted never. + +So: nothing is supervised, nothing has state, nothing has logs, nothing has +dependencies, and nothing can be managed remotely without a shell and an editor. + +A real example from this project. The serial control shell was one line in +`termrc`, non-interactive: + + rc </dev/eia0 >/dev/eia0 >[2=1] & + +One malformed command produced a syntax error, rc exited, and the machine was +unreachable over serial for the rest of the session. The fix was to make it +interactive and wrap it in a restart loop: + + while(){ rc -i </dev/eia0 >/dev/eia0 >[2=1]; sleep 1 }& + +That line is a service manager for exactly one service. Every daemon on the box +wants the same thing and none of them get it. + +## Roles are vocabulary + +"Terminal", "cpu server", "auth server", "file server" are not kernel concepts. +The kernel does not know which one it is running. Each is a bundle of userspace +programs: + +| Role | What it actually is | +|---|---| +| auth server | `auth/keyfs` + `auth/authsrv` on 567 | +| cpu server | a listener on 17010, plus `exportfs` | +| file server | `hjfs`/`cwfs`/`gefs` + a 9P listener on 564 | +| terminal | a window system, if you want one | + +Nothing ever stopped one machine being all four. The words are the obstacle: +they imply a machine *is* one thing, and that exactly one machine holds each +role in a single administrative domain. + +We delete the words. Role is emergent and plural. + +## How it works + +`init` runs services. It: + +1. reads service definitions from `/lib/svc` +2. starts the enabled ones in dependency order +3. supervises them, restarting per policy +4. serves `svcfs` — posted at `/srv/svc`, mounted at `/mnt/svcs` +5. handles `reset` and `reexec` + +None of that depends on how it was started, and an instance can be run by +anyone over any directory of services in its own namespace. Run as the program +that brings a machine up, it does four more things: builds the base namespace, +offers a rescue console, halts the root file server last, and carries out +`halt` and `reboot`. + +**init is not pid 1 on 9front today.** Pid 1 is `rc` running `/bin/bootrc`, +sitting in `Await`; init is an ordinary child — pid 143 on the machine this was +checked against. Since the boot chain is being replaced anyway, whether init +runs as pid 1 is a decision rather than an inherited fact, and nothing in this +design requires it. + +`svcfs` is a namespace, not a program. There is no second daemon. It's what init +looks like from outside, the same way `/proc` is what the kernel's process table +looks like from outside. + +**The control plane is optional.** Services start at boot whether or not anything +ever mounts `/mnt/svcs`. If posting the filesystem fails, init logs it and keeps +supervising. A bug in the 9P layer must never be a boot failure. + +## A service + +`/lib/svc/dns`, in ndb format — the parser already exists and it's the same +shape as factotum keys and `/lib/ndb/local`: + + svc=dns + exec=/bin/ndb/dns + args=-r + needs=cs + ready=srv:dns + restart=always + enable=yes + +Ten attributes exist in total: `svc exec args needs ready restart ns user +enable stop`. There are deliberately no tuning knobs — restart delays and +readiness timeouts are constants in the code until something proves it needs to +be configurable. + +An unknown attribute is an **error**, not a warning. ndb has no schema, so +`exce=/bin/ndb/dns` parses fine as ndb and means nothing. Ignoring unknowns is +how a typo silently does nothing forever. + +## Three shapes of service + +Not everything is a daemon that stays in the foreground, and the difference is +declared rather than guessed: + +| Shape | Liveness is | On exit | +|---|---|---| +| foreground | the pid | restart per policy | +| detaching | the `/srv` file | not a failure — don't restart | +| oneshot | nothing, it's done | never restart | + +Detaching matters because the `postmountsrv` idiom forks a server proc and lets +the original exit *on purpose*. A supervisor that assumes exit means death will +restart-loop something that is running fine. + +Which is why **readiness and liveness are separate questions**. `ready=srv:dns` +answers "is it up yet"; the same file answers "is it still up" for a service +whose pid is already gone. + +## Namespaces and identity + +Each service gets `rfork(RFNAMEG)` and a namespace built from its `ns=` file via +`newns` — the same machinery `cpu` already uses. + +This is the part that isn't a systemd transliteration. Two services on one +machine can hold completely different views of the filesystem: different `/net`, +different `/srv`, different roots. That is what would let one box participate in +two grids at once, each with its own `/mnt/factotum` and its own identity — +impossible today, because a machine has one hostowner, one `authdom` and one +namespace. + +Identity comes from the capability device (`/dev/caphash`, `/dev/capuse`), the +path `cpu` and `rx` already use, since Plan 9 has no setuid. That means init runs +as hostowner and is security-critical. Not a detail to discover later. + +**Services consume namespaces; they never construct the shared one.** A service +that runs `bind` inside its own namespace group affects nobody, and would appear +to work while doing nothing. Base namespace construction belongs to init, before +any service starts. + +## Talking to it + +The filesystem is the mechanism, commands are what people type. Both. + + svc list services, their state, and why + svc start dns + svc restart dns + svc log dns + halt stop services, halt the fs, power off + shutdown alias for halt + reboot ... and restart the kernel + reset stop services, then start them again + +Commands are pure translation — a few lines that open a file and write a word. +If `svc restart dns` can ever do something a write to `/mnt/svcs/dns/ctl` +cannot, we've built two interfaces and they will drift. + +`reset` is worth naming because it's the cheap one: it's the stopping half +followed by the starting half, both of which already exist. No new machinery. + +## Ctl files say what they accept + +The worst thing about Plan 9 ctl files is that they're write-only channels for +magic strings. `/net/tcp/0/ctl`, `#S/sdctl`, `/dev/mousectl` — the only way to +learn the verbs is to read source. + +So every ctl file here reads back its own command list: + + % cat /mnt/svcs/dns/ctl + start + stop + restart + note <string> post a note to the process + +and every rejected write names the alternatives: + + % echo frobnicate >/mnt/svcs/ctl + echo: write error: unknown command "frobnicate"; try + halt reboot reset start stop restart enable disable reexec + +This deviates from convention — most Plan 9 ctl files return nothing or return +state on read. We can afford to spend the read on documentation because `status` +carries the state. Given that the convention is the complaint, it's the right +one to break. + +## State is ndb too + +`/proc/mdstat` on Linux is the canonical disease: positional, bracket-encoded, +with an ASCII progress bar inside the data. Plan 9 does it too — +`/net/ipifc/0/status` opens with genuine attribute/value pairs (`maxtu 1514 +sendra 0`) and then collapses into unlabelled positional columns the moment +there's a list. + +So status output is ndb, not columns: + + svc=dns state=running pid=231 + svc=authsrv state=failed restarts=3 exit='cannot open /adm/keys' + svc=listen state=waiting needs=cs + +Self-describing, quoting handles the exit string, and fields can be added +without breaking readers. `svc` with no arguments prints aligned columns for +humans; the file stays ndb for programs. + +Nothing in the tree currently serves ndb — all 15 libndb consumers are readers +of static config. This would be the first, which also means it shouldn't be +called a convention until a second, genuinely different server carries it. + +## Configuration goes one way + +`/lib/svc` is the only source of truth. The filesystem accepts *verbs*, not +configuration. No writing `exec=` into `/mnt/svcs/dns/args` and having init +persist it back — that's two paths to one state, and they will disagree. It's +also how the system already behaves: you configure an interface by writing +`/net/ipifc/0/ctl`, not by editing `status`. + +`reload` re-reads `/lib/svc`, applies what parses, and reports what doesn't: + + % echo reload >/mnt/svcs/ctl + echo: write error: /lib/svc/dns:4: unknown attribute "exce" + +One bad file never costs you the whole machine — same behaviour at boot. + +## Permissions + +init is the file server, so it checks the 9P attach identity itself. One +mechanism covers local writes and remote mounts; there's no separate "is this +allowed remotely" path to get wrong later. + + /mnt/svcs/ctl 0644 hostowner halt, reboot, reset + /mnt/svcs/<n>/ctl 0664 hostowner:user start, stop, restart + /mnt/svcs/<n>/status 0444 anyone + /mnt/svcs/<n>/log 0640 + +Note the ctl files are world *readable*. Restricting reads was the first +thing tried and it immediately broke the rule above — reading a ctl file is +how you learn what it accepts, which is no use if only one user may do it. +Writing is the thing that wants restricting. + +This isn't cosmetic: if `/srv/svc` is exported, anyone who can mount it can halt +the machine. + +## Shutdown + +`fshalt` is the proof that this is needed. It spends ninety lines rediscovering +the system at shutdown — a hardcoded glob for every file server 9front has +shipped (`cwfs*cmd`, `hjfs*cmd`, `ext4*cmd`, `gefs*cmd`, `fscons*`), plus +hardcoded knowledge of each one's stop protocol (`echo halt` for most, +`echo fsys all halt` and a `sleep 2` for fscons). + +Then the strangest part: + + ramfs + cp /bin/echo /tmp + cp /bin/rc /tmp + # put this in a shell function so this rc script doesn't get read + # when it's no longer accessible + fn x { ... } + +It copies its own tools into a ramfs and hides its body in a function so rc +won't re-read the script — because the shutdown logic lives *on the filesystem +it is shutting down*. init has none of that problem: it's resident, it's process +1, and it re-reads nothing. + +So shutdown is: + +1. stop services in reverse dependency order — `hangup` note, wait, then `kill` +2. halt the adopted file server +3. power off or reboot + +A wedged service must never stall shutdown, because the thing being protected is +a clean filesystem. Timeout, hard kill, proceed. + +**Adopted services.** The root file server is started by `bootrc` before init +exists. init isn't its parent, can't `await` it, can't `kill` it — it can only +stop it the way fshalt does, by writing its `/srv` ctl file. So it's a second +category: known but not owned, stop-only, always last. Each file server declares +its own `stop=` rather than init carrying a table of how to kill everyone else. + +`fshalt` goes away. + +## Logging + +Service output is appended to `/log/<name>`. `/mnt/svcs/<name>/log` is a view of +that file, and a blocking read at EOF gives live tailing. + +**Rotation is a non-goal.** Appending is twenty lines; rotation is a subsystem, +and it isn't going in init. Files grow until something else deals with them. + +Two places this doesn't work, both the same problem: there's no filesystem yet, +or there won't be one shortly. Early boot messages go to console. Once teardown +starts, logging to disk stops and the rest goes to console. + +## Testing without booting + +Nothing about supervising services requires being process 1: + + init -d /tmp/svc.test -s svc.test -m /mnt/svcs.test + +runs the real init as an ordinary user, supervising toy services, serving a test +filesystem. No reboot, no risk. + +That only works if it's designed in, so it's a rule: **no hardcoded paths, no +assumption of being pid 1, every path a flag.** The only parts needing a real +boot are base namespace construction, the rescue console, and adopted-fs +shutdown — which is the argument for keeping those three thin and separable. + +This is where recoverability actually comes from, not from splitting init into +two programs. + +## Implementation + +C, and it isn't close: the program that brings a machine up should need nothing but libc at runtime. + +- **lib9p** — the file server. `threadpostmountsrv`, fill in a `Srv`. +- **libndb** — service definitions and status output. +- **libauth** — `newns`. +- **libthread** — `threadwaitchan()` hands you a channel of `Waitmsg*`, so the + main loop is literally a select over "a child exited" and "a 9P request + arrived." The concurrency model matches the problem. + +Roughly 1500–2500 lines. Small enough to hold in your head, which for +something this hard to restart is the point. + +Services written in rc are just `exec=/bin/rc args=/bin/foo`. **An rc service +script must `exec` its final program, not background it** — otherwise init +supervises rc, rc exits immediately, and the real work is orphaned. + +## Getting there + +Staged because you need a working machine tomorrow, not because the end state +keeps any of it. First: init starts `svcfs` and supervises a handful of services +alongside the existing `termrc`, fully reversible. Then service definitions +replace `/rc/bin/service/` and the loose daemon lines. Then `termrc`, `cpurc` +and `service=` are deleted. + +## Open questions + +- **`reexec` state handover.** Init replacing itself without dropping services + means supervision state survives the exec — handed over in memory, or + re-derived from `/proc` and `/srv`. The genuinely hard part. +- **Detaching liveness.** Plan 9 has no file-change notification, so noticing + that a detached service's `/srv` file vanished means polling, or only + noticing on a `status` read. +- **Who restarts init?** If init dies, nothing catches it. Confirm `bootrc` + honours `init=` in `plan9.ini` — that's the escape hatch for a bad init. +- **A machine-readable interface description.** An ndb file next to `ctl` + describing fields, actions, and which files block, so a generic client can + render a UI over any 9P server. Not part of init, but init is a reasonable + first carrier — and it shouldn't be frozen until a second server carries it + too. + +## Namespaces are the capability set + +This started as a federation feature and turned out to be the privilege +mechanism, which is a better justification and changes the defaults. + +In Plan 9 there is no access control for devices beyond what is in your +namespace. If `#S` is not bound, you cannot touch storage — not "you get +permission denied", but there is no name to open. That is stronger than +Unix, where `/dev/sda` exists for everyone and mode bits are the only guard. + +But termrc binds all nine kernel devices into `/dev` for everything: + + for(i in P S f æ t L A J '$') + bind -qa '#'^$i /dev + +So today `timesync` — which talks to an NTP server — can reach raw disk and +PCI config space. `#p` is worse: every process can walk `/proc`, read other +processes' memory, and post notes to them, so any service can kill init. + +### Consequences for the design + +**No default namespace.** A service with no namespace declaration is a load +error, like an unknown attribute. Inheriting by default means granting +everything by default to services that asked for nothing. + +**Ship profiles so explicit is not verbose.** The base is ~40 lines and most +services want most of it; hand-written namespaces per service would be +copy-pasted and would drift. + + /lib/ns/net #I #c #e #d — dials out, no disk, no /proc + /lib/ns/disk net plus #S + /lib/ns/draw net plus #i #m + /lib/ns/full everything termrc currently binds + +A service says `nsfile=/lib/ns/net` plus optional inline `ns=bind …` lines. + +**`full` will be abused, and that is survivable, because it is greppable.** +`grep -l ns/full /lib/services/*` is a list of over-privileged services, which +is a work queue. Inheritance produces no such list. + +### /srv is a hole + +Binding `#s` undoes most of the above. A service that can see `/srv` can +`mount /srv/boot` and get the whole root filesystem back, write +`/srv/hjfs.cmd` to halt the machine, or mount factotum and reach keys. + +Three layers, in order of strength: + +1. **Do not bind `#s`.** The only airtight one — the channels are not nameable. +2. **`rfork(RFNOMNT)` before exec.** Plan 9's pledge: a one-way drop after + which the process cannot mount, bind or unmount. Default on; `nsmount=yes` + is the visible opt-out for services that genuinely mount things. +3. **`user=`.** File permissions on whatever remains. + +RFNOMNT is not a wall. It blocks the `mount` call, not a process that opens +`/srv/foo` and speaks 9P down the fd by hand. It stops accidents and casual +escalation; only omitting `#s` stops intent. + +### Unresolved: how this interacts with everything else + +- **Providers of namespaces need mount privilege.** A service that imports a + remote `/net` for others must mount, and must post to `/srv` so dependents + can reach it — so the exception to rule 1 and the exception to rule 2 are + the same services. Circular and not thought through. +- **Namespace changes do not propagate.** A provider's mount dies with its + process group, so `/srv` is the only rendezvous, which is the thing we are + trying to restrict. +- **init's own bootstrap** predates all of this: it needs `/lib/services` and + `#s` before it can read a profile. +- **`user=` ordering.** The capability device must still be in the namespace + at the moment we drop privilege, so profiles cannot omit `#¤` blindly. +- **Adopted services** were started by bootrc in a namespace we never chose. +- **Migration.** `cs`, `dns` and `timesync` work today with no namespace + declaration. Making it mandatory breaks them until profiles exist. diff --git a/svc/doc/inventory.md b/svc/doc/inventory.md new file mode 100644 index 0000000..a63907d --- /dev/null +++ b/svc/doc/inventory.md @@ -0,0 +1,113 @@ +# What termrc and cpurc actually contain + +Taken from reading both scripts and from `ps` on a booted machine, which is +the more reliable of the two — the scripts are full of conditionals that never +fire. + +Kernel processes (0K, `Wakeme`/`Idle`: `pager`, `alarm`, `etherread4`, +`iasata`, …) are not in scope. They are not started by anything in userspace. + +## Adopted — init can stop these but never starts them + +Started by `bootrc`, before init exists. No `exec=`, `adopt=yes`, stop only. + +| what | evidence | how to stop it | +|---|---|---| +| `hjfs` | pids 320–328, `/srv/hjfs.cmd` | `stop=write:/srv/hjfs.cmd:halt` | +| `factotum` | pid 260, `/srv/factotum` | note | +| `paqfs` | pid 8, serves the boot archive | note | + +`hjfs` is the one that must be stopped last, after everything using it. + +## Base namespace — init does these directly, they are not services + +None of this survives as a service, because a service runs in its own namespace +group and so cannot change anyone else's. + +- `for(i in P S f æ t L A J '$') bind -qa '#'^$i /dev` +- `mount -qb /srv/cons /dev` +- binding the `mntgen` channels onto `/n`, `/mnt`, `/mnt/exportfs` +- `mount /srv/factotum /mnt/factotum` and the `bind -q` after it + +## Services + +| service | evidence | ready | needs | notes | +|---|---|---|---|---| +| `slashn` | pid 11 | `srv:slashn` | | `mntgen` for `/n` | +| `slashmnt` | pid 14 | `srv:slashmnt` | | `mntgen` for `/mnt` | +| `mntexport` | pid 17 | `srv:mntexport` | | `mntgen` for `/mnt/exportfs` | +| `kbdfs` | pids 70–74 | `exec` | | posts nothing in `/srv` | +| `usbd` | pids 82–83 | `exec` | | from `nusbrc` | +| `kb` | pids 91–92 | `exec` | `usbd` | from `nusbrc` | +| `cs` | pid 372 | `srv:cs` | | | +| `dns` | pid 446 | `srv:dns` | `cs` | | +| `timesync` | pid 450 | `exec` | `dns` | needs a name to resolve | +| `realemu` | pids 456–457 | `exec` | | vga real-mode calls | +| `webfs` | pid 500 | `exec` | `dns` | | +| `webcookies` | pid 497 | `exec` | | | +| `plumber` | pids 503–504 | see below | | | +| `ipconfig-ra6` | pid 439/440 | `exec` | | the lingering RA listener | + +Only on a machine that is meant to accept logins: + +| service | from | ready | needs | +|---|---|---|---| +| `listen` | `cpurc` | `exec` | `cs` | +| `keyfs` | `cpurc`, auth branch | `srv:keyfs` | | +| `authsrv` | `cpurc`, auth branch | `dial:tcp!*!567` | `keyfs` | + +## Oneshots — `ready=exit` + +- `diskparts` +- `swap` (only when `/dev/sd*/swap` exists) +- `ip/ipconfig -h $sysname ether $ether` — the DHCP one, as opposed to the RA + listener above, which stays +- setting `/dev/sysname` +- `screenrc` + +## What simply disappears + +- **The `service=` branch itself.** `termrc` and `cpurc` are 80% identical: the + same device binds, the same `mntgen` lines, the same `factotum` mount, the + same `cs`/`dns`/`diskparts`/`swap`. The difference is which of two nearly + identical scripts runs. +- **The role detection block.** `cpurc` decides whether this machine is an + authentication server by comparing `$sysname` against the `auth` attribute in + ndb, and starts `keyfs` plus a different `listen` if it matches. That is the + archetype logic, in one `if`. It becomes: is `keyfs` enabled or not. +- **`serviced=` selection** — `/cfg/$sysname/service`, `/cfg/default/service`, + `/rc/bin/service`, in that order. Replaced by `/lib/svc`. +- **The `.local` and `/cfg/$sysname` hooks** — `cpurc.local`, `termrc.local`, + `/cfg/$sysname/cpurc`, `/cfg/$sysname/cpustart`. Four hook points that exist + because a shell script has no other way to be extended. A directory of + service files does not need them. +- **`rm -f /env/i`, `rm -f /env/disk`, `rm -f /env/ether /env/addrs /env/addr`** + — these clean up after `for` loops and backquote assignments. They exist + only because this is a shell script, and vanish with it. +- **`NPROC`, `prompt`, `fn term%`** — shell configuration that has no business + in system startup. Belongs in `profile`. +- **`dontkill`** — a list of process names protected from `kill`. Worth + revisiting rather than porting: with a supervisor that knows what it started, + protecting things by name is the wrong shape. + +## Two problems this turned up + +**`plumber`'s `/srv` name is dynamic.** It posts `/srv/plumb.glenda.502` — +user and pid baked into the name. `ready=srv:name` cannot express that, and +neither can `stop=write:...`. Options: allow a glob in `ready=srv:`, accept +`ready=exec` and lose the liveness check, or treat it as a general escape and +add `ready=file:pattern`. Unresolved, and it will not be the only such server. + +**`ipconfig` appears twice with different lifetimes.** One does DHCP and exits +(a oneshot); the other listens for router advertisements and stays (a service). +They are the same binary with different arguments, so they must be two service +files with different names. Fine, but it means service name and program name +cannot be assumed to match — which the current implementation already allows, +since `svc=` and `exec=` are separate. + +## Also worth noting + +There is no `aux/listen` running on this machine, because it booted as +`service=terminal`. Every service above marked "only on a machine meant to +accept logins" is absent purely because of one word in `plan9.ini` — which is +the whole argument, visible in `ps`. diff --git a/svc/doc/todo.md b/svc/doc/todo.md new file mode 100644 index 0000000..5598ae3 --- /dev/null +++ b/svc/doc/todo.md @@ -0,0 +1,101 @@ +# TODO + +State as of the end of the second session. Ordered by what unblocks what, +not by size. + +## Do first: kill the serial dependency + +Everything else is cheaper once this is done. The serial shell wedged three +times in one session and is lossy, non-interactive, and hostile to quoting. + +- [ ] `/adm/keys` and a user: `auth/keyfs`, then `auth/changeuser glenda` +- [ ] service files for `keyfs`, `authsrv` (listener on 567), `listen` (rcpu + on 17019). This is literally `svc enable keyfs authsrv` from the man page +- [ ] add a 567 hostfwd to `run.sh` next to the existing 17019 one +- [ ] client: build conterm (github.com/0intro/conterm, text-only, drivable + over a pipe) or use the already-installed drawterm interactively + +`tcp17019` runs `tlssrv -a /bin/rc -c server`, so it authenticates through +factotum and needs the auth server to validate. `rc/bin/service.auth/` already +ships `tcp567`. `run.sh` already forwards 17019. + +## Unblocked, mechanical + +- [ ] drop `svc=`; the filename is the service name +- [ ] `.ndb` extension on service files, stripped to get the name +- [ ] migrate more services per `inventory.md` — `plumber`, `webfs`, + `webcookies`, `kbdfs`, `usbd`, `realemu` +- [ ] doc drift: `/log` → `/sys/log` in `design.md` and `man/init`, with the + reason (root is mounted without create permission) +- [ ] doc drift: ctl permissions are `0644`, not `0600` — reads must be open + or ctl cannot document itself +- [ ] record the session's gotchas in `CLAUDE.md`: non-interactive rc dies on + a syntax error and takes the serial with it; `pkill -f` kills the calling + shell even with the bracket trick; rc treats double quotes as literal + characters, so `|` inside them becomes a pipe + +## Needs a decision before code + +- [ ] **`stop` does not work for detaching services.** `Ksrv` services have no + pid init can signal, so `stopsvc` is a no-op and a later `restart` fails + with "another instance is running". Proposed: for `Ksrv` with no `stop=`, + remove the `/srv` entry, which hangs up the channel and a well-behaved + server exits. Untested. +- [ ] **Dynamic `/srv` names.** `plumber` posts `plumb.glenda.502`, `rio` posts + `rio.glenda.1483` — user and pid in the name. `ready=srv:` cannot express + it and it is a pattern, not an exception. Needs a glob, a new `ready=` + form, or accepting `ready=exec` and losing liveness. +- [ ] **Namespace profiles.** Ship `/lib/ns/{net,disk,draw,full}`, require + every service to name one, no default. Blocked on the item below. +- [ ] **The provider circularity.** A service that provides a namespace to + others (an `import` of a remote `/net`, say) must be able to mount *and* + must post to `/srv` so dependents can reach it — so it is simultaneously + the exception to "do not bind `#s`" and to freezing the namespace. Not + resolved. Profiles built before this is settled get built twice. + +## Unimplemented attributes and features + +Parsed and ignored today: `ns`, `user`, `stop`, `adopt`. + +- [ ] `ns=` inline lines and `nsfile=`, via `newns` +- [ ] `user=` via the capability device — note `#¤` must still be in the + namespace at the moment privilege is dropped +- [ ] `stop=` — `note:`, `write:file:word`, `exec:` forms +- [ ] `adopt=yes` for services init can stop but never started (the root file + server, `factotum`, `paqfs`) +- [ ] `halt`, `reboot`, `reset` ctl verbs and commands +- [ ] `reexec` +- [ ] rescue console when services will not come up +- [ ] base namespace construction by init, replacing the device-bind loop in + termrc + +## Environment + +- [ ] **The bootargs prompt needs a manual Enter every boot.** Listed as + unresolved in `CLAUDE.md`; it now blocks unattended testing, which makes + it worth actually fixing. +- [ ] sshfs has to be remounted by hand after every reboot — a candidate for + being a service itself, gated on a `sshkey` oneshot +- [ ] `9front-base.qcow2` is stale relative to `9front.qcow2`; overlays made by + `newvm.sh` lack the supervised serial shell and the rio change + +## Verification debts + +Claims the design rests on that have not been tested: + +- [ ] does `RFNOMNT` survive `exec`? That is the last step before a service + runs, and the plausible place for a flag to be cleared +- [ ] does binding a single `/srv` entry (`bind #s/cs /srv/cs`) work as cleanly + as binding the directory? The fine-grained capability story depends on it +- [ ] does `import` have a flag to post to `/srv`, or is `srvfs` needed? +- [ ] does `bootrc` honour `init=` in `plan9.ini`? That is the escape hatch if + we ever make init the boot program and get it wrong + +## Deferred deliberately + +- The filesystem hierarchy rework. Until then, do not churn `/lib/svc` → + `/lib/services`; every path in init is a flag, so it is a one-line change + whenever the hierarchy lands. +- `timesync` reaching an external NTP server. It fails on DNS resolution in a + NAT'd VM and is not worth chasing; the dependency machinery around it is + already proven. |
