Hermes and Artemis are now session-less fleet foundation, verified live on all three architectures (commit 52eb1bb) -- the real, load-bearing prerequisite D.7 itself identified is done. The rest of D.7 (message-only birth flow, WIREBIND/MSGMIGRATE reconciliation) remains open, not touched here.
2560 lines
185 KiB
Markdown
2560 lines
185 KiB
Markdown
# FABRIC-3.md — the Stadium, continued again
|
||
|
||
**Status:** Living working document, opened 2026-08-25 as the successor to `FABRIC-2.md`
|
||
(now closed/archival — see its own header). This document does not repeat `FABRIC-2.md`'s
|
||
design argument or history; it restates only outcomes, with pointers back to the section
|
||
that derived them. Read `FABRIC.md` for the original "why," `FABRIC-2.md` for everything
|
||
derived through 2026-08-25, this document for what's left as of that date onward.
|
||
|
||
**Provenance.** Everything in Section A below is a full, non-sampled carry-forward of every
|
||
open (`- [ ]`) item in `FABRIC-2.md` as of 2026-08-25 — 51 items, confirmed by
|
||
`grep -c '^- \[ \]' FABRIC-2.md`, none dropped (Section A itself holds 49: the other 2,
|
||
`FABRIC-2.md` §F.3's own two checkbox lines, were pure summaries cross-referencing items
|
||
already listed individually elsewhere — 4.4s/1.11/4.3/§17.4 and 5.1/ACL-RWT re-measurement,
|
||
both of which are carried forward as their own individual items above — not distinct content,
|
||
confirmed by diffing item text programmatically before writing this document, not assumed).
|
||
Extracted mechanically (a script pulling each checkbox item's own text, stopping at the first
|
||
blank line rather than the next checkbox, to avoid pulling in unrelated already-resolved
|
||
narrative that happened to sit between two open items in the source document) and spot-checked
|
||
against the original. Item numbers/labels are
|
||
carried forward unchanged, for traceability — this is not a renumbering or a re-prioritization.
|
||
Section groupings match `FABRIC-2.md`'s own (documentation debt, xHCI WRITE(10), Milestone
|
||
3–9 punch lists, etc.) — items are relocated, not reorganized.
|
||
|
||
**How to use this document going forward.** New findings, new punch-list items, and new
|
||
decisions get added here, not to `FABRIC-2.md`. Follow the same discipline `FABRIC-2.md`
|
||
§(intro) established for how work gets picked up, closed, and recorded.
|
||
|
||
---
|
||
|
||
## A. Carried forward from FABRIC-2.md (51 items, all still open as of 2026-08-25)
|
||
|
||
### From FABRIC-2.md §A — Blocked or scoped, not started
|
||
|
||
- [ ] **1.11 — Dirty-event granularity.** Leaning region-based. Blocked on item 4.3 — settled
|
||
as part of the console migration, not speculatively before it. *Refs (FABRIC.md):* §17.5,
|
||
§23.2, §23.4 #1.
|
||
|
||
- [ ] **4.3 — Console.** Umbrella item; settles 1.11 as part of the work. Nearly everything
|
||
under it (4.3.1–4.3.7f, 4.4–4.4ac) is done — the parent stays open only because 4.4s below
|
||
is still blocked and nothing has formally closed the umbrella. *Refs (FABRIC.md):* §17.5,
|
||
§27.
|
||
|
||
- [ ] **4.4s — `(user)` prompt segment.** Scoped, blocked, not started. Extends 4.4's prompt
|
||
format. *Refs (FABRIC.md):* §27.8, 4.4.
|
||
|
||
- [ ] **5.1 — Re-run the DoE on the new substrate.** A green POST suite is not evidence that
|
||
determinism holds under the Stadium migration — needs its own campaign. Not started.
|
||
|
||
### From FABRIC-2.md §D — Design questions still genuinely open
|
||
|
||
- [ ] **§17.4 — the framebuffer utility's internal heat/decay dynamics are undesigned.**
|
||
Explicitly "Open, deferred": not a Stadium patron, but what physics (if any) governs it
|
||
internally was never designed. Not blocking anything. Blocked on item 1.11 specifically
|
||
(dirty-event granularity), not "the framebuffer work" in general — see `FABRIC-2.md` §D's
|
||
own 2026-08-13 refinement of this item before assuming it's ripe.
|
||
|
||
### From FABRIC-2.md §E — Documentation debt
|
||
|
||
- [x] **Already done — stale carry-forward, closed 2026-08-26.** This item was never marked
|
||
complete when the actual work concluded. `FABRIC-2.md` Sections M through T (2026-08-19 to
|
||
2026-08-21) already re-ran this measurement — and in the process found the original
|
||
"ACL-RWT" name itself was wrong: the Rolling-Window-of-Truth mechanism it was named after
|
||
was dead code, removed 2026-07-08 (`ACL-RECHECK-RW` was never reachable — `acl_recheck()`
|
||
only ever looks up the 11-char `ACL-RECHECK`, never the 14-char `-RW` variant). What the
|
||
original campaign actually measured was the live `ACL-TTL` mechanism under a misleading
|
||
name. **Final, accepted result (`FABRIC-2.md` §T): +0.0603% ACL-TTL enforcement overhead,
|
||
architecture-independent (identical across amd64/aarch64/riscv64), fully deterministic
|
||
(CV=0.000%)** — see also memory `project_acl_ttl_overhead_final.md`. No new campaign run
|
||
here; this entry corrects the bookkeeping, not the measurement.
|
||
|
||
### From FABRIC-2.md §J — Maintainability sweep (2026-08-18)
|
||
|
||
- [ ] `docs/lithosananke/ROADMAP.md` and `M7.1.md` — stale `Branch: lithosananke` (no such
|
||
branch exists post-split), `M7.1.md`'s "Status: Design Complete" (shipped and live, not
|
||
just designed), `ROADMAP.md`'s self-contradiction (M8 marked OBSOLETE in one place, still
|
||
a live success criterion in another), and its stale "AHCI driver" claim for M9 (real
|
||
implementation is `virtio_blk.c`) — not fixed, flagged.
|
||
|
||
- [ ] Top-level `ROADMAP.md` (StarForth-era, "Phase 0 Complete... Phase 1 Starting," dated
|
||
2025-12-14) — badly stale, no historical/superseded banner to warn a reader. Not fixed.
|
||
|
||
- [ ] `docs/03-architecture/word-acl/DESIGN.md` says ACL Phase 7 (LithosAnanke kernel parity)
|
||
is still "remaining" — direct contradiction with `.claude/CLAUDE.md`, which states Phase 7
|
||
is independently verified complete. Not fixed.
|
||
|
||
- [x] Confirmed accurate, not stale (2026-08-26): `VM-FLEET-ATTRACTOR-DESIGN-20260705.md`'s
|
||
claim that `doe-campaign.4th` is "broken and being superseded." Live-ran `SMOKE-CAMPAIGN`
|
||
from the current capsule (amd64) — it completes without error and correctly conserves fleet
|
||
heat (`VM-PHYSICS: conserved=CONSERVED`, `fleet_heat_sum=65536`). Initially read that as
|
||
contradicting the doc's claim; **Captain Bob corrected this directly — it's broken.**
|
||
"Doesn't crash" and "runs" are not the same claim: the doc's actual argument is that the
|
||
capsule has no real controlled-experimental-factor mechanism (no manual heat-injection
|
||
point under the current design, so it cannot drive the fleet through controlled scenarios
|
||
the way a DoE campaign needs to), a methodological gap a clean execution trace doesn't
|
||
surface or disprove. Doc's claim stands; not touched.
|
||
|
||
- [x] Tracked (2026-08-26) — real proof-modeling gap, not just stale prose, so not fixed
|
||
here. Confirmed by reading `StarForth_Loop4_Pipeline.thy`'s own comment (lines 127–137):
|
||
`pipeline_metrics_state`'s `pm_last_accuracy_num`/`pm_last_accuracy_den` fraction pair
|
||
doesn't correspond to anything in the real C struct — `include/vm.h`'s
|
||
`PipelineGlobalMetrics` has a single `double last_checked_accuracy` field, no num/den pair
|
||
anywhere. The `.thy` file's own comment already scopes the real fix correctly: "a full
|
||
field-level pass over `pipeline_metrics_state` is its own separate task" — matches this
|
||
project's standing caution that each remaining Isabelle gap needs its own subsystem model,
|
||
not a documentation-sprint patch. The punch-list ask was tracking this outside the buried
|
||
`.thy` comment, which it now has here — the actual re-model stays unattempted, on purpose.
|
||
|
||
### From FABRIC-2.md §X, Milestone 2 — USB hardware stack
|
||
|
||
- [x] **CLOSED 2026-08-27 (FABRIC-3.md §F.3).** Decide and implement where the hotplug event
|
||
surfaces to the rest of the kernel. Re-checked directly against `sk_repl_idle()` and the
|
||
`bot_msc_attach_pending`/`bot_msc_detach_pending` doc comments in `xhci_driver.h`: Milestone
|
||
2h's flag-set-by-xHCI/flag-consumed-by-`sk_repl_idle()` pattern isn't literally a registered
|
||
callback, but its own doc comment confirms it was built deliberately to achieve the same
|
||
goal — *"keeps xhci.c decoupled from block_subsystem.c"* — and that decoupling is real,
|
||
live on all three arches. Closed as written; no further design work on this node itself.
|
||
|
||
- [x] **BUILT + VERIFIED 2026-08-28 (`FABRIC-3.md` §F.17).** CBW/data/CSW for SCSI WRITE(10),
|
||
a direct mirror of READ(10) with the data direction flipped. Verified live via a genuine
|
||
cold-reboot round-trip on all three architectures (amd64/aarch64/riscv64), not just a
|
||
same-session read. `blkio_usb.c` is read-write now; `read_only` flipped to `0`. Real hardware
|
||
(Milestone 8) remains untested — QEMU-only so far.
|
||
|
||
- [ ] Implement basic error/stall recovery (CSW failure status, endpoint stall clear) — at
|
||
minimum enough to not wedge the controller on a single bad transfer. **SCOPED 2026-08-27
|
||
(`FABRIC-3.md` §F.14)**: full BOT-spec recovery designed — new `STALL_ERROR` completion code,
|
||
new xHCI Reset Endpoint + Set TR Dequeue Pointer commands, `CLEAR_FEATURE(ENDPOINT_HALT)` via
|
||
existing control-transfer plumbing, escalating to a full Bulk-Only Mass Storage Reset on a
|
||
second stall, bounded via a new retry counter mirroring `bot_tur_retries`'s own precedent.
|
||
Real gap closed today: CSW status handling (PASS/FAILED/PHASE ERROR) already existed; nothing
|
||
distinguished a STALL specifically, and no recovery of any kind existed before this pass.
|
||
|
||
### From FABRIC-2.md §X, Milestone 3 — Block subsystem extensions
|
||
|
||
- [ ] Implement the CA-signed-cert verification path (Milestone 6 dependency — the cert chain
|
||
validator doesn't exist yet either). **Now confirmed a hard prerequisite of `WIREBIND` too
|
||
(`FABRIC-3.md` §F.5), not just an M3 item in isolation. SCOPED 2026-08-27 (§F.7)**: format
|
||
and trust root decided (Zuse-signed X.509/DER cert, not the offline capsule-PKI chain); the
|
||
actual DER signature-extraction code is new work, not yet built.
|
||
|
||
- [ ] Implement the first-touch allocation function: given a verified identity pubkey and a
|
||
requested block count, either read an existing range from the drive's map or claim a new
|
||
one at `g.total_user_lbn` and write it back. *(Single-block relocation itself — the
|
||
mechanism this would allocate ranges for — is done: `blk_subsys_relocate_block()`/
|
||
`RELOCATE-BLOCK`, `FABRIC-2.md`, commit `36d832f`. This item is about the identity→range
|
||
allocation that decides what to relocate blocks* into*, still unbuilt.)* **Scope clarified
|
||
2026-08-27 (`FABRIC-3.md` §F.6, decision 3): a user's pool is their entire thumbdrive by
|
||
default (never contested) plus any system-resident device blocks they additionally claim,
|
||
first-come-first-served — this function only governs that second, system-side extension,
|
||
not the drive itself.** **SCOPED 2026-08-27 (`FABRIC-3.md` §F.11)**: the "claim at
|
||
`g.total_user_lbn`" phrasing above is stale, predating `BMAPFMT`'s distributed-ownership
|
||
decision — claims are found by scanning Artemis's already-attached device's own `blk_meta_t`
|
||
records for unowned devblocks, not by extending a counter. Claims may be a scattered chain
|
||
(reusing `blk_meta_t`'s existing `prev_block`/`next_block`/`chain_length` fields), found via
|
||
a full linear scan each time, no cached index.
|
||
|
||
- [x] **SCOPED 2026-08-27 (FABRIC-3.md §F.4).** Design the on-drive block-map format (Section
|
||
U item 4). Resolved as: no separate table — repurpose the existing, fully-wired-but-
|
||
zero-callers `blk_meta_t`/`blk_get_meta()`/`blk_set_meta()` (`block_subsystem.c`), replacing
|
||
its stale POSIX-flavored ownership fields with a pubkey-fingerprint owner + a fast-deny ACL
|
||
bit + deliberate reserved slack. Field design done; the actual code edit is not.
|
||
|
||
- [x] **CLOSED 2026-08-27 (`FABRIC-3.md` §F.12) — already built, not a separate item.**
|
||
`blk_set_meta()`'s dirty-tracking already flows through the exact same `cache_writeback()`/
|
||
`blk_flush()` path ordinary block data uses; the whole devblock (data + metadata) already
|
||
gets written to the device, real and unstubbed. Blocked only on `WRITE(10)` itself, already
|
||
modeled in the graph — nothing else stands between this and working.
|
||
|
||
- [x] **CLOSED 2026-08-27 (`FABRIC-3.md` §F.13) — already built, not a separate item.**
|
||
`cache_load_devblock()` already unpacks and validates every devblock's metadata on load.
|
||
Tracing "on insertion" surfaced a real, separate, previously-unflagged conflict instead:
|
||
`homeblocks_sig_t` and the block-subsystem's own generic `'STFR'`/v2 header both wanted
|
||
devblock 0, which would leave a home-blocks drive permanently write-refused or overwrite its
|
||
own identity header. Fixed by relocating `homeblocks_sig_t` to devblock 1; `MINT` must also
|
||
run the ordinary format-confirm path at devblock 0.
|
||
|
||
- [ ] Design the migration state machine (Section U item 5) — states, transition triggers.
|
||
Session direction, 2026-08-25: **ACL manages *when* to relocate** (capacity pressure, or a
|
||
compudynamics heat/cold signal); migration itself is expected to be rare, not routine. The
|
||
`physics_hotwords_cache.c`-reuse question is settled differently than originally framed —
|
||
see this document's new §B below (Stadium unification), which reframes block/word placement
|
||
as a `compudynamics.c`-driven decision generically, not a `physics_hotwords_cache.c`
|
||
(`DictEntry*`-hardcoded) reuse question specifically.
|
||
|
||
- [x] **CLOSED 2026-08-27 (`FABRIC-3.md` §F.10), scoped alongside `DETACH`.** Unclean removal
|
||
runs the exact same VM-kill path `EJECT` (M5) uses, minus the flush step — skipped outright
|
||
since the device is confirmed gone by the time the existing `bot_msc_detach_pending` signal
|
||
fires, not attempted-and-ignored. Design done; the actual wiring into `sk_repl_idle()`'s
|
||
existing detach handling is not yet written.
|
||
|
||
### From FABRIC-2.md §X, Milestone 4 — Drive/credential security
|
||
|
||
- [x] **Designed (2026-08-26), not yet implemented — Phase 8 kickoff.** Mirrors two existing
|
||
precedents exactly: `CAPSULE_MAGIC_PACK`'s bit-packed magic (`include/starkernel/capsule.h`)
|
||
and `blk_volume_meta_t`'s magic+version+fields+pad-to-4096 structural convention
|
||
(`include/block_subsystem.h`). Lives at the first 4KiB devblock of the GPT metadata
|
||
partition (the ~1GB partition decided 2026-08-22) — the header format doesn't depend on the
|
||
still-missing GPT parser; it's just what gets written starting at that partition's first
|
||
devblock once something can locate it. Deliberately narrow: identifies/authenticates the
|
||
drive only, does **not** invent the block-map or credential/cert formats (both separate,
|
||
still-open items below) — reserves offset/size pointers to where they'll live instead of
|
||
embedding them.
|
||
|
||
```c
|
||
#define HOMEBLOCKS_SIG_MAGIC 0x4248414CULL /* 'LAHB' -- LithosAnanke Home Blocks,
|
||
* same little-endian ASCII packing as
|
||
* CAPSULE_DESC_MAGIC's 'CAPS' */
|
||
#define HOMEBLOCKS_SIG_VERSION_0 0
|
||
|
||
#define HOMEBLOCKS_SIG_PACK(ver) \
|
||
(HOMEBLOCKS_SIG_MAGIC | ((uint64_t)(ver) << 32))
|
||
#define HOMEBLOCKS_SIG_GET_MAGIC(m) ((uint32_t)((m) & 0xFFFFFFFFULL))
|
||
#define HOMEBLOCKS_SIG_GET_VERSION(m) ((uint8_t)(((m) >> 32) & 0xFF))
|
||
|
||
typedef struct {
|
||
uint64_t magic; /* HOMEBLOCKS_SIG_PACK(...) */
|
||
uint8_t drive_uuid[16]; /* unique per-mint instance id -- Phase 8 mints multiple
|
||
* distinct drives, needs something to tell them apart */
|
||
uint64_t minted_time_ns;
|
||
uint64_t metadata_devblocks; /* size of this GPT metadata partition, in 4KiB devblocks
|
||
* -- sanity/bounds check against the GPT entry once a
|
||
* parser exists */
|
||
uint32_t cert_offset; /* devblock offset within this partition where the
|
||
* CA-signed cert blob starts; 0 = not yet minted */
|
||
uint32_t cert_devblocks;
|
||
uint32_t blockmap_offset; /* devblock offset where the block-map (Milestone 3,
|
||
* format still undesigned) starts */
|
||
uint32_t blockmap_devblocks;
|
||
uint64_t hdr_crc; /* REAL from day one, not a placeholder like
|
||
* blk_volume_meta_t's "unused yet" hdr_crc -- this
|
||
* header's whole job is gating a warn-and-refuse
|
||
* security check below, so the crc has to actually work */
|
||
uint8_t _pad[4096 - 64]; /* pad to one devblock; 64 = sum of the fields above */
|
||
} homeblocks_sig_t;
|
||
```
|
||
|
||
`cert_offset`/`cert_devblocks` are 0 on first design pass — the real CA hasn't been
|
||
generated yet (Milestone 6), so this reserves the *shape* of where a cert will attach
|
||
without committing to a cert format that doesn't exist. Same reasoning for
|
||
`blockmap_offset`/`blockmap_devblocks` against Milestone 3's still-open block-map design.
|
||
**Implemented (2026-08-26):** `include/starkernel/homeblocks_sig.h` — `homeblocks_sig_t` +
|
||
`HOMEBLOCKS_SIG_PACK`/`_GET_MAGIC`/`_GET_VERSION` macros, a C99 compile-time size assertion
|
||
(same discipline `stadium.h`'s own header-size checks use), and the identical field layout
|
||
shown above. Verified standalone: `sizeof(homeblocks_sig_t) == 4096`, compiles clean under
|
||
`-std=c99 -Wall -Wextra -Werror`. Not yet consumed by any code — nothing in the block
|
||
subsystem or xHCI driver reads or writes it yet, so no functional kernel change and no
|
||
3-arch acceptance boot needed for this step; that starts with the signature-check
|
||
implementation, the next punch-list item below.
|
||
|
||
- [x] **Implemented (2026-08-26): the check function itself, real and complete — not yet
|
||
wired to any write path.** `include/starkernel/homeblocks_sig.h` +
|
||
`src/starkernel/homeblocks_sig.c`: `homeblocks_sig_check(dev, sig_start_fblock, out_sig)`
|
||
reads the 4 consecutive 1KB `blkio` forth-blocks the 4KB header spans, verifies magic →
|
||
version → CRC-64 in order, returns one of `HOMEBLOCKS_SIG_OK`/`_BLANK`/`_BAD_VERSION`/
|
||
`_BAD_CRC`/`_READ_ERROR`. Reuses `block_subsystem.c`'s existing CRC-64/ISO
|
||
(`compute_crc64`, previously `static`/file-local, now exposed) rather than a second CRC
|
||
implementation — same algorithm already proven via per-block checksums. Takes the header's
|
||
starting block as a plain parameter rather than resolving it internally: this function
|
||
verifies a signature given a location; finding that location (GPT-partition-relative,
|
||
once a parser exists) stays the caller's job, not invented here.
|
||
|
||
**Verified against the actual shipped code**, not a reimplementation: a standalone host
|
||
test links the real `homeblocks_sig.c` against a fake in-memory `blkio_dev` and exercises
|
||
all four outcomes — blank media → `BLANK`, a correctly-minted header → `OK` (round-trips
|
||
`drive_uuid`/`minted_time_ns` correctly), a flipped CRC → `BAD_CRC`, an unrecognized
|
||
version → `BAD_VERSION`. All four pass. A full QEMU-hotplug live test isn't proportionate
|
||
yet — nothing calls this function from the live kernel path (deliberately; wiring it into
|
||
the attach path is the next item below), so a live boot check has nothing to exercise.
|
||
Clean zero-warning compile and clean boot on all three architectures confirms no
|
||
build/link regression from exposing `compute_crc64` and adding the new source file to
|
||
every kernel build.
|
||
|
||
- [x] **Implemented (2026-08-26): the "warn" half, live and wired.** Wired into
|
||
`sk_repl_idle()`'s USB hotplug attach handler (`repl.c`), right between
|
||
`blkio_usb_open_msc()` succeeding and `blk_subsys_attach_device()` — calls
|
||
`homeblocks_sig_check(&usb_blk_dev, 0, &sig)` and logs a distinct message per outcome
|
||
(recognized / blank-or-foreign / bad-version / bad-crc / read-error).
|
||
|
||
**The "refuse" half is deliberately not implemented — there is nothing real to gate yet.**
|
||
`blkio_usb.c` has no SCSI `WRITE(10)` support at all (Milestone 2's biggest open item), so
|
||
there is no write path today to refuse; the only thing attach currently enables is
|
||
read-only access, which is also the general-purpose USB block I/O path this repo already
|
||
relies on for unrelated testing, not exclusively a home-blocks identity workflow. Refusing
|
||
attach on blank media would have broken that legitimate use without protecting anything
|
||
real — building that gate now would be enforcement with no live consumer, the same
|
||
"don't build ahead of a real caller" reasoning `EXPIRE`'s deferral used. Refuse belongs on
|
||
the write path once `WRITE(10)` exists to give it something to gate against.
|
||
|
||
`sig_start_fblock` is hardcoded to `0` at the call site — correct for today's unpartitioned
|
||
raw test/real media (no GPT parser exists yet), explicitly flagged in the code comment as
|
||
the one place that will need to change to a real GPT-partition-relative lookup once that
|
||
parser lands, isolated from `homeblocks_sig.c`'s own location-agnostic check logic.
|
||
|
||
**Verified live**, not just compiled: hot-attached `disk/usb-thumbdrive-test.img` (blank
|
||
media, no `LAHB` magic) through a running amd64 instance's QMP socket (`blockdev-add` +
|
||
`device_add usb-storage,bus=xhci0.0`) — captured exactly 4 real TUR+READ10 BOT cycles
|
||
(matching the header's 4 forth-block span) followed by `xhci: USB drive not recognized
|
||
(blank or foreign media) -- read-only general use only`, then normal attach completing
|
||
successfully afterward (no regression — no `MSC block-subsystem attach failed`).
|
||
Conservation intact, no panic. Clean zero-warning compile and clean boot on all three
|
||
architectures.
|
||
|
||
- [x] **Resolved (2026-08-26): reuse `acl_pinned` directly, no new flag needed.** The open
|
||
question assumed credential data "isn't a dictionary word" — but the design already chosen
|
||
for it (`ZUSE-CERT-LO`/`HI`, `ACL-CA-KEY-LO`/`HI`) are `CONSTANT` words, i.e. real
|
||
`DictEntry`s. `acl_pinned`'s enforcement is more general than assumed: `vm_create_word()`
|
||
(`dictionary_management.c:394-404`) — the single choke point every word-defining construct
|
||
goes through — unconditionally refuses to let *anything* shadow a pinned name ("Pin is
|
||
permanent: no word may shadow a pinned entry — ever, by anyone"), a real general
|
||
redefinition guard, not just an ACL-mode-change lock. `zuse.4th`'s `ACL-ZUSE-BOOT` already
|
||
pins both cert constants today — the mechanism is already wired for this.
|
||
|
||
**Real gap surfaced along the way, not yet fixed:** `ACL-ZUSE-BOOT` self-activates and pins
|
||
`ZUSE-CERT-LO`/`HI` unconditionally on *every* boot — before any legitimate minting step
|
||
could ever run, permanently locking in the `0` placeholder on the very first boot. The boot
|
||
sequence needs to distinguish "already minted, pin it" from "not yet minted, don't pin yet"
|
||
before minting can work at all.
|
||
|
||
**This directly shaped the next design pass (2026-08-26, Captain Bob):** a dedicated
|
||
`disk/zuse.img` QEMU test thumbdrive, "bleachable" back to pristine/unminted state for
|
||
repeated first-boot testing; a one-time first-boot mint-Zuse flow (mint → write real cert →
|
||
blow the fuse → *then* pin, resolving the gap above); a separate, ongoing `S" name" MINT`
|
||
word for an authenticated Zuse session to mint additional regular users; and a Zuse recovery
|
||
path, explicitly flagged as unresolved and risky if rushed — not to be designed casually,
|
||
since the earlier "no software recovery, mint a new one" rule existed specifically to close
|
||
a hole a careless recovery path could reopen.
|
||
|
||
**First piece implemented (2026-08-26): the `zuse.img` bleach mechanism.**
|
||
`disk/zuse.img` (64MB, blank, matching the existing USB-fixture convention exactly — see
|
||
`disk/README.md`) + `scripts/bleach_zuse_img.sh` (idempotent reset back to blank). Verified
|
||
live: hot-attached via QMP (same method as the warn-and-refuse verification above) — reads
|
||
back as `HOMEBLOCKS_SIG_BLANK` (`xhci: USB drive not recognized (blank or foreign media)`),
|
||
correctly simulating a genuine first boot. Deliberately flat/raw, not GPT-partitioned,
|
||
matching `homeblocks_sig_check()`'s current `sig_start_fblock=0` call site — both move to a
|
||
real GPT-partition-relative offset together once a parser exists, not attempted here. No
|
||
kernel code touched this step (host-side test tooling only), so no 3-arch acceptance boot
|
||
needed — single live amd64 QMP-hotplug confirmation is the right verification tier.
|
||
|
||
**Correction (Captain Bob, 2026-08-26): drives are not bound to any particular size.**
|
||
64MB was only ever this fixture's arbitrary test-convenience size, matching
|
||
`usb-thumbdrive-test.img`'s existing BOT-driver-testing precedent — never a real-world
|
||
constraint. Audited for anywhere this might have implied otherwise: the actual format
|
||
(`homeblocks_sig_t`) already carries its own `metadata_devblocks` field and hardcodes no
|
||
size anywhere, confirmed clean. `scripts/bleach_zuse_img.sh` gained a `--size-mb` override
|
||
so this was never a hidden assumption baked into the tooling either. The earlier
|
||
`project_usb_thumbdrive_gpt_layout.md` memory's "16GB reference size" phrasing (already
|
||
hedged as tentative, but risked reading as a target) corrected to state the point
|
||
explicitly — the GPT layout's design point is the *proportions* (small metadata partition,
|
||
everything else block storage), not any absolute size.
|
||
|
||
**Still open, not attempted:** the mint-then-pin boot-sequence fix itself, the `MINT` word,
|
||
and the Zuse recovery path.
|
||
|
||
**Correction, supersedes the "reuse `acl_pinned`" resolution above (2026-08-26): a pinned
|
||
`CONSTANT` is not actually tamper-proof.** `ACL-PIN`/`acl_pinned` only guards against
|
||
*redefinition* — `vm_create_word()`'s pin check blocks a second `: ZUSE-CERT-LO ... ;`, but
|
||
nothing stops `' ZUSE-CERT-LO >BODY !` from overwriting the same word's data field in place.
|
||
A `CONSTANT`'s value lives in its data field, so the earlier design left the cert mutable
|
||
from FORTH despite being "pinned." Found while starting the mint-then-pin boot-sequence fix
|
||
itself; fixing that gap came first since building a real mint flow on top of a tamperable
|
||
store would just re-open the hole later.
|
||
|
||
**Fixed (2026-08-26): moved cert storage out of the dictionary entirely.** New
|
||
`VM` struct fields (`include/vm.h`): `zuse_cert_lo`/`zuse_cert_hi` (the cert value) +
|
||
`zuse_cert_installed` (one-time fuse bit). New `vm_zuse_cert_install(vm, lo, hi)`
|
||
(`src/vm.c`) — C-only, no FORTH word wraps it, returns `-1` on a second call rather than
|
||
silently re-installing (a second call is a caller bug, not a runtime condition to recover
|
||
from). No FORTH store word exists or should exist for these fields, closing the `>BODY`
|
||
path structurally rather than by convention. Three new read-only C primitives
|
||
(`src/word_source/starforth_words.c`, same shape as the existing `HEARTBEAT-TICKS@`):
|
||
`ZUSE-CERT-LO@`, `ZUSE-CERT-HI@`, `ZUSE-CERT-INSTALLED?`. `capsules/zuse.4th`'s old
|
||
`ZUSE-CERT-LO`/`HI` `CONSTANT` words (and `ACL-ZUSE-BOOT`'s two now-pointless `ACL-PIN`
|
||
calls on them) deleted outright rather than left as dead/insecure scaffolding — `mkcapsule
|
||
--lint` clean (31/31) after the edit. `vm_zuse_cert_install()` has no caller yet: the real
|
||
mint flow still doesn't exist (Milestone 6 CA + the `MINT` word are both still open), and
|
||
calling it with a placeholder value would just be a stub wearing the shape of a fix — so
|
||
this stays an honest, complete slice (storage + read accessors) with the actual mint-then-
|
||
pin sequence still explicitly open, not faked. **Verified:** hosted `make` build clean,
|
||
zero warnings; clean boot to `ok>` on all three architectures (amd64/aarch64/riscv64),
|
||
Stadium conservation intact (43691/21845/65536) on all three, no panics or guest errors.
|
||
ACL is opt-in (`init.4th`'s `S" ACL.4th" EXEC` commented out by default) so the new words
|
||
weren't exercised live from the REPL this pass — compile/lint/boot verification only.
|
||
|
||
**MINT word design, picked up 2026-08-26.** Before scoping `MINT` itself, found a real
|
||
conflict with an existing, deliberate decision: `include/starkernel/ed25519.h` is verify-
|
||
only by design — "this kernel never signs or generates keys (no entropy source to do so
|
||
safely anyway); signing happens in the host-side build tool" (`FABRIC-2.md`, Milestone 6,
|
||
2026-08-22). But the vision for `MINT` (§D below) is an *interactive*, on-device
|
||
`S" name" MINT` word — an authenticated Zuse session signing a new user's cert live, at
|
||
runtime. A kernel that structurally never signs can't do that as envisioned. Raised
|
||
directly; **decided (Captain Bob, 2026-08-26): give the kernel a real signing capability**
|
||
rather than reshape `MINT` around verify-only. This reopens the prior "no entropy source"
|
||
constraint deliberately, not by accident.
|
||
|
||
**Phase A — `virtio-rng`, done 2026-08-26.** Checked what entropy is actually available
|
||
before choosing a design: `include/starkernel/vm_uuid.h` already found, for VM UUIDs, that
|
||
amd64 has RDRAND and riscv64 has the Zkr extension, but QEMU's aarch64 CPU models (including
|
||
`max`) expose neither RNDR nor any RNG property at all — confirmed directly against QEMU
|
||
10.2.1. That's why VM UUIDs use a deterministic PRNG uniformly instead of a per-arch split;
|
||
that same choice is **not safe for Ed25519 keygen** — a seed drawn from a known value makes
|
||
the private key predictable. Decided: add a `virtio-rng` device instead of a per-arch
|
||
RDRAND/Zkr split with a weaker aarch64 fallback — QEMU supplies real host entropy
|
||
identically on all three arches, closing the aarch64 gap directly (QEMU-only; real hardware
|
||
at Milestone 8 needs a real per-arch RNG driver, a separate later problem).
|
||
|
||
New `include/starkernel/virtio_rng.h` + `src/starkernel/virtio/virtio_rng.c`, transport
|
||
plumbing (PCI capability walk, common-cfg feature negotiation, split virtqueue) mirroring
|
||
the existing `virtio_blk.c` exactly — same device family, same quirks. Simpler shape than
|
||
block: one virtqueue, one device-writable descriptor, no request header or status byte (the
|
||
entropy device has none); `virtio_rng_get_bytes()` loops internally since the device may
|
||
return fewer bytes than requested per round. `-object rng-random,id=rng0,filename=/dev/urandom`
|
||
+ `-device virtio-rng-pci` added to all three arches' QEMU invocations
|
||
(`Makefile.starkernel`). Wired into boot (`kernel_main.c`, right after the existing
|
||
`virtio_blk_find_artemis()` call site, same graceful-noop-on-absence precedent).
|
||
|
||
**Verified live, not just compiled:** a temporary probe (written, run once, captured,
|
||
reverted — per this project's standing probe convention) pulled 16 real bytes through the
|
||
full request/notify/poll/used-ring round trip on all three architectures and printed them:
|
||
amd64 `be9223909b86a8ccbfff705ccae2caa6`, aarch64 `861487df65a6d7a26b2c9c34f5ff96ee`, riscv64
|
||
`fec51d80e8c169a35aad9d908eab2f39` — three different values, confirming real entropy, not a
|
||
stale or repeated buffer. Probe reverted; permanent code is just the driver + init call. A
|
||
second, final 3-arch acceptance boot ran against that reverted code (not the probe build) to
|
||
confirm the shipped state itself is clean. Clean zero-warning compile and clean boot to
|
||
`ok>` on all three architectures, Stadium conservation intact (43691/21845/65536), no panics
|
||
or guest errors on either pass.
|
||
|
||
**Still open: Phase B (real Ed25519 keygen/signing, seeded from this entropy) and Phase C
|
||
(the `MINT` word itself, cert format, and whether Zuse's own keypair needs to chain to the
|
||
Milestone 6 offline root CA or is a self-sovereign instance-local root of trust).**
|
||
|
||
**Phase B — real Ed25519 keygen/signing, done 2026-08-26.** Extended
|
||
`include/starkernel/ed25519.h`/`src/starkernel/crypto/ed25519.c` (previously verify-only)
|
||
with `ed25519_keygen(seed, pubkey_out)` and `ed25519_sign(seed, msg, msg_len, sig_out)`, per
|
||
RFC 8032 §5.1.5/5.1.6, reusing every point-arithmetic primitive verify already had
|
||
(`scalar_mult`, `point_compress`, the base-point constants) — no new curve code, only the
|
||
seed-expansion/clamping and per-message nonce derivation verify never needed. Signing is
|
||
deterministic (nonce derived from seed+message, not fresh randomness): only keygen ever
|
||
touches entropy, via a caller-supplied seed (`virtio_rng_get_bytes()`, Phase A) — keygen
|
||
itself still generates nothing and trusts the caller for randomness quality, matching this
|
||
file's original design philosophy exactly.
|
||
|
||
New `scalar_muladd()` (`scalar25519.c`/`.h`) for signing's `S = (k*a + r) mod L` step, the
|
||
one piece of scalar arithmetic verify never needed (verify only ever reduced or compared,
|
||
never multiplied scalars). Schoolbook 256×256-bit multiply into a `u128` wide accumulator
|
||
with exactly one final carry-propagation pass — deliberately the same shape as `fe25519.c`'s
|
||
existing field multiply, because that file's own history records a real bug from trying to
|
||
carry mid-accumulation instead of in one final pass; structurally can't repeat that mistake
|
||
this way. Reduces the result via the existing, already-proven `scalar_reduce512()` rather
|
||
than writing new modular-reduction logic.
|
||
|
||
**Verified against an independent implementation, not self-consistency** — this project's
|
||
own standing lesson (two real, invisible-by-inspection bugs in the original from-scratch
|
||
field arithmetic, an off-by-one-hex-digit hand-transcribed SHA-512 vector) means a passing
|
||
self-check proves nothing on its own. Built a throwaway host test harness (compiled, run,
|
||
discarded — the crypto files have no `__STARKERNEL__` gate, so they link as an ordinary
|
||
Linux binary) against Python's `cryptography` library (OpenSSL-backed). Six trials — five
|
||
random seed/message pairs (message lengths 1, 32, 255, 1000 bytes) plus the empty-message
|
||
case — every one produced a byte-for-byte identical public key and signature to the
|
||
independent implementation, not just a signature this codebase's own verify accepted.
|
||
**Verified on-target too:** clean zero-warning compile of the crypto files on all three
|
||
architectures, and a full 3-arch QEMU acceptance boot (amd64/aarch64/riscv64) — all clean to
|
||
`ok>`, Stadium conservation intact, no panics or guest errors. Nothing calls
|
||
`ed25519_keygen()`/`ed25519_sign()` from the live kernel path yet (Phase C's job); this pass
|
||
is compile/link/boot-regression verification for the crypto library itself.
|
||
|
||
**Still open: Phase C** — the `MINT` word itself, cert format, and whether Zuse's own
|
||
keypair needs to chain to the Milestone 6 offline root CA or is a self-sovereign
|
||
instance-local root of trust.
|
||
|
||
**Phase C scoping, 2026-08-26.** Three findings before any code:
|
||
|
||
1. **Resolved, not a real conflict: Zuse doesn't need Milestone 6's CA.** That CA chain is
|
||
specifically for *capsule/code signing* (root → snakeoil intermediate → per-capsule
|
||
Ed25519 signatures verified at capsule-load time) — a different trust domain from *user
|
||
identity*. The vision's own framing ("we mint one and only one Zuse user and blow a fuse
|
||
... the only way around is a new system") already implies Zuse's authority comes from
|
||
being the unique first-boot mint on *this instance*, not from an external chain.
|
||
**Decided: Zuse is a self-sovereign, instance-local root of trust**, keypair generated
|
||
on-device from real entropy (Phase A+B). Regular users, minted later via `MINT`, get
|
||
certs signed by *Zuse's* key, not the Milestone 6 CA — two independent PKI domains.
|
||
2. **A real gap in this session's own earlier work:** `vm_zuse_cert_install(vm, lo, hi)`
|
||
(the very first change this session made, before Phase A existed) only holds two
|
||
`uint64_t` (16 bytes) — sized against the old placeholder `ZUSE-CERT-LO`/`HI` FORTH-cell
|
||
design, not against what a real Ed25519 keypair needs (32-byte pubkey alone, well over
|
||
100 bytes for a full cert). Needs expanding before Phase C can store anything real.
|
||
3. **A genuine blocker, found by asking where the cert would actually live:** "mint once,
|
||
ever" requires surviving reboots, but `Makefile.starkernel`'s `qemu` target copied a
|
||
fresh, pristine `OVMF_VARS.fd` on *every* invocation (not just after `clean`) — so a
|
||
UEFI-NVRAM-based cert (the real-hardware-compatible option, and this codebase already has
|
||
a live `SetVariable`/`GetVariable` precedent via `SF_VAR_REBOOT_TRIES`/`SF_VAR_BOOT_ARGS`)
|
||
would never actually persist under this project's own normal test workflow. Digging
|
||
further: aarch64's `qemu` recipe had no persistent NVRAM store *at all* — a single
|
||
combined `-bios $AAVMF_CODE` argument, no separate writable VARS pflash drive like
|
||
amd64/riscv64 have. **Decided (on request): fix the harness rather than switch
|
||
substrates.** amd64/riscv64: the VARS-template copy is now conditional on the destination
|
||
not already existing, so `clean` (which deletes the whole `build/$(ARCH)/kernel` tree,
|
||
`OVMF_VARS.fd`/`RISCV_VARS.fd` included) is the bleach step, and a bare `make qemu` now
|
||
preserves NVRAM across runs — exactly matching the existing "always pass `clean` before
|
||
`qemu`" acceptance convention, no new bleach script needed. aarch64: restructured to
|
||
split CODE(ro)/VARS(rw) pflash drives matching the other two (host has
|
||
`/usr/share/AAVMF/AAVMF_VARS.fd` alongside the existing `AAVMF_CODE.fd`), with a
|
||
graceful fallback to the old single-`-bios` mode (and a console note) on a host that only
|
||
has non-split firmware packaged, so this doesn't regress environments without one.
|
||
**Verified live:** all three architectures still boot clean to `ok>` with the new
|
||
pflash arrangement, Stadium conservation intact, no panics or guest errors — this is
|
||
infrastructure-only (no cert code yet), so a plain boot-regression check is the right
|
||
verification tier.
|
||
|
||
**Still open:** the actual cert struct (expanding past the 16-byte placeholder), the
|
||
first-boot mint-vs-already-minted boot sequence using `SetVariable`/`GetVariable`, and the
|
||
`MINT` word itself.
|
||
|
||
**Cert struct expanded (2026-08-26):** `vm_zuse_cert_install()` (both `src/vm.c`'s hosted
|
||
copy and a new kernel-side duplicate in `src/starkernel/vm/vm_core.c` -- the kernel build's
|
||
`VM_EXCLUDE` list drops `src/vm.c` entirely, same reason `vm_set_base()` already has two
|
||
independent copies) now takes a real 32-byte seed + 32-byte pubkey instead of the old
|
||
16-byte placeholder. FORTH-side `ZUSE-CERT-LO@`/`HI@` replaced with `ZUSE-PUBKEY@ ( i -- u )`
|
||
(8-byte LE chunk `i`, 0..3, of the public half only -- the seed has no FORTH access at all).
|
||
`ACL-ZUSE-BOOT` now checks `ZUSE-CERT-INSTALLED?` before authenticating rather than
|
||
authenticating unconditionally. Verified: clean compile and clean boot on all three
|
||
architectures.
|
||
|
||
**NVRAM persistence attempt: crashed, root-caused, reverted -- do not retry as designed.**
|
||
First attempt placed the mint-or-load `GetVariable`/`SetVariable` logic right after
|
||
`virtio_rng_init()` (before `capsule_birth_mama()`); it page-faulted (`CR2` inside the OVMF
|
||
flash MMIO window, a supervisor write to a not-present page) partway through boot. Moved the
|
||
same logic to the one place in this codebase already calling `SetVariable` post-
|
||
`ExitBootServices` successfully (`SF_VAR_REBOOT_TRIES`, much later in boot) — **identical
|
||
crash, same RIP and CR2** — which disproved the "too early in boot" theory outright: it isn't
|
||
a timing issue.
|
||
|
||
**Localized precisely (advisor-directed, one boot, debug markers around each call):**
|
||
`GetVariable` returns fine. `SetVariable` **with real 64-byte data** never returns — that's
|
||
the exact fault site. The pre-existing `SF_VAR_REBOOT_TRIES` call that looked like a working
|
||
precedent is actually a **delete of a variable that's never existed** (`size=0, data=NULL`) —
|
||
a fundamentally different, much cheaper internal path than a real data write, so it proved
|
||
nothing about real persistence being safe. **Root cause: this kernel's VMM never maps
|
||
whatever memory region OVMF's variable service needs to actually write flash-backed variable
|
||
data** — a real gap in UEFI runtime-services support, not specific to Zuse. Fixing it for
|
||
real means walking the UEFI memory map for the relevant regions and mapping them into the
|
||
kernel's own page tables, and per Section U's own note, the flash window's location is
|
||
firmware/arch-specific (OVMF's differs from AAVMF's and EDK2-riscv64's), so "walk the map and
|
||
map everything" is not guaranteed 3-arch-uniform even once attempted.
|
||
|
||
**Second, independent finding (not a bug, a design flaw in the persistence choice): storing
|
||
the raw 32-byte seed in NVRAM was a real defect regardless of the crash.** `SetVariable` was
|
||
called with `EFI_VARIABLE_RUNTIME_ACCESS`, meaning any later-loaded UEFI application or the
|
||
booted OS itself could read Zuse's private key straight out of NVRAM. For an irrevocable
|
||
"one and only one Zuse, ever" root of trust, that undermines the property the design exists
|
||
to provide — this would have needed fixing even had the crash not happened.
|
||
|
||
**Decision needed, not yet made:** given virtio-blk writes are already proven working on all
|
||
three architectures in this repo (`vblk_write`, Artemis's own persistence across runs), a
|
||
dedicated file-backed system-identity disk (mirroring `disk/artemis.img`'s existing pattern,
|
||
separate from Artemis's internal storage and separate from home-blocks USB thumbdrives) is
|
||
the substrate with no open unknowns today — recommended over either fixing the UEFI
|
||
flash-mapping gap (real but large, unscoped VMM work) or accepting the NVRAM approach as
|
||
originally designed (has the exposed-seed defect regardless). Not decided or built yet.
|
||
**Reverted to a known-safe state:** all Zuse mint/NVRAM code removed from `kernel_main.c`
|
||
(only two harmless includes remain), `init.4th`'s `ACL.4th` line back to its documented
|
||
commented-out default. Verified clean compile and clean boot on all three architectures in
|
||
this reverted state.
|
||
|
||
**Substrate corrected (Captain Bob, 2026-08-26): no files, ever — this OS's entire reason
|
||
for being is anti-POSIX, anti-file.** The "dedicated system-identity disk" recommendation
|
||
above was framed in file/filesystem language by mistake; corrected before any code was
|
||
written. The only real persistence primitives here are content-addressed capsules and raw
|
||
LBN-numbered blocks (`block_subsystem.c`) — never a filesystem, never file paths. Saved as
|
||
`feedback_no_files_anti_posix.md` so this isn't re-learned next session.
|
||
|
||
**Design, agreed on request: a growable metadata fence at the TOP of a device's block
|
||
space, mirroring the bottom BAM reservation from the opposite end.** `block_subsystem.c`'s
|
||
BAM already reserves the bottom `BLK_DISK_SYS_RESERVED` (32) blocks of every attached
|
||
device, invisible to FORTH's `BLOCK`/`BUFFER`. Zuse's cert (and future system metadata)
|
||
gets a second reservation at the *top* of the same device, starting at
|
||
`BLK_META_FENCE_INIT` (128) blocks and growing downward as needed — the two reservations
|
||
grow toward each other from opposite ends, never colliding, same shape as a stack/heap.
|
||
Explicitly never RAM-backed (the fast-RAM/ramdrive LBN ranges are documented as volatile in
|
||
this same file's own header comment — losing Zuse's identity to a RAM eviction is exactly
|
||
the failure this is designed against). Reuses Artemis's own already-attached, already-proven
|
||
virtio-blk device — no new device attachment. Rejected reusing BAM's own bottom-reserved
|
||
zone directly: those 32 blocks are fully claimed by BAM/volume-metadata bookkeeping, not
|
||
free space.
|
||
|
||
**Step 1 (field round-trip) implemented and verified 2026-08-26, allocator not yet
|
||
touched.** New `meta_fence_blocks` field in `blk_volume_meta_t`, appended after
|
||
`reloc_devblocks` and carved from `_pad[]` — identical graceful-default technique the
|
||
`reloc_start`/`reloc_devblocks` fields already established (a pre-existing formatted volume
|
||
reads the field back as 0 via its zeroed former padding, not a format-breaking change).
|
||
Added a compile-time `_Static_assert(sizeof(blk_volume_meta_t) == 4096, ...)`, same
|
||
discipline `homeblocks_sig.h` already uses — caught a real bug immediately: the hand-summed
|
||
`_pad[]` size formula was off by 4 bytes (a compiler-inserted alignment gap before
|
||
`tracked_blocks` that the manual byte-count missed), found via `offsetof()` rather than by
|
||
re-deriving the arithmetic by hand again, consistent with this project's standing rule to
|
||
never trust a hand-derived numeric claim in this class of code.
|
||
|
||
Worked against disposable clones throughout, never the real `disk/artemis.img`
|
||
(`ARTDISK=...` is `?=`-overridable) — `disk/artemis-metafence-fresh.img` (blank, exercises
|
||
the fresh-format path) and `disk/artemis-metafence-test.img` (a copy of the pre-existing
|
||
`artemis.img`, exercises the graceful-default-on-reload path), both kept as regression
|
||
fixtures per `disk/README.md`'s existing convention (mirrors `artemis-reloc-test.img`
|
||
exactly). **Verified independently via direct byte reads of the disk image, not the
|
||
kernel's own self-report** (`log_message(LOG_INFO, ...)` turned out not to reach serial
|
||
output at all in this build — an unrelated, pre-existing log-level gap, not a regression):
|
||
fresh format writes `meta_fence_blocks=128` at header byte offset 184; a second boot without
|
||
reformatting reads it back unchanged; the pre-existing old-format image correctly reads back
|
||
0. Full 3-arch acceptance boot against the real, untouched `disk/artemis.img` also clean —
|
||
conservation intact, no panics.
|
||
|
||
**Step 2 (allocator + read/write accessors), done 2026-08-26.** Units corrected from "Forth
|
||
1 KiB blocks" to 4 KiB devblocks (matching `bam_devblocks`/`reloc_devblocks`) before anything
|
||
depended on the original meaning — a clean fix, not a migration, since nothing consumed the
|
||
field yet. This let the fence fold directly into `compute_totals_from_B()`'s existing
|
||
`payload4k` calculation (`total_devblocks - 1 - B - R - F`, F = `meta_fence_blocks`) instead
|
||
of needing a second, separate subtraction against `user_blocks` — `total_blocks`,
|
||
`user_blocks`, and `free_blocks` all shrink correctly for free, in both the fresh-format and
|
||
reload code paths, from this one formula change.
|
||
|
||
New `blk_meta_zone_read()`/`blk_meta_zone_write()` (`block_subsystem.c`/`.h`) — raw, unpacked
|
||
4 KiB devblock I/O (no Forth-block packing, same shape as the header/BAM/reloc-table
|
||
regions), addressed by `devblock_from_top` counting down from the device's last physical
|
||
devblock, refusing (not silently clamping) if the index isn't within the on-disk
|
||
`meta_fence_blocks`. No FORTH word wraps either — C-only, same discipline as
|
||
`vm_zuse_cert_install()` itself, which will be this zone's first real tenant.
|
||
|
||
**Verified independently at every step, never trusting the kernel's own report:**
|
||
- Capacity math: read a freshly-formatted image's header bytes directly and independently
|
||
recomputed the expected `total_blocks` in a separate Python script using the same formula
|
||
— exact match (22647, down from what it would have been without the fence).
|
||
- Accessor correctness: a temporary probe (written, run, captured, reverted) wrote a known
|
||
256-byte-repeating pattern via `blk_meta_zone_write(0, ...)`, read it back via
|
||
`blk_meta_zone_read(0, ...)`, and compared in-memory (`PASS`) — then, independently,
|
||
read the raw image file at the exact expected physical byte offset
|
||
(`(total_devblocks-1)*4096`) and confirmed the pattern landed there byte-for-byte.
|
||
- `log_message(LOG_INFO, ...)` still doesn't reach serial output in this build (same
|
||
pre-existing gap noted in Step 1) — all verification here used `console_println` (which
|
||
does reach serial) for the temporary probe, and direct file reads for everything else.
|
||
|
||
Full 3-arch acceptance boot (real, untouched `disk/artemis.img`, probe code fully reverted)
|
||
clean on all three architectures — conservation intact, no panics.
|
||
|
||
**Still open:** wiring `vm_zuse_cert_install()`'s seed+pubkey to actually persist through
|
||
these new accessors (the zone exists and works; nothing writes Zuse's cert into it yet), and
|
||
the `MINT` word itself.
|
||
|
||
**Step 3 (Zuse's cert wired to the fence), done 2026-08-26 -- first-boot mint-then-load is
|
||
real, end to end.** New `include/starkernel/zuse_cert_devblock.h`: a small, standalone
|
||
on-disk record format (`zuse_cert_devblock_t` -- magic + version + 32-byte seed + 32-byte
|
||
pubkey + a real CRC-64/ISO from day one, same "real from day one" discipline
|
||
`homeblocks_sig_t` already established, since this gates a real security check) occupying
|
||
devblock_from_top=0 of the fence. Deliberately its own header, not inlined at the boot-time
|
||
call site: the still-open `MINT` word will be a second consumer of this exact format later.
|
||
|
||
`kernel_main.c`'s mint-or-load logic moved from the crashed NVRAM approach to this: read
|
||
devblock 0 of the fence, and if magic/version/CRC all check out, install the existing
|
||
cert; otherwise, if `virtio_rng` is ready, mint a fresh one (Phase A+B) and write it. Runs
|
||
right after `virtio_rng_init()`, well before `capsule_birth_mama()` -- unlike the crashed
|
||
NVRAM attempt, raw block I/O against Artemis's already-proven virtio-blk device has no
|
||
boot-timing risk at all, so the earlier "re-invoke `ACL-ZUSE-BOOT` after Mama birth"
|
||
workaround is no longer needed; `ACL.4th`/`zuse.4th`'s self-activating `ACL-ZUSE-BOOT` sees
|
||
a correctly-populated cert on its one, ordinary first pass.
|
||
|
||
**Verified live, independently, across every real scenario, never trusting the kernel's own
|
||
report:**
|
||
- **Fresh mint** (blank `disk/artemis-metafence-fresh.img`): boot logs `Zuse: minted, fuse
|
||
blown`; the on-disk record at the exact expected physical offset independently decodes to
|
||
magic bytes `b'ZUSE'`, version 1, a real 32-byte seed and pubkey, and a CRC that an
|
||
independent from-scratch Python re-implementation of the exact CRC-64/ISO algorithm
|
||
(table generation included, not just the check) confirms byte-for-byte.
|
||
- **Reload** (reboot the same now-minted image, no reformat): boot logs `Zuse: cert loaded
|
||
from block fence`; the on-disk seed and pubkey are byte-for-byte identical to the first
|
||
boot's -- genuinely "mint once, ever," not a silent re-mint.
|
||
- **Graceful refusal on a pre-fence volume** (`disk/artemis-metafence-test.img`,
|
||
`meta_fence_blocks=0`): `blk_meta_zone_read`/`write` both correctly refuse (no space to
|
||
read or write), so the kernel mints a cert for RAM/this-boot-only use and honestly reports
|
||
`Zuse: minted but fence write FAILED (not persistent)` -- no crash, no silent data loss,
|
||
no corruption of a device with no fence at all.
|
||
- **Real disk regression check:** the same graceful-refusal path exercised identically
|
||
against the real, untouched `disk/artemis.img` (which has no fence yet either) on all
|
||
three architectures -- clean boot, conservation intact, no panics, `disk/artemis.img`
|
||
itself reverted afterward (no committed churn).
|
||
|
||
**Phase 8's core arc is now functionally complete:** real entropy (Phase A) → real signing
|
||
(Phase B) → real, anti-file, block-native persistence (Phase C) → a working first-boot mint
|
||
that survives reboots. **Still open:** the ongoing `S" name" MINT` word for an authenticated
|
||
Zuse session to mint additional regular users (needs `zuse_cert_devblock_t`-format certs
|
||
signed by Zuse's own key, not just installed) -- the real remaining piece of the original
|
||
vision.
|
||
|
||
### From FABRIC-2.md §X, Milestone 5 — Console/VM key-match binding
|
||
|
||
- [ ] Settle the still-open question: reuse `ACL-PIN`/`acl_allow` directly, or build a
|
||
separate key-matching primitive — `ACL-PIN` gates word execution specifically and nothing
|
||
today gates console-session-to-VM ownership, so this decision needs to happen before any
|
||
code gets written here.
|
||
|
||
- [ ] Design the key/lock data shape (what the console presents, what the VM carries, how
|
||
they're compared).
|
||
|
||
- [ ] Wire drive insertion (Milestone 2e's hotplug signal, post-identity-authentication) to a
|
||
call into `capsule_birth_baby()` (confirmed a real, callable, on-demand birth path already)
|
||
to spin up or re-attach that identity's VM. **Traced 2026-08-27 (`FABRIC-3.md` §F.5): this
|
||
has no mechanism behind it yet — depends on both `CERTVERIFY` (identity-authentication) and
|
||
a new `RUNCAP` mechanism (§F.6, scoped) for per-identity VM content, since
|
||
`capsule_birth_baby()` today only births fixed, build-time-baked capsules.**
|
||
|
||
- [ ] Implement the actual attach/bind step — extending `sk_repl_set_active_vm()` (confirmed
|
||
to exist, currently an unguarded raw pointer-set) with the key-match check from above, so a
|
||
console can only bind to the one VM whose lock matches its key. **SCOPED 2026-08-27
|
||
(`FABRIC-3.md` §F.9)**: real gate is `USE` (`mama_forth_words.c:430-480`), the live,
|
||
already-working, currently-unguarded retarget word — check re-verifies the attached drive
|
||
live on every call; `installed=0` VMs stay freely targetable.
|
||
|
||
- [ ] Implement detach behavior on console disconnect or VM teardown. **SCOPED 2026-08-27
|
||
(`FABRIC-3.md` §F.10)**: a new `EJECT` word — flush via the existing `blk_vm_flush_all()`,
|
||
reset the console's active VM if bound to the departing VM, then `capsule_vm_kill()` (already
|
||
complete, no new teardown mechanism needed). Needs one small new piece of state `WIREBIND`
|
||
must own: which `VMUuid` is currently attached via the home-blocks USB path.
|
||
|
||
### From FABRIC-2.md §X, Milestone 6 — Kernel/capsule PKI signing chain
|
||
|
||
- [ ] Generate (offline, outside the kernel/repo entirely) the real root CA keypair — "stays
|
||
unrevocable," never embedded, never loaded by any kernel code.
|
||
|
||
- [ ] Generate the "snakeoil" intermediate certificate, signed by that real root CA (this is
|
||
a real CA-signed intermediate, not a self-signed/untrusted cert despite the name —
|
||
"snakeoil" names its informal/private-project status).
|
||
|
||
- [ ] Embed the already-CA-signed snakeoil intermediate as a capsule blob at build time
|
||
(mechanically proven already via the font-capsule precedent — no new embedding
|
||
infrastructure needed, just a new payload). **Bootstrapping resolved: no kernel-boot-time
|
||
verification of a hardcoded CA public key is needed at all** — trust is established once,
|
||
at build time, by whoever holds the real root CA and produces the build.
|
||
|
||
- [ ] Add a signing step to the `mkcapsule` build tool (or a separate signing tool) that
|
||
produces a signature alongside each capsule's existing xxHash64.
|
||
|
||
- [ ] Extend `MANIFEST_AUTO.md`'s generation to add a signature-status column, matching the
|
||
existing xxHash64 column's generation pattern.
|
||
|
||
- [ ] Implement magic-number-based content-type detection (Section U item 14) — a shared
|
||
primitive, also usable for Milestone 4's foreign-drive check.
|
||
|
||
- [x] **Root CA + snakeoil intermediate generated 2026-08-26**, entirely offline, in a sibling
|
||
directory outside this repo (`/home/rajames/CLionProjects/lithosananke-ca/`, not tracked by
|
||
git here). Ed25519, OpenSSL 3.5.5. Root: self-signed, 20-year validity (2026–2046),
|
||
`CN=LithosAnanke Root CA`. Intermediate: a real CA-signed cert (not self-signed despite the
|
||
name), 10-year validity, `CA:TRUE, pathlen:0` (can sign capsules, can't mint further
|
||
intermediates), chain verified (`openssl verify` returns `OK`). Both private keys `chmod
|
||
600`. The root key never touches this repo or any kernel code, per the design's own
|
||
requirement.
|
||
|
||
- [x] **Snakeoil intermediate embedded as a capsule, 2026-08-26.** Exported to DER
|
||
(`capsules/pki/snakeoil-intermediate.der`, 418 bytes) and dropped under `capsules/` —
|
||
confirmed the font-capsule precedent needed zero new infrastructure: `mkcapsule`'s
|
||
`process_file()` embeds any non-`.4th` file verbatim already. Shows up as capsule
|
||
`pki:snakeoil-intermediate.der` in the generated capsule table (38 capsules total, up from
|
||
37) — retrieve via `capsule_find_by_name()` + `capsule_get_payload()`, never
|
||
`capsule_exec_payload()` (it's a passive data blob, not executable capsule code).
|
||
|
||
- [x] **Minimal DER/X.509 parser written and independently verified, 2026-08-26.** New
|
||
`include/starkernel/x509_ed25519.h` + `src/starkernel/crypto/x509_ed25519.c`:
|
||
`x509_extract_ed25519_pubkey()`, a from-scratch, narrow DER walker (not a general ASN.1/
|
||
X.509 parser, per this milestone's own design decision) — walks `Certificate →
|
||
TBSCertificate → SubjectPublicKeyInfo`, handles the optional `[0] EXPLICIT Version` field
|
||
(present on v3 certs), verifies the `AlgorithmIdentifier` OID is exactly `1.3.101.112`
|
||
(RFC 8410 Ed25519) rather than assuming, and extracts the raw 32-byte key from the trailing
|
||
`BIT STRING`. Handles both short-form and long-form DER lengths (a real cert with v3
|
||
extensions routinely exceeds the 127-byte short-form limit). Every step bounds-checked
|
||
against the buffer end — refuses malformed input, never faults.
|
||
|
||
**Verified against ground truth, not self-consistency:** run against the real embedded
|
||
`snakeoil-intermediate.der`, the extracted 32-byte key matched `openssl pkey -pubin -text`'s
|
||
own reported public key byte-for-byte. Refusal path verified too: truncated input, 10 random
|
||
garbage bytes, an empty file, and a real RSA certificate (algorithm-mismatch case, not just
|
||
structural malformation) all correctly return failure rather than misreading or crashing.
|
||
Compiles clean on all three kernel architectures (no `__STARKERNEL__` guard needed — same
|
||
freestanding-safe shape as the other crypto files).
|
||
|
||
**Still open:** the signing step in `mkcapsule` (needs a new `sig[64]` field on
|
||
`CapsuleEntry` and a parallel emitted array in `capsule_generated.c`, since `CapsuleDesc`
|
||
itself has no spare bytes — confirmed exactly 64 bytes, every field used), wiring
|
||
`ed25519_verify()` into the three `capsule_validate()` call sites in `capsule_birth.c`
|
||
(**decided: land as WARN-only first, prove correct on all three architectures against both a
|
||
valid and a deliberately-corrupted capsule, then flip to hard-refuse in a separate step** —
|
||
a bug here has a much larger blast radius than anything else in Phase 8, since a false
|
||
refusal on Mama's own capsule means no `ok>` at all, on any architecture), and the
|
||
signature-status column on `capsules/BLOCK_MAP.md` (confirmed the real, live manifest target
|
||
— `capsules/MANIFEST_AUTO.md` is stale/dead, not regenerated since 2026-07-05, flag as docs
|
||
drift rather than a real target).
|
||
|
||
**mkcapsule signing + capsule_birth.c wiring, done 2026-08-26 -- landed WARN-only exactly as
|
||
decided.** First attempt shelled out to `openssl pkeyutl -sign` (`fork`/`execlp`, not
|
||
`system()` -- interpolating the key path into a shell string would have been a real, if
|
||
narrow, injection vector). **Corrected on request: no new external host binary dependency
|
||
when the repo's own code can do the job** — this project's standing preference, same theme
|
||
as the earlier anti-file correction. Rewritten to link `ed25519_sign()`
|
||
(`src/starkernel/crypto/ed25519.c`, already independently verified against OpenSSL in Phase
|
||
B) directly into `mkcapsule`. This needed one more small piece: `tools/pkcs8_ed25519.c`, a
|
||
narrow DER walker (same shape as `x509_ed25519.c`, deliberately not shared code — small
|
||
enough that duplicating a few TLV-walking lines was simpler than threading a header between
|
||
the kernel crypto tree and host tooling) extracting the raw 32-byte seed from the
|
||
intermediate's PKCS#8 private key, plus a minimal self-written base64 decoder (PEM is
|
||
`openssl genpkey`'s default output, not raw DER — checked the repo for an existing decoder
|
||
first, found none). **Verified end-to-end before wiring anything in:** the extracted seed,
|
||
run through this project's own `ed25519_keygen()`, reproduces the exact pubkey embedded in
|
||
the cert; a full self-contained sign+verify round-trip (zero openssl involvement) passes.
|
||
|
||
`CapsuleDesc` had no spare bytes (confirmed exactly 64, every field used), so signatures live
|
||
in a new parallel array, `CapsuleSigEntry capsule_signatures[]` (`{ sig[64], has_sig }`,
|
||
mirroring `capsule_names[]`'s existing shape), emitted by `mkcapsule --sign-key <path>` (new
|
||
optional CLI flag; omitted or the key file missing → every capsule gets `has_sig=0`,
|
||
graceful, not a build failure -- CI has no access to the offline key). New
|
||
`Makefile.starkernel` variable `SIGN_KEY` (defaults to this machine's actual key path,
|
||
`?=`-overridable, gated by `$(wildcard ...)` so a fresh checkout or CI never fails for
|
||
lacking it).
|
||
|
||
New `capsule_sig.c`/`.h`: `capsule_verify_signature()`, deliberately a separate, additive
|
||
function, not folded into the already-tested `capsule_validate()` (whose signature and
|
||
behavior stay untouched). Finds and parses the embedded `pki:snakeoil-intermediate.der`
|
||
capsule's pubkey once per boot (cached — every later capsule birth reuses it, the cert can't
|
||
change mid-boot), then `ed25519_verify()`s the requested capsule's payload against its stored
|
||
signature. Wired into all three `capsule_validate()` call sites in `capsule_birth.c`
|
||
(`capsule_birth_mama`, `capsule_birth_baby`, `capsule_run_experiment`) via
|
||
`log_message(LOG_WARN, ...)` on anything other than `CAPSULE_SIG_OK` — never refuses.
|
||
(Hit one unrelated build snag wiring this in: `vm.h` and `log.h` both define `LOG_LINE_MAX`
|
||
for two completely unrelated concepts — vm.h's own is the persistent block-log line size,
|
||
unconditional `#define`; log.h's is `#ifndef`-guarded. `log.h` has to be included *after*
|
||
`vm.h` in any TU that needs both, or the build fails under `-Werror`; fixed and commented in
|
||
`capsule_birth.c` so the next person touching includes there doesn't rediscover it the hard
|
||
way.)
|
||
|
||
**Verified independently, both directions, live in the real kernel:** a full clean build (38
|
||
real capsules, all signed) boots clean on all three architectures with zero signature
|
||
warnings — the expected outcome when every signature is genuinely valid, not merely an
|
||
absence of testing. Separately, hand-corrupted one byte of Mama's own `init.4th` capsule's
|
||
*stored signature* (not its payload/hash, which `capsule_validate()` already checks
|
||
independently and would have masked the test) directly in the generated
|
||
`capsule_generated.c`, without regenerating from source, and rebuilt only the changed object:
|
||
produced exactly `capsule sig: init.4th: INVALID -- signature does not verify` on boot, and
|
||
**the kernel still reached `ok>`** — proving warn-only genuinely doesn't refuse anything yet,
|
||
precisely as decided. Reverted the tampered build before the final acceptance pass. Final,
|
||
untampered state: clean 3-arch acceptance boot, conservation intact, zero warnings, real disk
|
||
untouched.
|
||
|
||
**Still open:** flipping WARN to hard-refuse (a separate, deliberate step once this has run
|
||
clean for a while, not bundled into this pass) and the `BLOCK_MAP.md` signature-status
|
||
column.
|
||
|
||
**`BLOCK_MAP.md` signature-status column, done 2026-08-26.** `--manifest` mode's file scan
|
||
(`manifest_file()`) is a completely separate code path from build mode's `process_file()` —
|
||
confirmed it only ever walks `.4th` files (never the embedded PKI cert or font capsule), and
|
||
the Makefile invokes `mkcapsule` twice per build for two different purposes (once to emit
|
||
`capsule_generated.c`, once for the manifest). Extended `--manifest` to accept the same
|
||
optional `--sign-key <path>` prefix as build mode (factored the key-loading code, previously
|
||
inline in build mode's arg parsing, into a shared `load_sign_key()` used by both) so the
|
||
manifest can report real per-capsule signing status without touching or requiring a rebuild
|
||
of `capsule_generated.c`. New `Signed` column on the capsule summary table: `yes`/`no` when
|
||
`--sign-key` was given, `n/a` (with an explanatory footnote) when it wasn't — never a bare
|
||
blank that could be misread as "unsigned." `Makefile.starkernel`'s manifest-generation call
|
||
site updated to pass the same `$(SIGN_KEY_ARGS)` the real build already uses, so
|
||
`capsules/BLOCK_MAP.md` reflects this machine's actual signed state by default. Verified:
|
||
clean compile, `capsules/BLOCK_MAP.md` correctly shows `yes` for all 31 tracked `.4th`
|
||
capsules on a real signed build; a quick amd64 boot (no kernel code touched by this step,
|
||
only host tooling) confirmed no regression, zero signature warnings, conservation intact.
|
||
|
||
**This closes every open item from FABRIC-2.md's original Milestone 6 punch list except
|
||
magic-number content-type detection (Section U item 14, shared with Milestone 4, not
|
||
started) and the hard-refuse flip (deliberately deferred).**
|
||
|
||
**Hard-refuse flip, done 2026-08-26 -- and it landed narrower than the original plan
|
||
assumed.** Before flipping, found that `CAPSULE_SIG_MISSING` (no signature at all) is the
|
||
*normal* state on every machine except this one -- CI and any other checkout have no access
|
||
to the offline key, by design. Refusing on `MISSING` the same as `INVALID` would brick boot
|
||
everywhere but here, not catch anything real. **Decided (on request): enforce ONLY on
|
||
`CAPSULE_SIG_INVALID`** (a signature that IS present but doesn't verify -- unambiguous
|
||
tampering/corruption evidence, safe to refuse on regardless of who's building).
|
||
`MISSING`/`NO_ROOT_KEY` stay WARN-only permanently, not as a transitional state.
|
||
|
||
All three `capsule_birth.c` call sites now `return CAPSULE_RUN_ERR_INVALID` on
|
||
`CAPSULE_SIG_INVALID`, after logging the same WARN as before. **Verified on all three
|
||
architectures, both directions, exactly per the original rollout commitment:** the positive
|
||
case (real signed capsules) reboots clean with zero warnings on amd64/aarch64/riscv64,
|
||
identical to before this flip. The negative case (the same one-byte signature corruption
|
||
technique used for the WARN-only proof, on Mama's own `init.4th`) now genuinely refuses --
|
||
`capsule_birth_mama()` returns the error -- on all three architectures identically:
|
||
`capsule sig: init.4th: INVALID` followed by `Init: Mama birth FAILED`. **The feared "no
|
||
ok> at all" blast radius didn't materialize**: `kernel_main.c` already had graceful
|
||
error handling for a failed Mama birth (log and continue, pre-existing code, not written for
|
||
this feature) -- the kernel reaches a degraded `ok>` rather than crashing or hanging, on all
|
||
three architectures. Final acceptance pass (real, correctly-signed capsules, all tampering
|
||
reverted) clean on all three, zero warnings, conservation intact, real disk untouched.
|
||
|
||
**Milestone 6 is now fully closed** except magic-number content-type detection (shared with
|
||
Milestone 4, genuinely separate scope, not started).
|
||
|
||
### From FABRIC-2.md §X, Milestone 7 — Contributor capsules / trust tiers
|
||
|
||
- [ ] Create the `capsules/contrib/` directory (mechanically trivial, matches existing
|
||
subdirectory convention — the directory itself is not the work).
|
||
|
||
- [ ] Add a `FLAG_CONTRIB` bit to `mkcapsule.c`'s flag system, assigned by path match
|
||
(`contrib/` prefix), same pattern as how `init.4th` already gets `FLAG_MAMA_INIT`.
|
||
|
||
- [ ] Decide and implement one of the four spitballed trust-tier directions (signature-
|
||
authority tiers / block-namespace sandboxing / QEMU-vs-real-hardware conditional
|
||
enforcement) — none chosen yet, this is a real decision point, not just an implementation
|
||
task.
|
||
|
||
- [ ] If block-namespace sandboxing is chosen: extend `mkcapsule`'s existing conflict-
|
||
detection logic to also reject a `contrib/`-path capsule claiming blocks outside its
|
||
reserved range.
|
||
|
||
### From FABRIC-2.md §X, Milestone 8 — Bare-metal boot from physical USB
|
||
|
||
- [ ] Build a fresh `starkernel.iso` via `make -f Makefile.starkernel ARCH=amd64 clean` + the
|
||
ISO-build step.
|
||
- [ ] Identify the exact block device path for the target USB drive on the host doing the
|
||
flashing (`lsblk`/`dmesg` after insertion — care needed, wrong device = data loss).
|
||
- [ ] `dd if=build/amd64/kernel/starkernel.iso of=/dev/sdX bs=4M status=progress` (or
|
||
equivalent) — confirm `dd` is the right tool for an El Torito ISO vs. needing `isohybrid`
|
||
first (open question, not yet verified).
|
||
- [ ] Physically boot the real machine from the flashed drive (BIOS/UEFI boot-order menu,
|
||
Secure Boot may need disabling — unknown until tried).
|
||
- [ ] Capture what happens with no serial-socket log available (real hardware has no
|
||
`qemu-serial-*.sock` to `socat` into) — decide the observation method.
|
||
- [ ] Confirm POST reaches the same 1012/0/0 result on real hardware as every QEMU acceptance
|
||
run.
|
||
- [ ] Confirm `ok>` prompt is reachable and a basic command (e.g. `HEARTBEAT-TICKS@ .`) works
|
||
identically to QEMU.
|
||
- [ ] Document the result (pass/fail, and if fail, what diverged from QEMU) — first real
|
||
external validation this project has ever had outside QEMU TCG emulation.
|
||
|
||
### From FABRIC-2.md §X, Milestone 9 — Networking / capsule distribution server
|
||
|
||
- [ ] (Deferred) Revisit and punch-list this milestone once Milestone 7 closes, not before.
|
||
|
||
---
|
||
|
||
## B. Stadium unification — words/VMs/blocks/messages on the same engine
|
||
|
||
Raised 2026-08-25: "words are stadium patrons, VMs are patrons, blocks are patrons, messages
|
||
are stadium patrons, all should be operated on by THE SAME ENGINE." Investigated before
|
||
designing anything — the real state is more nuanced than "everything's a stub," verified via
|
||
direct reads and `git log`, not assumed:
|
||
|
||
**`FABRIC.md` §18.3 already decided the mapping** (not invented here): blocks → `MIGRATE`,
|
||
messages → `DELIVER`, ACLs → `EXPIRE`, words and VMs both → `COOL`. `stadium_evict()`
|
||
(`src/starkernel/vm/stadium.c`) — real, tested infrastructure: bitmap tracking, pin/`contains`
|
||
refusal, the Hera-patron-zero panic guard, heat-conservation back to the owner's reservoir on
|
||
every reap — calls `stadium_dispatch()` for the actual payload action when a patron departs.
|
||
|
||
**Per-behaviour status, as of 2026-08-25:**
|
||
|
||
- **`MIGRATE` (blocks)** — zero consumer, genuinely stub (`stadium_dispatch()`'s case prints
|
||
`"MIGRATE (stub)"` and returns). This session already built the real mechanical primitive
|
||
it needs: `blk_subsys_relocate_block()`/`RELOCATE-BLOCK` (`FABRIC-2.md`, commit `36d832f`),
|
||
live-verified (redirect + content survive an abrupt kill and reboot) but never wired to
|
||
`stadium_dispatch()` — it's a separate, parallel, already-working mechanism today, not
|
||
routed through Stadium at all.
|
||
- **`COOL` (words *and* VMs, same tag)** — half real. **Words are fully live**, but via a
|
||
*separate, bespoke* mechanism, `stadium_word_dispatch()` (`stadium_words.c`, item 4.1),
|
||
wired directly into the real VM word-execution hot path (`vm_core.c:690,885,896`) — it does
|
||
**not** go through the generic `stadium_dispatch()` switch at all. `ONTOLOGY.md` §IX
|
||
claiming words are "not yet migrated" is itself stale documentation drift (same class of
|
||
bug as the "glibc" misattribution corrected earlier this session — flagged as a small,
|
||
separate fix below, not blocking). **VM cooling has no evidence of ever being wired
|
||
anywhere** — still genuinely stub.
|
||
- **`DELIVER` (Hermes messages)** — `FABRIC.md` (~line 3452) records this explicitly as
|
||
"Open, surfaced not resolved": Hermes's message/channel heat already integrates with
|
||
Stadium's reservoir accounting (`STADIUM-HEAT@`, `STADIUM-RES-PULL/PUSH`), but which Hermes
|
||
lifecycle event maps to `DELIVER` vs. `EXPIRE` was never decided, let alone wired. Real,
|
||
substantial, Hermes-specific integration work.
|
||
- **`EXPIRE` (ACL TTL expiry)** — no evidence of any wiring anywhere; `ACL.4th`/
|
||
`acl_recheck()` has zero Stadium involvement today. Also substantial, separate work.
|
||
|
||
**Why `DELIVER`/`EXPIRE` aren't being resolved in the same pass as `MIGRATE`:** each is a
|
||
full subsystem integration (Hermes lifecycle mapping; ACL-to-Stadium wiring where none has
|
||
ever existed) in its own right — attempting all four stubs at once risks exactly the rushed,
|
||
shipped-but-incomplete outcome the no-stubs rule (below) exists to prevent. `MIGRATE` gets
|
||
resolved for real because this session already has a tested primitive underneath it; the
|
||
other three become honest, explicit punch-list items instead of being touched speculatively.
|
||
|
||
**Punch list:**
|
||
|
||
- [x] **Investigated (2026-08-25): block-patron admission does not exist yet, but is
|
||
architecturally straightforward, not blocked.** Confirmed via `stadium_admit()`'s own doc
|
||
(`stadium.h`) that it REFUSES any candidate with `mass != 1`, and that a `mass > 1`
|
||
multi-cell patron would need a continuation chain nothing has ever designed — this looked
|
||
at first like a hard blocker for a 1024-byte block. It isn't: confirmed via
|
||
`stadium_word_dispatch()`'s real candidate construction (`stadium_words.c:245-252`) that
|
||
Stadium cells carry pure identity/heat/bookkeeping only — `candidate.identity = word_id`,
|
||
`payload[32]` unused — the actual word content stays in the dictionary; Stadium never holds
|
||
it. By the same pattern, a block patron's cell would carry `identity = LBN`, `mass = 1`,
|
||
`payload` unused — the actual 1024 bytes of block content stays exactly where it already
|
||
lives (block cache / disk via `block_subsystem.c`), unmoved. So `mass != 1` is a non-issue;
|
||
the real gap is just that nothing has ever built the LBN→cell_index residency map (the
|
||
block-patron analogue of `stadium_word_dispatch()`'s `resolve_resident_cell()`) or the
|
||
touch-on-access hook (the analogue of `vm_core.c`'s three `stadium_word_dispatch()` call
|
||
sites). Not yet built — this is real, scoped, buildable work, not a stub-around candidate.
|
||
**Still open**, plan to be presented before implementation per the no-stubs/no-early-coding
|
||
conventions.
|
||
- [x] **Resolved (2026-08-25): real block-patron admission + real `MIGRATE` dispatch, both
|
||
live.** New `stadium_blocks.h`/`stadium_blocks.c` mirror `stadium_words.c`'s shape (Option B
|
||
starter-grant admission, redirected Loop #3 cooling, self-healing stale-entry detection) but
|
||
key residency by `(quota_slot, lbn)` in a fixed-capacity open-addressing hash table sized off
|
||
`stadium_cell_count()` (tombstone-based deletion, since LBN space isn't densely bounded like
|
||
`word_id`), not a dense array. Wired into `block_word_block()`/`block_word_buffer()`/
|
||
`block_word_update()` (`block_words.c`), `#ifdef __STARKERNEL__`-guarded. `stadium_dispatch()`'s
|
||
`MIGRATE` case now calls `blk_flush(lbn)` for real (confirmed `blk_flush()`, not
|
||
`blk_subsys_relocate_block()`, is the right primitive — the latter is for compudynamics-driven
|
||
relocation to a *different* LBN mid-residency, not ordinary reap write-back). Three new Kconfig
|
||
tuning constants (`STADIUM_BLOCK_HEAT_QUANTUM`/`STADIUM_BLOCK_COOL_RATE_Q48`/
|
||
`STADIUM_BLOCK_TRACK_CAP_MULT`) mirror the word-patron ones exactly, same three-layer wiring.
|
||
**Verified:** clean compile, zero warnings, on all three architectures; clean boot to
|
||
`zuse)ok>`/`ok>` REPL on all three, conservation (`resident_sum + reservoir == Q48_ONE`) intact
|
||
identically across all three; `BLOCK`/`BUFFER` touches exercised live from the REPL on amd64 and
|
||
riscv64 with no crash; a 22,000-distinct-block flood loop (amd64, artificially shrunk to a
|
||
20,971-cell Stadium via a one-off smaller `-m` to make quota pressure reachable) ran clean under
|
||
heavy admission-path load with no corruption. **Live `MIGRATE` fire confirmed (2026-08-26):**
|
||
interactive flooding alone never triggered it — Hera's reservoir was already sitting exactly at
|
||
the `Q48_ONE / 3` floor from the boot-time self-tests, so every block-touch candidate pulled 0
|
||
heat, and a 0-heat candidate can never be *strictly denser* than an existing resident, so
|
||
`stadium_admit()`'s eviction fallback correctly refuses rather than evicts once the free list is
|
||
exhausted (a pre-existing reservoir-floor/density-eviction interaction, applies equally to word
|
||
patrons, not introduced by this pass). Closed deterministically instead with a temporary boot
|
||
probe (`kernel_main.c`, inserted into the existing Artemis 4.6 self-test block, reverted
|
||
immediately after capture — no code left behind): `100 65536 0 STADIUM-ADMIT ... STADIUM-EVICT`
|
||
against the live Artemis VM. Captured live: `Stadium: dispatch cell=63257 behaviour=MIGRATE
|
||
lbn=100`, followed by `DBG err after MIGRATE probe=0` — `blk_flush(100)` fired for real, with
|
||
the correct LBN threaded through from the departing patron's `identity` field exactly as
|
||
designed. (Artemis's own reservoir went briefly out of Q48_ONE-balance during the probe — raw
|
||
`STADIUM-ADMIT` doesn't debit the reservoir on its own, by its own doc, so a heat value handed
|
||
to it directly is invented, not pulled; harmless here since Artemis is killed and her whole
|
||
economy discarded immediately after, and Hera's own conservation was independently confirmed
|
||
back at the normal 43691/21845/65536 baseline afterward.)
|
||
- [x] **Bug found, reported, then fixed on request (2026-08-25/26): `capsules/lib.4th:13-14`
|
||
shadowed the C primitives `USE`/`RUN`.** While chasing the live-MIGRATE test above,
|
||
`S" Artemis" USE` (meant to redirect the REPL into Artemis's own vocabulary,
|
||
`mama_word_use()`) instead printed `EXEC: failed: Artemis`. Root cause: `capsules/lib.4th:13`
|
||
defined `: USE ( addr u -- ) EXEC ;` — a FORTH word with the same name but a completely
|
||
different meaning ("load/exec a capsule"), which shadowed the C-registered `USE` in
|
||
dictionary search order since `lib.4th` loads after primitive registration. `lib.4th:14` did
|
||
the identical thing to `RUN`, which CLAUDE.md also names as an untouchable C primitive
|
||
("BIRTH, RUN, USE are primitives — registered in C exactly like DUP, BYE, EXEC"). Same bug
|
||
class as the K-PUSH dictionary-shadowing issue
|
||
(`docs/working/architecture/K-PUSH-DICTIONARY-SHADOWING-BUG-20260704.md`). Reported first
|
||
per CLAUDE.md's rule against unrequested fixes; Captain Bob then explicitly asked for the fix.
|
||
**Fix:** traced every real caller before touching anything — `RUN`'s alias was dead (never
|
||
called anywhere as bare `RUN`); `USE`'s alias had exactly one real caller,
|
||
`capsules/hermes/init.4th:397` (`S" common:msg.4th" USE`, intentionally exploiting the shadow
|
||
to load that capsule right after `lib.4th` itself loaded). Both aliases were pure
|
||
`EXEC` wrappers with zero added behavior, so the fix deleted both definitions from `lib.4th`
|
||
outright and changed the one real call site plus its matching doc comment
|
||
(`capsules/common/msg.4th:4`) to call `EXEC` directly — no new names invented, the C
|
||
primitives untouched, `mkcapsule --lint` clean (31/31 pass). **Verified live:** rebuilt and
|
||
booted amd64 — Hermes still births and her `COMMON-CH`-eviction self-test (which depends on
|
||
`common:msg.4th` having loaded) still passes exactly as before; interactively, `S" Artemis"
|
||
USE` now correctly prints `USE: now using Artemis` and switches the REPL's console-name
|
||
coloring, confirming the C primitive runs unshadowed. Clean compile and clean boot with
|
||
conservation intact on all three architectures (amd64/aarch64/riscv64).
|
||
- [x] **Resolved (2026-08-26): real VM-patron admission + real explicit-KILL eviction, both
|
||
live.** Re-scoped on request: confirmed `capsule_vm_kill()` had zero Stadium involvement
|
||
(`vm_cleanup()`/`sf_free()` only) and child-VM birth only ever called
|
||
`stadium_grant_quota()` (a resource pool for the VM's *own* future word/block patrons) —
|
||
never `stadium_admit()` for the VM *itself*. The only precedent, `stadium_birth_hera()`,
|
||
admits Hera into her own quota as a permanently pinned cell 0, which can never reach
|
||
`stadium_evict()` — not a working example of `COOL` firing for a VM. On closer look this
|
||
turned out NOT to be entangled with the still-iterating Tripod/Zuse/messaging vision after
|
||
all (§D) — birth and kill already funnel through two single choke points, so the earlier
|
||
2026-08-25 deferral was overcautious. **Design:** added `size_t stadium_patron_cell` to
|
||
`VMRegistryEntry` (`capsule_run.h`). At birth, right after the existing
|
||
`stadium_grant_quota()` call (`capsule_birth.c`), admit a candidate into the new VM's own
|
||
quota mirroring `stadium_birth_hera()`'s shape (`identity=0`, `mass=1`, `behaviour=COOL`)
|
||
but deliberately **unpinned** — pinning would need a new "unpin" primitive (none exists) to
|
||
ever evict it later, and adding a pin-bypass to `stadium_evict()`'s refusal logic isn't
|
||
something to do casually; unpinned costs nothing since nothing wires `COOL`'s dispatch body
|
||
to actually kill anything, so the worst case of an unrelated natural eviction is
|
||
`stadium_patron_cell` going stale, which is tolerated the same way `stadium_word_forget()`
|
||
already tolerates staleness. At `capsule_vm_kill()` and `capsule_vm_kill_all_nonmama()`:
|
||
`stadium_evict()` the tracked cell if still resident, silently tolerating refusal (already
|
||
gone). `stadium_dispatch()`'s `COOL` case needed no new payload body — same as it already is
|
||
for words, where `COOL` has no defined extra action beyond `stadium_evict()`'s own universal
|
||
reservoir credit; the missing piece was admission and a genuine trigger, not dispatch-body
|
||
logic. **Verified live:** a second, new `Stadium: dispatch cell=... behaviour=COOL` now
|
||
fires immediately before every `PARITY:KILL` line, for both Hermes and Artemis, confirmed on
|
||
amd64 (distinct from the pre-existing `COMMON-CH` word-eviction self-test's own COOL print).
|
||
Conservation (`resident_sum + reservoir == Q48_ONE`) intact throughout. Clean zero-warning
|
||
compile and clean boot on all three architectures (amd64/aarch64/riscv64).
|
||
- [x] **Re-scoped and resolved (2026-08-26): `DELIVER` (Hermes) was never actually a gap —
|
||
the earlier "zero consumer, needs substantial Hermes lifecycle mapping" framing above was
|
||
wrong, carried over unverified from `FABRIC.md`'s old "open, not resolved" note about
|
||
*which* Hermes event maps to `DELIVER` vs. `EXPIRE`. Item 4.2 already answered that in code
|
||
(messages → `DELIVER`, channels → `COOL`) without the prose ever catching up — same
|
||
documentation-drift class as the stale `ONTOLOGY.md` words note and the earlier glibc
|
||
misattribution. Confirmed live: `capsules/hermes/init.4th`'s `MSG-ALLOC` already admits
|
||
every message with `SB-DELIVER`, and `MSG-FREE-NODE` (called from both `MSG-ACK-LAST` and
|
||
heat-driven `MSG-REAP`) already evicts it — `behaviour=DELIVER (stub)` has been printing on
|
||
boot logs since at least 2026-08-05. Checked whether the dispatch body needed a real payload
|
||
action the way `MIGRATE` did: `MSG-DELIVER` (the FORTH word) already runs the actual
|
||
delivery (`VM-EXEC` of the payload) *before* eviction, decoupled from Stadium reap — so by
|
||
dispatch time delivery is already done, same shape as `COOL`, which needs no extra action
|
||
beyond `stadium_evict()`'s own universal reservoir credit. **Fix:** `stadium_dispatch()`'s
|
||
`DELIVER` case now prints the departing message's real identity (`DELIVER msg_idx=N`, same
|
||
shape as `MIGRATE`'s `lbn=` print) instead of a misleading `(stub)` label — confirmed live
|
||
via a forced `MSG-SEND`/`MSG-DELIVER-ALL`/`MSG-ACK-LAST` sequence from Hermes's own REPL
|
||
context (`Stadium: dispatch cell=73653 behaviour=DELIVER msg_idx=1`). `COOL`'s case was in
|
||
the identical situation (real for both words and VMs, no extra action needed) and, on
|
||
request, got the same fix (2026-08-26): now prints `COOL identity=N` (word_id for a word, 0
|
||
— the patron-zero convention — for a VM) instead of `(stub)`. Confirmed live: both shapes
|
||
fired correctly on the same boot — `COOL identity=0` at Hermes's/Artemis's own explicit
|
||
channel-eviction self-test and again at their VM-patron eviction at `PARITY:KILL`,
|
||
`COOL identity=1` at a second channel eviction — conservation intact throughout. Clean
|
||
zero-warning compile and clean boot on all three architectures for both fixes.
|
||
- [ ] `EXPIRE` (ACL) — confirmed genuinely unscoped (2026-08-26), not a case of stale
|
||
documentation like `DELIVER` turned out to be. Two findings below, then four open questions
|
||
— **decided directly on request (2026-08-26)**, decision recorded after the questions, no
|
||
code written (a decision isn't a green light to build, per this session's own convention):
|
||
|
||
**Finding 1 — `acl_ttl` and Stadium's `ttl` are different things wearing the same name.**
|
||
`DictEntry.acl_ttl` (`vm_core.c:756`) is a per-word countdown that batches how often
|
||
`ACL-RECHECK` runs — when it hits 0, `acl_recheck()` calls the FORTH word `ACL-RECHECK`
|
||
(`ACL.4th:50`), which **always renews**: STRICT mode sets `allow=1, ttl=0` (recheck every
|
||
time); TTL mode computes a fresh heat-based TTL and sets `allow=1`. Denial isn't a live path
|
||
anywhere in current policy. This is a *renewal* cycle, not a *residency-ending* event —
|
||
nothing about it resembles "leaving the Stadium floor."
|
||
|
||
**Finding 2 — `StadiumPatronHeader.ttl` is completely inert.** Every candidate constructor
|
||
across the whole codebase (`stadium.c`, `stadium_words.c`, `stadium_blocks.c`, Hermes's
|
||
`MSG-ALLOC`/`CH-ALLOC`) sets `candidate.ttl = 0`, and nothing anywhere ever reads,
|
||
decrements, or reaps on it. The generic TTL-expiry *mechanism* `EXPIRE` would need to fire
|
||
from doesn't exist in Stadium's own engine at all — a gap one level deeper than "ACL isn't
|
||
wired to Stadium."
|
||
|
||
**Open questions:**
|
||
1. Is "ACL patron" even the right model, or was `FABRIC.md`'s original "ACLs → EXPIRE"
|
||
mapping a category mismatch from the start — conflating `acl_ttl`'s recheck-amortization
|
||
counter with Stadium's residency `ttl`?
|
||
2. If it is the right model: what gets admitted as a patron? One per ACL-guarded word would
|
||
be redundant with the word's own patron cell item 4.1 already tracks. A different unit
|
||
(e.g. per zuse session) might fit better once PKI/session auth lands (Phase 8, still
|
||
open per `.claude/CLAUDE.md`'s ACL section).
|
||
3. Building this for real means building Stadium's generic ttl-decrement/reap-on-zero
|
||
mechanism first — nothing to hook `EXPIRE` into today. Is that in scope here, or its own
|
||
separate item?
|
||
4. What should the reap action actually *do*, given current ACL policy never revokes — would
|
||
`EXPIRE` force an `ACL-RECHECK`, or something else entirely?
|
||
|
||
**Decision:**
|
||
1. Not at the per-word level. `acl_ttl`-hits-zero always renews, never revokes — forcing
|
||
`EXPIRE`'s residency-ending tag onto it would misuse the tag. But "ACLs → EXPIRE" isn't
|
||
wrong in spirit, just aimed at the wrong unit: the one place in this system where
|
||
something ACL-related genuinely has a lifetime and should be revoked is a **zuse
|
||
superuser session** (Phase 8, not yet built) — authenticate, hold elevated privilege for
|
||
a bounded time, then actually drop back to non-zuse. That's a real residency-ending
|
||
event; per-word recheck isn't.
|
||
2. A zuse session, not a per-word ACL entry — a session is the thing with a genuine
|
||
start/lifetime/end. A word already has its own patron via item 4.1; a second one for ACL
|
||
purposes would be redundant bookkeeping, not a new concept.
|
||
3. **Not in scope now.** This is the decision that actually resolves the item: Phase 8
|
||
doesn't exist yet, so there is no session to admit as a patron regardless of any other
|
||
choice made here. Building the generic ttl-decrement/reap mechanism now, with nothing
|
||
real to feed it, would be speculative infrastructure ahead of its only consumer — close
|
||
in spirit to what the no-stubs standard exists to prevent, just inverted (a real
|
||
mechanism with no real caller, instead of a fake mechanism with a real caller).
|
||
4. Revoke the session's elevated privilege and drop the console back to its non-zuse state —
|
||
a real action, unlike `DELIVER`/`COOL`, which needed none.
|
||
|
||
**`EXPIRE` stays explicitly deferred until Phase 8 (PKI/zuse session minting) lands** — not
|
||
abandoned, not left ambiguous: revisit it as part of that work, admitting the session itself
|
||
(not a word) as the patron, once there is something real for it to represent.
|
||
- [x] Fixed (2026-08-26): `ONTOLOGY.md` §IX's "words (dictionary, warehouse-resident today,
|
||
not yet migrated)" line was stale — corrected to state words are fully migrated and live
|
||
via `stadium_word_dispatch()`. Doc-only, no build/boot verification needed.
|
||
|
||
---
|
||
|
||
## C. Standing rule: no stubs or TODOs, ever
|
||
|
||
Stated directly, 2026-08-25, after the `stadium_dispatch()` stub investigation above:
|
||
**"I've never allowed stubs before."** Saved as a persistent memory
|
||
(`feedback_no_stubs_or_todos.md`) so this applies across sessions, not just this one. Full
|
||
statement: no stub function that prints a placeholder and returns, no `TODO`-and-move-on
|
||
comment in place of real logic, in any language, ever committed as if it were finished work.
|
||
Small, honest increments are still fine and encouraged — each increment just has to be a
|
||
complete, real implementation of whatever slice it covers, never a placeholder for a later
|
||
slice. A pre-existing stub found while working nearby (as here) gets flagged and resolved,
|
||
not built on top of or left in place.
|
||
|
||
---
|
||
|
||
## D. Tripod final shape — minting, one-time Zuse fuse, messaging-only (vision, substantially expanded 2026-08-27)
|
||
|
||
Stated directly by Captain Bob, 2026-08-25: "we're going to have to iterate because I know
|
||
what the final shape of the tripod will be." Originally captured as a forward-looking vision,
|
||
not yet broken into implementable items. **2026-08-27: expanded into a much fuller picture**
|
||
after Phase 8/Milestone 6 closed and unblocked this design pass — captured here in full before
|
||
any of it gets scoped into implementable items, per direct request ("capture EVERYTHING first
|
||
then we'll build a plan"). Full detail also in memory `project_tripod_final_shape_vision.md`.
|
||
|
||
### D.1 — Original vision (2026-08-25), still accurate
|
||
|
||
- **Thumbdrive presentation → legality check → mint.** A newly presented thumbdrive is
|
||
checked for legality (against the CA-root-derived identity/certificate scheme). If not
|
||
legal, it is "minted" — formatted for system use — which (1) spins a new user VM and (2)
|
||
attaches the console VM to it.
|
||
- **One-time Zuse mint + fuse-blow on first install.** A brand-new system instance
|
||
("Install"/"Try It") mints exactly one Zuse superuser, then irreversibly "blows a fuse":
|
||
direct quote, "we mint one and only one Zuse user and blow a fuse. The only way around is a
|
||
new system." Post-fuse, no further Zuse can ever be minted on that instance — but the
|
||
system is NOT bricked: the existing Zuse superuser keeps working, and ordinary users can
|
||
still "thumb in" via regular thumbdrives. **This half is now actually built** — Phase 8's
|
||
entropy→signing→block-fence persistence work (2026-08-26, `project_phase8_milestone6_complete.md`)
|
||
implements exactly this, minus the physical thumbdrive itself (see D.4, WRITE(10) gap).
|
||
- **Messaging-only once Tripod is fully live.** All inter-VM interaction becomes Hermes
|
||
messaging, not direct calls/shared state — a stated end-state, not the current
|
||
implementation. **Reaffirmed and sharpened 2026-08-27: hardwired/direct-call
|
||
implementations are acceptable *during development only* — no piece of this design is
|
||
considered actually finished until its hardwired calls are replaced with real Hermes
|
||
messages.** This is a standing completion criterion for everything in this section, not a
|
||
separate future milestone.
|
||
- **Polymorphic block-boundary behavior for user VMs.** Still just one sentence, still not
|
||
elaborated — genuinely unresolved since 2026-08-25, needs its own follow-up conversation.
|
||
**CLOSED 2026-08-27, see `D.4`/§F.16** — that follow-up happened; already true of the live
|
||
block-subsystem code.
|
||
- **`ClaudeEXPORT/`** — a prior Claude data export, "concepts and thoughts as guidelines,"
|
||
explicitly flagged as possibly containing superseded/conflicting ideas, not authoritative.
|
||
See memory `project_claude_export_archive.md`.
|
||
|
||
### D.2 — The session model (2026-08-27): a session IS a VM
|
||
|
||
Direct correction to how this document and the earlier `EXPIRE` framing (§B) had been
|
||
thinking about "sessions": **a session is any VM client running in the Stadium fabric — Zuse
|
||
is no different from any other VM.** The Stadium/FABRIC metaphor restated by Captain Bob:
|
||
the fabric floats inside the Stadium; VMs actually doing work are "players on the field"
|
||
(system perturbances); user interaction is "fans in the stands" providing patron-driven noise.
|
||
Zuse is a player like any other, not a special boolean flag on Hera.
|
||
|
||
**This directly invalidates the `EXPIRE`-as-TTL-on-`zuse_session`-boolean plan this session
|
||
had started scoping (§B's `EXPIRE` decision, and the implementation research begun
|
||
2026-08-27 before this correction landed).** Today's `zuse_session` field on the VM struct is
|
||
not what a "session" actually is in this model — a session ending is a **VM detaching**, far
|
||
closer to the existing, real, working `COOL`/`capsule_vm_kill()` path than to a new
|
||
Stadium-behaviour-tag TTL sweep. **`EXPIRE` as originally scoped is superseded, not merely
|
||
deferred** — when Stadium/ACL session-lifetime work is picked up again, it should be framed
|
||
as "what happens when a user-VM's owning thumbdrive detaches," reusing `COOL`, not as a
|
||
timeout on a flag.
|
||
|
||
When Hera has no VM attached beyond fleet-servicing strata, she idles, watching services and
|
||
maintaining a serviced fleet foundation — her normal steady state, not a special case.
|
||
|
||
**Validated against the original Stadium foundation, not just today's intuition (mined
|
||
2026-08-27 from `FABRIC.md` §17.1/§20, the oldest, most foundational layer of this whole
|
||
design — see D.2b below for the full mining pass).** `FABRIC.md` §17.1's own patron/departure
|
||
table already says VMs depart via heat decay (`COOL`) and only messages/ACLs ever used TTL —
|
||
meaning the `EXPIRE`-as-TTL-on-a-flag plan this session had started scoping didn't just become
|
||
inconvenient once "session = VM" landed, **it contradicted the original design from the
|
||
start**: a VM was never one of the two patron kinds TTL governs. §20's outer/inner Stadium
|
||
nesting (VMs as outer-Stadium patrons; words/blocks/ACLs/messages as inner-per-VM patrons)
|
||
already anticipated exactly "attach = admit a VM patron" as the outer-Stadium case — no new
|
||
Stadium theory is needed for this, only a new *admission trigger* (thumbdrive attach) feeding
|
||
the existing admit/evict machinery.
|
||
|
||
### D.2b — The pentagon: five VMs, minimum, to service one attached user (2026-08-27)
|
||
|
||
Stated directly: **"Just to get a user, 5 VMs are needed — if we draw every connection to
|
||
every node they can make a pentagon."** The five: **Hera, Hermes, Artemis, the user's own VM,
|
||
and the Console.** Drawing every pairwise connection among five nodes (a complete graph, K5)
|
||
draws a pentagon with all its diagonals — visually a pentagram. Noted in passing, and worth
|
||
keeping since this project's naming is already steeped in Greek mythology (Hera, Hermes,
|
||
Artemis, Ananke/Lithos, Zuse for Zeus): a pentagram at the foundation of a "pagan" system fits
|
||
the theme, not an accident to explain away.
|
||
|
||
**The Console is a full peer node, not a passive terminal that merely "attaches to" a user
|
||
VM** — D.3 step 4 undersells this; the Console is architecturally one of the five, with (at
|
||
minimum) its own binding relationship to the user's VM and, per the messaging end-state,
|
||
eventually its own message traffic to Hera/Hermes/Artemis too, not just a one-way relay.
|
||
Reconciling D.3's "console attaches to the new user VM" phrasing against this five-node,
|
||
fully-connected picture is real remaining work, not done here — captured as a gap in D.4.
|
||
|
||
Why each of the five is load-bearing, best understanding as of this capture (not all edges
|
||
independently confirmed — see D.4's gaps):
|
||
- **Hera** — governs the fleet, births/kills the user VM (existing `capsule_birth_baby()`/
|
||
`capsule_vm_kill()` path), holds the outer-Stadium quota the new VM draws from.
|
||
- **Hermes** — the messaging fabric itself; per the standing "nothing is done until it's
|
||
messaging" criterion, every other edge in this pentagon is ultimately meant to route through
|
||
Hermes, making Hermes both a node in the graph and (eventually) the medium connecting every
|
||
other edge — worth being explicit that this is a two-level relationship (Hermes as a peer VM
|
||
*and* as the substrate other peers communicate over), not a design tension to smooth over
|
||
by treating it as an ordinary fifth node.
|
||
- **Artemis** — persistent block storage; the identity/system-metadata store (Phase 8's
|
||
block-fence) and, per D.3, wherever a user's own working data actually lives during a
|
||
session, live here.
|
||
- **The user's own VM** — where the user plays (D.3 step 5).
|
||
- **Console** — the human-facing binding point (D.3 step 4, Milestone 5).
|
||
|
||
### D.3 — The attach/mint flow, end to end
|
||
|
||
1. **Idle loop watches for thumbdrive connects** (`sk_repl_idle()`, already the right hook —
|
||
confirmed 2026-08-27 during the (superseded) `EXPIRE` research: ~1s cadence, already used
|
||
for `xhci_poll_events()`/MSC attach).
|
||
2. **Cert-only authentication — no password, no username.** A presented thumbdrive's identity
|
||
is *entirely* the cert on it. Verification means checking the cert's fields directly (chain
|
||
of trust, most likely signed by *Zuse's own key* — the same `x509_ed25519.c`/
|
||
`ed25519_verify()` primitives Milestone 6 already built and proved work end to end, just
|
||
verifying against Zuse's pubkey instead of the snakeoil intermediate's). **The system has
|
||
no requirement to maintain any single identity itself** — no user directory, no central
|
||
registry of who exists. Each thumbdrive is a fully self-contained, portable identity
|
||
record.
|
||
3. **A dedicated user VM is born** from the identity's own data — "the users/agents/services
|
||
metadata can reside in their capsule and [get] encoded directly into their dictionary when
|
||
they attach and their init.4th executes." This is a *runtime*-constructed capsule from
|
||
drive content, not a build-time-embedded one like `pki:snakeoil-intermediate.der` — a
|
||
materially different mechanism from anything `mkcapsule` does today (gap, see D.4).
|
||
4. **The console VM attaches to the new user VM** — this is exactly Milestone 5's still-open
|
||
"Console/VM key-match binding" item, now with its open question *resolved*: "the bumps on
|
||
a thumbdrive have to fit the holes on the VM... (ACL territory)" — **reuse the existing
|
||
ACL primitive (`acl_mode`/`acl_allow`/`acl_pinned`) for the key-match check, don't build a
|
||
separate mechanism.** Exact comparison semantics still undesigned (see D.4).
|
||
5. **The user plays entirely inside their own VM** — all their FORTH activity happens in
|
||
their own isolated VM context, never Hera's or anyone else's.
|
||
6. **Ongoing minting**: while Zuse is attached/active, a second blank thumb presented can be
|
||
minted with a new named identity (worked example given: `S" CaptBob" MINT`-shaped flow)
|
||
and ejected — this is the still-open `MINT` word from Phase 8's own punch list, now with a
|
||
concrete attach-flow context to design it against.
|
||
7. **Detach**: working state is copied back to the user's own thumb and flushed before eject.
|
||
**Explicitly deferred — stated directly as "going to be a maintenance operation later,"
|
||
not needed now.**
|
||
|
||
### D.4 — Gaps identified 2026-08-27, sorted by whether they're answered
|
||
|
||
**Answered (resolved during capture, not deferred):**
|
||
- Milestone 5's "reuse `ACL-PIN` vs. build a new key-match primitive" question — reuse ACL.
|
||
**Now grounded, not just intuited** (mined 2026-08-27 from `FABRIC-2.md` §C/§D,
|
||
lines ~2884-2918): `acl_pinned`'s one-way-ratchet (`include/vm.h:346`, C-only mutation, no
|
||
FORTH clear path) was *already* identified as the right-shaped precedent for exactly two
|
||
things — Zuse's own one-way "burn" and console-session ownership — but in both cases only
|
||
the *pattern* was precedented, never actually built past word-execution gating. The
|
||
"still genuinely open" framing in `FABRIC-2.md` is accurate as of that writing; today's
|
||
decision closes it, the comparison *semantics* remain the real gap (below).
|
||
- `EXPIRE`'s framing — superseded by "session end = VM detach," see D.2. **Now shown to be
|
||
required, not just cleaner**, by `FABRIC.md` §17.1's own patron/departure table (see D.2's
|
||
addendum above) — TTL was never a valid mechanism for a VM patron in the first place.
|
||
- Regular-user cert chain-of-trust mechanism — signed by Zuse's own key, verified with
|
||
already-built, already-proven Ed25519/X.509 primitives (Milestone 6). No new crypto work
|
||
needed, only a new verification *call site* using existing functions. **Correction,
|
||
2026-08-27 (`FABRIC-3.md` §F.7): partially wrong.** `x509_extract_ed25519_pubkey()`
|
||
deliberately stops at `SubjectPublicKeyInfo` — no signature extraction, no TBS-byte-range
|
||
capture, no chain/issuer check of any kind (`x509_ed25519.h`'s own doc comment). Verifying a
|
||
*signed* cert (not just reading a pubkey out of one) is new DER-walking work, not a pure
|
||
reuse. Trust root is also confirmed separate from the capsule-PKI chain: Zuse's own
|
||
on-device key (`zuse_cert_devblock_t`), never the offline root CA / snakeoil intermediate.
|
||
- Whether the system needs a central user directory — no, by design; each thumbdrive is
|
||
self-contained (reinforces D.3 point 2, was implicit, now explicit).
|
||
- **The concrete target shape for message-bus migration** (mined 2026-08-27 from
|
||
`FABRIC-2.md`, cross-checked against live `capsules/hermes/init.4th`): a message is
|
||
`MSG-CELLS` = 9 cells (`TYPE, FROM, TO, PADDR, PLEN, STADIUM-CELL, SEQ, CH, ORIG-TYPE`), 72
|
||
bytes, payload **out-of-line** (a pointer+length to caller-owned bytes — there was never a
|
||
fixed inline payload field, an earlier open question in `FABRIC-2.md` §23.4 that its own
|
||
design already closed as moot). Live API: `MSG-ALLOC ( heat -- addr|0 )`,
|
||
`CH-ALLOC ( heat -- addr|0 )`, `MSG-DELIVER ( m -- )`, `MSG-DELIVER-ALL ( -- )`. A future
|
||
"thumbdrive attach event" or "cert verify request" message type follows this exact shape —
|
||
allocate, fill `TYPE`/`FROM`/`TO`/`CH`, point `PADDR`/`PLEN` at an out-of-line event struct,
|
||
send. Not built, but no longer an abstract "use messaging someday" — there's a real API to
|
||
target.
|
||
|
||
**Real, unanswered gaps — deferred on purpose (per "answer what we can, defer what we
|
||
can't"):**
|
||
- **How does thumbdrive content actually become a capsule at runtime?** `mkcapsule` only
|
||
ever builds capsules at build time, baked into `capsule_generated.c` — nothing today
|
||
constructs a capsule from data read off a live-attached device. This is new mechanism, not
|
||
a variation on anything that exists. **SCOPED 2026-08-27, `FABRIC-3.md` §F.6**: no new
|
||
birth machinery needed — `capsule_birth_baby()` is already generic; construct a
|
||
self-contained heap-allocated single-entry directory (mirroring `kernel_main.c`'s own
|
||
heap-copy of the compile-time directory) and feed it straight in. Source-location-on-drive
|
||
and read-path details still open.
|
||
- **Exact ACL "bumps and holes" comparison semantics.** Which specific ACL fields, compared
|
||
how, constitute a console-to-VM match? Direction is settled (reuse ACL), the actual
|
||
comparison logic is not designed.
|
||
- **Message-bus migration scope.** Which specific interactions (attach detection, cert
|
||
verify, console bind) move to Hermes messages, and what do those message shapes look like?
|
||
Entirely unscoped, explicitly deferred until after a hardwired version exists to migrate
|
||
*from*. **SCOPED 2026-08-27 (`FABRIC-3.md` §F.15)**: `MSG-DELIVER` already executes arbitrary
|
||
FORTH text on the target VM (`VM-EXEC`), so no new dispatch mechanism is needed. Real target
|
||
is a genuine Console VM (already real: serial+framebuffer+PS2), two-hop flow (Hera→Console
|
||
reports attach; Console→Hera requests the privileged operation with literal arguments).
|
||
Surfaced a substantial new vision detail along the way: blank-media minting is a Console-
|
||
driven interactive onboarding form, not a bare word call (§D.6).
|
||
- **Polymorphic block-boundary behavior for user VMs** — still just the one original sentence
|
||
from 2026-08-25, never elaborated. **CLOSED 2026-08-27 (`FABRIC-3.md` §F.16)**: clarified by
|
||
the user as an architectural principle (the block layer should have no idea what backs it),
|
||
then confirmed already true of `blk_get_buffer()`'s live RAM/raw/disk-cache abstraction —
|
||
not a missing feature.
|
||
- **SSD identity-store scope** — does the system-resident store (the block-fence built in
|
||
Phase 8) ever hold anything about *regular* users, or is it purely Zuse/system bookkeeping
|
||
with regular users staying purely thumbdrive-resident? Best read of what was said: the
|
||
latter, but not stated explicitly enough to treat as settled. **CLOSED 2026-08-27
|
||
(`FABRIC-3.md` §F.16)**: confirmed the latter, both by D.3's own explicit "no central
|
||
registry" statement and structurally by where this session's own scoping put every piece of
|
||
user identity data (§F.6-F.8, all on the user's own drive).
|
||
- **Does a user's VM/session state round-trip across multiple attaches** (unplug, come back
|
||
later, resume exactly where you left off), given detach explicitly copies state back to the
|
||
drive? Implied yes by the detach description, not confirmed directly. **CLOSED 2026-08-27
|
||
(`FABRIC-3.md` §F.16)**: confirmed as a structural consequence of decisions already made —
|
||
the user's pool being their own drive plus `EJECT`'s flush-before-kill means the data simply
|
||
never leaves; no separate resume mechanism needed. **Checked against
|
||
`FABRIC.md` §24's mutation/identity-stability rules (mined 2026-08-27) — no conflict either
|
||
way**: identity is only pinned stable *within one residency*, so a thumbdrive re-attaching
|
||
later and being admitted as a fresh VM patron (possibly reusing an identity value derived
|
||
from its cert/pubkey) is simply a new residency each time. Nothing in the foundational
|
||
theory blocks or answers round-tripping; it stays a pure implementation decision.
|
||
- **Reconcile D.2b's five-node, fully-connected pentagon against D.3 step 4's "console
|
||
attaches to the user VM" phrasing.** The Console is a full peer node (Hera/Hermes/Artemis/
|
||
User-VM/Console, K5), not just a passive relay that binds to one other node — D.3's
|
||
wording undersells this. Which of the ten pentagon edges are real *today* versus aspirational
|
||
once messaging lands is not worked out; only Hera↔UserVM (birth/kill) and
|
||
UserVM↔Artemis-via-Hera's-quota (existing Stadium quota mechanics) are clearly already
|
||
real in some form.
|
||
- **Confirmed baseline before any message-bus migration can start** (mined 2026-08-27,
|
||
`FABRIC-2.md` cross-checked against live `src/starkernel/repl.c` and the xHCI/USB driver):
|
||
today's hotplug→MSC-attach→`homeblocks_sig_check()`→`blk_subsys_attach_device()` chain is
|
||
**100% hardwired, direct C calls, zero messaging at any point** — confirmed empirically, not
|
||
assumed. This is the exact, real starting point "hardwire now, message later" describes;
|
||
there is no partial messaging implementation anywhere in this path to build on yet.
|
||
- **The single biggest hard blocker underneath all of it: SCSI `WRITE(10)` still doesn't
|
||
exist** (`blkio_usb.c` is read-only — Milestone 2's biggest open item, unchanged). Minting
|
||
a blank thumb, and flushing state back on detach, both need real writes to physical USB
|
||
media. None of D.3's flow can run on real hardware until this lands; QEMU testing would need
|
||
to proxy through a writable virtio-blk stand-in the same way Phase 8's cert persistence did
|
||
for Zuse's own identity.
|
||
|
||
**Small, unrelated, real bug found along the way:** the prompt shows `zuse)ok>` (a stray
|
||
trailing paren, "never corrected typo") — should be `(Zuse)ok>`, both parens around the
|
||
identity name, generalizing to any attached identity (e.g. `(CaptBob)ok>`). Independent of
|
||
everything else in this section; safe to fix any time. **CLOSED 2026-08-27 (`FABRIC-3.md`
|
||
§F.16)**: traced for the literal string — it doesn't exist in any live code path today
|
||
(`SK_PROMPT_TEXT` is unconditionally `"ok> "`); the typo lives only in a doc comment
|
||
describing not-yet-implemented behavior. Correct intended form recorded for whenever it's
|
||
actually built.
|
||
|
||
**Why this affects §B:** VM-`COOL` (and any inter-VM Stadium wiring) sits close to this
|
||
design — see D.2's `EXPIRE` supersession above, the concrete example of exactly this risk
|
||
materializing.
|
||
|
||
**Next step:** no implementation yet — this section is capture only, per direct request. Next
|
||
session should turn D.3/D.4 into an actual scoped, ordered plan (the user's own framing:
|
||
"identify gaps, answer what we can, defer what we can't... push the pile out in front of us").
|
||
|
||
---
|
||
|
||
## E. The pile as a graph (2026-08-27)
|
||
|
||
Stated directly: "if this is all documented, scoped and correctly interwoven we can begin
|
||
iterating... I see no reason to not create this as a graph, it might better handle the way I
|
||
think." Auditing D.1-D.4/D.2b against the older Milestone 2/3/5 punch lists (§X in this
|
||
document) confirms the instinct is right, not just stylistic — several items across
|
||
*different* milestones are, on inspection, **literally the same open question**, and one
|
||
single blocker (`WRITE(10)`) gates work in at least four unrelated-looking places. A flat
|
||
punch list hides that; a graph doesn't.
|
||
|
||
```mermaid
|
||
graph TD
|
||
W10["✅ WRITE(10) SCSI support<br/>BUILT + VERIFIED 2026-08-28 (§F.17), all 3 arches"]
|
||
HOTPLUG["✅ Hotplug event surfacing<br/>CLOSED 2026-08-27, M2 (§F.3)"]
|
||
STALL["❌ USB error/stall recovery (M2)<br/>SCOPED 2026-08-27 (§F.14) — full BOT-spec recovery designed"]
|
||
|
||
M6["✅ Milestone 6 — capsule PKI<br/>DONE 2026-08-26"]
|
||
PH8["✅ Phase 8 — Zuse identity<br/>+ block-fence, DONE 2026-08-26"]
|
||
|
||
CERTVERIFY["❌ Zuse-signed user-cert verification (M3)<br/>SCOPED 2026-08-27 (§F.7) — X.509/DER, own trust root"]
|
||
FIRSTTOUCH["❌ First-touch identity→block-range<br/>allocation (M3) — SCOPED 2026-08-27 (§F.11)"]
|
||
BMAPFMT["❌ On-drive block-map format (M3)<br/>SCOPED 2026-08-27 (§F.4) — repurpose blk_meta_t"]
|
||
BMAPWRITE["✅ Write block-map to drive (M3)<br/>CLOSED 2026-08-27 (§F.12) — already built, blocked only on W10"]
|
||
BMAPREAD["✅ Read/validate block-map on insert (M3)<br/>CLOSED 2026-08-27 (§F.13) — already built"]
|
||
MIGSM["🟡 Migration state machine (M3)<br/>partially answered: ACL decides *when*"]
|
||
UNCLEAN["❌ Unclean-removal handling (M3)<br/>DESIGN CLOSED 2026-08-27 (§F.10), code not yet written"]
|
||
|
||
ACLKEY["❌ ACL 'bumps and holes' comparison<br/>semantics — SCOPED 2026-08-27 (§F.2)"]
|
||
WIREBIND["❌ Wire hotplug → capsule_birth_baby() (M5)"]
|
||
BINDSTEP["❌ Extend sk_repl_set_active_vm()<br/>with key-match (M5) — SCOPED 2026-08-27 (§F.9)"]
|
||
DETACH["❌ Detach behavior / new EJECT word (M5)<br/>SCOPED 2026-08-27 (§F.10)"]
|
||
|
||
EXPIRE["✅ EXPIRE reframed: session end<br/>= VM detach via COOL (§B + D.2)"]
|
||
RUNCAP["❌ Runtime capsule construction<br/>from thumbdrive content — SCOPED 2026-08-27 (§F.6)"]
|
||
MINT["❌ Ongoing MINT word (Phase 8 + D.3)<br/>SCOPED 2026-08-27 (§F.8) — GPT dropped, single-device confirmed OK"]
|
||
PENTAGON["📍 Pentagon: Hera/Hermes/Artemis/<br/>User-VM/Console, K5 (D.2b)"]
|
||
MSGSHAPE["✅ Hermes message shape known<br/>(MSG-CELLS, MSG-ALLOC/DELIVER)"]
|
||
MSGMIGRATE["❌ Message-bus migration of<br/>attach/verify/bind (D.4) — SCOPED 2026-08-27 (§F.15)"]
|
||
|
||
SSDSCOPE["✅ SSD identity-store scope<br/>for regular users — CLOSED 2026-08-27 (§F.16)"]
|
||
ROUNDTRIP["✅ Session state round-trip<br/>across attaches — CLOSED 2026-08-27 (§F.16)"]
|
||
POLYBLOCK["✅ Polymorphic block-boundary<br/>behavior — CLOSED 2026-08-27 (§F.16), already true"]
|
||
PROMPTBUG["🐛 (Zuse)ok> prompt typo<br/>CLOSED 2026-08-27 (§F.16) — doc-comment only, not live code"]
|
||
EMPTYBUFBUG["🐛 EMPTY-BUFFERS mass-zeros every<br/>block on every device (§F.17) — found 2026-08-28, NOT FIXED"]
|
||
|
||
W10 -->|unblocked, §F.17| BMAPWRITE
|
||
W10 -->|unblocked, §F.17| MINT
|
||
W10 -->|unblocked, §F.17| DETACH
|
||
W10 -.->|QEMU-verified; real hardware still untested| M8["Milestone 8 — bare metal"]
|
||
|
||
M6 -->|unblocks| CERTVERIFY
|
||
PH8 -->|supplies crypto primitives| CERTVERIFY
|
||
PH8 -->|supplies mint-then-pin precedent| MINT
|
||
|
||
BMAPFMT --> BMAPWRITE
|
||
BMAPFMT --> BMAPREAD
|
||
BMAPFMT --> FIRSTTOUCH
|
||
BMAPREAD -.->|found: sig must relocate off devblock 0, §F.13| MINT
|
||
|
||
CERTVERIFY --> RUNCAP
|
||
RUNCAP --> MINT
|
||
|
||
HOTPLUG --> WIREBIND
|
||
CERTVERIFY -->|"post-identity-authentication" per M5 wording, SCOPED 2026-08-27 §F.5| WIREBIND
|
||
RUNCAP -->|no per-identity VM content until this exists, SCOPED 2026-08-27 §F.5| WIREBIND
|
||
ACLKEY --> BINDSTEP
|
||
WIREBIND --> BINDSTEP
|
||
BINDSTEP --> DETACH
|
||
UNCLEAN -.->|same kill path, no flush| DETACH
|
||
DETACH -->|triggers| EXPIRE
|
||
|
||
MSGSHAPE --> MSGMIGRATE
|
||
WIREBIND -.->|hardwired version must exist first| MSGMIGRATE
|
||
BINDSTEP -.->|hardwired version must exist first| MSGMIGRATE
|
||
CERTVERIFY -.->|hardwired version must exist first| MSGMIGRATE
|
||
PENTAGON -->|which of the 10 edges get messaged| MSGMIGRATE
|
||
|
||
classDef done fill:#1a3,stroke:#333,color:#fff
|
||
classDef blocked fill:#a33,stroke:#333,color:#fff
|
||
classDef unblocked fill:#a83,stroke:#333,color:#fff
|
||
classDef open fill:#666,stroke:#333,color:#fff
|
||
classDef partial fill:#883,stroke:#333,color:#fff
|
||
class M6,PH8,EXPIRE,MSGSHAPE,HOTPLUG,BMAPWRITE,BMAPREAD,SSDSCOPE,ROUNDTRIP,POLYBLOCK,W10 done
|
||
class STALL,FIRSTTOUCH,WIREBIND,BINDSTEP,DETACH,MINT,BMAPFMT,CERTVERIFY,RUNCAP,UNCLEAN,MSGMIGRATE,ACLKEY blocked
|
||
class MIGSM partial
|
||
```
|
||
|
||
**Legend:** ✅ done · 🔓 unblocked-but-not-built · ❌ not started, no open design question left
|
||
(just needs building) · ❓ real open design question, not just missing implementation ·
|
||
🟡 partially addressed · 📍 topology/context node, not a work item itself · 🐛 trivial,
|
||
independent bug. Solid arrows = hard dependency (A must exist before B can be built or
|
||
finished). Dashed arrows = softer "gates/informs" relationships.
|
||
|
||
**What the graph makes visible that the flat lists didn't:**
|
||
- **`WRITE(10)` is a true hub**, not just "the M2 punch list's biggest item" — it independently
|
||
gates the block-map write path, `MINT`, detach/flush-back, *and* all real-hardware testing.
|
||
Landing it once unblocks four separate-looking fronts at once.
|
||
- **`ACLKEY` (the exact ACL comparison semantics) is one node, not two** — Milestone 5's "key
|
||
and lock data shape" and D.4's "bumps and holes" gap are the literal same open question,
|
||
asked in two different sessions/documents. Answering it once closes both.
|
||
- **`RUNCAP` (runtime capsule construction) sits on the critical path to `MINT`** — a gap that
|
||
read as "D.4 leftover detail" is actually load-bearing for the ongoing-minting flow, not a
|
||
side note.
|
||
- **`MSGMIGRATE` has three separate hardwired prerequisites**, all currently unbuilt — the
|
||
message-bus migration can't even start until `WIREBIND`/`BINDSTEP`/`CERTVERIFY` exist to
|
||
migrate *from*, confirming this is correctly the last thing to tackle in this whole area, not
|
||
a parallel track.
|
||
- **Only `SSDSCOPE`, `ROUNDTRIP`, `POLYBLOCK`, and `PROMPTBUG` are genuinely standalone** —
|
||
everything else in the pile connects to at least one other node.
|
||
- **`WIREBIND` was missing two real incoming edges** (found scoping it, §F.5): `CERTVERIFY`
|
||
and `RUNCAP` both gate it, not just `HOTPLUG`. Without a verified identity there's nothing
|
||
to bind to, and without `RUNCAP` there's no per-identity VM content to birth — today's
|
||
`capsule_birth_baby()` only runs fixed, build-time-baked capsules by name, so "spin up that
|
||
identity's VM" had no real mechanism behind it at all until this pass traced it.
|
||
- **`CERTVERIFY` and the capsule-PKI chain are two separate trust roots, not one** (§F.7) —
|
||
easy to conflate since both use Ed25519/X.509, but user certs are signed by Zuse's own
|
||
on-device key while capsules are signed by the offline root CA's snakeoil intermediate.
|
||
Neither chain validates the other; a future audit pass should keep this distinction explicit
|
||
rather than let "PKI" get treated as one undifferentiated mechanism.
|
||
- **`MINT` closed out the GPT question for the whole arc, not just itself** (§F.8) — GPT was
|
||
never actually load-bearing anywhere in this design; every node that referenced it
|
||
(`CERTVERIFY`, `WIREBIND`, `RUNCAP`, `HOTPLUG`) was already treating raw devblock-0 as the
|
||
working format. Dropping GPT converts all of those from "interim, revisit later" to simply
|
||
"the design," retroactively.
|
||
- **`BINDSTEP` already has a live target to gate, not new plumbing** (§F.9) — `USE`
|
||
(`mama_forth_words.c`) is a real, working, currently-unguarded retarget word. The only
|
||
genuinely new code this node needs is a small cross-file accessor for the currently-attached
|
||
device (mirroring `sk_repl_get_active_vm()`'s own precedent) plus the comparison itself.
|
||
- **`UNCLEAN` (M3) and `DETACH` (M5) were the same node wearing two names, in a different way
|
||
than `ACLKEY` was** (§F.10) — not identical questions asked twice, but one shared teardown
|
||
path (`capsule_vm_kill()`, already complete) forked by whether a flush is physically possible.
|
||
Scoping them together closed a long-open M3 item as a side effect of an M5 pass.
|
||
- **Found a real, pre-existing, independent bug while tracing `DETACH`** (§F.10): plain `KILL`
|
||
today never resets `g_repl_active_vm`, so killing the VM a console is currently `USE`'d onto
|
||
leaves a dangling pointer. Reported, not fixed — the new `EJECT`/hot-unplug call sites handle
|
||
this correctly, but the existing `KILL` word's own call sites do not.
|
||
- **`FIRSTTOUCH`'s own punch-list wording was stale** (§F.11) — "claim at `g.total_user_lbn`"
|
||
predates `BMAPFMT`'s distributed-ownership decision and doesn't describe a workable
|
||
mechanism against the current design. A second, unrelated discovery in the same pass —
|
||
`blk_meta_t`'s existing chain fields — turned what looked like a fragmentation problem into
|
||
a non-issue for free.
|
||
- **`BMAPWRITE` was already done** (§F.12) — `BMAPFMT`'s decision to repurpose the existing
|
||
`blk_meta_t` accessors instead of a new table meant "write the map" and "write a block" were
|
||
never actually two operations. The only reason this hasn't been seen working is `WRITE(10)`
|
||
itself, already the graph's known hub.
|
||
- **`BMAPREAD` closed the same way, but tracing it surfaced a real, load-bearing bug that had
|
||
nothing to do with reading at all** (§F.13) — `homeblocks_sig_t` and the block-subsystem's
|
||
own generic disk header physically collide at devblock 0. Every earlier node that touched
|
||
`homeblocks_sig_t` (`CERTVERIFY`, `WIREBIND`, `RUNCAP`, `MINT`) inherited this without anyone
|
||
noticing, because each only traced `homeblocks_sig_check()` itself, never the ordinary attach
|
||
path running alongside it. Fixed now (relocate to devblock 1) rather than left to surface
|
||
later as a real, confusing runtime failure.
|
||
- **`STALL` closes out Milestone 2 entirely** (§F.14) — the last item on that punch list.
|
||
Unlike most nodes this session, this one designs genuinely new protocol machinery rather than
|
||
finding existing infrastructure already covers it: no completion-code distinction and no
|
||
recovery of any kind existed before this pass, only a bounded-timeout safety net.
|
||
- **`MSGMIGRATE` needed no new mechanism, only a routing decision** (§F.15) — `MSG-DELIVER`
|
||
already executes arbitrary FORTH text on the target VM. Scoping it also surfaced a real,
|
||
substantial vision expansion (§D.6): blank-media minting is meant to be an interactive,
|
||
Console-driven onboarding form, connecting forward into both `RUNCAP`'s deferred
|
||
"default personality content" question and `MINT`'s own scope.
|
||
- **`WRITE(10)` (§F.17), the graph's original highest-leverage node, is built and verified —
|
||
the whole arc's single hard blocker is gone.** `BMAPWRITE`, `MINT`, and `DETACH`/`EJECT` all
|
||
move from "scoped, blocked" to "scoped, genuinely buildable." Validating it surfaced a real,
|
||
separate, serious bug (`EMPTYBUFBUG`) — a stark reminder that this pile still has more
|
||
surprises in it than the scoping pass alone can find; the "full codebase audit pass" already
|
||
flagged as deferred (§D.5) has real work waiting for it.
|
||
|
||
**Not yet done:** an ordered plan (which node to attack first, given the graph). Per Captain
|
||
Bob's own framing, that's the next pass — "start asking and answering questions iteratively
|
||
until we've planned as much as possible."
|
||
|
||
## F. Scoped plans (iterative Q&A pass, started 2026-08-27)
|
||
|
||
One node at a time: surface the concrete decisions the node's own doc comments/code don't
|
||
already answer, get a ruling, record it here. This section grows as the pass continues — it's
|
||
the output of "the punch list of punch lists," not a duplicate of §E.
|
||
|
||
### F.1 — `WRITE(10)` (the graph's highest-leverage node)
|
||
|
||
Traced against the live READ(10) implementation in `src/starkernel/usb/xhci.c`
|
||
(`xhci_bot_read_block()` / `xhci_bot_send_read10()` / the `XHCI_XFER_*`/`BOT_CMD_*`/
|
||
`XHCI_NEXT_ACTION_*` deferred-dispatch state machine) before asking anything — the mechanism
|
||
already exists for READ10 and WRITE10 is a direct mirror of it, not new protocol work:
|
||
|
||
- **Data direction flips.** READ10's Data-In stage rides `bulk_in_ring`; WRITE10's Data-Out
|
||
stage must ride `bulk_out_ring` instead (new `XHCI_XFER_BOT_DATA_OUT` /
|
||
`XHCI_NEXT_ACTION_BOT_DATA_OUT`, reusing the existing `xhci_bulk_out_enqueue_and_ring()`
|
||
against `bot_data_buf` rather than the CBW). CBW's `bmCBWFlags` clears the DATA_IN bit
|
||
(host→device) instead of setting it.
|
||
- **New SCSI opcode** — `SCSI_CMD_WRITE10` (0x2A) — plus `BOT_CMD_WRITE10` /
|
||
`BOT_TUR_CHAIN_WRITE10`, mirroring READ10's TUR-preflight chain exactly (same first-command
|
||
UNIT ATTENTION handling on a fresh target).
|
||
- **`blkio_usb.c`'s `usb_blk_write()`** stops being the `BLKIO_ENOSUP` stub: `memcpy` the
|
||
caller's `src` into `xdev->bot_data_buf`, then call the new `xhci_bot_write_block()` — same
|
||
shape as `usb_blk_read()`.
|
||
|
||
**Decisions made 2026-08-27 (iterative Q&A pass):**
|
||
|
||
1. **`read_only` flag in `blkio_info()`** — flips to 0 immediately once `xhci_bot_write_block()`
|
||
is proven in QEMU. No separate "bring-up mode" gate to design or later retire.
|
||
2. **Validation target** — a new disposable QEMU-only test image (e.g.
|
||
`disk/usbwrite-test.img`, following the same disposable-clone convention as
|
||
`disk/artemis-metafence-*.img`), attached via `-drive` + `-device usb-storage,drive=...`.
|
||
Never `disk/artemis.img` itself, matching [[project_disk_images_in_repo]]'s standing rule
|
||
and how Phase 8's block-fence work was validated
|
||
([[project_metadata_fence_design]]'s verification method).
|
||
3. **Scope boundary** — `WRITE(10)` alone is the unit of work. `MINT`'s write-the-identity step
|
||
and `DETACH`'s flush-back-to-thumb step are separate graph nodes; each gets its own scoping
|
||
pass once `WRITE(10)` lands, rather than being pulled into this one preemptively.
|
||
|
||
**Not yet scoped (deferred within this node):** exact CSW failure/residue handling for a
|
||
partial write (READ10's PASS/FAILED logic likely transfers unchanged, not independently
|
||
verified); whether a write needs a follow-up `TEST UNIT READY` or cache-flush SCSI command
|
||
before the transfer is considered durable on real media (QEMU's `usb-storage` backend may mask
|
||
this — a real-hardware-only question, revisit at Milestone 8).
|
||
|
||
### F.2 — `ACLKEY` (the "bumps and holes" comparison semantics)
|
||
|
||
Traced against the actual code before asking anything: `acl_mode`/`acl_allow`/`acl_pinned`
|
||
(`include/vm.h:343-346`, `src/word_source/acl_words.c`) are **`DictEntry` fields — per word,
|
||
not per VM.** There is no VM-level ACL structure anywhere today. So D.3/D.4's "reuse the
|
||
existing ACL primitive" resolution can only ever have meant reuse the *pattern* (a cached
|
||
decision plus a C-enforced one-way ratchet — `if (e->acl_pinned) return;` at every mutating
|
||
primitive) — the actual data has to live somewhere new. `sk_repl_set_active_vm()`
|
||
(`starkernel/repl.c:64`) is confirmed to be exactly what its own doc comment says: an
|
||
unguarded raw pointer swap, no credential concept at all.
|
||
|
||
**Decisions made 2026-08-27 (iterative Q&A pass):**
|
||
|
||
1. **Where the lock lives.** A new dedicated type in its own header —
|
||
`include/starkernel/vm_identity.h`, a `VMIdentity { uint8_t owner_pubkey[32]; uint8_t
|
||
installed; }` — embedded by value as a field on `struct VM`, exactly the way `VMUuid
|
||
stadium_vm_id` (`vm_uuid.h`, its own header, embedded field) already does. **Explicit
|
||
standing instruction from Captain Bob**: don't grow `struct VM`/`vm.h` feature-by-feature
|
||
with more inline fields — when a feature's data has real shape, it gets its own header and
|
||
gets *integrated* as a field, not inlined ad hoc. `VMUuid` was already the right precedent;
|
||
`VMIdentity` follows it. Deliberately holds only the public key, not a seed — unlike
|
||
Zuse's own `zuse_cert_seed`/`zuse_cert_pubkey` pair, a regular VM's lock never needs to
|
||
sign anything itself, so no private material belongs on it at all.
|
||
2. **Zuse override.** Zuse keeps an always-allowed override to attach to any VM (consistent
|
||
with the existing `emergency_console`/`zuse_session` bypass precedent, and avoids the
|
||
bricking risk of Zuse ever being locked out of a VM by its own cert state) — **but the
|
||
override must be a distinct, explicit call path** (e.g. a separate function/word, not a
|
||
silently-taken branch inside the normal bind check), so invoking it requires a deliberate
|
||
acknowledgment rather than happening as a side effect of an ordinary bind attempt. Exact
|
||
acknowledgment UX (a confirm prompt? a two-step word?) is not designed yet — the
|
||
architectural commitment is only that it can't be silent.
|
||
3. **Binding pin.** No pin. `sk_repl_set_active_vm()` stays freely retargetable — matches
|
||
today's Tripod-fleet `USE`-word behavior, and is needed for real cases where a single
|
||
console legitimately moves between VMs mid-session (Zuse doing maintenance work in another
|
||
service VM; a user's console retargeting to update something in a second VM they own). The
|
||
credential check runs at each bind call, not just once at first attach.
|
||
|
||
**Not yet scoped (deferred within this node):** the exact acknowledgment UX for the Zuse
|
||
override path; whether `VMIdentity.installed` needs its own one-way fuse semantics (probably
|
||
yes — the *lock itself* being immutable once set at a VM's birth is a different axis from
|
||
"binding stays retargetable," and shouldn't be conflated with decision 3 above) — worth a
|
||
dedicated look when this is actually built, not decided in this pass.
|
||
|
||
### F.4 — `BMAPFMT` (on-drive block-map format)
|
||
|
||
Worked through conversationally, one step at a time, rather than dumped as a finished design —
|
||
each step below only followed once the previous one was grounded in real code, not assumption.
|
||
|
||
**Step 1 — does a map cell need a state field, not just ownership?** Yes, confirmed against
|
||
existing precedent before answering: the internal Artemis BAM entry
|
||
(`blk_bam_entry_t{allocated, dirty}`, `block_subsystem.h:257-261`) already conflates ownership
|
||
and state in the simplest map this codebase has. More importantly, `MIGSM` and `UNCLEAN` (two
|
||
already-identified, currently-blocked graph nodes) have nowhere else to record "this range is
|
||
mid-migration" or "this range's flush was interrupted" without a state field — this node was
|
||
quietly upstream of both.
|
||
|
||
**Step 2 — should a cell also carry an ACL?** Yes, and it can't reuse `acl_mode`/`acl_allow`/
|
||
`acl_pinned` directly for the same reason `ACLKEY` (F.2) couldn't — those are `DictEntry`
|
||
fields. Decided: the owner field and any future ACL grant should share **the same identity
|
||
representation** `VMIdentity` already established (a pubkey), not a second encoding — keeps
|
||
"identity is one common primitive" (D.5) actually true in practice, not just in name.
|
||
|
||
**Step 3 — ACL check ordering.** "ACL denial is the fast exit path" — grounded directly
|
||
against `vm.c:611-624`'s live word-execution ACL check: a cached `acl_allow` bit is
|
||
checked *first*, before any other work, and denial short-circuits immediately. A block cell's
|
||
ACL needs the identical shape at its top: one cheap cached allow bit, checked before range/
|
||
state/owner logic runs.
|
||
|
||
**Step 4 — buffering/granularity, and a major discovery.** Confirmed: this kernel already
|
||
buffers a whole 4 KiB devblock on any block touch (`LOAD` and friends), and
|
||
`BLK_META_PER_BLOCK` (`block_subsystem.h:83`, `341u /* 341×3 ~= 1023, padded to 1024 */`)
|
||
already packs **exactly** 3×1 KiB Forth-block metadata slices into that same devblock's spare
|
||
1 KiB — the "3-block cluster + 1 KiB metadata" shape volunteered in conversation *already
|
||
exists as `blk_meta_t`*, with real, wired, non-stub accessors (`blk_get_meta()`/
|
||
`blk_set_meta()`, cached, dirty-tracked, `block_subsystem.c:1175-1207`). **Grepped and
|
||
confirmed it has zero callers anywhere in the codebase** — fully built, fully unused. Its
|
||
existing "Security & ownership" 40 bytes (`owner_id`/`permissions`/`acl_block`/`signature[2]`)
|
||
predate and directly conflict with both the anti-POSIX principle and `VMIdentity`'s
|
||
pubkey-based model — flagged, not silently reused.
|
||
|
||
**Decisions made 2026-08-27:**
|
||
|
||
1. **`BMAPFMT` is not a new structure — it's repurposing `blk_meta_t`.** No separate on-drive
|
||
block-map table gets built. Ownership/ACL/state travel *with* the block itself
|
||
(distributed), not in a centralized table (the shape Milestone 3's original wording
|
||
pictured). One I/O gets payload and map info together; nothing separate to keep in sync.
|
||
2. **Consequence flagged, not silently absorbed:** `homeblocks_sig_t`'s reserved
|
||
`blockmap_offset`/`blockmap_devblocks` fields (`homeblocks_sig.h:93-95`) become unnecessary
|
||
under this decision — that header reserved space for a centralized table this design no
|
||
longer needs. Needs a comment update in that file once this is actually built, not left
|
||
silently stale.
|
||
3. **Replacement for the 40-byte "Security & ownership" block** (same budget the old
|
||
`owner_id`/`permissions`/`acl_block`/`signature[2]` occupied; nothing else in `blk_meta_t`
|
||
moves):
|
||
```
|
||
uint8_t owner_fp[8]; /* truncated fingerprint of owner's VMIdentity pubkey, not the
|
||
full 32 bytes -- keeps this cheap per-block; full pubkey
|
||
resolves via the drive's own identity record. */
|
||
uint8_t acl_allow; /* cached fast-deny bit, checked first -- vm.c:611-624's exact
|
||
pattern, applied to a block instead of a word. */
|
||
uint8_t acl_reserved[7]; /* explicitly undecided -- deliberate slack per "flexibility
|
||
until we understand the recipe," not a placeholder to fill
|
||
reflexively. */
|
||
uint64_t reserved_future; /* untouched budget, same reasoning. */
|
||
```
|
||
`flags` (already existing, already generic) does double duty as the **state** field from
|
||
Step 1 — no new field, just future-defined bit values (`CLAIMED`/`MIGRATING`/`STALE`/etc.).
|
||
Everything else in `blk_meta_t` (`checksum`, timestamps, `content_type`, hash, chain links,
|
||
`app_data[15]`) is untouched.
|
||
4. **Allocation granularity**: claims quantize to whole devblocks (3 Forth blocks), matching
|
||
the existing packing — a cell never needs to describe partial-devblock ranges.
|
||
|
||
**Not yet scoped (deferred within this node):** the actual allow-list/grant shape beyond the
|
||
single fast-deny bit (lands in `acl_reserved`, once designed); the specific `flags` bit
|
||
values for each state; whether `blk_get_meta()`/`blk_set_meta()` need new FORTH word wrappers
|
||
or stay C-only like `zuse_cert_seed`'s "no FORTH access" precedent; the actual repurposing
|
||
edit to `block_subsystem.h`/`.c` itself (this pass produced the field design, not the code
|
||
change).
|
||
|
||
### F.5 — `WIREBIND` (breadcrumb only — followed into `RUNCAP` instead, 2026-08-27)
|
||
|
||
Traced against the live code before asking anything: `capsule_birth_baby()`
|
||
(`capsule_birth.c:473-614`) only ever births a fixed, build-time-baked `(p)` capsule looked up
|
||
by name — `BIRTH`'s own mapping is literally `S" Artemis" → "artemis:init.4th"`
|
||
(`mama_forth_words.c:216`). The PERSONALITY stage — where a baby would load *its own*
|
||
per-identity content — is a confirmed no-op today: `dispatch_init_forth(new_vm)`
|
||
(`capsule_birth.c:605`) does nothing, with its own comment stating "per-VM block storage is
|
||
M9 scope; no-op until then."
|
||
|
||
**Finding: the M5 punch-list wording — "spin up or re-attach *that identity's VM*" — has no
|
||
mechanism behind it yet.** Two real prerequisites were missing from the §E graph and are now
|
||
added:
|
||
- **`CERTVERIFY`** — M5's own wording calls this step "post-identity-authentication"; there's
|
||
nothing to bind an identity to without a verified identity first. **Decision: required, not
|
||
optional** — matches the literal M5 wording and avoids birthing VMs for unauthenticated
|
||
drives before any lock/key concept (`ACLKEY`/`BINDSTEP`) exists to protect them.
|
||
- **`RUNCAP`** — without it, "that identity's VM" can only ever mean the one generic,
|
||
same-for-everyone capsule that happens to exist at build time. Real per-identity content is
|
||
blocked on `RUNCAP` (or M9's per-VM block storage, a larger and further-out mechanism).
|
||
|
||
**Working assumption recorded for whenever this node is actually scoped:** a `WIREBIND`-
|
||
triggered birth always creates a fresh VM (matches `capsule_birth_baby()`'s existing shape).
|
||
Recognizing a *returning* identity and re-attaching to a prior VM/session is `ROUNDTRIP`'s
|
||
question, not `WIREBIND`'s — deliberately not folded in here.
|
||
|
||
**Not scoped further — by design.** Rather than decide `WIREBIND`'s generic-capsule shape in
|
||
the abstract, the pass followed the real blocker down into `RUNCAP` itself (§F.6). Revisit
|
||
`WIREBIND` once `CERTVERIFY` and `RUNCAP` both exist.
|
||
|
||
### F.6 — `RUNCAP` (runtime capsule construction from thumbdrive content)
|
||
|
||
Traced against `capsule.h`/`capsule_birth.h` before assuming new birth machinery was needed.
|
||
Two things, read directly from the code, changed the shape of this node:
|
||
|
||
1. **`capsule_birth_baby()` is already generic** — it takes `dir`/`descs`/`names`/`arena` as
|
||
plain parameters; it is not wired to the compile-time `capsule_descriptors[]` global in any
|
||
way. `mkcapsule` sets `.desc_capacity = capsule_count` always (`mkcapsule.c:955`) — despite
|
||
`capsule.h`'s own comment calling this "fixed at compile time for **Phase A**" (implying
|
||
headroom for a later phase), there is zero spare capacity reserved today, and no
|
||
runtime-append mechanism exists into the baked-in directory. Not a blocker, though —
|
||
2. **...because `RUNCAP` doesn't need to touch the baked-in directory at all.** It can
|
||
construct its own self-contained, heap-allocated `CapsuleDirHeader` + one `CapsuleDesc` +
|
||
one `CapsuleNameEntry` + a small arena — the exact same shape `kernel_main.c:729-750`
|
||
already builds when it heap-copies the compile-time directory before Mama's own birth, just
|
||
sized for a single entry instead of the whole set — fill the arena with raw FORTH source
|
||
text read off the thumbdrive, and hand that straight to the existing, unmodified
|
||
`capsule_birth_baby()`. No directory-merge logic, no changes to the birth path itself.
|
||
|
||
**Decisions made 2026-08-27:**
|
||
|
||
1. **Mechanism: heap-built single-entry capsule directory, not baked-in-directory extension.**
|
||
Confirmed safe and minimal per the trace above.
|
||
2. **Location of the source content on the drive.** `homeblocks_sig_t`'s `blockmap_offset`/
|
||
`blockmap_devblocks` fields are already dead weight per `BMAPFMT`'s decision (§F.4) that
|
||
ownership/ACL/state travel with `blk_meta_t`, not a centralized on-drive table. **Repurpose
|
||
those two fields** (rename to `identity_src_offset`/`identity_src_devblocks`) to point at
|
||
this identity's raw FORTH init/personality source instead of adding new fields — reuses
|
||
already-reserved header space, keeps `homeblocks_sig_t`'s byte budget untouched. (Comment
|
||
update in `homeblocks_sig.h` still owed when this is actually built, same as `F.4` already
|
||
flagged.)
|
||
3. **Scope of a user's pool — clarified 2026-08-27, corrects the framing this node started
|
||
from:** a user's pool is **their entire thumbdrive by default**, plus any system-resident
|
||
device blocks they additionally claim, first-come-first-served, plus the required metadata.
|
||
The thumbdrive itself is never contested — it's already theirs, no claiming logic needed
|
||
there. `FIRSTTOUCH`'s "claim a new range at `g.total_user_lbn`" logic (M3) applies only to
|
||
the *system*-device extension, not the drive itself. This simplifies the personality-source
|
||
question in decision 2 above: no allocation contention to resolve, just a pointer into the
|
||
drive's own metadata partition.
|
||
|
||
**Not yet scoped (deferred within this node):** the exact read path (which forth-blocks within
|
||
the metadata partition, how `identity_src_devblocks` bounds a variable-length FORTH source);
|
||
whether the source is plain FORTH text authored once at mint time or something a user can edit
|
||
and have re-read on next attach; the interaction with `CERTVERIFY` (does verification gate
|
||
reading this content, or only gate the resulting birth). **Partially settled by `MINT`'s own
|
||
scoping pass (§F.8, decision 2):** this region's first devblock holds a new
|
||
`user_identity_seed_t` record, and the FORTH source itself occupies the remaining devblocks —
|
||
narrows "the exact read path" question above, though the read-side code is still unwritten.
|
||
|
||
### F.7 — `CERTVERIFY` (Zuse-signed user-cert verification)
|
||
|
||
Traced against the actual crypto/cert code before assuming D.4's "no new crypto work needed"
|
||
framing was still accurate. Two files settle the trust-root question outright:
|
||
`x509_ed25519.h`'s own doc comment ("no signature verification, no chain validation, no
|
||
extension parsing — this only answers *what public key does this cert claim to hold*") and
|
||
`zuse_cert_devblock_h`'s design (Zuse's own on-device Ed25519 keypair, persisted in the system
|
||
block-fence, `FABRIC-3.md` §C). **Confirmed: a regular user's cert has a completely separate
|
||
trust root from the capsule-PKI chain** — it's signed by Zuse's own locally-resident key, never
|
||
the offline root CA / snakeoil intermediate. No chain walk is needed at verify time; it's one
|
||
signature check against a pubkey the kernel already holds.
|
||
|
||
**Decisions made 2026-08-27 (iterative Q&A pass):**
|
||
|
||
1. **Format: X.509/DER**, not a simple custom struct (the `zuse_cert_devblock_t` precedent
|
||
would have been simpler, but explicitly not chosen). The cert's `SubjectPublicKeyInfo`
|
||
holds the owner's Ed25519 pubkey (same field `x509_extract_ed25519_pubkey()` already reads).
|
||
2. **Drive binding: the cert's `serialNumber` field holds the 16-byte `drive_uuid`** (matching
|
||
`homeblocks_sig_t.drive_uuid`) rather than adding a new X.509v3 extension — `serialNumber`
|
||
is an early, shallow `SEQUENCE` member (actually simpler to reach than
|
||
`SubjectPublicKeyInfo`, which the existing parser already walks past), so this avoids
|
||
needing any extension-parsing support at all. Binding to the drive means a valid cert's
|
||
bytes alone, copied onto a different physical drive, will not verify there.
|
||
3. **Revocation: explicitly deferred.** Matches this project's incremental-scope precedent
|
||
elsewhere (capsules have a `REVOKED` flag; user-cert revocation is separate, unbuilt
|
||
surface). `CERTVERIFY` v1 only checks magic/structure, the embedded algorithm OID
|
||
(Ed25519, rejecting anything else — same check `x509_extract_ed25519_pubkey()` already
|
||
does for the pubkey field), the `serialNumber`-vs-`drive_uuid` match, and the signature
|
||
itself.
|
||
4. **Real added scope, flagged honestly rather than assumed away:** verifying (not just
|
||
reading) this cert requires genuinely new DER-walking code beyond
|
||
`x509_extract_ed25519_pubkey()` — that function stops at `SubjectPublicKeyInfo` by design.
|
||
`CERTVERIFY` additionally needs: the raw `TBSCertificate` byte range (the exact bytes
|
||
Zuse's signature covers), the outer `signatureValue` `BIT STRING` contents, and the
|
||
`serialNumber` `INTEGER` bytes. Verification itself is then one call:
|
||
`ed25519_verify(zuse_pubkey, tbs_bytes, tbs_len, signature)`, where `zuse_pubkey` comes
|
||
from this system's own `zuse_cert_devblock_t`, not from anything read off the user's drive.
|
||
5. **Location on drive:** `homeblocks_sig_t.cert_offset`/`cert_devblocks` — already reserved
|
||
for exactly this ("the CA-signed cert blob") since that header was first designed. No
|
||
header changes needed here, unlike `RUNCAP`'s repurposing of the block-map fields.
|
||
|
||
**Not yet scoped (deferred within this node):** the actual new DER TBS/signature-extraction
|
||
function's design (this pass decided *what* it must extract, not its implementation); whether
|
||
`x509_ed25519.c` grows this capability in place or a sibling file holds it; how `MINT` (which
|
||
shares this exact cert format on the signing side) constructs the DER bytes in the first place
|
||
— DER *encoding* has never been needed in this kernel before, only decoding.
|
||
|
||
### F.8 — `MINT` (the last piece of the original Tripod vision)
|
||
|
||
Two real dependencies surfaced while scoping this node that weren't in the §E graph, both
|
||
resolved before any design decision was needed:
|
||
|
||
1. **GPT.** The earlier-decided drive layout (`project_usb_thumbdrive_gpt_layout`:
|
||
~1GB GPT metadata partition + remainder) has no parser or writer anywhere in kernel code.
|
||
**Dropped entirely, per direct instruction (2026-08-27) — not deferred, decided against.**
|
||
The raw `homeblocks_sig_t`-at-devblock-0 layout every other node in this arc (`CERTVERIFY`,
|
||
`WIREBIND`, `RUNCAP`, `HOTPLUG`'s own `sig_start_fblock=0`) already treats as an interim
|
||
stand-in is now the **permanent** format. The "small metadata region + remainder is user
|
||
storage" *proportion* from the old GPT design still holds — it's just a plain devblock-count
|
||
convention within one raw device now (metadata = devblocks `0..N` covering the sig header +
|
||
cert + identity-source regions; everything past that is the user's own general block-storage
|
||
pool directly, consistent with `FIRSTTOUCH`'s pool clarification in §F.6). No GPT-parser
|
||
node needed in the graph at all — this closes that thread rather than deferring it.
|
||
**Correction, 2026-08-27 (§F.13): `homeblocks_sig_t` itself moved off devblock 0** to
|
||
devblock 1 (forth-block 4) once a real collision with the block-subsystem's own generic
|
||
disk header was found — the "devblocks `0..N`" metadata-region framing above still holds,
|
||
it just now starts one devblock later than stated here.
|
||
2. **Single-device USB driver.** `xhci_dev_t`'s BOT/MSC state (`device_ctx`, `bulk_in_ring`,
|
||
`bulk_out_ring`, `bot_data_buf`) are singular fields, not per-slot arrays — only one USB MSC
|
||
device is usable at a time today. This looked like it might conflict with the vision's
|
||
"while Zuse is active, mint a second blank thumb" (implying two drives attached at once).
|
||
**Confirmed no conflict**: Zuse's own identity is system-resident, loaded from Artemis's own
|
||
top-of-device block-fence at boot (`kernel_main.c:634-687`), never from a removable thumb.
|
||
`MINT` only ever needs the one target blank drive attached — today's single-device path is
|
||
sufficient, no driver work needed for this node.
|
||
|
||
**Decisions made 2026-08-27:**
|
||
|
||
1. **The minted identity gets a real keypair**, not just a pubkey badge. `virtio_rng` +
|
||
`ed25519_keygen()` generate a fresh seed/pubkey pair for the new identity, stored on *their*
|
||
thumbdrive (never system-resident — matches `SSDSCOPE`'s "regular users stay
|
||
thumbdrive-resident" read). Keeps the door open for a user VM to sign something itself
|
||
later, per `D.5`'s "identity is common to every VM" direction, without redesigning the cert
|
||
format again.
|
||
2. **Storage layout** (direct extension of `RUNCAP`'s already-decided `identity_src_offset`/
|
||
`identity_src_devblocks` region, §F.6 — no further header growth needed): that region's
|
||
*first* devblock holds a new `user_identity_seed_t` record (`magic`/`version`/`seed[32]`/
|
||
`pubkey[32]`/`crc` — same shape as `zuse_cert_devblock_t`, a new type rather than reusing
|
||
that Zuse-specific one, per this project's own "dedicated header per real-shaped feature"
|
||
convention); the *remaining* devblocks in that region hold the raw FORTH personality/init
|
||
source `RUNCAP` reads at birth.
|
||
3. **`drive_uuid`**: a fresh, independent 16 random bytes from `virtio_rng` at mint time (not
|
||
derived from the identity seed — two separate random draws, matching `homeblocks_sig_t`'s
|
||
own field being logically "which physical drive," not "whose identity").
|
||
4. **Cert construction**: builds the exact `CERTVERIFY` (§F.7) format — `SubjectPublicKeyInfo`
|
||
= the new pubkey, `serialNumber` = `drive_uuid`, signed with **Zuse's own seed** via
|
||
`ed25519_sign()` (real signing capability already exists, Phase 8). This is the first
|
||
confirmed caller that needs DER *encoding*, not just the decoding `x509_ed25519.c` already
|
||
has — flagged in `F.7` already, restated here since `MINT` is where it's actually needed.
|
||
5. **Header assembly**: `hdr_crc` via the existing `homeblocks_sig_compute_crc()` — no new
|
||
checksum logic, this function already does exactly what's needed.
|
||
|
||
**Not yet scoped (deferred within this node):** the default personality/init FORTH content a
|
||
freshly minted identity starts with (shared open question with `RUNCAP`, §F.6: mint-time-fixed
|
||
vs. user-editable); a monotonic-ns source for `minted_time_ns` — checked, and there is no
|
||
working precedent anywhere in this codebase today (`CapsuleDesc.created_ns` is hardcoded to `0`
|
||
at generation time, not a real example to follow); the `S" name" MINT` FORTH word's own
|
||
stack/argument shape; whether `MINT` refuses to overwrite a drive that already reads as a
|
||
recognized home-blocks drive (`HOMEBLOCKS_SIG_OK`), mirroring `WRITE(10)`'s
|
||
refuse-on-non-blank-media posture — reasonable by analogy, not decided here. The hard blocker
|
||
is unchanged and already modeled in the graph: `WRITE(10)` still doesn't exist, so `MINT`
|
||
cannot write anything to real or QEMU-emulated USB media yet regardless of the above.
|
||
|
||
### F.9 — `BINDSTEP` (extending `sk_repl_set_active_vm()` with the `ACLKEY` check)
|
||
|
||
Traced against the live retarget path before assuming new plumbing was needed:
|
||
`USE` (`mama_forth_words.c:430-480`) is the real, already-working console-retarget word —
|
||
`S" name" USE` looks a VM up by name (case-insensitive) and calls `sk_repl_set_active_vm()`
|
||
directly, completely unguarded today. `BINDSTEP` is concretely "add the `ACLKEY` (§F.2)
|
||
comparison to this one call site," not a new mechanism.
|
||
|
||
**Real gap found, not just a policy question:** the "re-verify live" approach (decided below)
|
||
needs access to whatever drive is currently attached, but `usb_blk_dev`/`xdev`
|
||
(`repl.c:107-108`) are function-static inside `sk_repl_idle()` — invisible to `USE`, which
|
||
lives in `mama_forth_words.c`. `BINDSTEP` needs a small new accessor exposing "the currently
|
||
attached home-blocks device," mirroring the existing `sk_repl_get_active_vm()` precedent
|
||
(`repl.h:67-69`) for exactly this kind of cross-file REPL-state access.
|
||
|
||
**Decisions made 2026-08-27 (iterative Q&A pass):**
|
||
|
||
1. **Check freshness: re-verify live, not cached.** Every `USE` call re-runs the
|
||
drive-check/`CERTVERIFY` path against whatever is currently attached, rather than trusting a
|
||
pubkey cached at original attach time. `USE` is a rare, human-triggered, interactive
|
||
operation — the repeated DER-parse + `ed25519_verify()` cost is a non-issue, and this avoids
|
||
any staleness question if a drive was swapped without a clean detach.
|
||
2. **VMs with no `VMIdentity` installed yet (`installed=0`) — Hera/Hermes/Artemis today, until
|
||
`D.5`'s per-VM-identity work lands — stay freely targetable, no check applied.** Preserves
|
||
`USE`'s current working behavior exactly; refusing on `installed=0` would be a real
|
||
regression today for VMs that were never in scope for this lock in the first place.
|
||
3. **For an `installed=1` target:** `BINDSTEP` refuses (matching `USE`'s existing clean
|
||
early-refusal style for not-found/dead-VM cases) whenever either no drive is currently
|
||
attached, or the freshly re-verified cert's owner pubkey doesn't match the target's
|
||
`VMIdentity.owner_pubkey`. No drive attached is a plain refusal, not a special case — it
|
||
simply means "no identity presented," which can never match.
|
||
4. **Zuse's override UX: deferred**, per direct instruction — this pass confirms the
|
||
architectural constraint from `F.2` still stands (a distinct, explicit call path, not a
|
||
branch folded into `USE`'s own logic) without committing to the exact word/interaction yet.
|
||
|
||
**Not yet scoped (deferred within this node):** the new accessor's exact name/signature; the
|
||
refusal message text (matching `USE`'s existing console-message style); whether a refused
|
||
`BINDSTEP` attempt should produce an audit-log record (this codebase has a real precedent for
|
||
this — `capsule_parity_log_birth_failed()` and friends, `parity.c` — but nothing says a
|
||
security-relevant refusal here needs the same treatment; not decided).
|
||
|
||
### F.10 — `DETACH` (the last M5 node) — and `UNCLEAN` closed alongside it
|
||
|
||
Traced `capsule_vm_kill()` (`capsule_birth.c:295-364`) fully before scoping, per D.2's own
|
||
framing that detach should reuse it: it's real and complete — evicts the Stadium patron cell
|
||
via `stadium_evict()`, `vm_cleanup()`+`sf_free()`s the VM, marks it `DEAD`, logs via
|
||
`capsule_parity_log_kill()`, is idempotent, and refuses to kill Hera. `DETACH` doesn't need a
|
||
new teardown mechanism, only a caller for this one plus the flush/bookkeeping around it.
|
||
|
||
**Real finding: D.3's "flush before eject" only makes physical sense as a deliberate
|
||
pre-removal step — once a drive is physically pulled, there's nothing left to write to.**
|
||
That splits this into two genuinely different paths, and per direct instruction this pass
|
||
scopes **both**, closing the previously-separate, long-open `UNCLEAN` node (M3) at the same
|
||
time rather than leaving it for later:
|
||
|
||
- **Graceful path (new `EJECT` word, no name conflict found):** the drive is still physically
|
||
present. Flush, then kill, then it's safe to remove.
|
||
- **Abrupt path (the existing `bot_msc_detach_pending` hot-unplug signal, already wired and
|
||
consumed in `sk_repl_idle()` for `blk_subsys_detach_device()`):** the drive is already gone.
|
||
No flush is attempted — data since the last flush is lost, which *is* correct unclean-removal
|
||
semantics, matching what every real OS does on a surprise USB yank. This is `UNCLEAN`'s
|
||
entire remaining scope, now closed as "same kill path as `EJECT`, minus the flush attempt."
|
||
|
||
**One genuinely new, small piece of state required for both paths** (not a new mechanism, a
|
||
missing link): a way to know *which VM* corresponds to the currently home-blocks-attached
|
||
drive. `BINDSTEP`'s live re-verify-and-compare approach (§F.9) only works while the drive is
|
||
still physically present and readable — useless for the abrupt path, where the device is
|
||
already gone by the time the signal fires. Decided: `WIREBIND` (§F.5, itself not yet built)
|
||
must set a small tracked `VMUuid` — "the VM currently attached via the home-blocks USB path" —
|
||
at successful birth; both `EJECT` and the hot-unplug handler read and clear it. No new registry
|
||
machinery needed beyond this: `capsule_vm_registry_get(vm_id, &entry)` (already exists) resolves
|
||
the `VMUuid` to a live entry, and `capsule_vm_kill(entry.name)` (already exists, takes a name
|
||
not an ID — chain the two rather than adding a kill-by-id variant) does the rest.
|
||
|
||
**Decisions made 2026-08-27:**
|
||
|
||
1. **`EJECT` (graceful):** resolve the tracked attached-VM id → entry; `blk_vm_flush_all(vm)`
|
||
(already exists, same function `sk_repl_idle()`'s own periodic sync already calls) while the
|
||
VM is still alive; if the console's active VM (`sk_repl_get_active_vm()`) is this same VM,
|
||
reset it to Hera (`sk_repl_set_active_vm(NULL)`) *before* teardown — necessary, not
|
||
optional, see the dangling-pointer finding below; then `capsule_vm_kill(entry.name)`; clear
|
||
the tracked attached-VM state. `capsule_vm_kill()`'s own idempotency covers the case where
|
||
the VM was already gone by some other path — no extra guard needed.
|
||
2. **Hot-unplug (`UNCLEAN`, closed):** identical steps minus the flush — skipped outright, not
|
||
attempted-and-ignored-on-failure, since the device is confirmed gone by the time this signal
|
||
fires. Same active-VM reset-before-kill step applies.
|
||
3. **`W10`'s edge to `DETACH` in the §E graph is specifically about the `EJECT` path's flush
|
||
step** — the hot-unplug/`UNCLEAN` path never touches `WRITE(10)` at all (nothing is written
|
||
during an unclean removal), so `UNCLEAN` itself is *not* blocked on `WRITE(10)`, only
|
||
`EJECT` is. Worth keeping distinct now that both share one node.
|
||
|
||
**Separate, already-existing bug found while tracing this, reported per standing instruction
|
||
(not fixed):** `capsule_vm_kill()` never touches `g_repl_active_vm`. Today, plain `KILL` on a
|
||
VM the console is currently `USE`'d onto leaves `g_repl_active_vm` dangling — the REPL would
|
||
fault on the next command dispatched through it. This is real and reachable *today*,
|
||
independent of `DETACH`/`EJECT`; decision 1 above only fixes it for the new `EJECT`/hot-unplug
|
||
call sites, not the pre-existing plain `KILL` word.
|
||
|
||
**Not yet scoped (deferred within this node):** the exact new tracked-state variable's
|
||
name/location (logically `WIREBIND`'s to own, but `WIREBIND` itself isn't built yet); whether
|
||
`EJECT` needs a name argument or always targets "whatever's currently attached" (leaning toward
|
||
the latter, given the single-USB-device constraint confirmed in `F.8` means there's never more
|
||
than one candidate — not committed here); the console message text distinguishing a graceful
|
||
eject from an unclean one in the transcript.
|
||
|
||
### F.11 — `FIRSTTOUCH` (identity → system-device block-range allocation)
|
||
|
||
Traced `g.total_user_lbn` (`block_subsystem.c:634,650,655,677,688,753`) and `blk_meta_t`'s
|
||
existing chain fields before assuming the M3 wording was still accurate. **Finding: it isn't,
|
||
fully.** "Claim a new range at `g.total_user_lbn`" predates `BMAPFMT`'s decision (§F.4) to drop
|
||
the centralized block-map in favor of distributed per-block ownership. `total_user_lbn` is a
|
||
global high-water mark that only grows when an entirely new *device* attaches (RAM, Artemis's
|
||
disk, a USB drive) — it says nothing about claiming space *within* an already-attached,
|
||
fixed-size device. `FIRSTTOUCH`'s real job is scanning Artemis's own system-resident device's
|
||
existing `blk_meta_t` records for unowned devblocks, not extending any counter.
|
||
|
||
Also confirmed live and unrelated to `BMAPFMT`'s repurposing: `blk_meta_t`'s "Link/chain
|
||
support" fields (`prev_block`/`next_block`/`parent_block`/`chain_length`,
|
||
`block_subsystem.h:232-236`) are real, general-purpose, block-number-granularity linkage — not
|
||
device-chain-specific — and completely untouched by `F.4`'s field redesign.
|
||
|
||
**Decisions made 2026-08-27:**
|
||
|
||
1. **A claim is a scattered chain, not a required contiguous run.** Reuses the existing
|
||
`prev_block`/`next_block`/`chain_length` fields directly — immune to fragmentation, and
|
||
this mechanism already exists, unused, waiting for exactly this. The claim's identity to
|
||
the caller is its chain-head devblock number; `owner_fp` is written to *every* devblock in
|
||
the chain (not just the head), so ownership is directly readable from any member block
|
||
without first walking the chain — consistent with `BMAPFMT`'s whole point of making
|
||
ownership locally readable per-block.
|
||
2. **Discovery: full linear scan, every time, no cached index.** Matches `BMAPFMT`'s own
|
||
explicit "no centralized table, nothing extra to keep in sync" philosophy exactly. One scan
|
||
pass serves both questions this node needs answered — "does this identity already own a
|
||
claim" (`owner_fp` match) and "which devblocks are free" (`owner_fp` all-zero) — collected
|
||
together in the same walk, not two separate scans.
|
||
3. **Failure mode: fail outright, no partial claim.** If the device doesn't have enough free
|
||
devblocks (scattered or not) to satisfy the request, return an error and let the caller
|
||
decide — matches this codebase's existing return-code convention throughout (`capsule_birth_baby()`
|
||
and friends), no new partial-allocation semantics invented.
|
||
4. **Allocation unit: whole devblocks**, already decided in `BMAPFMT` (§F.4, decision 4) —
|
||
`FIRSTTOUCH` doesn't revisit this, just inherits it.
|
||
|
||
**Not yet scoped (deferred within this node):** the actual function signature/call site (this
|
||
pass decided the algorithm, not its C interface); who calls `FIRSTTOUCH` and when — most
|
||
likely `MIGSM` (the migration state machine, M3, still 🟡 partial) deciding "this identity
|
||
needs N more devblocks," but that call site doesn't exist yet either; whether a first-touch
|
||
scan should skip/short-circuit once the caller's own already-attached thumbdrive still has
|
||
room (matching the pool-scope clarification in §F.6: the thumb is the default pool, this
|
||
node is specifically the *overflow* case) — implied yes by that clarification, not explicitly
|
||
re-confirmed here.
|
||
|
||
### F.12 — `BMAPWRITE` (closed — already built, blocked only on `WRITE(10)`)
|
||
|
||
Traced `blk_set_meta()`/`cache_writeback()`/`blk_flush()` fully before assuming any new write
|
||
path was needed. **Finding: this node is already done.** `blk_set_meta()`
|
||
(`block_subsystem.c:1198-1210`) marks the same cache-buffer entry's `meta_dirty` flag that
|
||
ordinary block-data writes use; `cache_writeback()` (`block_subsystem.c:274-285`) — called from
|
||
both `blk_flush()`'s targeted and flush-all paths, the exact function `blk_vm_flush_all()` and
|
||
`SAVE-BUFFERS` already exercise — packs the in-memory `blk_meta_t[]` array back into the
|
||
devblock's metadata region (`meta_to_slice()`) and writes the *whole* 4 KiB devblock (data +
|
||
metadata together, since they share one physical buffer) via `write_devblock_4k()`, real and
|
||
unstubbed all the way down to `dev->write()`.
|
||
|
||
**Why this closes rather than just narrows:** the M3 item as originally written assumed a
|
||
*separate* block-map structure that would need its own bespoke write mechanism. `BMAPFMT`'s
|
||
decision (§F.4) to repurpose the already-existing, already-wired `blk_meta_t` accessors instead
|
||
of building a new table means "writing the map" and "writing a block" were never actually two
|
||
different operations — the infrastructure that already existed for ordinary block persistence
|
||
already covers metadata identically, with zero new code. The only reason this hasn't been
|
||
observed working end-to-end is the same hard blocker already modeled in the graph: `WRITE(10)`
|
||
doesn't exist yet, so `dev->write()` never actually reaches physical media for a USB-attached
|
||
drive today. Nothing else stands between this and working.
|
||
|
||
**Handoff note for `BMAPREAD`'s own future pass:** the read side (`cache_load_devblock()`,
|
||
`block_subsystem.c:308-322`) already unpacks and validates metadata (`meta_from_slice()`,
|
||
magic-checked, resets to a clean default on a bad/missing magic) on every devblock load — very
|
||
likely the same "already built, not a separate item" outcome, but not confirmed here; that
|
||
node may have its own distinct question (e.g. whether a home-blocks drive needs an explicit
|
||
validate-on-insert step beyond ordinary lazy per-devblock read-through-cache), left for its own
|
||
pass rather than assumed away.
|
||
|
||
### F.13 — `BMAPREAD` (closed — already built) — and a real header collision found and fixed
|
||
|
||
Confirmed the handoff note from `F.12`: `cache_load_devblock()` (`block_subsystem.c:308-322`)
|
||
already unpacks and validates every devblock's metadata on load (`meta_from_slice()`,
|
||
magic-checked, resets cleanly on a bad/missing magic) — the read side is exactly as "already
|
||
built, not a separate item" as the write side. Same closure, no new mechanism needed.
|
||
|
||
**But tracing "validate on insertion" turned up a real, previously-unflagged conflict that has
|
||
nothing to do with `blk_meta_t` at all.** `blk_subsys_attach_device()` — the *general* attach
|
||
path every device goes through, USB drives included — always calls `blk_format_or_load_disk()`
|
||
(`block_subsystem.c:581-613`), which reads devblock 0 looking for the block-subsystem's own
|
||
`'STFR'`/v2 volume header. `homeblocks_sig_t`'s magic is `'LAHB'`, not `'STFR'`, so a minted
|
||
home-blocks drive always reads as "unrecognized" here and lands in `BLK_FMT_PROVISIONAL` —
|
||
and `blk_get_buffer()` (`block_subsystem.c:894`) **refuses all writes** to a `PROVISIONAL`
|
||
slot until `blk_subsys_confirm_format()` is called. That function
|
||
(`blk_commit_format()`, `block_subsystem.c:546-571`) writes a fresh STFR header to the *same*
|
||
devblock 0 — which would physically overwrite `homeblocks_sig_t`. Neither outcome (stay
|
||
`PROVISIONAL` forever, or get STFR-formatted and lose the identity header) works. This was
|
||
invisible to `CERTVERIFY`/`WIREBIND`/`MINT` because each only ever traced
|
||
`homeblocks_sig_check()` itself, never the generic attach path running alongside it.
|
||
|
||
**Decision made 2026-08-27:** `homeblocks_sig_t` relocates to **devblock 1** (forth-block 4),
|
||
leaving devblock 0 for the generic STFR/v2 header untouched — no changes to the already-tested
|
||
block-subsystem code. `homeblocks_sig_check()` already takes its starting forth-block as a
|
||
plain parameter (`sig_start_fblock`, not hardcoded), so this is a call-site change, not a
|
||
format-logic change. `MINT` (§F.8) must also run the ordinary `blk_subsys_confirm_format()`
|
||
path at mint time (writing a ordinary fresh STFR/v2 header at devblock 0) so the drive is
|
||
immediately writable through the normal block-buffer path, in addition to writing
|
||
`homeblocks_sig_t`/cert/identity-source starting at devblock 1. `F.8`'s own text has been
|
||
corrected in place to point here rather than restate this.
|
||
|
||
**Not yet scoped (deferred within this node):** whether `MINT`'s STFR-format-then-sig-write
|
||
sequence needs a specific ordering/atomicity guarantee (e.g. what state a drive is left in if
|
||
power is lost between the two writes) — real question, not addressed here; the exact
|
||
`sig_start_fblock` value call sites (`CERTVERIFY`, `WIREBIND`, `RUNCAP`, `MINT`) all need
|
||
updating from their currently-recorded devblock-0-relative assumption to devblock-1, a
|
||
mechanical follow-up once any of this is actually coded, not re-litigated here.
|
||
|
||
### F.14 — `STALL` (the last M2 item) — full BOT-spec recovery designed
|
||
|
||
Traced the actual completion-code handling before designing anything. **Real finding: every
|
||
transfer completion, control or bulk, shares one gate** — `if (code != XHCI_COMPLETION_CODE_SUCCESS)
|
||
{ console_println("xhci: control transfer failed"); break; }` (`xhci.c:1267-1270`). A STALL
|
||
isn't distinguished from any other failure today, and there is no recovery of any kind — no
|
||
xHCI Reset Endpoint command, no USB `CLEAR_FEATURE(ENDPOINT_HALT)`. There is a real safety net
|
||
though: `xhci_bot_wait_for_idle()` (`xhci.c:831-839`) has a bounded iteration count and returns
|
||
`BOT_STATUS_TIMEOUT` rather than hanging forever, since the generic bail-out never resets
|
||
`bot_cmd_kind`. So today's failure mode is "clean timeout, endpoint left permanently wedged for
|
||
everything after it" — not a crash, but not recoverable either.
|
||
|
||
Also confirmed by inspection: none of the pieces real recovery needs exist yet —
|
||
`XHCI_COMPLETION_CODE_STALL_ERROR` isn't defined (only `_SUCCESS`), nor are the `RESET_ENDPOINT`
|
||
(xHCI spec value 14) or `SET_TR_DEQUEUE_POINTER` (value 16) command TRB types. Per direct
|
||
instruction, full recovery is designed now rather than deferred to Milestone 8.
|
||
|
||
**Decisions made 2026-08-27 — the recovery sequence, mirroring the existing bounded-retry
|
||
precedent `bot_tur_retries`/`XHCI_BOT_TUR_MAX_RETRIES` already establishes for TUR:**
|
||
|
||
1. **Distinguish the completion code.** Add `XHCI_COMPLETION_CODE_STALL_ERROR` (6, per xHCI
|
||
spec) alongside the existing `_SUCCESS` (1). The shared completion-code gate branches: a
|
||
plain non-success/non-stall code keeps today's behavior (log and bail); a stall specifically
|
||
enters the new recovery path below instead of falling straight to the generic message.
|
||
2. **xHCI-level reset (two new command TRB types + two new command functions, mirroring the
|
||
existing `xhci_cmd_disable_slot()`/`xhci_cmd_configure_endpoint()` shape exactly):**
|
||
- `xhci_cmd_reset_endpoint(dev, slot_id, ep_id)` — new `XHCI_TRB_TYPE_RESET_ENDPOINT_CMD`
|
||
(14), transitions the stalled endpoint from Halted back to Stopped in the xHC's internal
|
||
context. Which endpoint is "the stalled one" is already known — it's whichever of
|
||
`bulk_in_ep_addr`/`bulk_out_ep_addr` the in-flight transfer purpose
|
||
(`BOT_DATA_IN`/`BOT_DATA_OUT`/`CBW_SENT`/`CSW_RECEIVE`) was using, both already tracked
|
||
fields.
|
||
- `xhci_cmd_set_tr_dequeue_pointer(dev, slot_id, ep_id, new_dequeue, dcs)` — new
|
||
`XHCI_TRB_TYPE_SET_TR_DEQUEUE_POINTER_CMD` (16), repositions the ring's dequeue pointer
|
||
past the failed TRB so the next enqueued transfer resumes cleanly.
|
||
3. **USB-level clear (reuses the existing control-transfer infrastructure already built for
|
||
`GET_DESCRIPTOR`/`SET_CONFIGURATION` — no new transfer-stage machinery, just a new request
|
||
payload):** send `CLEAR_FEATURE(ENDPOINT_HALT)` (standard request, `wValue=0`, `wIndex=`
|
||
the stalled endpoint address) to clear the device's own halt condition and reset its data
|
||
toggle.
|
||
4. **Escalation on a second stall (or if step 3 itself stalls):** issue Bulk-Only Mass Storage
|
||
Reset (BOT class request, `bmRequestType=0x21`, `bRequest=0xFF`, no data stage — again the
|
||
existing control-transfer machinery, a different request payload) followed by
|
||
`CLEAR_FEATURE(ENDPOINT_HALT)` on *both* bulk endpoints (BOT spec 5.3.4's full procedure),
|
||
then retry the original command from scratch — the same "fresh target" restart shape the
|
||
existing TUR-chain retry already uses.
|
||
5. **Bounded, not infinite:** a new `bot_stall_recoveries` counter capped at a new
|
||
`XHCI_BOT_STALL_MAX_RECOVERIES`, exact value TBD but same shape as `XHCI_BOT_TUR_MAX_RETRIES`.
|
||
Exhausting it sets `bot_last_status = BOT_STATUS_FAILED` and `bot_cmd_kind = BOT_CMD_NONE`
|
||
explicitly — a clean terminal failure signaled to `xhci_bot_wait_for_idle()`'s caller,
|
||
rather than relying purely on the outer timeout the way today's un-recovered stall does.
|
||
|
||
**Not yet scoped (deferred within this node):** the exact `XHCI_BOT_STALL_MAX_RECOVERIES` value;
|
||
whether a `WRITE(10)`-specific residual-data concern exists on a stalled Data-Out stage (a
|
||
partially-written SCSI command's recovery semantics may differ subtly from a stalled Data-In
|
||
read — not analyzed here, `WRITE(10)` itself still doesn't exist); real-hardware validation is
|
||
explicitly out of reach until Milestone 8, same caveat `WRITE(10)`'s own CSW-residue question
|
||
carries (§F.1) — this design is believed spec-correct but has only QEMU (which rarely if ever
|
||
issues real stalls) to test against for now.
|
||
|
||
### D.5 — Scope expansion (2026-08-27): identity is common to every VM, not just users
|
||
|
||
Surfaced while scoping `ACLKEY`, stated directly: *"the whole object is to deliver a
|
||
functional Tripod plus identities common to all VM's... this means now that we have to
|
||
create certificates for Hermes, Artemis and Console along with Zuse and CaptBob's
|
||
'thumbdrives.'"* This reframes `VMIdentity` (F.2 above) from a user-VM-specific mechanism into
|
||
the **general per-VM identity primitive** — every node in the pentagon (Hera, Hermes,
|
||
Artemis, a user's own VM, the Console) is expected to eventually carry one, not only VMs born
|
||
from an attached thumbdrive. Doesn't change F.2's design, just its intended reach — worth
|
||
remembering when `VMIdentity` actually gets built, so it isn't accidentally scoped narrower
|
||
than this.
|
||
|
||
### F.3 — `HOTPLUG` (closed)
|
||
|
||
Re-checked directly against the live code before asking anything, per Captain Bob's own
|
||
standing note not to "grep-sample" — read `sk_repl_idle()` (`starkernel/repl.c:82-190`) and
|
||
the `bot_msc_attach_pending`/`bot_msc_attached`/`bot_msc_detach_pending` doc comments in
|
||
`xhci_driver.h:244-265` in full.
|
||
|
||
**Finding: closed.** The original Milestone 2 ask was "decide and implement where the hotplug
|
||
event surfaces... likely a callback registered by whatever owns the home-blocks logic, not
|
||
xHCI code calling into `block_subsystem.c` directly." What's built is a flag-set-by-xHCI,
|
||
flag-consumed-by-`sk_repl_idle()` pattern — not literally a registered callback, but the
|
||
`bot_msc_detach_pending` field's own doc comment states the goal directly: *"keeps xhci.c
|
||
decoupled from block_subsystem.c"* — and that decoupling is real and working, confirmed live
|
||
on all three arches (attach: `blkio_usb_open_msc()` → `homeblocks_sig_check()` →
|
||
`blk_subsys_attach_device()`; detach: `blk_subsys_detach_device()`, symmetric shape).
|
||
|
||
**Confirmed 2026-08-27:** close this node as done. No separate design work remains on
|
||
`HOTPLUG` itself — the answer for `WIREBIND` (the node depending on it) is not "build a new
|
||
hotplug-detection mechanism," it's "reuse this one."
|
||
|
||
**Handoff note for `WIREBIND`'s own future scoping pass:** the existing attach point already
|
||
calls `homeblocks_sig_check()`, which already distinguishes a recognized home-blocks drive
|
||
(`HOMEBLOCKS_SIG_OK`) from blank/foreign/corrupt media (`HOMEBLOCKS_SIG_BLANK`/
|
||
`BAD_VERSION`/`BAD_CRC`/`READ_ERROR`). That result is the natural branch point for `WIREBIND`:
|
||
a recognized drive continues into cert-verify + `capsule_birth_baby()`; a blank/foreign one
|
||
stays plain read-only block storage, matching the reasoning already documented for the
|
||
`WRITE(10)` refuse-on-blank-media logic. Not built yet — `WIREBIND` still needs its own Q&A
|
||
pass — but it doesn't start from zero.
|
||
|
||
**Two new standing items that came with this, both explicitly deferred (not part of the
|
||
current iterative pass, recorded so they aren't lost):**
|
||
- **A full codebase audit pass**, using this same iterative Q&A workflow, to scavenge for more
|
||
"same gap hiding in two places" opportunities the way `ACLKEY` turned out to be one gap
|
||
wearing two names (Milestone 5's "key/lock shape" and D.4's "bumps and holes"), and the way
|
||
`WRITE(10)` turned out to gate four separate-looking fronts at once. Not scoped, not
|
||
started — a future pass of its own.
|
||
- **v2.0.0 release target**: once Tripod is functional end to end, every VM has its identity
|
||
mechanism, and the codebase/documentation are clean per the above audit, tag a v2.0.0
|
||
release. Stated as the destination this whole planning arc is walking toward, not an
|
||
immediate next step.
|
||
|
||
### F.16 — The last four standalone nodes: `SSDSCOPE`, `ROUNDTRIP`, `POLYBLOCK`, `PROMPTBUG`
|
||
|
||
Closes out every node in §E's graph except `WRITE(10)` itself. Three of the four close by
|
||
cross-referencing decisions already made this session, not new tracing; `POLYBLOCK` needed the
|
||
user's own clarification first, since it had never been elaborated beyond one sentence.
|
||
|
||
**`SSDSCOPE` — closed.** "Does the system-resident store (Phase 8's block-fence) ever hold
|
||
anything about *regular* users, or is it purely Zuse/system bookkeeping?" This was already
|
||
answered, just not cross-referenced: D.3 states directly — *"the system has no requirement to
|
||
maintain any single identity itself — no user directory, no central registry of who exists.
|
||
Each thumbdrive is a fully self-contained, portable identity record."* This session's own later
|
||
scoping confirms it structurally rather than just by assertion: `CERTVERIFY`/`RUNCAP`/`MINT`
|
||
(§F.6-F.8) all put a user's cert, keypair, and personality source *on their own thumbdrive*,
|
||
never on Artemis's system-resident fence. `FIRSTTOUCH` (§F.11) does let a user's claim extend
|
||
onto system-resident devblocks, but that's anonymous per-block ownership (`blk_meta_t.owner_fp`,
|
||
a pubkey fingerprint) written directly on the claimed blocks themselves — not a lookup-by-name
|
||
directory entry anywhere. The system-resident fence holds exactly one identity record type:
|
||
Zuse's own (`zuse_cert_devblock_t`).
|
||
|
||
**`ROUNDTRIP` — closed.** "Does a user's session state round-trip across multiple attaches?"
|
||
Implied yes by D.3's detach description, now confirmed structurally rather than left implicit:
|
||
since a user's pool *is* their own thumbdrive by default (§F.6, decision 3) and `EJECT`
|
||
(§F.10) flushes working state back to wherever those blocks physically live before killing the
|
||
VM, the data simply never leaves the drive between attaches. Reattaching the same drive and
|
||
re-birthing via `WIREBIND`+`RUNCAP` naturally resumes from wherever the blocks were left — no
|
||
separate "session resume" mechanism needs building; the drive's own persistence *is* the
|
||
round-trip mechanism, a consequence of decisions already made rather than a new one.
|
||
|
||
**`POLYBLOCK` — closed, reframed as an existing principle, not a missing feature.** Clarified
|
||
directly by the user: the block abstraction at this level should have no idea what backs it —
|
||
"a re-usable, polymorphic definition of a storage device." Traced against `blk_get_buffer()`
|
||
(`block_subsystem.c:866-900`) before concluding anything: it already returns a uniform `uint8_t*`
|
||
regardless of whether the backing LBN resolves to RAM, a raw in-memory device
|
||
(`blk_subsys_add_raw_device()`), or a real disk-cache-backed device — callers (every FORTH
|
||
block word, `LOAD`, etc.) never know or care which. The one backend-specific behavior found
|
||
(`BLK_FMT_PROVISIONAL` write-refusal, disk-backed slots only) doesn't violate this — it's
|
||
encapsulated *within* the uniform interface (an ordinary failed `blk_get_buffer()` call), not
|
||
leaked as caller-visible backend-awareness. **This principle is already true of the live code**,
|
||
confirmed rather than assumed.
|
||
|
||
**`PROMPTBUG` — closed, and found to be less than originally described.** Traced for the
|
||
literal string before closing: no runtime code anywhere prints `"zuse)ok> "` today —
|
||
`SK_PROMPT_TEXT` (`repl.c:50`) is unconditionally `"ok> "`; there is no `zuse_session`-gated
|
||
prompt-text branch in the live REPL at all yet (only `emergency_console` gating exists,
|
||
`repl.c:412,459`). The typo lives in a **doc comment** describing not-yet-implemented intended
|
||
behavior (`repl.c:372`), not in running code. Closed as: whenever a Zuse-specific prompt is
|
||
actually implemented, use `"(Zuse)ok> "` (both parens, generalizing to any attached identity —
|
||
`"(CaptBob)ok> "`), matching the already-corrected form recorded in `D.4`. The comment itself
|
||
is a trivial one-line fix whenever code in this file is next touched — not applied here,
|
||
per standing instruction against unrequested code changes.
|
||
|
||
**Every node in the §E graph is now scoped or closed except `WRITE(10)` itself** — the one
|
||
remaining hard blocker gating real execution of nearly everything captured across this entire
|
||
`§F` pass.
|
||
|
||
### D.6 — Console-driven interactive mint onboarding (vision capture, 2026-08-27)
|
||
|
||
Surfaced live while scoping `MSGMIGRATE`'s message-target question (§F.15) — capture only, per
|
||
this arc's own "capture first, plan second" discipline; not designed in detail here.
|
||
|
||
**Stated directly:** the Console VM (real hardware ownership: serial + framebuffer + PS2
|
||
keyboard) is where blank-media minting actually happens interactively, not a bare programmatic
|
||
`MINT` call. Zuse's own thumbdrive becoming physically present is itself what starts
|
||
authentication (no separate manual step). For **blank** media specifically, the system pulls
|
||
up an interactive "user StarshipOS (LithosAnanke+StarForth) Mint" onboarding form on the
|
||
Console:
|
||
|
||
```
|
||
Full Name:
|
||
Address 1:
|
||
Address 2:
|
||
City:
|
||
State/Province:
|
||
Country:
|
||
Metadata: (hexdump of some encrypted metadata OR QR code later)
|
||
```
|
||
|
||
**Why this matters beyond `MSGMIGRATE` itself:** this directly informs two already-open
|
||
questions elsewhere rather than sitting alone —
|
||
- **`RUNCAP`'s deferred "default personality content" question (§F.6)**: this onboarding data
|
||
is very likely *part of* what a freshly minted identity's personality/init source encodes,
|
||
not a separate concern. Not confirmed as a final answer — flagged as the likely connection.
|
||
- **`MINT`'s own scope (§F.8)**: minting a new identity now has a real interactive-collection
|
||
step in front of the cert/keypair/header-writing mechanics already scoped there. The Console
|
||
becomes an active participant in `MINT`, not just Zuse triggering it standalone.
|
||
|
||
**Not designed here, explicitly deferred (per the user's own "later" on the metadata field):**
|
||
the "hexdump of some encrypted metadata OR QR code" field's actual mechanism, encoding, and
|
||
purpose; the exact validation/editing UX for the form itself (can a field be corrected before
|
||
submit? what happens on a blank/skipped field?); how collected form data actually reaches
|
||
`MINT`'s execution (addressed at the mechanism level only, in `MSGMIGRATE` below — the message
|
||
carries the values, the *encoding* of the metadata field itself is separate and unaddressed).
|
||
|
||
### F.15 — `MSGMIGRATE` (the last real design question)
|
||
|
||
Traced the actual Hermes mechanism before designing anything, rather than treating "message"
|
||
as an abstract placeholder. **Real finding: `MSG-DELIVER` (`capsules/hermes/init.4th:200-203`)
|
||
does `MSG-TO@ IDX>NAME VM-EXEC`** — a message's out-of-line payload (`PADDR`/`PLEN`) is
|
||
**arbitrary FORTH source text, executed on the destination VM via the ordinary interpreter**,
|
||
not a structured/typed RPC call. Migrating an interaction to Hermes needs no new dispatch
|
||
machinery at all — only a decision about what FORTH text goes where.
|
||
|
||
Also worth being explicit about, since it reframes why this migration is even worth doing:
|
||
every "VM" in this system is a Forth VM instance living inside **one kernel address space**,
|
||
not a separate OS process. There is no correctness reason `WIREBIND`/`BINDSTEP`/`CERTVERIFY`
|
||
*must* become messages — Hera's REPL can already call their C functions directly, today, once
|
||
built. The stated reason to migrate anyway is architectural discipline: routing through Hermes
|
||
gives uniform heat-tracking/`STADIUM-CELL` participation in Compudynamics and an audit trail
|
||
via `MSG-SEQ`, matching the standing "nothing is done until it's messaging" completion
|
||
criterion — not solving an isolation problem that doesn't exist here.
|
||
|
||
**Decisions made 2026-08-27:**
|
||
|
||
1. **Real target: the Console VM**, not a self-addressed message to Hera. There is a real
|
||
Console VM today (serial + framebuffer + PS2 keyboard) — this isn't waiting on D.2b's
|
||
pentagon to become real, it already is. Corrects this pass's own first framing (a
|
||
self-addressed-to-Hera fallback was floated and explicitly rejected in favor of this).
|
||
2. **The flow is two hops, not one:**
|
||
- **Hop 1 (Hera → Console):** Hera's existing hardwired attach detection
|
||
(`homeblocks_sig_check()`, unchanged — this is Milestone 2 hardware-driver work, not
|
||
something that itself becomes a message; nothing exists to receive a message before
|
||
detection happens) results in a message to the Console VM reporting the outcome —
|
||
recognized identity, or blank/foreign media.
|
||
- **Console-side behavior (not itself a message):** for a recognized identity, the Console
|
||
proceeds toward the existing cert-verify/bind flow; for blank media, the Console runs the
|
||
interactive mint-onboarding form (§D.6) using its own owned hardware (framebuffer/PS2).
|
||
- **Hop 2 (Console → Hera):** once the Console has what it needs — either confirmation to
|
||
proceed with a recognized identity, or the completed onboarding fields for a new one — it
|
||
sends a message *back* to Hera to actually execute the privileged operation
|
||
(`MINT`/`WIREBIND`/`BINDSTEP`), since Hera owns the VM registry these operations mutate.
|
||
3. **Payload shape: arguments encoded as literals directly in the payload text**, not a
|
||
dedicated no-argument word reading global state. E.g. the Console's hop-2 message to Hera
|
||
for a mint would look like a human-typed command line with the collected fields pushed as
|
||
string literals ahead of the word call (`S" Robert James" S" 123 Main St" ... MINT`) — more
|
||
flexible than a fixed no-argument word, at the cost of the Console needing to build that
|
||
text safely (not addressed here — see deferred list).
|
||
|
||
**Not yet scoped (deferred within this node):** the exact literal-encoding/escaping mechanism
|
||
for building a multi-field FORTH command string safely on the Console side (a real concern —
|
||
untrusted-ish human-typed onboarding text landing in FORTH source text merits care, not
|
||
assumed away); the precise message `TYPE` values for each of the three migrated interactions
|
||
(this pass decided the mechanism and direction, not the constant catalog); whether
|
||
`CERTVERIFY`'s own re-verify-live behavior (`BINDSTEP`, §F.9) changes shape once it's Console-
|
||
mediated rather than a direct Hera-side check; how this reconciles with `PENTAGON`'s still-open
|
||
"which of the 10 edges are real today" question (`D.4`) — this node answers it for exactly the
|
||
Hera↔Console edge, not the other nine.
|
||
|
||
|
||
|
||
### F.17 — `WRITE(10)` implemented and verified (2026-08-28) — the graph's last hard blocker, closed
|
||
|
||
Implemented exactly as scoped in `F.1`, a direct mirror of the existing `READ(10)` machinery
|
||
with the data direction flipped: new `XHCI_XFER_BOT_DATA_OUT`/`XHCI_NEXT_ACTION_BOT_DATA_OUT`
|
||
states, `xhci_bot_send_write10()`/`xhci_bot_write_block()`/`xhci_bot_write_data_out()`
|
||
(`xhci.c`), new `SCSI_CMD_WRITE10` opcode and `BOT_CMD_WRITE10`/`BOT_TUR_CHAIN_WRITE10` enum
|
||
values (`xhci_driver.h`, kept as fully separate fields from the `bot_read10_*` ones rather than
|
||
renamed/reused, so the already-tested `READ10` path is never touched). `usb_blk_write()`
|
||
(`blkio_usb.c`) is real now, no longer the `BLKIO_ENOSUP` stub — `read_only` in `blkio_info()`
|
||
flips to `0`, per `F.1` decision 1, now that it's proven.
|
||
|
||
**Verified live, all three architectures, with a genuine cold-reboot round-trip** — not just a
|
||
same-session read, the real proof this class of change needs: `BLK-CONFIRM-FORMAT`'s BAM/reloc
|
||
zero-page writes and an explicit block-content write both completed via clean `WRITE10` cycles
|
||
(CSW PASS every time), then a full kernel rebuild + fresh boot read the written byte back
|
||
correctly — `amd64=65`, `aarch64=170`, `riscv64=201`, each at LBN 32734 on a disposable
|
||
`disk/usbwrite-test.img` attached via QEMU `usb-storage`, matching `F.1` decision 2's validation
|
||
plan exactly.
|
||
|
||
**One real methodology finding, not project-specific:** the disposable test drive must be
|
||
hotplugged via QMP *after* boot reaches `ok>`, never attached at QEMU launch — attaching before
|
||
`xhci_bringup()`'s controller reset means the port already reads as connected when the driver
|
||
starts, so no fresh Port Status Change event ever fires and hotplug detection looks broken even
|
||
though it isn't (already documented in `project_xhci_milestone_2d_polling`, re-confirmed here
|
||
the hard way on the first attempt).
|
||
|
||
**A real, serious, pre-existing bug found and reported (not fixed) during validation:**
|
||
`EMPTY-BUFFERS` (`empty_all_buffers()`, `block_words.c:148-158`) does not implement standard
|
||
Forth-79 semantics — it force-writes zero to *every* block on *every* attached device, rather
|
||
than discarding cache-slot assignments without writing. Calling it during testing (reasonably
|
||
assuming standard semantics) mass-zeroed blocks across the whole system, including
|
||
`disk/artemis.img` — ~3.3 million bytes, essentially its entire content region. Restored from
|
||
git, confirmed byte-identical to `HEAD`. Avoided for the rest of validation (detach/reattach via
|
||
QMP used instead to force a fresh device read, which is non-destructive).
|
||
|
||
**Consequence for the rest of the graph:** `W10` was the single hub gating `BMAPWRITE`
|
||
(already independently confirmed working, §F.12), `MINT`, and `DETACH`/`EJECT`'s flush step —
|
||
all three are now genuinely unblocked, not just scoped. Nothing about their own design changes;
|
||
they can now actually be built and tested rather than only designed. Real hardware (Milestone
|
||
8) remains untested — this verification is QEMU-only, per `F.1`'s own acknowledged scope.
|
||
|
||
### D.7 — Birth-by-message-only, universal to Zuse and user VMs (vision, 2026-08-28)
|
||
|
||
Stated directly, prompted by planning the freshly-minted-`zuse.img`-thumbdrive-to-`(Zuse)ok>`
|
||
flow: **"Hera gives birth only by request once messaging and storage are functional and
|
||
should display no coupling outside of messages. This will include 'user' VMs too."** A
|
||
sharper, universal restatement of the standing "nothing is done until it's messaging"
|
||
completion criterion (D.1) — not a new principle, but a concrete commitment that this
|
||
specific, next flow should go straight to the message-only shape rather than pass through
|
||
a hardwired interim first, since messaging (`Hermes`) and storage (`Artemis`) are both now
|
||
genuinely live.
|
||
|
||
**Concretely, for the mint-`zuse.img`-thumbdrive-to-`(Zuse)ok>` flow:**
|
||
1. Hera's own hardware polling (`sk_repl_idle()`/`xhci_poll_events()`) is unavoidable —
|
||
she's the only VM whose C code has xHCI MMIO access. This stays.
|
||
2. But Hera must not *decide or act* on what she detects. Per direct instruction: **detection
|
||
ownership moves to a running Artemis VM instance** — a real change from what `HOTPLUG`
|
||
(closed) and `WIREBIND` (§F.5, still just a breadcrumb) already built/scoped, both of which
|
||
currently have Hera deciding and acting directly. Hera's role narrows to relaying the raw
|
||
hardware event; Artemis owns the identity-verification decision and the birth *request*.
|
||
3. Grounded, not assumed: block LBN space is one flat, global address space
|
||
(`block_subsystem.c`'s `g.total_user_lbn`), not per-VM — any VM's dictionary can already
|
||
read any LBN once told which one, no new C-level read plumbing needed for the
|
||
identity-check content itself. The gap is coordination (when, which LBN), not capability.
|
||
|
||
**Real, load-bearing prerequisite surfaced by tracing, not assumed:** Artemis is not
|
||
persistently alive today. Every boot this session confirms she's birthed for her own 4.6
|
||
migration self-test, then explicitly killed (`KILL: Artemis dead`) before the REPL ever
|
||
reaches `ok>`. For any message to reach a running Artemis instance, she has to actually stay
|
||
born — a real, new requirement this direction depends on, not something `HOTPLUG`/`WIREBIND`
|
||
ever needed before. **CLOSED 2026-08-28**: `kernel_main.c`'s item 4.2/4.6 self-test blocks
|
||
(birth → exercise diagnostics → `KILL`) stripped to birth-only; verified live on all three
|
||
architectures — `BIRTH: Hermes live`/`BIRTH: Artemis live`, zero `KILL:` anywhere in any log,
|
||
and an interactive `USE` round-trip confirmed Hermes genuinely reachable post-boot. Found and
|
||
fixed a real regression along the way: Hermes's `CD-INIT` (arena init, and the thing that
|
||
loads `lib.4th` into her own dictionary) was only ever invoked by the removed self-test —
|
||
added an unconditional call at the end of her own `init.4th` so a real birth actually
|
||
initializes her. Also added `STARTUP-BANNER` (scaffolding — prints `(no identity)` until
|
||
`CERTVERIFY`/`MINT` exist to populate it from a thumbdrive's PKI fields), wired into each
|
||
Tripod leg's own `init.4th`.
|
||
|
||
**What this reshapes, not yet re-scoped:**
|
||
- `WIREBIND` (§F.5) was deliberately left a breadcrumb, not fully designed, specifically
|
||
because "wire hotplug → `capsule_birth_baby()` directly" is exactly the hardwired coupling
|
||
this principle now rules out — this direction is the real answer that breadcrumb was
|
||
waiting for, not a contradiction of it.
|
||
- `MSGMIGRATE` (§F.15) scoped a two-hop Hera↔Console flow (Hera reports attach, Console
|
||
requests the privileged operation). Artemis is a **new participant**, not a replacement —
|
||
identity verification/storage (Artemis's role) and human-facing onboarding UX (Console's
|
||
role, §D.6) are both real and distinct; the full flow likely needs both, reconciled together
|
||
rather than re-litigated as either/or.
|
||
- Applies universally, per direct instruction — the same message-only shape governs ordinary
|
||
user-VM birth too, not a Zuse-specific carve-out.
|
||
|
||
**Not yet scoped:** whether Artemis becoming persistent changes her own self-test's shape or
|
||
timing; the exact message types/payloads for Hera→Artemis (raw attach event) and
|
||
Artemis→Hera (birth request); how/whether Console fits into this specific flow versus the
|
||
already-scoped `MSGMIGRATE` two-hop; whether `CERTVERIFY` (§F.7) itself should now run as
|
||
Artemis-mediated logic rather than a bare function call. Capture only, per this arc's own
|
||
established discipline — design happens in its own pass.
|