diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 132a27aa..782a0cf4 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -341,9 +341,15 @@ audit artifacts — they are committed to the repo. Do not delete them. There is timestamped convention (self-documented via its own README); not a contradiction, just a second, less-structured log location. +**The disks (Captain Bob, 2026-10-26, the identity gate):** with no identity thumbdrive the system +is headless -- no prompt -- so every boot needs Zuse. `kernel/Makefile`'s `ARTDISK` and `ZUSEDISK` +default to the minted Zuse pair, `disk/zuse-scratch/artemis.img` and `zuse-thumb-ident.img` (a +pair: her genesis marker is on that Artemis disk; `disk/README.md`). Put both back as committed +(`git checkout -- disk/zuse-scratch`) before each architecture. The prompt is `[zuse@Hera] ok> `. + Do not claim a change is accepted until all three architectures have booted, and their logs are present in `logs/`: -- **as built (v4)** to `ok> `, with `PARITY:V4_POST tests=538 pass=538 fail=0` (the count +- **as built (v4)** to `[zuse@Hera] ok> `, with `PARITY:V4_POST tests=538 pass=538 fail=0` (the count grows with v4's POST) and `POST: PASSED`, and `PARITY:V4_SYSTEM ... dict_hash=` equal across the three; - **the 32-bit build** (Captain Bob, 2026-10-14: "the 32 bit build needs to go into the diff --git a/kernel/Makefile b/kernel/Makefile index 29fd3b77..dfc4f54f 100644 --- a/kernel/Makefile +++ b/kernel/Makefile @@ -121,7 +121,12 @@ DOE_RUNS_DIR := experiments/bare_metal/runs DOE_LATEST_DIR := experiments/bare_metal/latest CAPSULES_DIR ?= capsules -ARTDISK ?= disk/artemis.img +# ARTDISK and ZUSEDISK default to the minted Zuse pair (disk/zuse-scratch/, +# disk/README.md; Captain Bob, 2026-10-26, the identity gate's D4): with no +# identity drive the system is headless, so a boot with the blank +# disk/artemis.img and thumbdrives/zuse-thumb-ident.img never reaches a +# prompt. The two are a pair: Zuse's genesis marker is on that Artemis disk. +ARTDISK ?= disk/zuse-scratch/artemis.img # XHCI_PORTS -- real top-level port count the qemu-xhci controller exposes on the # xhci0.0 bus (p2 == p3 == this value; these are the SAME physical dual-role ports # counted from the USB2 and USB3 register sets respectively, NOT additive to p2+p3 -- @@ -157,7 +162,7 @@ XHCI_ARGS_SLOT4 := $(if $(strip $(XHCI_QEMU_ARGS)),$(XHCI_QEMU_ARGS),$(XHCI_D # xhci_poll_events() alone is purely hotplug-event-driven and would never # see a device present before controller reset. Override to "" (empty) to # boot without Zuse attached, e.g. `make -f kernel/Makefile qemu ZUSEDISK=`. -ZUSEDISK ?= disk/thumbdrives/zuse-thumb-ident.img +ZUSEDISK ?= disk/zuse-scratch/zuse-thumb-ident.img # Precomputed (not inlined as $(if ...,...)) because the drive/device specs # below are comma-heavy and GNU make's $(if) function splits its own # arguments on every top-level comma -- inlining it silently truncated the @@ -640,8 +645,8 @@ V4_ENGINE_SRCS := $(addprefix v4/src/,node.c exec.c stack.c iword.c heat.c guard V4_ROM_C := $(BUILD_DIR)/v4_rom_64.c V4_MAP_C := $(BUILD_DIR)/v4_map_64.c -LOADER_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c -KERNEL_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c +LOADER_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c $(KERNEL_SRC)/v4/sk_v4_identity.c +KERNEL_EXTRA_SRCS += $(KERNEL_SRC)/v4/sk_v4.c $(KERNEL_SRC)/v4/sk_v4_identity.c # v4/system/boot.c is the boot the hosted v4 system runs too: the ROM # assembles the system, then the capsules from this kernel's own capsule # directory, then the prompt. @@ -789,10 +794,14 @@ endif # StarForth v4: the ROM and the map, built on this machine by v4's Makefile, # and the engine, compiled like any other kernel source. +# One recipe makes both (a grouped target, GNU make 4.3+): as two targets of +# one plain rule it ran once for each, at the same time under -j, and the +# two copies met ("cp: cannot create regular file ... File exists", fixed +# 2026-10-26). ifeq ($(STARFORTH_V4),1) -$(V4_ROM_C) $(V4_MAP_C): FORCE - @mkdir -p $(dir $@) - @echo " V4ROM capsules/v4/rom -> $@" +$(V4_ROM_C) $(V4_MAP_C) &: FORCE + @mkdir -p $(dir $(V4_ROM_C)) + @echo " V4ROM capsules/v4/rom -> $(V4_ROM_C) $(V4_MAP_C)" @$(MAKE) --no-print-directory -C v4 CC=cc $(CURDIR)/v4/build/rom-64.c $(CURDIR)/v4/build/v4_map_64.c @cmp -s v4/build/rom-64.c $(V4_ROM_C) || cp v4/build/rom-64.c $(V4_ROM_C) @cmp -s v4/build/v4_map_64.c $(V4_MAP_C) || cp v4/build/v4_map_64.c $(V4_MAP_C) diff --git a/kernel/include/starkernel/v4/sk_v4_identity.h b/kernel/include/starkernel/v4/sk_v4_identity.h new file mode 100644 index 00000000..dcbde69c --- /dev/null +++ b/kernel/include/starkernel/v4/sk_v4_identity.h @@ -0,0 +1,27 @@ +/* sk_v4_identity.h -- the identity gate for the v4 boot: no identity drive, + * headless; Zuse's minted thumbdrive, a console. + * docs/v4.0.0/plans/2026-10-26-engine-step4-identity-gate.md. + * + * Built only with STARFORTH_V4=1 (Kconfig.kernel). + */ +#ifndef STARKERNEL_V4_SK_V4_IDENTITY_H +#define STARKERNEL_V4_SK_V4_IDENTITY_H + +/* After POST and the block chain: Zuse's public key from the genesis marker, + * every xHCI controller brought up, and the heartbeat started, as the v3 + * path does before its headless wait. */ +void sk_v4_identity_boot(void); + +/* 1 while Zuse's session holds: her drive is attached and checked. */ +int sk_v4_identity_present(void); + +/* Service the heartbeat and, once a second, the USB drives that have arrived + * or left (v3's idle beat, its USB part). Called while waiting for a key + * with nothing typed. */ +void sk_v4_identity_beat(void); + +/* Headless: no prompt, no input, until Zuse's session begins (v3's + * sk_repl_headless_wait()). */ +void sk_v4_identity_wait(void); + +#endif /* STARKERNEL_V4_SK_V4_IDENTITY_H */ diff --git a/kernel/src/kernel_main.c b/kernel/src/kernel_main.c index ddc85d86..649f95b2 100644 --- a/kernel/src/kernel_main.c +++ b/kernel/src/kernel_main.c @@ -464,6 +464,23 @@ void kernel_main(BootInfo *boot_info) { kernel_main_deep(boot_info); } +#ifdef STARFORTH_ENABLE_VM +/* The logger after POST: whatever --log-level asked for (default: LOG_WARN). + * Both systems' boots call it once POST is done. */ +static void sk_log_level_after_post(const BootInfo *boot_info) +{ + LogLevel repl_level; + switch (boot_info->args.log_level) { + case KARGS_LOG_DEBUG: repl_level = LOG_DEBUG; break; + case KARGS_LOG_INFO: repl_level = LOG_INFO; break; + case KARGS_LOG_ERROR: repl_level = LOG_ERROR; break; + case KARGS_LOG_WARN: + default: repl_level = LOG_WARN; break; + } + log_set_level(repl_level); +} +#endif + #ifdef STARFORTH_V4 /* THE BLOCK CHAIN, for the v4 node. It asks this kernel for its blocks * (v4/include/v4/blocks.h, docs/v4.0.0/MESH.md 8.3). The chain is set up @@ -483,6 +500,10 @@ static void sk_v4_block_chain(void) static blkio_dev_t artemis_dev; size_t i; + /* POST is done: the logger drops to --log-level, as on the v3 path + * (fixed 2026-10-26: the v4 boot left it where POST had it, and + * INFO lines showed after POST that v3 does not show) */ + sk_log_level_after_post(sk_v4_boot_info); if (!blk_ram || !krd) { console_println("StarForth v4: no memory for the block chain"); for (;;) { } @@ -659,17 +680,7 @@ static void kernel_main_deep(BootInfo *boot_info) { * LOG_WARN) so the per-word "ECW: w=... func=... 'NAME'" trace from * vm_core.c doesn't flood every REPL command. --log-level=info/debug * re-enables it if you actually want to watch word dispatch. */ - { - LogLevel repl_level; - switch (boot_info->args.log_level) { - case KARGS_LOG_DEBUG: repl_level = LOG_DEBUG; break; - case KARGS_LOG_INFO: repl_level = LOG_INFO; break; - case KARGS_LOG_ERROR: repl_level = LOG_ERROR; break; - case KARGS_LOG_WARN: - default: repl_level = LOG_WARN; break; - } - log_set_level(repl_level); - } + sk_log_level_after_post(boot_info); /* Wire parity log so PARITY:MAMA_INIT/BIRTH/RUN/KILL reach serial */ capsule_parity_set_output(NULL, console_puts); diff --git a/kernel/src/v4/sk_v4.c b/kernel/src/v4/sk_v4.c index e7dddb3c..d4df370c 100644 --- a/kernel/src/v4/sk_v4.c +++ b/kernel/src/v4/sk_v4.c @@ -10,7 +10,10 @@ * - what the node prints goes to the kernel's console; * - the prompt, and the line: it is read and edited here and handed to the * node whole (docs/v4.0.0/ENGINE.md 3.1), and how it ended is said here; - * - blocks are kept in memory, and are gone at power-off. + * - blocks are kept in memory, and are gone at power-off; + * - the identity gate: with no identity drive the system is headless, + * and a console exists only while Zuse's drive is attached and checked + * (sk_v4_identity.c; ENGINE.md 2, ruled 2026-10-20). * * This is the lone node, started before the kernel's fleet and beside it. * It goes when the node is brought up in the VM's place (ENGINE.md step 4). @@ -18,13 +21,19 @@ * Nothing of the v3 VM is started. docs/v4.0.0/DECOMPOSITION.md. */ #include "starkernel/v4/sk_v4.h" +#include "starkernel/v4/sk_v4_identity.h" #include "starkernel/console.h" #include "starkernel/arch.h" +#include "starkernel/timer.h" #include "v4/boot.h" #include "block_subsystem.h" #define SK_V4_LINE 255u /* what the line editor here takes */ +/* v3's prompt (repl.c's SK_PROMPT_TEXT): "ok> " in bright cyan, after the + * console's own "[zuse@Hera] " line prefix. */ +#define SK_V4_PROMPT "\x1b[38;2;85;255;255mok> \x1b[39m" + static v4_node sk_v4_node; static v4_exec_state sk_v4_es; static v4_heat sk_v4_heat; @@ -67,17 +76,28 @@ static void sk_v4_serve(v4_node *node, unsigned request) } /* Read a line from the keyboard: characters are echoed, backspace rubs one - * out, Enter ends it. Returns its length. */ -static unsigned sk_v4_readline(void) + * out, Enter ends it. Returns its length; or -1 when, with nothing typed, + * Zuse's session has ended (her drive was pulled): the caller goes headless. + * While nothing is typed the idle beat runs, as v3's sk_console_readline(). */ +static int sk_v4_readline(void) { - unsigned len = 0; + int len = 0; for (;;) { int c = console_getc(); - if (c < 0) continue; + if (c < 0) { + if (len == 0) { + sk_v4_identity_beat(); + if (!sk_v4_identity_present()) return -1; + } else { + heartbeat_service(); + } + arch_relax(); + continue; + } if (c == '\r' || c == '\n') { console_putc('\n'); return len; } if (c == 8 || c == 127) { if (len > 0) { len--; console_putc(8); console_putc(' '); console_putc(8); } - } else if (c >= 32 && c < 127 && len < SK_V4_LINE) { + } else if (c >= 32 && c < 127 && len < (int)SK_V4_LINE) { sk_v4_text[len++] = (char)c; console_putc((char)c); } @@ -107,24 +127,29 @@ void sk_v4_run(void (*chain)(void)) for (;;) { } } } + sk_v4_identity_boot(); /* The prompt is the host's: read a line, hand it to the node, say how it * ended. */ for (;;) { - unsigned len; + int len; int how; - console_puts("ok> "); + /* no identity, no console: headless until Zuse's drive is checked */ + if (!sk_v4_identity_present()) sk_v4_identity_wait(); + console_puts(SK_V4_PROMPT); len = sk_v4_readline(); - how = v4_boot_line(&boot, sk_v4_text, len); + if (len < 0) continue; + how = v4_boot_line(&boot, sk_v4_text, (unsigned)len); /* The line is waiting for an answer no one here can send. The next * line typed breaks the wait: the line that was waiting ends in an * error, and then the line just typed is done (v4/boot.h). */ while (how == V4_BOOT_LINE_WAITING) { len = sk_v4_readline(); - how = v4_boot_line(&boot, sk_v4_text, len); - if (how == V4_TEXT_ERROR) console_puts(" ERROR\nok> "); - else if (how == V4_TEXT_COMPLETED) console_puts(" ok\nok> "); + if (len < 0) { sk_v4_identity_wait(); continue; } + how = v4_boot_line(&boot, sk_v4_text, (unsigned)len); + if (how == V4_TEXT_ERROR) console_puts(" ERROR\n" SK_V4_PROMPT); + else if (how == V4_TEXT_COMPLETED) console_puts(" ok\n" SK_V4_PROMPT); else break; how = v4_boot_line(&boot, 0, 0); } diff --git a/kernel/src/v4/sk_v4_identity.c b/kernel/src/v4/sk_v4_identity.c new file mode 100644 index 00000000..5de513aa --- /dev/null +++ b/kernel/src/v4/sk_v4_identity.c @@ -0,0 +1,200 @@ +/* sk_v4_identity.c -- the identity gate for the v4 boot: no identity drive, + * headless; Zuse's minted thumbdrive, a console. + * docs/v4.0.0/plans/2026-10-26-engine-step4-identity-gate.md, ENGINE.md 2. + * + * As v3 (kernel_main.c, repl.c's sk_repl_headless_wait()/sk_repl_idle(), + * capsule_zuse_boot.c), done by the kernel with no v3 VM behind it: Artemis + * and Hera are the kernel's (ENGINE.md 2, Q1), so a drive that arrives is + * given to the block subsystem here, and Zuse's session is kept here, for + * the first node, Hera. The check that the drive is hers is v3's own + * (capsule_zuse_boot_check()). + * + * Not here (the plan's "not in this gate"): WIREBIND identities, EJECT, + * ACL-ZUSE-BOOT, and genesis -- which writes Artemis's disk, and the v4 boot + * does not write it yet (Phase 5). + */ +#include "starkernel/v4/sk_v4_identity.h" +#include "starkernel/console.h" +#include "starkernel/arch.h" +#include "starkernel/apic.h" +#include "starkernel/timer.h" +#include "starkernel/kmalloc.h" +#include "starkernel/xhci_driver.h" +#include "starkernel/blkio_usb.h" +#include "starkernel/homeblocks_sig.h" +#include "starkernel/capsule_zuse_boot.h" +#ifdef ARCH_AMD64 +#include "starkernel/ioapic.h" +#include "starkernel/i8042.h" +#endif +#include "block_subsystem.h" +#include + +#define SK_V4_IDLE_BEAT 100u /* ticks between idle beats, v3's SK_IDLE_BEAT_INTERVAL (1 s at 100 Hz) */ + +/* ZUSE'S STATE, for Hera. The cert is installed once and never replaced + * (v3's vm_zuse_cert_install(), one-way); the session ends when her drive + * leaves and begins again when it comes back. */ +static int g_zuse_session; +static int g_zuse_cert_installed; +static uint8_t g_zuse_seed[32], g_zuse_pubkey[32]; +static uint8_t g_zuse_root_pubkey[32]; +static int g_zuse_root_known; +static struct blkio_dev *g_zuse_dev; + +/* A blkio_dev per slot, per xHCI controller, as repl.c's g_usb_ctrl. */ +static struct { + blkio_dev_t *slots; + uint32_t count; +} g_usb[XHCI_MAX_CONTROLLERS]; + +static uint64_t g_last_beat; +static int g_said_no_genesis; + +static const char *sk_v4_user_prefix(void) +{ + return g_zuse_session ? "zuse" : (const char *)0; +} + +int sk_v4_identity_present(void) +{ + return g_zuse_session; +} + +void sk_v4_identity_boot(void) +{ + unsigned xi; + static xhci_dev_t xhci_devs[XHCI_MAX_CONTROLLERS]; + + /* Zuse's public key, from the genesis marker on Artemis's disk */ + g_zuse_root_known = capsule_zuse_boot_root_pubkey(g_zuse_root_pubkey) == 0; + + /* every xHCI controller, as kernel_main.c's v3 path: a drive already in + * a port is found by the bring-up's own port scan */ + for (xi = 0; xi < XHCI_MAX_CONTROLLERS; xi++) { + int rc = xhci_find_and_map(&xhci_devs[xi], xi); + if (rc == 1) break; + if (rc == 0) (void)xhci_bringup(&xhci_devs[xi]); + } + + /* the heartbeat, as kernel_main.c's v3 path starts it */ + console_println("Starting heartbeat..."); + apic_timer_start(); +#ifdef ARCH_AMD64 + i8042_drain_stale(); + ioapic_unmask_legacy_irq(1); + console_println("I/O APIC: keyboard IRQ1 unmasked"); +#endif + arch_enable_interrupts(); + console_println("Heartbeat running."); + g_last_beat = heartbeat_ticks(); +} + +/* A drive that has arrived: given to the block subsystem (Artemis's, the + * kernel's), then, if it is Zuse's, her session begins. */ +static int sk_v4_drive_arrived(blkio_dev_t *dev) +{ + homeblocks_sig_t sig; + homeblocks_sig_result_t sig_rc = homeblocks_sig_check(dev, HOMEBLOCKS_SIG_START_FBLOCK, &sig); + uint8_t seed[32], pubkey[32]; + zuse_drive_t which; + + if (blk_subsys_attach_device(dev) != BLK_OK) { + console_println("xhci: USB MSC block-subsystem attach failed"); + return 0; + } + if (g_zuse_session) return 1; + + which = capsule_zuse_boot_check(dev, sig_rc, &sig, seed, pubkey); + if (which == ZUSE_DRIVE_GENESIS) { + if (!g_said_no_genesis) { + console_println("Zuse: no genesis on this disk -- the system stays headless"); + g_said_no_genesis = 1; + } + return 1; + } + if (which != ZUSE_DRIVE_HERS) return 1; + + console_set_vm_name("Hera"); + console_set_user_prefix_provider(sk_v4_user_prefix); + console_println("Zuse: identity confirmed from attached thumbdrive"); + if (!g_zuse_cert_installed) { + memcpy(g_zuse_seed, seed, 32); + memcpy(g_zuse_pubkey, pubkey, 32); + g_zuse_cert_installed = 1; + } + memcpy(g_zuse_root_pubkey, pubkey, 32); /* as v3's install_and_activate() */ + g_zuse_root_known = 1; + g_zuse_dev = dev; + g_zuse_session = 1; + return 1; +} + +static void sk_v4_drive_left(blkio_dev_t *dev) +{ + blk_subsys_detach_device(dev); + if (dev != g_zuse_dev) return; + g_zuse_session = 0; + g_zuse_dev = (struct blkio_dev *)0; + console_println("Zuse: session ended -- reattach to re-authenticate"); +} + +/* One idle beat: the xHCI event ring, then every drive that has arrived or + * left since the last (repl.c's sk_repl_idle(), its USB part). */ +static void sk_v4_idle(void) +{ + uint32_t xc, slot; + + console_ensure_line_start(); + { + uint64_t tx_before = console_tx_count(); + xhci_poll_events(); + for (xc = 0; xc < xhci_dev_count() && xc < XHCI_MAX_CONTROLLERS; xc++) { + xhci_dev_t *xdev = xhci_dev_at(xc); + if (!xdev) continue; + if (!g_usb[xc].slots || g_usb[xc].count < xdev->max_slots + 1) { + size_t bytes = (size_t)(xdev->max_slots + 1) * sizeof(blkio_dev_t); + blkio_dev_t *fresh = (blkio_dev_t *)kmalloc_aligned(bytes, 64); + if (!fresh) continue; + memset(fresh, 0, bytes); + g_usb[xc].slots = fresh; + g_usb[xc].count = xdev->max_slots + 1; + } + for (slot = 1; slot <= xdev->max_slots; slot++) { + xhci_msc_slot_t *ms = xhci_msc_slot_for(xdev, slot); + if (!ms || !ms->bot_msc_attach_pending) continue; + ms->bot_msc_attach_pending = 0; + if (blkio_usb_open_msc(&g_usb[xc].slots[slot], xdev, slot) == 0 && + sk_v4_drive_arrived(&g_usb[xc].slots[slot])) + ms->bot_msc_attached = 1; + } + for (slot = 1; slot <= xdev->max_slots; slot++) { + xhci_msc_slot_t *ms = xhci_msc_slot_for(xdev, slot); + if (!ms || !ms->bot_msc_detach_pending) continue; + ms->bot_msc_detach_pending = 0; + sk_v4_drive_left(&g_usb[xc].slots[slot]); + } + } + if (console_tx_count() == tx_before) console_cancel_deferred_line_start(); + } +} + +void sk_v4_identity_beat(void) +{ + uint64_t now; + + heartbeat_service(); + now = heartbeat_ticks(); + if (now - g_last_beat >= SK_V4_IDLE_BEAT) { + g_last_beat = now; + sk_v4_idle(); + } +} + +void sk_v4_identity_wait(void) +{ + while (!g_zuse_session) { + sk_v4_identity_beat(); + arch_relax(); + } +}