New `make -f Makefile.starkernel thumbdrive` goal builds a generic
UEFI-bootable GPT disk image (disk image -> GPT + one FAT32 "STARKERNEL"
EFI System partition with EFI/BOOT/BOOT<ARCH>.EFI + startup.nsh) that can be
written directly to a USB thumbdrive with dd and boots on any real amd64 UEFI
firmware (Beelink SER5 path) as well as under QEMU. The monolithic loader
embeds the whole kernel, so the ESP needs only the UEFI fallback boot path.
Verified under QEMU by attaching the image as a USB mass-storage device (not
cdrom): OVMF BDS auto-selected "UEFI QEMU QEMU USB HARDDRIVE" (Boot0002),
then kernel booted normally -- LithosAnanke v2.0.0, POST 1012/0/0 + ok>,
rng: backend = virtio-rng + entropy: ready, Artemis ready, and the USB BOT/
xHCI storage path enumerated (READ CAPACITY10 -> MSC device ready -> READ10
CSW PASS). This mirrors the real-hardware SER5 flow: firmware boots the USB
thumbdrive's ESP, and the OS's own storage rides the same USB BOT/xHCI path.
Works for all three arches via the arch-mapped EFI boot name.
The §G v2.0.0 QEMU release-gate punch list is complete (G.1 xHCI stall
recovery, G.2 unified rng_get_bytes() entropy entry point, G.3 NVRAM
de-scoped). Bump the kernel version to 2.0.0 (even major = LTS, per the
Release Versioning Policy: X.0.0 = QEMU release, X.5.0 = hardware bare-metal
release). Update README and .claude/CLAUDE.md version references and the
Makefile.starkernel version-roadmap comment.
Verified: all three arches (amd64/aarch64/riscv64) build clean with v2.0.0
embedded; QEMU amd64 boot shows "LithosAnanke v2.0.0", POST 1012/0/0 + ok>,
rng: backend = virtio-rng + entropy: ready, and Zuse identity confirmed from
the attached thumbdrive.
The QEMU-verifiable slice of the real-hardware RNG driver (per FABRIC-3.md
§G.2). New include/starkernel/rng.h + src/starkernel/rng/rng.c provide the
single entropy entry point: rng_init() probes the backend set (v2.0.0:
virtio-rng only) and, on no backend, prints a loud boot-time warning while
rng_get_bytes() returns RNG_ERR_NO_BACKEND - never silently degrading to a
deterministic seed. The backend-selection switch in rng.c is the exact seam
v2.5.0's per-arch drivers (amd64 RDRAND, riscv64 Zkr, aarch64 peripheral) plug
into without touching the call path.
Consumers route through the unified layer instead of virtio-rng directly:
capsule_mint.c (identity seed + drive_uuid) and kernel_main.c phase 8
(rng_init()). virtio_rng.c stays as the sole backend. Built clean on
amd64/aarch64/riscv64. QEMU amd64 boot: POST 1012/0/0 + ok>, "rng: backend =
virtio-rng" + "entropy: ready", Zuse identity confirmed from thumbdrive -
mint/cert behavior unchanged.
FABRIC-3.md §G.2 v2.0.0 slice marked BUILT+VERIFIED.
Root cause of the G.1 follow-up boot-time attach race: on pathological
controller behavior the xhci_poll_events() drain loop had no hard ceiling.
ERDP is written back only when the loop exits, so the controller cannot
reclaim event TRBs mid-drain; if it keeps producing events the head can
chase the software dequeue pointer forever. xhci_poll_events() never returns,
sk_repl_idle() never reaches its bot_msc_attach_pending check, and a fresh
USB BOT device that finished SET_CONFIGURATION is left flagged-but-never-
attached while the guest appears hung.
Fix: bound the drain to a full ring (XHCI_EVT_RING_MAX_DRAIN = 256), so
xhci_poll_events() always terminates and always writes ERDP each call.
Unprocessed events keep their cycle bit and are re-read next poll; nothing
is dropped. On the healthy path one drain processes only the one-or-few
events the controller posts per chained command, so the bound never triggers
except in the pathological case it breaks.
Beyond the G.1 additions: a new macro in include/starkernel/xhci.h and a
bounded loop in src/starkernel/usb/xhci.c. Builds clean on amd64. Verified
across six consecutive fresh QEMU boots (previously intermittently hung).
Full BOT-spec stall recovery per FABRIC-3.md F.14: new STALL_ERROR handling,
Reset Endpoint + Set TR Dequeue Pointer commands, CLEAR_FEATURE(ENDPOINT_HALT),
escalating to Bulk-Only Mass Storage Reset, capped retries
(XHCI_BOT_STALL_MAX_RECOVERIES=2) mirroring bot_tur_retries, clean terminal
failure via xhci_stall_fail().
Purely additive recovery path off the non-success transfer-event branch; the
normal path is unchanged. Builds clean on amd64/aarch64/riscv64. QEMU amd64
boot regression passes: zero stalls, BOT attach (READ CAPACITY10 -> READ10 ->
home-blocks) completes, normal-path xHCI trace identical to baseline. Live
stall injection is not provable under qemu-xhci; deferred to v2.5.0 hardware.
FABRIC-3.md G.1 documented; ROADMAP release-versioning policy folded in.
Code review fixes, all compile clean (hosted gcc + aarch64/riscv64 kernel flags):
- repl.c (H1): reentrancy guards on the MSG-TICK idle pump. sk_repl_idle()
now defers when Hera is mid-interpret (g_mama_interpreting) or when its
own vm_interpret is on the stack (g_idle_pump_active), so a blocking
KEY/EXPECT/QUERY inside a dispatched line can no longer re-enter the
interpreter and clobber the in-flight input buffer.
- virtio_rng.c: clamp device-returned used_len to VRNG_BUF_SIZE before the
caller's data_buf copy, closing a device-controlled OOB read.
- block_subsystem.c: first-write path now keys off created_time==0 instead
of dead magic==0 so fresh blocks get a real created_time stamp; first_free/
last_allocated fixed to absolute Forth LBNs (set in blk_compute_fresh_geometry
from slot->start_lbn, no longer the wrong physical-BAM-index values from
compute_totals_from_B); physical-bounds guard on blk_meta_zone_read/write
prevents unsigned underflow on a corrupt fence >= device size.
- capsule_zuse_boot.c / capsule_wirebind.c: identity seed validated magic ->
version -> CRC-64 (compute_crc64 over offsetof(crc)) before trusting it,
so a corrupt/format-mismatched record is refused, never loaded.
- log.h / starkernel/log.h: unused LOG_LINE_MAX 256 renamed LOG_MSG_LINE_MAX
to lift the include-order collision with vm.h's LOG_LINE_MAX 64; stale
include-order comments dropped (kernel_main.c, shim.c, capsule_birth.c).
- FABRIC-3.md: three stale-doc carry-forward items closed [x] with cbe7b49
notes.
Real KEY/?TERMINAL/QUERY/EXPECT bodies (console WIP):
- repl.h/repl.c: sk_console_getkey()/sk_console_key_available()/
sk_console_readline() public bodies; non-destructive peek buffers the
found byte so a following KEY returns it.
- shim.c: getchar()/fgetc()/fgets()/sf_terminal_ready() routed through the
real console paths instead of stubs; sf_terminal_ready() in platform_io.h
with sf_terminal_ready() implemented for the hosted build (linux/io.c,
POSIX select on fd 0) wired into Makefile.
- io_words.c: ?TERMINAL now returns actual terminal-readiness, not constant 0.
Artifacts: minted disk/artemis.img + rebuilt lfs kernel; BLOCK_MAP.md,
doe csv + qemu log regenerated.
capsule_mint_identity()'s MINT_DEFAULT_PERSONALITY only ever defined
WELCOME, unlike capsule_console.c's own CONSOLE_IDENTITY_SRC. The moment
WIREBIND made a freshly-minted identity's VM live, Hera's per-idle-tick
MSG-TICK pump (repl.c Phase C) began erroring on it every tick forever --
UNKNOWN WORD: 'MSG-TICK'. Every identity minted before this fix would hit
the same infinite error-spam on going live, not just this one.
Fixed by loading common:messaging.4th + calling MSG-CD-INIT before
WELCOME, mirroring capsule_console.c's own pattern exactly.
Found and verified minting the first real second identity end to end:
disk/captain-bob.img, hot-attached via QMP alongside an already-
authenticated Zuse session, MINTed, WIREBIND-attached, switched into
live with USE, WELCOME confirmed. Zero MSG-TICK errors over 18+ seconds
of idle-loop cycling after the fix (was immediate and continuous before).
Full three-architecture regression clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBjfeLPo71sUQ8zC7V7P5m
Documents the mismatched-marker/blank-drive root cause, the LOG_INFO-vs-
LOG_WARN filtering trap that made the debug probes look like they weren't
firing, the three-architecture acceptance results, and the answered "why
not just reboot after MINT" design question (both branches already share
install_and_activate(), so there's no duplicate path to reconcile).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBjfeLPo71sUQ8zC7V7P5m
xhci_bringup() now scans for already-connected ports at bring-up
(xhci_scan_ports_for_already_connected()), not just later hotplug events,
so a USB device present on the QEMU command line at launch is detected.
Makefile.starkernel attaches disk/zuse.img on the xhci0 bus by default in
all three arch qemu targets (ZUSEDISK=, empties for a bare boot).
capsule_mint_identity() gained a drive_known_blank param to skip a fully
redundant second homeblocks_sig_check() when the caller already confirmed
HOMEBLOCKS_SIG_BLANK itself.
Root-caused what looked like a hang after the drive attached: Artemis's
fence still carried a genesis marker from before a mid-session reformat,
while the reformatted disk/zuse.img read back BLANK -- a mismatched pair
capsule_zuse_boot_try_attach() correctly declined to act on, leaving the
boot idling at a plain ok> with nothing left to log (indistinguishable
from a hang under slow TCG). Fixed by zeroing both disk/artemis.img and
disk/zuse.img at their original sizes, giving a matched blank pair.
Verified full three-architecture acceptance: amd64 fresh genesis-mint,
aarch64/riscv64 clean reload against the same now-minted images.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBjfeLPo71sUQ8zC7V7P5m
Two items, closed together per direct instruction.
1. Fence-persistence root cause, found and fixed: meta_fence_blocks
(the field gating whether blk_meta_zone_write() can succeed at all)
was carved out of what used to be unused padding in blk_volume_meta_t
-- the code's own comment already documented this. disk/artemis.img
was formatted before that field existed, so its on-disk bytes there
have always read back as 0, and the existing-volume load path
(blk_format_or_load_disk()) never recomputes it -- only a fresh
format does. Every "fence write FAILED" message this entire session,
old block-fence flow and new zuse_genesis_marker_t alike, traces to
this one thing. Patching the field in place without redoing the rest
of the geometry would risk corrupting whatever's already allocated
near the top of the volume, so the only safe fix is a genuine
reformat -- done, with explicit confirmation, since it discards
disk/artemis.img's accumulated persistent test state (regenerated
fresh at next boot regardless, not real data). Verified: fence write
now succeeds with no failure suffix, and the full mint-once ->
reboot -> reattach -> re-authenticate cycle works for the first time
this session ("Zuse: identity confirmed from attached thumbdrive",
ZUSE-SESSION? goes 0 -> -1 without re-minting).
2. BINDSTEP (FABRIC-3.md §F.9): capsule_wirebind_verify_cert() extracted
as a shared function so WIREBIND (the original attach) and BINDSTEP
(every USE of an identity-locked VM) check the exact same thing the
exact same way. mama_word_use() now re-verifies live, not cached,
whenever the target VM has VMIdentity.installed=1 -- reads whatever
drive is CURRENTLY attached, re-verifies its cert, compares owner
pubkey against the target's own installed identity, refuses on any
mismatch or no drive attached. A target with installed=0 (Hera,
Hermes, Artemis, any console VM) stays freely targetable, unchanged.
Two related bugs found and fixed live while testing BINDSTEP, not
assumed away: USE was Mama-only, so a console-paired session (§F.22)
had no way back to Hera at all -- any attempt to call USE from inside
a console VM hit "UNKNOWN WORD: USE", a genuine dead end. Per direct
instruction, USE isn't console-specific -- it should work VM-to-VM
universally, same as VM-EXEC already does -- so it's now registered in
register_child_vm_words() too. That alone wasn't enough: the console
relay (sk_repl_dispatch_line()) would have captured a bare USE call and
sent it to the paired user VM as a message instead of running it.
Fixed with a small suffix-match guard (sk_repl_line_calls_use()) --
real FORTH syntax always puts USE last, so a trailing-token check
reliably recognizes it without needing a full tokenizer, and it always
runs directly, never relayed.
Verified live end to end: USE on an unlocked VM works unconditionally;
USE escaping a console back to Hera now works; USE on an identity-
locked VM succeeds while its own drive is attached and is refused
once detached ("USE: FinT~user refused -- no matching identity
currently attached"). Clean 3-architecture regression, including
confirming disk/artemis.img's reformatted geometry loads correctly as
an already-recognized volume ("Artemis: LithosAnanke disk -- resuming")
on aarch64 and riscv64 too, not just the amd64 boot it was reformatted
under.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD
Assembles pieces already built and individually verified this session
-- CERTVERIFY (vm_identity_from_cert(), Phase A/B), RUNCAP, the
console-VM + user-VM pair (§F.22) -- into one automatic sequence,
replacing the RUNCAP-TEST/PAIR-TEST diagnostic words that exercised
each piece by hand.
New capsule_wirebind_try_attach() (capsule_wirebind.h/.c), called from
sk_repl_idle() alongside capsule_zuse_boot_try_attach() on every
HOMEBLOCKS_SIG_OK attach: sig->cert_offset==0 means this is Zuse's own
genesis-mode drive (no cert region) -- that's already
capsule_zuse_boot_try_attach()'s job, skip. Otherwise, with Zuse already
authenticated this boot (nothing to verify a regular cert against
otherwise), reads the cert devblock(s) and calls vm_identity_from_cert()
against mama_vm's own zuse_cert_pubkey and the drive's own drive_uuid.
On success: reads the drive's own user_identity_seed_t for its
username, births a console VM + RUNCAP-born user VM pair (idempotent --
no-ops if that username is already live this session), installs the
verified VMIdentity onto the user VM, and registers the "<username>~user"
pairing sk_repl_dispatch_line() (repl.c, §F.22) looks for. Deliberately
does NOT auto-USE the new console -- that stays an explicit,
ACL-gated step (BINDSTEP, §F.9), not something a bare attach should
trigger silently.
Verified end-to-end live in QEMU, including a genuine negative case:
attached disk/user1.img (signed by a different, earlier-session Zuse
instance) and got a correct "cert verification FAILED -- drive
refused" -- proof the check is real, not a rubber stamp. Minted a
fresh identity with this boot's own Zuse, reattached, and got
"WIREBIND: SamS attached and ready" printed with zero manual commands,
followed by a working USE + async WELCOME relay end to end (queued,
no UNKNOWN WORD, delivered and executed in the paired user VM on the
next idle tick). Clean 3-architecture regression: Hermes/Artemis both
birth live, no unexpected ACL denials or UNKNOWN WORD.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD
Console sessions now route through the same general VM-to-VM messaging
system (Phase C) any VM can already use for its own reasons -- not a
synchronous shortcut. Per direct instruction: real async MSG-SEND/
MSG-DELIVER (Option B), not a VM-EXEC-based synchronous relay, because
messaging is a general capability, not a console-specific mechanism.
New CONSOLE-CMD-EVENT message type (common:messaging.4th). New
sk_repl_dispatch_line() (repl.c), called from both sk_repl_step and
sk_repl_run in place of a direct vm_interpret(): if the active VM's own
name has a live "<name>~user" counterpart registered, the raw input
line is wrapped as an S"-embedded CONSOLE-CMD-EVENT MSG-SEND and
interpreted on the console VM instead of being run directly -- the
console's own next MSG-TICK (Hera's idle pump) delivers it into the
paired user VM via VM-EXEC, same mechanism every other message already
uses. Falls back to direct interpretation if there's no pairing, or if
the line contains a `"` (known v1 limitation, warned about explicitly
rather than silently mishandled).
New capsule_console_birth() (capsule_console.h/.c): a bare VM whose
only content is loading common:messaging.4th -- the console side of a
pairing, parallel in shape to RUNCAP's user-VM birth but with fixed
embedded content instead of a devblock read (no identity, no thumbdrive
involved). New PAIR-TEST diagnostic word (mama_forth_words.c, matches
RUNCAP-TEST's own precedent): births both halves of a pairing and
registers the "<name>~user" mapping. Not the real pairing call site --
that's the eventual attach/onboarding flow -- this exists to exercise
the relay live before that flow exists.
Found and fixed a real, serious bug live: console_set_vm_name() stored
the caller's raw pointer instead of copying it. mama_word_use() (USE)
passes a VMRegistryEntry field living on its own stack frame -- once
USE returns, that pointer dangles, corrupting every console tag after
the first USE (observed directly as garbled "[[]" / binary-looking
prefixes instead of "[CaptBob]"). Fixed at the source: console_set_
vm_name() now copies into internal storage. That surfaced a second,
related bug across every console_get_vm_name()-based save/restore call
site in mama_forth_words.c (BIRTH, VM-STEP, VM-EXEC, CONNECT-HERMES,
CONNECT-ARTEMIS): saving just a pointer into the single internal buffer
meant an intervening console_set_vm_name() call silently corrupted the
saved value before the restore ever ran. New console_save_vm_name()
copies into caller-owned storage; every save/restore site updated.
Verified end-to-end, live in QEMU: typed WELCOME at a paired console
VM -- it did not execute directly (no UNKNOWN WORD), printed ok
immediately (queued, async), and on the next idle tick
"[CaptBob~user] Minted identity -- default personality" appeared on
its own -- genuine delivery and execution in the paired user VM through
the real MSG-SEND/MSG-DELIVER pipeline. Console tags confirmed clean
(no garbling) across all three architectures' full regression boot.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD
Three tightly-coupled changes, verified together per Captain Bob's own
"getting rid of the emergency cli" direction:
1. Zuse's identity is thumbdrive-resident, never system-resident. New
zuse_genesis_marker_t (magic/version/zuse_pubkey[32]/crc) replaces
zuse_cert_devblock_t's slot in the top-of-device fence -- the system
now remembers only that a root identity exists and its pubkey, never
a seed. zuse_cert_devblock_t is kept in the repo, marked superseded,
no longer written by any code path.
capsule_mint_identity() grows a genesis mode (issuer_vm=NULL): no
cert is built or written (Zuse isn't verified against a separate
signer -- she's recognized by pubkey match against the marker) and
two new optional out-params (out_pubkey/out_seed) let the caller
install the cert immediately after a genesis mint.
New capsule_zuse_boot_try_attach() (capsule_zuse_boot.c), called
from sk_repl_idle() on every fresh USB attach (the only point in the
boot lifecycle a thumbdrive can actually be detected -- attach
polling doesn't exist yet at kernel_main.c's old one-shot mint point,
which is why that whole block is gone): no marker + blank drive ->
genesis-mint; marker present + matching drive -> read its own
user_identity_seed_t, install the cert. Either way, re-runs
ACL-ZUSE-BOOT (zuse.4th) so zuse_session activates exactly like it
always has for a same-boot cert install -- ACL-PIN only blocks
redefinition, not re-execution, so no new C-side auth logic needed.
2. ACL.4th activated (capsules/init.4th) -- inactive all session until
now. Found and fixed a real bug this immediately surfaced: zuse.4th's
ACL-ZUSE-BOOT tried `['] ACL-ZUSE-BOOT ACL-PIN` from inside its own
still-compiling definition -- the word isn't findable yet at that
point, so the whole definition silently failed to compile every
previous boot this session (dormant, since ACL.4th never loaded).
Fixed: pin after the definition closes, not from within it -- it
only needs to happen once anyway, and pinning doesn't block the
re-invocation genesis/attach needs.
3. The unauthenticated emergency-CLI ACL bypass is retired
(repl.c): `emergency_console = is_hera ? (zuse_session ? 0 : 1) : 0`
deleted from both sk_repl_step and sk_repl_run. Every word run from
Hera's own bare prompt now goes through ordinary ACL enforcement;
emergency_console is driven only by the genuine C-level fault
handler again.
Added ZUSE-SESSION? (starforth_words.c), a read-only diagnostic
matching ZUSE-PUBKEY@'s own precedent, to verify the whole chain
directly rather than by inference.
Verified end-to-end live in QEMU: fresh boot, no thumbdrive ->
ZUSE-SESSION? reads 0. Attach a genuinely blank drive via QMP -> genesis
mint fires automatically (no typing) -> ZUSE-SESSION? reads -1 (true).
Hermes/Artemis both birth clean on all three architectures with ACL
now actually enforced for the first time all session -- no denials, no
UNKNOWN WORD beyond the deliberate POST self-test cases.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD
disk/zuse.img and disk/user1.img now hold real Ed25519 keypairs, a
Zuse-signed cert, and real profile data (Konrad Suse/Zuse and Captain
Bob/CaptBob respectively), minted this session via MINT and verified
byte-exact against the raw devblocks. Keeping them as tracked fixtures
for the Apollo test phase.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD
Extends user_identity_seed_t (version 2) with fixed-size full_name/
username/email/phone fields -- plenty of unused pad space (4016 bytes)
was already there. Deliberately NOT encoded into the DER cert's Subject
field: that would mean building a real X.509 RDNSequence (OIDs for
commonName/emailAddress, PrintableString/UTF8String tagging), well past
this project's own stated "deliberately not a general ASN.1/X.509
[builder]" scope. This human-readable profile data isn't
security-relevant the way pubkey/serial are (the only two fields
CERTVERIFY/BINDSTEP actually check) -- it travels alongside the keypair
in the plain identity record instead.
capsule_mint_identity() takes full_name/username (required, validated
non-empty and within their fixed field widths) and email/phone (NULL or
empty = null, matching the schema's own nullable convention). The MINT
FORTH word's stack signature grows to 4 string pairs
( fname-c fname-u uname-c uname-u email-c email-u phone-c phone-u -- ok? ).
Verified live in QEMU: minted two real identities with real profile
data -- Zuse (full_name "Konrad Suse", username "Zuse",
zuse@pantheon.org) onto disk/zuse.img, and a regular user (full_name
"Captain Bob", username "CaptBob", capt.bob@pantheon.org) onto
disk/user1.img -- then read the raw devblock bytes back off both images
directly and confirmed every field byte-exact at its correct struct
offset. Clean 3-architecture regression boot confirms no side effects.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD
capsule_mint_identity() (new capsule_mint.h/.c): mints a fresh identity
onto a blank/unminted thumbdrive -- real Ed25519 keypair from virtio_rng,
a fresh drive_uuid (independent random draw, not derived from the
identity seed, per FABRIC-3.md §F.8 decision 3), a Zuse-signed DER cert
in the CERTVERIFY format, and a small working default personality (a
real WELCOME word, not a stub -- FABRIC-3.md §F.6/§F.8's own "default
personality content" question stays open, but whatever mints today must
actually do something when RUNCAP births it). Refuses to overwrite a
drive that already reads as a recognized home-blocks drive, mirroring
WRITE(10)'s own refuse-on-non-blank posture (decided now, not just
"reasonable by analogy" as §F.8 left it).
x509_build_user_cert() (x509_ed25519.h/.c): the encode-side counterpart
to the existing decode functions (x509_extract_ed25519_pubkey(),
x509_verify_signature(), x509_extract_serial()) -- a minimal DER TLV
writer producing exactly the fields those functions read. Host-tested
round-trip against the real decoder before trusting it in the kernel,
including a high-bit-serial case that exercises the DER integer-padding
rule; all assertions pass (pubkey/serial round-trip, signature verifies
against the real issuer, correctly rejects the wrong key and a
corrupted signature).
New user_identity_seed_t (user_identity_seed.h): the on-disk record for
a minted identity's own keypair, same magic+version+fields+pad-to-4096+
real-CRC convention as zuse_cert_devblock_t and homeblocks_sig_t. Fixed
devblock layout: sig(1), cert(2), seed record(3), default personality(4).
New MINT word (mama_forth_words.c) and a small accessor
(sk_repl_get_attached_blk_dev(), repl.h/.c) exposing the currently
attached USB device regardless of home-blocks recognition -- MINT's own
target is a blank drive, which by definition never sets Phase D's
sk_repl_get_homeblocks_dev().
Verified end-to-end live in QEMU: MINT on a genuinely blank test drive,
then (after a detach/reattach so the sig cache picks up the fresh
header -- a known workflow gap, not fixed here, flagged for whoever
builds the real Console onboarding flow) RUNCAP birthed a VM from that
drive's own newly-minted content, and VM-EXECing its WELCOME word
printed the default personality banner. The full mint-to-birth Tripod
identity flow works end to end for the first time. Clean 3-architecture
regression boot confirms no side effects on normal boot.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD
capsule_runcap_birth() (new capsule_runcap.h/.c): builds a heap-only,
single-entry CapsuleDirHeader + CapsuleDesc + CapsuleNameEntry + arena
from a home-blocks drive's identity_src region (skipping the first
devblock, reserved for MINT's user_identity_seed_t record) and hands it
to the existing, unmodified capsule_birth_baby() -- no new birth
mechanism, matching FABRIC-3.md §F.6's own trace.
Found and closed a real gap in that trace along the way:
capsule_birth_baby()'s signature check calls capsule_get_signatures(),
which unconditionally returns the compile-time-baked global array --
meaningless for a heap-built directory, where index 0 would compare
RUNCAP's own content against whatever real capsule happens to occupy
that slot in the baked array (guaranteed-wrong, not a security check).
Added an explicit skip_pki_sig flag (0 for all 4 existing call sites,
1 for RUNCAP): that content's trust comes from CERTVERIFY, a separate
root, not the capsule-PKI chain.
Also found live: capsule_birth_baby() never sets the registry entry's
own .name (every existing caller does this itself afterward via
capsule_vm_registry_set_name() -- RUNCAP now does too), and
capsule_exec_payload() requires a "Block NNNN" header per chunk of
content or it's silently skipped, never executed -- not a bug, but
necessary context for whoever authors MINT's default personality
content next.
Added a small accessor pair (repl.h/.c) exposing the currently attached
home-blocks device/sig -- the same gap F.9's own BINDSTEP scoping had
already flagged, needed by both.
Verified end-to-end live in QEMU: synthetic identity-source content
written directly to a thumbdrive image's raw devblocks (no capsule
build, no mkcapsule) was read, compiled, and executed by a genuinely
new VM via a diagnostic RUNCAP-TEST word -- confirmed via VM-EXEC
invoking a word defined only in that source. Clean 3-architecture
regression boot (no RUNCAP drive attached) confirms no side effects.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD
Extract the messaging vocabulary (arenas, MSG-*/CH-*/MBR-* words) out of
capsules/hermes/init.4th into a new shared capsules/common/messaging.4th
that Hermes and Artemis each load at birth, giving every VM its own
private MSG-ARENA/CH-ARENA instead of only Hermes having one. Hermes
stays the owner of the one real, canonical COMMON-CH; Artemis subscribes
into it via VM-EXEC at her own birth, and Hermes proactively subscribes
Hera (idx 0) since Hera always exists first.
Hera does NOT get her own copy: register_child_vm_words()'s own doc
comment explains why the STADIUM-* primitives common:messaging.4th
depends on are deliberately never registered in her dictionary (keeps
her dict_hash off item 4.1's baseline). Confirmed live by loading it
into her dictionary anyway first -- every colon-definition referencing
an unregistered Stadium primitive was silently dropped (MSG-HEAT@/!,
CH-HEAT@/!, MSG-COOL-ALL, MSG-TICK all missing after boot). Reverted
that path; she orchestrates via BIRTH/VM-EXEC/VM-CALL instead.
Added capsule_vm_registry_get_by_index() (capsule_birth.c/.h) for
registry enumeration by birth-order position, and a pump in repl.c's
existing idle hook that walks every live VM once per idle beat and
VM-EXECs MSG-TICK into each one except Hera's own entry.
Verified clean (no UNKNOWN WORD / VM-EXEC errors after birth) on all
three architectures.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD
Phase B of the identity pipeline (FABRIC-3.md §F.7/§F.17):
- x509_ed25519.c/.h: two new DER walkers alongside the existing pubkey
extractor -- x509_verify_signature() (verifies a cert's outer Ed25519
signature over the raw, exactly-as-encoded tbsCertificate bytes, real
signature verification against issuer_pubkey, rejects non-Ed25519
signatureAlgorithm) and x509_extract_serial() (extracts the
serialNumber INTEGER, stripping a DER padding byte if present, for the
drive_uuid binding decided in §F.7).
- vm_identity.c: vm_identity_from_cert(), ties the three DER primitives
together into the actual CERTVERIFY check -- signature verifies against
issuer_pubkey, serialNumber matches this drive's own drive_uuid,
subject pubkey extracts cleanly -- and populates a VMIdentity on
success. acl_caps is caller-supplied, not read from the cert (nothing
in the decided cert fields encodes capabilities); deciding what a
verified identity is allowed to do is policy for the caller (WIREBIND,
not yet built), not this function's job.
Verified two ways: a standalone host-side test harness (not part of the
kernel build) links the real source files against a real openssl-
generated Ed25519 X.509 cert -- extracted pubkey, extracted serial, and
signature verification all match ground truth, plus two negative tests
(wrong issuer pubkey, corrupted signature) both correctly rejected. Then
the actual kernel build verified live on all three architectures: clean
compile, clean boot to ok>, Hermes/Artemis both live with no KILL. Same
pre-existing, unrelated Zuse fence-write anomaly observed on all three
(not caused by this change, not chased here).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD
Phase A of the identity pipeline (FABRIC-3.md §F.2/§F.16, §F.13):
- New include/starkernel/vm_identity.h: VMIdentity{owner_pubkey[32],
installed, acl_caps}, its own header per the project's "give real-shaped
data its own header" convention (VMUuid's own precedent), embedded by
value on struct VM. acl_caps is an independent capability bitmask, not
an ordered privilege tier (decided 2026-08-28) -- Zuse isn't a
structurally special VM, her identity just has every bit set
(VM_IDENTITY_CAP_ALL). No individual capability bits assigned yet,
deliberate slack matching blk_meta_t's own acl_reserved precedent --
real bits get names once the operation they gate (BINDSTEP, MINT, ...)
actually gets built.
- Applied the devblock-1 relocation decided in §F.13: new
HOMEBLOCKS_SIG_START_FBLOCK constant (homeblocks_sig.h), repl.c's
homeblocks_sig_check() call site updated from the literal 0. Also
applied the still-owed blockmap_offset/blockmap_devblocks ->
identity_src_offset/identity_src_devblocks rename decided in §F.6 (no
other code referenced the old names). Updated the header's own stale
GPT-relative doc comments to match -- GPT was dropped permanently, this
location is final, not an interim stand-in.
Verified live on all three architectures: clean compile, clean boot to
ok>, Hermes/Artemis both confirmed live with no KILL (no regression from
last session's session-less fix). One pre-existing, unrelated anomaly
observed consistently on all three arches, not caused by this change (no
code touched here intersects Zuse's own fence-write path): "Zuse: minted
but fence write FAILED (not persistent)" -- flagged for its own
investigation, not chased here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD
Found while auditing messaging capabilities: artemis/init.4th defines its own CD-INIT (block 4141, "Hera calls this at fleet boot") but nothing calls it -- same latent gap as Hermes's CD-INIT (fixed last session), just undiscovered because the earlier check only searched for ART-BOOT-ENTRY, not CD-INIT specifically. ART-BOOT-ENTRY runs unconditionally but never calls CD-INIT internally. Added an unconditional call in the same trailing block that already calls STARTUP-BANNER.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD
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.
Per FABRIC-3.md D.7 (birth-by-message-only): the Tripod legs must be alive
session-less before any thumbdrive-attach flow has a running Hermes/Artemis
to message. kernel_main.c's item 4.2/4.6 self-tests previously birthed
both, exercised diagnostics, then explicitly KILLed them before ok> every
boot -- production boot never actually kept either alive. Stripped both
blocks down to birth-only, diagnostics and KILL removed.
Found and fixed a real regression this surfaced, not left broken: Hermes's
CD-INIT (message/channel arena init, and the thing that loads lib.4th into
her own dictionary) was only ever invoked by the self-test just removed --
nothing in hermes/init.4th itself called it. Added an unconditional CD-INIT
call at the end of her own init.4th so a real birth actually initializes
her arena, matching how Artemis's own ART-BOOT-ENTRY already runs
unconditionally at her own capsule load.
Added STARTUP-BANNER (lib.4th): a shared word reading a common
IDENTITY-BANNER buffer, printing "(no identity)" until CERTVERIFY/MINT
exist to populate it from a thumbdrive's PKI fields -- scaffolding only,
per direct instruction. Wired into each Tripod leg's own init.4th
(Hera/Hermes/Artemis); Artemis didn't load lib.4th before, added that too.
Verified live on all three architectures: BIRTH for both, no KILL anywhere
in any log, and an interactive USE round-trip confirmed Hermes is
genuinely reachable post-boot, not just logged as born.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD
Direct instruction while planning the next goal (mint zuse.img thumbdrive -> (Zuse)ok>): Hera must give birth only by message request, no coupling outside messages, applying universally to user VMs too, not just Zuse -- a sharper commitment to the standing "nothing is done until it's messaging" criterion (D.1) for this specific next flow, since messaging and storage are both genuinely live now.
Traced before capturing anything further: detection ownership moving to a running Artemis VM instance is confirmed a real architecture change from what HOTPLUG/WIREBIND already built/scoped (both currently have Hera deciding and acting directly, not just detecting). Found a real, load-bearing prerequisite this direction depends on: Artemis is not persistently alive today -- every boot log this session confirms she's birthed for her own migration self-test then explicitly killed before the REPL reaches ok>. Capture only, no design commitments yet -- WIREBIND/MSGMIGRATE both need their own reconciliation pass against this.
WRITE(10) moves from "scoped" (§F.1) to "built and verified" (§F.17) -- the graph's original highest-leverage node and single remaining hard blocker is gone. Updates the §E graph (W10 to done, its outgoing edges from "blocks" to "unblocked"), the M2 punch-list checkbox, and the "what the graph makes visible" summary.
Also records a new standalone bug node (EMPTYBUFBUG, matching PROMPTBUG's precedent) for the EMPTY-BUFFERS mass-zero bug found and reported (not fixed) during WRITE(10) validation -- so it isn't lost track of now that the immediate incident (corrupted then restored disk/artemis.img) is resolved.
Direct mirror of the existing READ(10) implementation (FABRIC-3.md §F.1),
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()
in xhci.c, new SCSI_CMD_WRITE10 opcode and BOT_CMD_WRITE10/BOT_TUR_CHAIN_WRITE10
enum values. usb_blk_write() in blkio_usb.c is real now, no longer the
BLKIO_ENOSUP stub. read_only flips to 0 in blkio_info() now that it's proven.
Verified live end-to-end on all three architectures with a genuine cold-reboot
round-trip (not just a same-session read): BLK-CONFIRM-FORMAT's BAM/reloc
writes and an explicit block content write both completed via clean WRITE10
cycles (CSW PASS), and the written byte read back correctly after a full
kernel rebuild + fresh boot -- amd64=65, aarch64=170, riscv64=201, each at
LBN 32734 on a disposable usbwrite-test.img attached via QEMU usb-storage.
Added Makefile.starkernel's QEMU_EXTRA (empty by default, no behavior change)
to attach the disposable test image for this validation; the drive must be
hotplugged via QMP after boot reaches ok>, not attached at QEMU launch --
attaching before xhci_bringup()'s controller reset means no fresh Port
Status Change event fires (see project_xhci_milestone_2d_polling memory).
Found and reported, not fixed, during testing: EMPTY-BUFFERS
(empty_all_buffers(), block_words.c) does not implement standard Forth-79
semantics -- it force-writes zero to every block on every attached device
instead of discarding cache assignments. This corrupted disk/artemis.img
during an earlier test run; restored from git, confirmed byte-identical.
Avoided in the final validation runs (detach/reattach used instead to force
a fresh read).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD
SSDSCOPE and ROUNDTRIP close by cross-referencing decisions already made this session (D.3's "no central registry" statement, and the pool-ownership/EJECT-flush model from F.6/F.10) rather than new tracing -- both turn out to have zero remaining work of their own. POLYBLOCK needed the user's own clarification first (never elaborated beyond one sentence since 2026-08-25): reframes as an architectural principle (the block layer shouldn't know what backs it), confirmed already true of blk_get_buffer()'s live RAM/raw/disk-cache abstraction. PROMPTBUG traced for the literal string and found to be less than described -- no live code prints "zuse)ok>" today, only a doc comment describing not-yet-implemented behavior; correct intended form recorded, comment itself not touched per standing instruction against unrequested code changes.
Also corrects two stale graph classifications noticed while cleaning up: ACLKEY's own node had never been updated despite being fully scoped back in F.2, and the empty "open" classDef bucket is now removed since every §E node is closed or scoped except WRITE(10) itself.
Traced the actual Hermes mechanism before scoping: MSG-DELIVER already executes arbitrary FORTH source text on the destination VM via VM-EXEC, not a structured RPC -- migrating an interaction needs no new dispatch machinery, only a routing decision. Noted that every "VM" lives in one kernel address space, not a separate process, so messaging WIREBIND/BINDSTEP/CERTVERIFY is an architectural-discipline choice (uniform heat/audit participation in Compudynamics via Hermes), not a correctness requirement.
Decisions: real target is the Console VM (already real hardware: serial+framebuffer+PS2, not waiting on D.2b), a two-hop flow (Hera->Console reports attach outcome, Console->Hera requests the privileged operation), payload arguments encoded as literals directly in the FORTH text.
Captured a substantial new vision detail surfaced live while scoping this (D.6): blank-media minting is meant to be an interactive Console-driven onboarding form (Full Name/Address/City/State/Country/Metadata), not a bare programmatic MINT call -- connects forward into RUNCAP's deferred "default personality content" question and MINT's own scope. Capture only, not designed in detail, per this arc's own capture-first discipline.
Traced the completion-code handling before designing anything: every transfer completion (control or bulk) shares one gate that logs and bails on any non-success code -- STALL isn't distinguished from any other failure, and no recovery exists (no xHCI Reset Endpoint, no CLEAR_FEATURE(ENDPOINT_HALT)). A bounded-timeout safety net in xhci_bot_wait_for_idle() prevents a hang, but the endpoint stays wedged for everything after it. Per direct instruction, designed full recovery now rather than deferring to Milestone 8: new STALL_ERROR completion code, new xHCI Reset Endpoint + Set TR Dequeue Pointer command TRB types (neither exists today), CLEAR_FEATURE(ENDPOINT_HALT) reusing the existing control-transfer plumbing, escalating to a full Bulk-Only Mass Storage Reset on a second stall, bounded via a new retry counter mirroring the existing bot_tur_retries/XHCI_BOT_TUR_MAX_RETRIES precedent exactly. This closes the last open Milestone 2 item.
Confirmed the F.12 handoff: cache_load_devblock() already unpacks/validates metadata on load, same "already built" story as BMAPWRITE. But tracing "validate on insertion" surfaced a real, load-bearing conflict unrelated to blk_meta_t: blk_subsys_attach_device() always runs the generic block-subsystem's own STFR/v2 header check at devblock 0, which would always read a home-blocks drive's 'LAHB' magic as "unrecognized" and leave it permanently write-refused (BLK_FMT_PROVISIONAL) -- or, if force-formatted via blk_subsys_confirm_format(), overwrite homeblocks_sig_t outright, since both want the same devblock 0. This was invisible to CERTVERIFY/WIREBIND/RUNCAP/MINT because none of them traced the generic attach path alongside homeblocks_sig_check(). Fixed: homeblocks_sig_t relocates to devblock 1 (a call-site change only, sig_start_fblock was already a plain parameter); MINT must also run the ordinary format-confirm path at devblock 0 so the drive is writable through the normal block-buffer path.
Traced blk_set_meta()/cache_writeback()/blk_flush() before assuming a new write path was needed. BMAPWRITE is already done: BMAPFMT's decision to repurpose the existing blk_meta_t accessors instead of a new table means the flush path that already exists for ordinary block data already covers metadata identically -- real, unstubbed, all the way to dev->write(). The only blocker is WRITE(10) itself, already modeled in the graph. Handoff note for BMAPREAD's own pass: the read side looks like it closes the same way but wasn't confirmed here.
Also corrects a self-inconsistency from the previous commit: UNCLEAN was marked with a "done" checkmark even though its wiring code isn't written yet, unlike BMAPWRITE which really is working code today. Moved UNCLEAN back to the "scoped, not built" bucket alongside BMAPFMT/CERTVERIFY/etc.
Traced g.total_user_lbn and blk_meta_t's chain fields before scoping: the M3 punch-list wording ("claim at g.total_user_lbn") predates BMAPFMT's distributed-ownership decision and doesn't describe a workable mechanism -- total_user_lbn only grows when a whole new device attaches, not when claiming space within one already attached. Real job is scanning Artemis's already-attached device's own blk_meta_t records for unowned devblocks. Also found blk_meta_t already has real, unused prev_block/next_block/chain_length linkage fields, untouched by BMAPFMT's redesign. Decisions: claims are a scattered chain via those fields (fragmentation-immune, free), discovered via full linear scan every time (no cached index, matches BMAPFMT's own no-centralized-table philosophy), fail outright with no partial-claim fallback if the device can't satisfy a request.
Traced capsule_vm_kill() fully -- it's real and complete, so DETACH needs only a caller plus flush/bookkeeping, not new teardown machinery. Found that D.3's "flush before eject" only makes sense as a deliberate pre-removal step, splitting this into a graceful path (new EJECT word) and the abrupt hot-unplug signal already wired -- per direct instruction, scoped both, closing the previously-separate UNCLEAN (M3) node as "same kill path, no flush attempt." Needs one small new piece of state (which VMUuid is attached via the home-blocks path) since BINDSTEP's live-reverify approach doesn't work once the device is already gone. Reports, without fixing, a separate pre-existing bug found while tracing this: capsule_vm_kill() never resets g_repl_active_vm, so plain KILL on a USE'd VM leaves a dangling pointer today.
Traced the actual retarget path before assuming new plumbing: USE (mama_forth_words.c:430-480) already looks up a VM by name and calls sk_repl_set_active_vm() directly, completely unguarded. BINDSTEP is concretely "add the ACLKEY comparison to this one call site." Found one real gap: usb_blk_dev/xdev are function-static inside sk_repl_idle(), invisible to USE -- needs a small new accessor mirroring sk_repl_get_active_vm()'s own precedent. Decisions: re-verify the attached drive live on every USE call rather than trust a cached pubkey; VMs with no VMIdentity installed yet (Hera/Hermes/Artemis today) stay freely targetable, no regression; installed=1 targets refuse on no-drive-attached or pubkey mismatch, matching USE's existing refusal style. Zuse's override UX stays deferred per direct instruction.
Traced two hidden dependencies before scoping: (1) no GPT parser/writer exists anywhere in kernel code, and per direct instruction GPT is dropped entirely rather than deferred -- the raw homeblocks_sig_t-at-devblock-0 layout every other node (CERTVERIFY/WIREBIND/RUNCAP/HOTPLUG) already treated as interim becomes the permanent format. (2) xhci_dev_t's BOT/MSC state is singular, not per-slot, raising a concern about the vision's "mint a second thumb while Zuse is active" implying two simultaneous USB devices -- resolved: Zuse's identity is system-resident (loaded from Artemis's own block-fence at boot), never thumbdrive-based, so MINT only ever needs one attached target drive. Decisions: minted identities get a real keypair (virtio_rng + ed25519_keygen), stored via a new user_identity_seed_t record occupying RUNCAP's identity_src region's first devblock; drive_uuid is a separate random draw; cert construction reuses CERTVERIFY's exact format, signed with Zuse's own seed -- first confirmed need for DER encoding, not just decoding.
Traced x509_ed25519.h and zuse_cert_devblock.h before scoping: a regular user's cert has a fully separate trust root from the capsule-PKI chain (signed by Zuse's own on-device key, not the offline root CA/snakeoil intermediate), so verification is a single ed25519_verify() call, no chain walk. Corrects D.4's earlier "no new crypto work needed" claim -- x509_extract_ed25519_pubkey() deliberately stops at SubjectPublicKeyInfo, so verifying (not just reading) a cert needs new DER-walking code to capture the TBSCertificate byte range and signature. Decisions: X.509/DER format, drive_uuid bound via the cert's serialNumber field (avoids needing extension parsing), revocation deferred, cert lives in homeblocks_sig_t's already-reserved cert_offset/cert_devblocks.
WIREBIND traced against live capsule_birth_baby()/dispatch_init_forth() and found to have no real mechanism behind it yet -- depends on CERTVERIFY (identity-authentication) and a new RUNCAP mechanism for per-identity VM content, neither shown in the §E graph before now. Followed the thread into RUNCAP: capsule_birth_baby() is already generic, so RUNCAP needs only a heap-built single-entry capsule directory, not new birth machinery. Repurposes homeblocks_sig_t's now-dead blockmap_offset/blockmap_devblocks fields (per BMAPFMT, §F.4) to point at the identity's init source instead. Also captures a user-pool scope clarification: a thumbdrive is a user's pool by default, uncontested; FIRSTTOUCH's claim logic applies only to system-device extension.
Fourth node in the iterative Q&A pass, worked conversationally step by
step: state field justified against blk_bam_entry_t precedent and the
MIGSM/UNCLEAN nodes' own needs; ACL ownership tied to the same VMIdentity
pubkey representation decided for ACLKEY; ACL check ordering grounded in
vm.c's live fast-deny word-execution pattern.
Biggest finding: BLK_META_PER_BLOCK's existing 341x3-into-1KiB packing is
exactly the "3-block cluster + 1KiB metadata" shape raised in discussion --
it's blk_meta_t, with real wired accessors (blk_get_meta/blk_set_meta) but
confirmed zero callers anywhere in the codebase, and stale POSIX-flavored
ownership fields (owner_id/permissions/acl_block) that predate the
anti-POSIX principle and the pubkey-based identity model.
Decided: BMAPFMT is not a new structure, it's repurposing blk_meta_t
(distributed ownership/ACL/state per block, not a separate centralized
table) -- flagged that this makes homeblocks_sig_t's reserved
blockmap_offset/blockmap_devblocks fields unnecessary. New field layout
for the 40-byte security/ownership block: an 8-byte owner pubkey
fingerprint, a fast-deny acl_allow bit, and deliberate reserved slack
per "flexibility until we understand the recipe."
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
Third node in the iterative Q&A pass: re-checked sk_repl_idle() and the
bot_msc_attach_pending/detach_pending doc comments directly against the
original Milestone 2 punch-list wording. The flag-set-by-xHCI,
flag-consumed-by-sk_repl_idle() pattern isn't literally a registered
callback but achieves the same documented decoupling goal
("keeps xhci.c decoupled from block_subsystem.c"), confirmed live on all
three arches. Closed as written.
Recorded a handoff note for WIREBIND's own future scoping: reuse the
existing homeblocks_sig_check() result as the branch point (recognized
drive -> cert-verify+birth, blank/foreign -> stays plain block storage)
rather than inventing new hotplug detection. Updated the §E graph's
HOTPLUG node from partial to done.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
Second node in the iterative Q&A pass: traced acl_mode/acl_allow/acl_pinned
to DictEntry (per-word, not per-VM) before proposing anything, confirming
"reuse ACL" could only ever mean reuse the pattern, not the data. Decided:
a new VMIdentity type in its own header (mirroring the existing VMUuid
precedent, not another inline VM struct field), Zuse keeps an always-allowed
but explicit-acknowledgment override, and console/VM binding stays freely
retargetable (no one-way pin).
Also captured a scope expansion surfaced during this pass: identity is a
general per-VM primitive needed by Hera/Hermes/Artemis/Console too, not
just user thumbdrives, plus two new standing items (a full codebase
scavenging audit, and v2.0.0 as the eventual release target) in new §D.5.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
First pass of the iterative Q&A planning loop over §E's dependency graph:
traced the existing READ(10) implementation in xhci.c to establish that
WRITE(10) is a direct mirror (data direction, new SCSI opcode, new BOT
state) rather than new protocol work, then recorded the three scoping
decisions made (read_only flip timing, QEMU disposable-image validation
target, scope boundary excluding MINT/DETACH) in new §F.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
No code changes -- per direct request to represent the accumulated
Milestone 2/3/5 punch-list items plus D's Tripod-vision gaps as a graph
rather than flat lists, since several items turn out to be the exact
same open question asked in different places, and several converge on
the same blocker.
Mermaid dependency graph in FABRIC-3.md §E, cross-referencing:
- WRITE(10) (Milestone 2) as a true hub: independently gates the
block-map write path, the ongoing MINT word, detach/flush-back, and
all real-hardware testing -- landing it once unblocks four
separate-looking fronts.
- Milestone 5's "key/lock data shape" and D.4's "ACL bumps and holes"
gap are literally the same open question, not two separate ones.
- Runtime capsule construction (a D.4 detail) sits on the critical path
to the MINT word, not a side note.
- Message-bus migration has three currently-unbuilt hardwired
prerequisites, confirming it's correctly last in this whole area.
- Only 4 of the ~20 nodes are genuinely standalone; everything else
connects to at least one other open item.
No ordered plan yet -- this is the graph itself, not a sequence.
Documented as the input to the next iterative planning pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
No code changes -- continued design capture, per direct request to
link today's vision into the existing FABRIC.md/FABRIC-2.md material
("the existing pile of dirt") before planning.
Mined FABRIC.md's original Stadium theory (§17.1, §20, §24) and found
the EXPIRE-as-TTL correction wasn't just cleaner once "session=VM"
landed -- it was required from the start: the foundational
patron/departure table already says VMs depart via heat decay (COOL),
never TTL (TTL only ever governed messages/ACLs). §20's outer/inner
Stadium nesting already anticipated "attach = admit a VM" as a case,
so no new Stadium theory is needed, only a new admission trigger.
§24's identity-stability rules don't block session-state round-
tripping across separate attaches either way.
Mined FABRIC-2.md and found real grounding for two more decisions: the
acl_pinned one-way-ratchet was already identified as the right shape
for both Zuse's "burn" and console-session ownership, just never built
past word-execution gating -- and the real Hermes message shape
(MSG-CELLS, 9 fields, out-of-line payload, MSG-ALLOC/CH-ALLOC/
MSG-DELIVER) gives message-bus migration a concrete target instead of
an abstract goal. Confirmed empirically that today's xHCI hotplug
attach chain is 100% hardwired, zero messaging anywhere in it -- the
real baseline to migrate from.
Captured the pentagon topology: "just to get a user, 5 VMs are needed"
-- Hera, Hermes, Artemis, the user's own VM, and the Console, fully
interconnected (K5, a pentagon with every diagonal drawn). Flagged the
Console as a full peer node, not the passive relay D.3's flow
description implied -- reconciling that phrasing against the five-node
picture is new open work.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
No code changes -- pure design capture, per direct request ("capture
EVERYTHING first then we'll build a plan").
Core correction: a session IS a VM (Zuse is a player in the Stadium
fabric like any other, not a special boolean flag on Hera). This
supersedes an in-progress plan this session to implement EXPIRE as a
Stadium-TTL sweep on the zuse_session boolean -- caught mid-research,
before any code was written. Session end = VM detach, much closer to
the existing COOL/capsule_vm_kill() path than a new TTL mechanism.
Captured the full attach/mint flow: idle-loop thumbdrive watch,
cert-only auth (no password/username, no central user directory),
runtime capsule construction from drive content, ACL-based console/VM
binding (resolves Milestone 5's "reuse ACL-PIN vs. new primitive"
question in favor of reuse), ongoing MINT, and deferred detach/flush-
back. Standing completion criterion: nothing here is done until
hardwired calls are replaced by real Hermes messages.
Sorted the resulting gaps into answered (chain-of-trust mechanism,
ACL-reuse decision, no-central-directory) vs. genuinely deferred
(runtime capsule construction, exact ACL comparison semantics,
message-bus migration scope, polymorphic block-boundary behavior,
SSD-store scope, session state round-tripping, and the underlying
WRITE(10) hard blocker). Also flagged a small unrelated bug: the
prompt should read (Zuse)ok>, not zuse)ok>.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
Before flipping, found CAPSULE_SIG_MISSING (no signature at all) is the
normal state everywhere except this machine -- 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.
Decided (on request): enforce ONLY on CAPSULE_SIG_INVALID (a signature
that IS present but doesn't verify -- unambiguous tampering/corruption
evidence). MISSING/NO_ROOT_KEY stay WARN-only permanently.
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, per the original
rollout commitment: positive case (real signed capsules) reboots clean
with zero warnings on amd64/aarch64/riscv64. Negative case (same
one-byte signature corruption used for the WARN-only proof, on Mama's
own init.4th) now genuinely refuses identically on all three:
"capsule sig: init.4th: INVALID" then "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); the kernel reaches a degraded ok> rather
than crashing, on all three architectures. Final acceptance pass (real
signed capsules, tampering reverted) clean on all three.
Milestone 6 is now fully closed except magic-number content-type
detection (shared with Milestone 4, separate scope, not started).
Documented in FABRIC-3.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
--manifest mode's file scan is a completely separate code path from
build mode (only ever walks .4th files, never the embedded PKI cert or
font capsule) -- extended it to accept the same optional --sign-key
<path> prefix build mode already has, factoring the key-loading code
into a shared load_sign_key(), 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 now passes the same
SIGN_KEY_ARGS the real build uses, so capsules/BLOCK_MAP.md reflects
this machine's actual signed state by default.
Verified: clean compile, BLOCK_MAP.md correctly shows "yes" for all 31
tracked capsules on a real signed build; a quick amd64 boot (no kernel
code touched, host tooling only) confirmed no regression.
This closes every open Milestone 6 item except magic-number
content-type detection (shared with Milestone 4, not started) and the
hard-refuse flip (deliberately deferred). Documented in FABRIC-3.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
First attempt shelled out to `openssl pkeyutl -sign` (fork/execlp, not
system() -- avoided shell string interpolation of the key path).
Corrected on request: no new external host binary dependency when the
repo's own code can do the job -- same standing preference as the
earlier anti-file correction. Rewritten to link ed25519_sign() (already
verified against OpenSSL in Phase B) directly into mkcapsule.
New tools/pkcs8_ed25519.c: a narrow DER walker (same shape as
x509_ed25519.c, deliberately not shared -- small enough that
duplicating a few TLV-walking lines beat threading a header between the
kernel crypto tree and host tooling) extracting the raw seed from the
intermediate's PKCS#8 private key, plus a minimal self-written base64
decoder (PEM is openssl genpkey's default output; no decoder existed
anywhere in the repo). Verified end-to-end before wiring anything in:
the extracted seed's derived pubkey matches the cert's exactly, and a
full self-contained sign+verify round-trip (zero openssl) passes.
CapsuleDesc had no spare bytes, so signatures live in a new parallel
CapsuleSigEntry array, emitted by a new `mkcapsule --sign-key <path>`
flag (omitted/missing key -> has_sig=0 everywhere, graceful, not a
build failure -- CI has no access to the offline key).
New capsule_sig.c/.h: capsule_verify_signature(), a separate function,
not folded into the already-tested capsule_validate(). Finds and caches
the embedded intermediate cert's pubkey once per boot, then verifies
against it. Wired into all three capsule_validate() call sites in
capsule_birth.c via log_message(LOG_WARN, ...) -- never refuses yet,
per the earlier staged-rollout decision.
Verified independently, both directions, live in the real kernel: a
full clean build (38 signed capsules) boots clean on all three
architectures with zero warnings. Separately, hand-corrupted one byte
of Mama's own init.4th capsule's stored signature (not its payload/hash,
which capsule_validate() already catches and would have masked the
test) and rebuilt just 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 doesn't refuse anything
yet. Reverted before the final, untampered 3-arch acceptance pass.
Still open: flipping WARN to hard-refuse (separate, deliberate step)
and the BLOCK_MAP.md signature-status column. Documented in FABRIC-3.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
Root CA + snakeoil intermediate generated entirely offline
(/home/rajames/CLionProjects/lithosananke-ca/, outside this repo,
private keys chmod 600) per this milestone's own requirement: Ed25519,
root self-signed 20-year validity, intermediate real-CA-signed
(CA:TRUE, pathlen:0), chain verified via openssl.
Snakeoil intermediate embedded as a capsule (capsules/pki/
snakeoil-intermediate.der) -- confirmed the font-capsule precedent
needed zero new infrastructure, any non-.4th file under capsules/
embeds verbatim already.
New x509_ed25519.c: a from-scratch, narrow DER walker (not general
ASN.1/X.509, per this milestone's design decision) extracting the raw
Ed25519 pubkey from a cert's SubjectPublicKeyInfo -- handles the
optional v3 version field, verifies the AlgorithmIdentifier OID is
Ed25519 rather than assuming, handles both DER length forms. Verified
against ground truth: the extracted key from the real embedded cert
matches openssl's own reported pubkey byte-for-byte; refusal path
checked against truncated/garbage/empty/wrong-algorithm (real RSA cert)
input. Compiles clean on all three architectures.
Still open: mkcapsule signing step, wiring ed25519_verify() into
capsule_birth.c's three validate call sites (landing warn-only first,
per decision -- a bug here could stop every capsule from birthing,
including Mama's own, on all three arches), and the BLOCK_MAP.md
signature-status column. Documented in FABRIC-3.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
Replaces the crashed NVRAM approach entirely. New
include/starkernel/zuse_cert_devblock.h: a standalone on-disk record
(magic + version + 32-byte seed + 32-byte pubkey + a real CRC-64/ISO
from day one, same discipline homeblocks_sig_t established) occupying
devblock_from_top=0 of the fence. Its own header, not inlined at the
boot call site, since the still-open MINT word will be a second
consumer of this exact format.
kernel_main.c's mint-or-load logic now reads the fence, installs an
existing valid cert, or mints fresh via virtio_rng+ed25519_keygen and
writes it. Runs right after virtio_rng_init(), before
capsule_birth_mama() -- unlike the crashed NVRAM attempt, raw block I/O
against Artemis's already-proven device has no boot-timing risk, so the
earlier "re-invoke ACL-ZUSE-BOOT after Mama birth" workaround is gone;
ACL.4th's self-activating ACL-ZUSE-BOOT sees a correct cert on its one
ordinary pass.
Verified independently across every real scenario, never trusting the
kernel's own report: fresh mint decodes correctly on disk with a CRC
confirmed by a from-scratch Python re-implementation of the algorithm;
a reboot without reformatting loads back byte-for-byte identical
seed/pubkey (genuinely "mint once, ever"); a pre-fence volume refuses
cleanly (no crash, no silent data loss, honest "not persistent"
reporting); the real, untouched disk/artemis.img exercises the same
graceful-refusal path identically on all three architectures.
Phase 8's core arc is now functionally complete: real entropy -> real
signing -> real anti-file block-native persistence -> a first-boot mint
that survives reboots. Still open: the ongoing MINT word for minting
additional regular users. Documented in FABRIC-3.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
Corrected meta_fence_blocks units 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. This
let the fence fold directly into compute_totals_from_B()'s existing
payload4k formula (total_devblocks - 1 - B - R - F) instead of a
separate user_blocks subtraction: total_blocks/user_blocks/free_blocks
all shrink correctly for free, in both the fresh-format and reload
paths, from one formula change.
New blk_meta_zone_read()/blk_meta_zone_write() -- raw, unpacked 4 KiB
devblock I/O, same shape as the header/BAM/reloc-table regions,
addressed by devblock_from_top counting down from the device's last
physical devblock. Refuses rather than clamps if the index exceeds the
on-disk meta_fence_blocks. C-only, no FORTH word wraps either -- same
discipline as vm_zuse_cert_install(), which will be this zone's first
real tenant.
Verified independently at every step, never trusting the kernel's own
report: capacity math cross-checked against a from-scratch Python
recomputation of the same formula (exact match); accessor correctness
via a temporary probe (written/run/captured/reverted) that wrote a
known pattern and read it back, then independently confirmed via a raw
read of the disk image at the exact expected physical byte offset.
Full 3-arch acceptance boot against the real, untouched disk/artemis.img,
probe code fully reverted -- clean, conservation intact.
Still open: wiring vm_zuse_cert_install() to actually persist through
these accessors, and the MINT word itself. Documented in FABRIC-3.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
Corrected substrate: this OS is anti-POSIX, anti-file by design -- the
prior "dedicated system-identity disk" framing was wrong vocabulary,
caught before any code was written (saved as
feedback_no_files_anti_posix.md). The real primitives are
content-addressed capsules and raw LBN blocks, never a filesystem.
Design (agreed on request): a growable metadata fence at the TOP of a
device's block space, mirroring block_subsystem.c's existing bottom BAM
reservation from the opposite end -- the two grow toward each other,
never colliding, same shape as a stack/heap. Starts at
BLK_META_FENCE_INIT (128 blocks), explicitly never RAM-backed. Reuses
Artemis's already-attached, already-proven virtio-blk device -- no new
device. Rejected reusing BAM's own reserved zone directly: those blocks
are fully claimed by BAM bookkeeping, not free space.
Step 1 only: new meta_fence_blocks field in blk_volume_meta_t, appended
after reloc_devblocks and carved from _pad[] -- identical graceful-
default technique reloc_devblocks already established (a pre-existing
volume reads it back as 0, not a format break). Added a compile-time
_Static_assert on the struct's total size, same discipline
homeblocks_sig.h uses -- caught a real bug immediately: the hand-summed
_pad[] formula was off by 4 bytes (a compiler alignment gap the manual
count missed), found via offsetof() rather than re-deriving by hand.
Worked against disposable clones throughout, never the real
disk/artemis.img (ARTDISK is ?=-overridable) -- artemis-metafence-fresh.img
(blank, fresh-format path) and artemis-metafence-test.img (copy of the
pre-existing artemis.img, graceful-default-on-reload path), kept as
regression fixtures matching disk/README.md's existing convention.
Verified independently via direct byte reads of the disk image, not the
kernel's own log output (log_message(LOG_INFO,...) doesn't reach serial
in this build -- unrelated pre-existing gap): fresh format writes 128 at
header offset 184, a reboot without reformatting preserves it, the old
pre-fence image reads back 0. Full 3-arch acceptance boot against the
real, untouched disk/artemis.img also clean.
Allocator (user_blocks math) and zone read/write accessors both still
open -- next steps, documented in FABRIC-3.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
Cert storage expanded from the old 16-byte placeholder to a real
32-byte seed + 32-byte pubkey. vm_zuse_cert_install() now has a
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), so the hosted-only
version added earlier this session was never actually linked into the
kernel. FORTH-side ZUSE-CERT-LO@/HI@ replaced with ZUSE-PUBKEY@ (i -- u)
over the public half only; ACL-ZUSE-BOOT now checks
ZUSE-CERT-INSTALLED? before authenticating instead of unconditionally.
Attempted NVRAM-based persistence (GetVariable/SetVariable) for the
first-boot mint flow: page-faulted inside OVMF's variable service
(CR2 in the flash MMIO window). Moving the call site to match the one
proven-safe existing SetVariable call site in this codebase produced
the identical crash -- not a timing issue. Localized with debug
markers (one boot): GetVariable works; SetVariable with real data
never returns. The existing "working" precedent call is actually a
delete-of-nonexistent-variable (size=0, data=NULL), a cheaper path
that never touches flash, so it proved nothing about real writes.
Root cause: this kernel's VMM never maps the region OVMF's variable
service needs for real flash writes -- a genuine gap in UEFI runtime-
services support, not Zuse-specific, and not obviously fixable in a
3-arch-uniform way (flash window location is firmware/arch-specific).
Independently, storing the raw seed in RUNTIME_ACCESS NVRAM would have
been a real security defect regardless of the crash -- readable by any
later-loaded UEFI app or the booted OS.
Reverted to a known-safe state: all NVRAM/mint code removed from
kernel_main.c, init.4th's ACL.4th line back to its documented
commented-out default. Verified clean compile and clean boot on all
three architectures. Cert storage expansion (the part that works)
stays. A dedicated system-identity disk (virtio-blk, already proven
for writes via Artemis) is the recommended next substrate -- not yet
decided or built. Full investigation documented in FABRIC-3.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
Scoping Phase C (the MINT word) surfaced a real blocker: "mint one
Zuse, ever" needs the cert to survive reboots, but the qemu target
copied a fresh, pristine OVMF_VARS.fd on every invocation -- a
UEFI-NVRAM-based cert would never persist under this project's own
normal test workflow. Digging further, aarch64 had no persistent NVRAM
store at all (single -bios arg, no split VARS pflash like amd64/riscv64).
Fixed rather than switching storage substrates: amd64/riscv64 now only
copy the VARS template if the destination doesn't already exist, so
`clean` (which deletes the whole build tree) is the bleach step and a
bare `make qemu` preserves NVRAM -- matching the existing "always clean
before qemu" acceptance convention exactly. aarch64 restructured to
split CODE(ro)/VARS(rw) pflash drives matching the other two, with a
graceful fallback to the old -bios mode on hosts without split firmware.
Also resolves two design questions before any cert code: Zuse doesn't
need Milestone 6's CA (that's the capsule-signing chain, a separate
trust domain -- Zuse is a self-sovereign instance-local root of trust),
and flags that this session's own earlier vm_zuse_cert_install() storage
(16 bytes) is too small for a real Ed25519 keypair.
Verified: all three architectures boot clean to ok> with the new pflash
arrangement, Stadium conservation intact, no panics or guest errors.
Infrastructure-only -- no cert code yet. Documented in FABRIC-3.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
Extends the previously verify-only ed25519.c with ed25519_keygen() and
ed25519_sign() per RFC 8032 5.1.5/5.1.6, reusing every point-arithmetic
primitive verify already had -- only seed expansion/clamping and
per-message nonce derivation are new. Signing is deterministic; only
keygen ever touches entropy, via a caller-supplied seed (virtio_rng,
Phase A) -- keygen still generates nothing itself.
New scalar_muladd() (scalar25519.c) for signing's S = (k*a + r) mod L,
the one scalar op verify never needed. Schoolbook multiply into a u128
wide accumulator with one final carry pass -- deliberately the same
shape as fe25519.c's existing multiply, which has a documented history
of a real bug from carrying mid-accumulation instead of in one pass.
Verified against an independent implementation, not self-consistency:
a throwaway host harness against Python's cryptography library (OpenSSL-
backed) across 6 trials (5 random seed/message pairs + the empty-message
case) produced byte-for-byte identical pubkeys and signatures every
time. Clean compile on all three architectures and a full 3-arch QEMU
acceptance boot, conservation intact, no panics or guest errors.
Nothing calls the new functions from the live kernel path yet -- that's
Phase C (the MINT word itself), still open, documented in FABRIC-3.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
The kernel's ed25519_verify() is deliberately verify-only -- no signing,
no keygen, no entropy source. That conflicts with the on-device MINT
word vision (Zuse signing new user certs live at runtime), so this
reopens that constraint on request rather than reshaping MINT around
verify-only.
vm_uuid.h already found the real gap: amd64 has RDRAND, riscv64 has Zkr,
but QEMU's aarch64 CPU models have neither -- confirmed against QEMU
10.2.1. A deterministic PRNG (fine for VM UUIDs) is not safe for key
generation, so this adds a virtio-rng device instead of a per-arch split:
real host entropy, identical guest-side protocol on all three arches.
New src/starkernel/virtio/virtio_rng.c + include/starkernel/virtio_rng.h,
transport plumbing mirroring the existing virtio_blk.c driver exactly.
Wired into kernel_main.c boot, -device virtio-rng-pci added to all three
QEMU targets.
Verified live (temp probe, written/run/captured/reverted): 16 real bytes
pulled through the full request/notify/poll round trip on all three
arches, three different values confirming real entropy. Final boot
against the reverted, permanent code: clean compile, clean boot to ok>
on amd64/aarch64/riscv64, Stadium conservation intact, no panics or
guest errors.
Ed25519 keygen/signing itself (Phase B) and the MINT word design
(Phase C) remain open, documented in FABRIC-3.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
Found that a pinned CONSTANT is not actually tamper-proof: ACL-PIN only
blocks redefinition, not a >BODY-then-store on the word's existing data
field. Moves the Zuse cert value into C-only VM struct fields
(zuse_cert_lo/hi + zuse_cert_installed fuse bit) with a one-time
vm_zuse_cert_install() and read-only ZUSE-CERT-LO@/HI@/INSTALLED? FORTH
accessors, closing the tamper path structurally instead of by convention.
Deletes the now-insecure ZUSE-CERT-LO/HI CONSTANT words from zuse.4th.
vm_zuse_cert_install() has no caller yet -- the real mint flow (Milestone
6 CA, the MINT word) is still open; this is storage + accessors only, not
a stand-in mint. Documented in FABRIC-3.md. Verified: hosted build clean,
mkcapsule --lint clean (31/31), clean boot to ok> on amd64/aarch64/riscv64
with Stadium conservation intact and no panics.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd
Captain Bob's correction. 64MB (disk/zuse.img, usb-thumbdrive-test.img)
was always just an arbitrary QEMU-test convenience size, never a
real-world constraint -- but the docs and script didn't say so
explicitly, and an earlier memory's "16GB reference size" phrasing
(already hedged as tentative) risked reading as a decided target.
Audited the actual format for hardcoded size assumptions: none found.
homeblocks_sig_t already carries its own metadata_devblocks field,
recording whatever size a real drive's partition actually is.
Changes: scripts/bleach_zuse_img.sh gains a --size-mb override (tested
both the override and the unchanged default); disk/README.md's
zuse.img entry and FABRIC-3.md both now state the point explicitly;
the memory file and its MEMORY.md index line corrected to match (the
GPT layout's design point is the *proportions* -- small metadata
partition, everything else block storage -- not any absolute size).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Resolves the acl_pinned punch-list item's open question: credential
data (ZUSE-CERT-LO/HI, ACL-CA-KEY-LO/HI) are CONSTANT words, i.e. real
DictEntrys, and acl_pinned's enforcement (vm_create_word()'s
unconditional shadow-refusal for any pinned name) already covers this
generally -- no new flag needed, zuse.4th's ACL-ZUSE-BOOT already pins
both cert constants today.
That investigation surfaced a real gap: ACL-ZUSE-BOOT pins the cert
constants unconditionally on every boot, before any legitimate mint
could ever run, permanently locking in the 0 placeholder on the very
first boot. This directly shaped Captain Bob's next design pass: a
dedicated zuse.img test thumbdrive, "bleachable" back to pristine
state for repeated first-boot testing; a one-time mint-then-pin flow
(fixing the gap above); a separate ongoing S" name" MINT word for
minting additional regular users; and an explicitly-deferred Zuse
recovery path question.
This commit is the first piece: disk/zuse.img (64MB blank, matching
the existing USB-fixture convention) + scripts/bleach_zuse_img.sh
(idempotent reset). Verified live via QMP hotplug -- reads back as
HOMEBLOCKS_SIG_BLANK, correctly simulating a genuine first boot.
Deliberately flat/raw, not GPT-partitioned, matching
homeblocks_sig_check()'s current sig_start_fblock=0 assumption; both
move to a real GPT-relative offset together once a parser exists. No
kernel code touched -- host-side test tooling only, no 3-arch
acceptance boot needed.
Still open: the mint-then-pin boot fix, the MINT word, Zuse recovery.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Wired into sk_repl_idle()'s USB hotplug attach handler, right between
blkio_usb_open_msc() succeeding and blk_subsys_attach_device() -- 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, so
there is no write path today to refuse; attach currently only enables
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 break that legitimate use without protecting anything
real -- same "don't build ahead of a real caller" reasoning EXPIRE's
deferral used. Refuse belongs on the write path once WRITE(10) exists.
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),
flagged in the comment as the one place that changes once a real
GPT-partition-relative lookup exists, isolated from
homeblocks_sig.c's own location-agnostic check logic.
Verified live: hot-attached disk/usb-thumbdrive-test.img (blank media)
through a running amd64 instance's QMP socket (blockdev-add +
device_add usb-storage) -- captured exactly 4 real TUR+READ10 BOT
cycles (matching the header's 4 forth-block span) followed by the
correct "not recognized" warning, then normal attach completing
successfully afterward (no regression). Conservation intact, no
panic. Clean zero-warning compile and clean boot on all three
architectures.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Real, complete verification logic -- not yet wired to any write path.
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 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
via block_subsystem.h) 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: verifies a signature given a location, finding that
location (GPT-partition-relative) stays the caller's job.
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,
a correctly-minted header (round-trips drive_uuid/minted_time_ns), a
flipped CRC, an unrecognized version. All four pass. A full
QEMU-hotplug live test isn't proportionate yet since nothing calls
this function from the live kernel path -- wiring it into the attach
path is the next punch-list item. 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.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Implements the design from the previous commit as-is: homeblocks_sig_t
(4096 bytes, magic+version+drive_uuid+timestamp+cert/blockmap offset
reservations+real hdr_crc), HOMEBLOCKS_SIG_PACK/_GET_MAGIC/_GET_VERSION
macros mirroring CAPSULE_MAGIC_PACK's bit layout, and a C99
compile-time size assertion matching stadium.h's own discipline.
Verified standalone (sizeof == 4096, clean under -std=c99 -Wall
-Wextra -Werror) -- nothing consumes this header yet, so no
functional kernel change and no 3-arch acceptance boot needed for this
step.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Mirrors CAPSULE_MAGIC_PACK's bit-packed magic and blk_volume_meta_t's
magic+version+fields+pad-to-4096 structural convention exactly, per
the punch-list item's own direction. Lives at the first 4KiB devblock
of the ~1GB GPT metadata partition -- format doesn't depend on the
still-missing GPT parser.
Deliberately narrow: identifies/authenticates the drive only, does not
invent the block-map or credential/cert formats -- reserves offset/
size pointers to where they'll live instead of embedding them, since
neither format is designed yet (block-map is Milestone 3, cert format
needs Milestone 6's still-ungenerated real CA). hdr_crc is real from
day one, unlike blk_volume_meta_t's unused placeholder, since this
header's whole job is gating the warn-and-refuse security check.
Design only, not yet implemented as a header file -- presented for
confirmation first.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
The June 2026 campaign's +0.0054%-0.0088% number was cited as current,
but it measured the wrong thing under the wrong name -- the
Rolling-Window-of-Truth mechanism it was named after was dead code,
never reachable, and the measurement itself predates real compiler
optimization. Kept the original entry for historical record, added a
correction pointing to the actual current figure: +0.0603% ACL-TTL
enforcement overhead (FABRIC-2.md SS T), the same closure just applied
to FABRIC-3.md's stale re-measurement punch-list item.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Corrects bookkeeping, not measurement. FABRIC-2.md Sections M-T
(2026-08-19/21) already re-ran this and found the original "ACL-RWT"
name was itself wrong -- the Rolling-Window-of-Truth mechanism it
named was dead code removed 2026-07-08, never reachable. What the
campaign actually measured was the live ACL-TTL mechanism. Final
accepted result: +0.0603% ACL-TTL enforcement overhead,
architecture-independent, CV=0.000% (FABRIC-2.md SS T,
project_acl_ttl_overhead_final.md). The checkbox was simply never
marked done when that work concluded -- no new campaign run.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
- docs/lithosananke/ROADMAP.md + M7.1.md: fixed stale "Branch: lithosananke"
(no such branch post-split), M7.1's "Design Complete" status (shipped
and live, redirected to FABRIC*.md), the M8/success-criteria
self-contradiction (OBSOLETE marking vs. unqualified live criterion),
and the stale AHCI/SATA claim for M9 (real implementation is
virtio_blk.c) -- also corrected BLOCK/BUFFER/UPDATE/FLUSH and block
device abstraction to [x] since both are confirmed live in
src/word_source/block_words.c and block_subsystem.c.
- Top-level ROADMAP.md: marked OBSOLETE (Captain Bob's call -- more than
"stale," the architecture/branch topology/terminology it describes no
longer exist), pointing to docs/lithosananke/ROADMAP.md and
FABRIC*.md for current status.
- docs/03-architecture/word-acl/DESIGN.md: fixed the ACL Phase 7
contradiction -- Phase 7 (LithosAnanke kernel parity) is independently
verified complete per .claude/CLAUDE.md, not "remaining"; removed the
stale lithosananke-branch-parity framing.
- VM-FLEET-ATTRACTOR-DESIGN-20260705.md's doe-campaign.4th "broken" claim:
investigated, ran SMOKE-CAMPAIGN live (completes clean, fleet heat
conserved) -- initially read as contradicting the claim, corrected
directly by Captain Bob: a clean execution trace doesn't disprove the
doc's actual argument (no real controlled-experimental-factor
mechanism). Confirmed accurate, left untouched.
- Isabelle/HOL pipeline-metrics model/C-struct mismatch: confirmed a real
proof-modeling gap (pm_last_accuracy_num/den has no analogue in the
real PipelineGlobalMetrics struct), not stale prose -- tracked here
rather than fixed, matching the .thy file's own scope boundary and
this project's standing caution that each Isabelle gap needs its own
subsystem model.
ACL-RWT DoE overhead re-measurement (the 6th item) intentionally not
started -- a full multi-architecture DoE campaign, not a doc-text fix,
holding for explicit confirmation given the scale.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Decided directly on request, no code written. "ACLs -> EXPIRE" isn't
wrong in spirit but was aimed at the wrong unit: acl_ttl-hits-zero
always renews (never revokes), so it can't carry EXPIRE's
residency-ending meaning. The unit that actually has a real 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.
Resolves to: admit the session (not a per-word ACL entry, which would
be redundant with item 4.1's existing word patrons) as the Stadium
patron, once Phase 8 exists. Not in scope now -- there is no session
to admit regardless of any other choice, and building Stadium's
missing generic ttl-decrement/reap mechanism ahead of its only real
consumer would be speculative infrastructure, the same shape of
premature build the no-stubs standard exists to prevent, just
inverted. EXPIRE stays explicitly deferred until Phase 8 lands, with
a concrete trigger for revisiting it -- not abandoned, not left
ambiguous.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Same fix as DELIVER, on request. COOL is real and live for both words
(item 4.1) and VMs (this session) -- stadium_evict()'s own universal
reservoir credit is the whole of what "cooling off the floor" means
for both, no extra payload action needed. stadium_dispatch()'s COOL
case now prints the departing patron's identity (word_id for a word,
0 -- the patron-zero convention -- for a VM) instead of "(stub)".
Verified 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 with
conservation intact on all three architectures.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Investigated on request. acl_ttl (DictEntry, per-word ACL-RECHECK
amortization) and StadiumPatronHeader.ttl (residency countdown) are
different mechanisms wearing the same name -- ACL-RECHECK always
renews (allow=1, fresh ttl), never revokes, so it doesn't resemble a
reap event at all. Separately, StadiumPatronHeader.ttl is completely
inert across the whole codebase: every candidate constructor sets it
to 0, nothing ever reads or decrements it -- the generic TTL-expiry
mechanism EXPIRE would fire from doesn't exist in Stadium's own engine
yet, a gap one level deeper than "ACL isn't wired to Stadium."
Recorded four open questions that need a real decision before any
code: whether "ACL patron" is even the right model, what unit would
be admitted (redundant per-word vs. a session-scoped patron once
Phase 8 PKI/zuse work lands), whether building Stadium's missing
generic ttl-reap mechanism is in scope here, and what the reap action
should actually do given ACL policy never revokes today.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Words have been fully migrated and live via stadium_word_dispatch()
since item 4.1 -- this line just never got updated. Small, independent
doc fix flagged in FABRIC-3.md's punch list, addressed on request.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Re-scoped on request. capsules/hermes/init.4th's MSG-ALLOC already
admits every message onto Stadium 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. The earlier "zero consumer, needs
substantial Hermes lifecycle mapping work" framing 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 without the prose catching up. Same
documentation-drift class as the stale ONTOLOGY.md words note and the
earlier glibc misattribution.
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. COOL is in the
identical situation (real for both words and VMs) but left as-is --
out of scope for this pass, noted in stadium.c's own comment.
Also confirmed EXPIRE (ACL) is genuinely unscoped, not stale docs like
DELIVER turned out to be -- zero Stadium involvement anywhere in
ACL.4th/acl_recheck()/the ACL design doc. Stays open pending real
design decisions.
Verified live via a forced MSG-SEND/MSG-DELIVER-ALL/MSG-ACK-LAST
sequence from Hermes's own REPL context ("USE" now works after the
previous fix): "Stadium: dispatch cell=73653 behaviour=DELIVER
msg_idx=1". Clean zero-warning compile and clean boot with
conservation intact on all three architectures.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
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. stadium_birth_hera() looked like a
precedent but 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.
Adds size_t stadium_patron_cell to VMRegistryEntry. At birth, right
after the existing stadium_grant_quota() call, admits 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 unpinned costs nothing since nothing wires COOL's
dispatch body to kill anything; the worst case of an unrelated natural
eviction is stale bookkeeping, tolerated the same way
stadium_word_forget() already tolerates staleness elsewhere. 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.
On investigation this turned out not to be entangled with the
still-iterating Tripod/Zuse/messaging vision after all -- birth and
kill already funnel through two single choke points, so the earlier
deferral (previous commit) was overcautious.
Verified live: a second, new "Stadium: dispatch cell=... behaviour=
COOL" now fires immediately before every PARITY:KILL line, for both
Hermes and Artemis, 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.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
capsules/lib.4th:13-14 defined ": USE ( addr u -- ) EXEC ;" and the
same for RUN, shadowing the C-registered mama_word_use()/RUN primitives
(CLAUDE.md names both, with BIRTH, as untouchable C primitives) with an
unrelated "load/exec a capsule" meaning. This broke the interactive
USE-based VM-redirect (S" Artemis" USE printed "EXEC: failed: Artemis"
instead of redirecting), discovered while chasing FABRIC-3.md's
live-MIGRATE verification.
Traced every real caller before touching anything: RUN's alias was
dead code, never called anywhere as bare RUN. USE's alias had exactly
one real caller -- capsules/hermes/init.4th:397, intentionally
exploiting the shadow to load common:msg.4th right after lib.4th
itself loaded. Both aliases were pure EXEC wrappers with zero added
behavior, so this deletes both definitions outright and switches the
one real call site (plus its matching doc comment in
capsules/common/msg.4th) to call EXEC directly. No new names invented,
the C primitives untouched.
Verified live: Hermes still births and her COMMON-CH-eviction
self-test (depends on common:msg.4th having loaded) still passes;
interactively, S" Artemis" USE now correctly redirects the REPL and
prints "USE: now using Artemis". mkcapsule --lint clean (31/31).
Clean compile and clean boot with Stadium conservation intact
(resident_sum + reservoir == Q48_ONE) on all three architectures.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Confirmed the real MIGRATE dispatch fires live: a temporary boot probe
(STADIUM-ADMIT + STADIUM-EVICT against the live Artemis VM, inserted
into the existing 4.6 self-test and reverted immediately after capture)
produced "Stadium: dispatch cell=63257 behaviour=MIGRATE lbn=100" with
blk_flush(100) firing and zero error -- closes the honest gap left open
in the previous commit. Interactive flooding alone couldn't reach this:
Hera's reservoir sits at the Q48_ONE/3 floor from boot self-tests, so
block-touch candidates pull 0 heat and can never out-density an
existing resident, a pre-existing reservoir-floor/eviction interaction
unrelated to this pass.
Also reports (not fixes, per CLAUDE.md) a real dictionary-shadowing bug
found while chasing this: capsules/lib.4th:13 redefines USE as EXEC,
shadowing the C primitive mama_word_use() (REPL VM-redirect) with an
unrelated capsule-loading meaning -- same for RUN at lib.4th:14. Same
bug class as the K-PUSH dictionary-shadowing issue.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
stadium_admit()'s mass==1 refusal looked like a hard blocker for 1024-byte
blocks, but stadium_word_dispatch()'s real candidate construction proves
Stadium cells carry pure identity/heat/bookkeeping, never the resident's
actual content -- a block patron follows the same shape (identity=LBN,
payload unused), so this was real, scoped work, not a case for stubbing.
New stadium_blocks.h/.c mirror stadium_words.c's admission/cooling shape,
keyed by (quota_slot, lbn) in a fixed-capacity open-addressing hash table
(tombstone deletion) instead of a dense array, since LBN space isn't
densely bounded like word_id. Wired into block_word_block()/buffer()/
update() (block_words.c), __STARKERNEL__-guarded. stadium_dispatch()'s
MIGRATE case now calls blk_flush(lbn) for real instead of printing
"(stub)". Three new Kconfig constants (STADIUM_BLOCK_HEAT_QUANTUM/
STADIUM_BLOCK_COOL_RATE_Q48/STADIUM_BLOCK_TRACK_CAP_MULT) mirror the
word-patron ones, same three-layer wiring.
VM-COOL/DELIVER/EXPIRE stay explicit punch-list items -- VM-COOL
deferred pending the still-iterating Tripod/Zuse/messaging vision,
DELIVER/EXPIRE are their own future subsystem integrations per
FABRIC.md's own "open, not resolved" notes.
Verified clean compile (zero warnings) and clean boot to REPL with
conservation intact (resident_sum + reservoir == Q48_ONE) on all three
architectures (amd64/aarch64/riscv64); BLOCK/BUFFER touches exercised
live from the REPL with no crash; a 22,000-distinct-block flood loop
against an artificially shrunk Stadium ran clean under heavy admission
load. A live MIGRATE console fire was not directly observed this
session (root-caused to a pre-existing reservoir-floor/density-eviction
interaction unrelated to this change, documented in FABRIC-3.md) --
flagged as an honest follow-up, not silently claimed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Investigated whether stadium_admit()'s mass==1 refusal blocks block-patron
Stadium admission -- it doesn't: stadium_word_dispatch()'s real candidate
construction confirms Stadium cells carry identity/heat/bookkeeping only,
never the resident's actual content, so a block patron (identity=LBN,
payload unused) fits the same pattern words already use. The real gap is
just that no LBN->cell_index residency map or touch-on-access hook exists
yet -- real, scoped, buildable work, not a blocker.
Deferred VM-COOL pending the Tripod final-shape vision Captain Bob laid
out (thumbdrive minting, one-time Zuse fuse-blow, messaging-only inter-VM
interaction, polymorphic block-boundary behavior) -- recorded in a new
§D so near-term Stadium work doesn't ignore it, without treating a
one-sentence vision as a ready-to-implement spec.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
FABRIC-2.md closed archival at ~4,400 lines / 51 open items, same reasoning
FABRIC.md itself was closed for at 7,595 lines -- continuing to append made
still-open work hard to find. All 51 open items carried forward into
FABRIC-3.md's new Section A, verified complete via programmatic diff against
the source (49 unique + 2 confirmed pure duplicates from FABRIC-2.md's own
F.3 cross-reference section, not dropped content). .claude/CLAUDE.md's
pointer note updated to name FABRIC-3.md as current.
New Section B: full audit of stadium_dispatch()'s four behaviour stubs
(MIGRATE/DELIVER/EXPIRE/COOL), triggered by investigating "words/VMs/blocks/
messages should all be on the same engine". Found the picture is more
nuanced than "everything's a stub" -- words are already fully live via a
separate bespoke mechanism (stadium_word_dispatch(), wired into vm_core.c's
real word-execution path, item 4.1), contradicting ONTOLOGY.md's stale
"not yet migrated" claim (flagged for a follow-up fix). MIGRATE (blocks)
and VM-COOL are genuinely stub with zero consumers; DELIVER (Hermes) and
EXPIRE (ACL) are substantial, undecided subsystem integrations FABRIC.md
itself already flagged as open, not touched here.
New standing rule, saved as memory feedback_no_stubs_or_todos.md: stub
implementations and TODO placeholders are never acceptable in this
workflow, in any language, ever -- triggered by finding stadium_dispatch()'s
stub handlers during this investigation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Implements the full design from the prior commit in one pass. resolve_lbn()
is the single choke point threaded through the ten public LBN-consuming
entry points (blk_get_buffer, blk_update, blk_flush, blk_is_allocated,
blk_mark_allocated, blk_mark_free, blk_is_valid, blk_get_meta, blk_set_meta,
plus blk_get_empty_buffer covered via delegation) -- an LBN->LBN redirect,
not a new storage allocator, since the LBN space is already unified across
every attached blkio_dev backend. VM window cache staleness across a
relocation reuses the existing blk_vm_check_epoch() mechanism from
Milestone 2h's hot-detach fix for free -- g.epoch bumps on relocation too.
Persistence lands in the same pass: two new uint32_t fields
(reloc_start/reloc_devblocks) appended after hdr_crc in blk_volume_meta_t,
carved from existing padding without moving any earlier field's byte
offset -- an old formatted volume's zeroed padding reads back as
reloc_devblocks=0 ("no reloc capacity"), gracefully, not a format-breaking
change. compute_totals_from_B() generalized to account for the new
reserved region. reloc_flush_to_disk()/reloc_load_from_disk() mirror the
BAM I/O functions' own absolute-devblock-addressing shape; the persisted
copy's owner is first_disk_slot() (already existed, already used for this
exact "which device is canonical" question by blk_get_volume_meta()).
blk_subsys_relocate_block() is a mechanical primitive only -- copies
content (staged through a local buffer, since obtaining the target's
blk_get_buffer() result can evict and invalidate the source's cache
pointer if they share a device), frees the source BAM entry, appends the
exception entry, bumps the epoch, flushes to disk. RELOCATE-BLOCK exposes
it to FORTH, no policy of its own (ACL's job, per this session's direction).
A first live-test attempt gave a false negative against disk/artemis.img
(predates reloc capacity, so relocation only ever existed in memory that
boot) -- traced to the test's own setup before being mistaken for a bug,
then re-verified correctly against a fresh volume (new fixture,
disk/artemis-reloc-test.img): relocated a RAMDRIVE block to the fresh
disk, confirmed live resolution through the redirect, then confirmed both
the redirect and the relocated content survived an abrupt QEMU kill and
full reboot. Also fixed three lingering "glibc" doc-comment
misattributions from Milestone 2h (the actual allocator is this kernel's
own kmalloc) that survived an earlier FABRIC-2.md-only correction. All
three architectures re-verified clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Complete design, not just the earlier flag: resolve_lbn() as the single
choke point threaded through ten public entry points, LBN->LBN redirect
mechanism (no new storage allocator needed, the LBN space is already
unified across all backend-agnostic blkio_dev devices), VM window
staleness solved for free by reusing the existing epoch mechanism from
Milestone 2h's hot-detach fix, in-memory table shape, and on-disk
persistence via two new fields carved from blk_volume_meta_t's existing
padding (byte-compatible with old formatted volumes) using the same
absolute-devblock I/O pattern the BAM already uses. Ownership of the
persisted copy assigned to first_disk_slot() (already exists, already
used for this exact "which device is canonical" question), with the
multi-primary-device question for a future multi-SSD/cloud world
explicitly punted rather than hand-waved. Noted the known, separate
limitation: USB relocation targets aren't exercisable until WRITE(10)
exists in the xHCI driver -- verification will use two already-writable
devices instead. Policy (when to relocate, whether a target is validly
owned) stays ACL's job, not this primitive's.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Milestone 3 direction from this session: ACL decides when to relocate
(capacity pressure or a compudynamics heat/cold signal), migration
expected rare not routine. Flagging only, not designing yet: current
lbn_to_slot() is pure contiguous-range routing, one device owns one
unbroken range -- incompatible with relocating an individual block to a
different device while its LBN stays fixed. Direction sketched (a sparse
LBN-to-actual-device exception table, consulted before the range walk,
zero cost for the common never-relocated case) but the actual structure
is still open. Block-migration implementation now explicitly depends on
this revision landing first.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Dropped the hash-modulo-MAX_IDENTITIES approach: there is no way to bound
how many identities might ever exist, so no population size can be
pre-divided into slots regardless of hash function. Resolution: first
time an identity is seen, claim a range at the SSD's existing free-space
frontier (g.total_user_lbn, already used by every device attach) sized
to whatever it needs, write the assignment into the drive's own map;
every later visit just reads it back. No central directory, no quota,
matches Section U's existing requirements exactly.
Identity itself is a certificate signed by this project's own (not yet
created) CA root, extending Milestone 6's PKI design to users instead of
inventing a separate mechanism. Derivation-relevant value is the cert's
embedded pubkey, not its raw bytes -- a renewal shouldn't relocate an
already-allocated range.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Checked the math with Captain Bob: a PBAM for ~15GB of user blocks needs
only ~1.92MB (15M blocks / 32768 per 4KB page), fitting easily alongside
identity/credential/PKI material in a 1GB reserved region. No partition
table needed -- same whole-device pattern artemis.img already uses today,
just with a generously fixed ~1GB reserved region instead of tight
bam_start+bam_devblocks packing. Eliminates the GPT-parser work item
entirely (no GPT code exists in src/starkernel/ today). Confirmed no
conflict with Milestone 8's USB boot drive, which is a separate physical
drive (flashed starkernel.iso) with its own independent GPT/ESP need.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Makes blk_vm_flush_all() (block_words.c) non-static and declares it in
block_words.h -- it's already the entire implementation behind
SAVE-BUFFERS (block_word_save_buffers() is a one-line wrapper), so
sk_repl_idle() can call the exact same flush path outside word dispatch
without duplicating any logic. Cheap every idle tick regardless of dirty
state: every check inside is a small fixed-size scan, so no separate
pre-check was needed on top of it.
Caught a real bug via a live persistence test before trusting the
feature: the first version gated the flush on sk_repl_get_active_vm()
returning non-NULL, but NULL is that accessor's documented default
(Tripod's own USE-redirect override, "restore default dispatch") --
without an active USE redirect, the flush silently no-op'd for the
entire session. Confirmed live: wrote a byte via BUFFER (no
UPDATE/SAVE-BUFFERS), waited past the idle cadence, killed QEMU abruptly,
rebooted with the same disk image, read back 0 instead of the written
65. Fixed by threading the VM sk_repl_run()'s own loop already resolves
each iteration (g_repl_active_vm ? g_repl_active_vm : vm) down as a
parameter through sk_readline() into sk_repl_idle(), rather than trying
to re-derive it from an accessor with the wrong default. Re-ran the same
test after the fix: read back 65, matching the written byte -- the write
survived an abrupt kill with no explicit flush call anywhere in the
test, proving the idle-tick auto-flush genuinely ran.
All three architectures re-verified clean. FABRIC-2.md Section V item 6
and the corresponding Milestone 3 punch-list item marked done.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
block_subsystem.c is shared/vendored source -- its calloc()/free() calls
resolve to real glibc in the hosted build, but to this kernel's own
freestanding shims (src/starkernel/vm/host/shim.c, backed by kmalloc()/
kfree() in src/starkernel/memory/kmalloc.c) in the kernel build, which is
where the address-reuse bug was actually diagnosed live. The previous
writeup said "glibc's allocator" -- wrong environment entirely. Verified
kmalloc_aligned() is a plain first-fit walk from heap_head with no
coalescing, confirming the same free-then-immediate-same-size-alloc reuse
behavior originally observed, just attributed to the right allocator.
Caught by the user asking directly whether this project has any glibc
dependency, given .claude/CLAUDE.md's strict ANSI C99/no-GNU-extensions
requirement (confirmed -std=c99 throughout both Makefiles). Documentation
correction only -- no code changed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
blk_subsys_detach_device() (block_subsystem.c) walks the device chain,
refuses removal of anything but the current tail (a mid-chain removal
would corrupt every later slot's start_lbn -- this architecture's own doc
already argues USB stays last specifically to avoid that), unlinks,
shrinks total_user_lbn, closes and frees the slot. Discards rather than
flushes dirty state -- the device is physically gone by the time this
runs (PORTSC disconnect only). Trigger wiring mirrors the attach path:
bot_msc_attached (set only once attach actually succeeds) gates a new
bot_msc_detach_pending flag set at PORTSC disconnect (not Disable Slot
completion, which is conditionally skipped and would miss concurrent
connect/disconnect pairs), consumed in sk_repl_idle().
Advisor flagged the real hazard ahead of time: block_words.c's VM block
window (blk_vm_lbn[]/blk_vm_cbuf[]) can go stale across a detach then a
same-LBN re-attach, and suggested a pointer-identity re-check in
blk_vm_load() as a minimal fix. That fix was implemented, then directly
falsified by its own designed-for-this test: attach a blank device, read
a block (populating the cache), detach, re-attach a device with distinct
content at the identical LBN, read again -- served stale content from
the first device. Root cause, confirmed live: glibc's allocator hands
free(slot) straight back to the very next same-size calloc(), so the
"fresh" and stale pointers were bitwise identical despite being two
different devices. Fixed properly with a monotonic blk_subsys_epoch()
counter (bumped on every attach/detach) checked by a new
blk_vm_check_epoch() helper at the one choke point (blk_vm_find(), plus
blk_vm_flush_all() which reads the same arrays directly) that covers
every path touching the window cache -- unfooled by address reuse.
Verified live with a new disk/usb-thumbdrive-test2.img fixture (distinct
content from the existing blank test image): attach A, read (cache hit
populated), detach, re-attach B at the same LBN, read again -- correctly
ran a fresh device read and returned B's real content, not A's stale
cached zeros. The failing pointer-comparison attempt's own capture log
kept as evidence, not deleted. All three architectures re-verified clean.
FABRIC-2.md Section X 2h marked complete -- enumeration through
hot-detach all live and verified; only WRITE(10) (2g's own still-open
item) remains unimplemented in the driver, not blocking anything here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Wires a hot-plugged USB Mass Storage device into the block subsystem's
unified LBN chain. blkio_usb.c/blkio_usb.h mirror virtio_blk.c/
virtio_blk.h's established shape exactly (singleton state, blkio_vtable_t,
a blkio_usb_open_msc() "find" function playing virtio_blk_find_artemis()'s
role): read() translates a Forth block into a SCSI LBA/count pair and
calls xhci_bot_read_block() + xhci_bot_wait_for_idle(); write() returns
BLKIO_ENOSUP (no SCSI WRITE(10) exists yet, and blk_format_or_load_disk()
never writes at attach time, so read-only is sufficient -- confirmed by
reading that function first, not assumed). Refuses (-2) if the reported
SCSI block size doesn't evenly divide the 1024-byte Forth block size.
Connect-time wiring reuses the bot_msc_attach_pending/consume-in-
sk_repl_idle() shape the prior increment's temp probe already validated,
now made permanent: SET_CONFIGURATION sets the flag, sk_repl_idle()
(strictly after its own xhci_poll_events() call returns) calls
blkio_usb_open_msc() then blk_subsys_attach_device().
Verified live via hot-attach: full chain from USB connect through
'blkio_usb: MSC device ready' to 'blk: disk 'StarForth Volume' v2 LBN
26074..75184 (49111 user blocks)' -- real attachment, disk image confirmed
byte-for-byte untouched after. Chased a real debugging detour along the
way: the attach initially appeared silent (no blk: log line) -- traced to
LOG_INFO filtering at the default LOG_WARN boot level, not a functional
bug (settled via a temporary log-level bump, reverted after capture; also
found and reported, but did not fix, a pre-existing unrelated
Makefile.starkernel bug where --log-level=info via KERNEL_ARGS breaks
printf parsing). All three architectures re-verified clean. FABRIC-2.md
Section X 2h updated -- only hot-detach remains for 2h.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Closes the gap block_subsystem.c needs before any of 2h's real work
(blkio_usb.c, attach wiring, hot-detach) can start: this driver is fully
async/polled with no way for a synchronous caller (blkio_read()/
blkio_info() etc.) to get a result back. xhci_bot_wait_for_idle() is a
bounded busy-wait over xhci_poll_events() -- MUST be called only from
outside xhci_poll_events()'s own call frame, never from within it or a
next_action dispatch (recursion into live Event Ring/ERDP processing,
same class of hazard already documented for doorbell rings in this
driver). xhci_get_dev() exposes the module-static device handle to
outside callers that didn't observe the original hotplug event.
SCSI READ CAPACITY(10) (opcode 0x25) is the other half -- nothing could
learn a device's block size/capacity before this. First attempt sent it
bare and hit the classic first-command UNIT ATTENTION (CSW FAILED); fixed
with the same TUR-guard pattern READ(10) already used, generalized via a
new bot_tur_chain_target field so TEST UNIT READY's PASS handling can
chain into either command. bot_data_buf grown 512->1024 bytes (one Forth
block = two 512-byte SCSI blocks, per block_subsystem.c's own 1KiB-unit
convention).
Verified live via a temporary probe (hot-attached disk/usb-thumbdrive-
test.img via QMP, reverted after capture): TUR-guarded READ CAPACITY10
correctly reported last LBA=0x1ffff, block size=0x200 -- exactly 64MiB,
matching the test image byte for byte -- followed by a TUR-guarded
1024-byte/2-block READ10, both PASS. All three architectures re-verified
clean, probe-free boot to ok> on the reverted tree. FABRIC-2.md Section X
2h updated with the writeup; the blkio_usb.c backend itself is next.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
Roots out the CSW status FAILED left unexplained in the prior increment: a
freshly attached SCSI target's standing UNIT ATTENTION condition, which a
bare READ(10) with no retry can never clear. xhci_bot_send_test_unit_ready()
sends SCSI TEST UNIT READY (SPC-4 6.33) ahead of the real command; the CSW
handler now tags command kind (bot_cmd_kind) to distinguish a TUR completion
from a READ10 completion, chains TUR PASS into the real READ(10), and
bounded-retries TUR on FAILED/PHASE ERROR (bot_tur_retries, capped at
XHCI_BOT_TUR_MAX_RETRIES). xhci_bot_read_block() is the new intended entry
point tying lba/num_blocks/block_size + the TUR-first sequencing together.
Verified live via a temporary probe (hot-attached disk/usb-thumbdrive-test.img
through the running instance's QMP socket), captured on amd64: full chain
CBW(TUR) -> FAILED -> retry -> PASS -> CBW(READ10) -> Data-In -> CSW PASS.
Probe reverted after capture; all three architectures re-verified clean,
probe-free boot to ok>. FABRIC-2.md Section X 2g updated with the writeup.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn