wip(kernel): the proxy Zuse -- a stick's first boot mints its own Zuse (#92, Task 1) [skip ci]

Work in progress on StarForth-v4.0.0; not yet accepted (plan
2026-10-29-stick-5, Task 1).

- zuse_genesis_marker.h: the marker's version 2 -- the proxy's key and the
  real Zuse's -- beside version 1.
- capsule_zuse_boot.c: capsule_zuse_boot_check_key() (the identity-record
  half of the check) and capsule_zuse_boot_genesis_mint() (the mint half of
  v3's genesis) split out; v3's two callers unchanged in behaviour.
- sk_v4_marker.c: the v4 boot reads version 1 as a proxy with no real Zuse,
  and writes version 2.
- sk_v4_identity.c: no marker and a blank identity partition -> genesis:
  mint, read back, then the marker; the proxy logs in. A marker with a real
  key: no session for the proxy (the fixed screen is Task 5).
- qemu_session.py: boot()/shutdown(), so a mode can boot twice; modes
  genesis and stick_genesis; stick_boot now expects genesis and the prompt.

Logs: genesis on amd64 before the change (both boots: "no genesis on this
disk", FAIL) and after (minted, then recognised: PASS). The DoE CSV of
032532 is from a run whose QEMU never started (no display; its empty log
directory was removed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GE3SDvQn5EoxdGufk84zz2
This commit is contained in:
Claude
2026-11-08 03:43:33 +00:00
parent 8f1ba02b3c
commit bfe3506aec
17 changed files with 996 additions and 96 deletions
+88 -43
View File
@@ -46,7 +46,13 @@ the stick's, plan 2026-10-26-stick-1-screen-and-keyboard.md):
said so, not attached
virtio_kbd aarch64/riscv64: QEMU's virtio keyboard types 7 ok
stick_boot --target qemu-stick (amd64): the PC stick booted as a USB stick through OVMF; its
blank identity partition attached (500 MiB, not the 2 GiB disk); headless
blank identity partition attached (500 MiB, not the 2 GiB disk); the proxy Zuse
minted onto it (genesis), [zuse@Hera] ok> (plan 2026-10-29-stick-5, Task 1)
genesis a fresh data stick (DATASTICK=1, no Zuse drive), booted twice, one QEMU at a time:
first the genesis line, [zuse@Hera] ok>, 7 ok; then the same copy again:
identity confirmed, no genesis, the prompt (plan 2026-10-29-stick-5, Task 1)
stick_genesis --target qemu-stick, the PC stick as built (no STICK_PAIR): its first boot mints
the proxy Zuse and reaches [zuse@Hera] ok> with nothing set up (plug and play)
stick_artemis ARTDISK= (no virtio disk): Artemis and Zuse from the Zuse stick alone -- the
stick's Artemis partition attached first (785938 user blocks), then its identity
(48724); [zuse@Hera] ok> (plan 2026-10-27-stick-4-one-disk.md, Task 4)
@@ -88,7 +94,8 @@ NO_ZUSE_MODES = {"gpt_zuse", "gpt_hotplug", "gpt_foreign", "gpt_bad_crc", "gpt_b
"gpt_read_error", "headless", "hotplug", "midpull", "waitpull", "typeahead", "emergency",
"alttab_after_logout", "alttab_before_pull", "emergency_pull",
"screen_dark_until_login", "screen_console_after_login", "screen_dark_after_logout",
"kbd_typeahead", "stick_boot", "stick_v3", "stick_zuse", "screen_boot"}
"kbd_typeahead", "stick_boot", "stick_v3", "stick_zuse", "screen_boot", "genesis",
"stick_genesis"}
label = a.label or f"{a.mode}-{a.arch}"
work = os.path.join(ROOT, "build", "qemu-session", label)
shutil.rmtree(work, ignore_errors=True)
@@ -128,38 +135,18 @@ if a.mode == "screen_boot":
make_args.append("KERNEL_ARGS=screen=boot")
if a.mode == "stick_zuse":
make_args.append(f"STICK_PAIR={work}") # the PC stick with this session's copy of the pair
start = time.time()
make_log = open(os.path.join(work, "make.txt"), "w")
mk = subprocess.Popen(make_args, cwd=ROOT, stdout=make_log, stderr=subprocess.STDOUT)
deadline = time.time() + 1800
ok = True
if a.mode == "genesis":
# a fresh data stick, made once into this session's directory and booted twice: the second
# boot is the same copy, so it finds what the first wrote (plan 2026-10-29-stick-5, Task 1)
dstick = os.path.join(work, "data.img")
subprocess.run(["make", "-f", "kernel/Makefile", f"ARCH={a.arch}", "datastick", f"DATASTICK_IMG={dstick}"],
cwd=ROOT, check=True, stdout=subprocess.DEVNULL)
make_args += ["DATASTICK=1", f"DATASTICK_IMG={dstick}", "DATASTICK_DEP="]
def newest(pattern):
c = [p for p in glob.glob(pattern) if os.path.getmtime(p) >= start]
return max(c, key=os.path.getmtime) if c else None
log = ser = qmp = None
while time.time() < deadline and not (log and ser and qmp):
if mk.poll() is not None:
print(f"MAKE FAILED (exit {mk.returncode}); see {make_log.name}", flush=True)
sys.exit(1)
log = log or newest(f"{ROOT}/logs/*/{a.arch}/qemu-{a.arch}-*.log")
ser = ser or newest(f"{ROOT}/build/{a.arch}/kernel/qemu-serial-{a.arch}-*.sock")
qmp = qmp or newest(f"{ROOT}/build/{a.arch}/kernel/qemu-qmp-{a.arch}-*.sock")
time.sleep(1)
print(f"LOG: {os.path.relpath(log, ROOT)}", flush=True)
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
for _ in range(60):
try:
s.connect(ser)
break
except OSError:
time.sleep(1)
def drain():
while True:
try:
@@ -169,14 +156,6 @@ def drain():
return
threading.Thread(target=drain, daemon=True).start()
q = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
q.connect(qmp)
qf = q.makefile("r")
qf.readline()
def qcmd(cmd, **args):
msg = {"execute": cmd}
if args:
@@ -190,7 +169,48 @@ def qcmd(cmd, **args):
return r
qcmd("qmp_capabilities")
def boot(args):
"""One `make ... qemu` boot: QEMU in the foreground of make, its serial log, serial and QMP
sockets found and connected (the globals every check reads)."""
global start, mk, log, s, q, qf
start = time.time()
make_log = open(os.path.join(work, "make.txt"), "a")
mk = subprocess.Popen(args, cwd=ROOT, stdout=make_log, stderr=subprocess.STDOUT)
deadline = time.time() + 1800
log = ser = qmp = None
while time.time() < deadline and not (log and ser and qmp):
if mk.poll() is not None:
print(f"MAKE FAILED (exit {mk.returncode}); see {make_log.name}", flush=True)
sys.exit(1)
log = log or newest(f"{ROOT}/logs/*/{a.arch}/qemu-{a.arch}-*.log")
ser = ser or newest(f"{ROOT}/build/{a.arch}/kernel/qemu-serial-{a.arch}-*.sock")
qmp = qmp or newest(f"{ROOT}/build/{a.arch}/kernel/qemu-qmp-{a.arch}-*.sock")
time.sleep(1)
print(f"LOG: {os.path.relpath(log, ROOT)}", flush=True)
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
for _ in range(60):
try:
s.connect(ser)
break
except OSError:
time.sleep(1)
threading.Thread(target=drain, daemon=True).start()
q = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
q.connect(qmp)
qf = q.makefile("r")
qf.readline()
qcmd("qmp_capabilities")
def shutdown():
"""Quit QEMU over QMP and let make finish: one QEMU at a time (.claude/CLAUDE.md)."""
q.send(json.dumps({"execute": "quit"}).encode())
mk.wait()
time.sleep(2)
ok = True
boot(make_args)
def text():
@@ -371,12 +391,12 @@ elif m in ("stick_boot", "two_sticks"):
check("System: StarForth v4", re.search(r"System: StarForth v4", text()) is not None)
check("Artemis: the stick's partition attached",
wait_for(r"Artemis: the stick's partition attached", 120))
check("Zuse: no genesis on this disk", wait_for(r"Zuse: no genesis on this disk", 60))
time.sleep(5)
check("Zuse: genesis minted onto attached thumbdrive",
wait_for(r"Zuse: genesis minted onto attached thumbdrive", 120))
check("[zuse@Hera] ok>", wait_for(PROMPT, 60))
d = disks()
check(f"Artemis attached first, then the identity partition (user blocks {d})",
len(d) >= 2 and d[0] > 1000000 and 300000 < d[1] < 512000)
check("no ok> for 30 s (a blank identity: headless)", not wait_for(ANYPROMPT, 30))
else:
check("Zuse: identity confirmed", wait_for(r"Zuse: identity confirmed from attached thumbdrive", 120))
check("[zuse@Hera] ok>", wait_for(PROMPT, 60))
@@ -418,6 +438,32 @@ elif m in ("artemis_unsigned", "raw_ident"):
check("no blk: disk line (nothing attached)", count(r"blk: disk ") == 0)
check("no fault", count(r"EXCEPTION|PANIC|panic") == 0)
elif m in ("genesis", "stick_genesis"):
if m == "stick_genesis":
check("Boot disk: a StarshipOS stick", re.search(r"Boot disk: a StarshipOS stick", text()) is not None,
near=r"Boot successful!")
check("Zuse: genesis minted onto attached thumbdrive",
wait_for(r"Zuse: genesis minted onto attached thumbdrive", 180), near=r"Artemis: |Zuse: ")
check("[zuse@Hera] ok>", wait_for(PROMPT, 60))
check("no identity confirmed on a first boot", count(r"Zuse: identity confirmed") == 0)
n = count(PROMPT)
type_line("3 4 + .")
check("7 ok", wait_for(PROMPT, 60, n) and re.search(r"7\s+ok", text()[-400:]) is not None)
check("no fault", count(r"EXCEPTION|PANIC|panic") == 0)
if m == "genesis":
shutdown()
print("-- the second boot, the same copy of the data stick", flush=True)
boot([x for x in make_args if x != "clean"])
check("POST: PASSED (second boot)", wait_for(r"POST: PASSED", 900))
check("Zuse: identity confirmed (second boot)",
wait_for(r"Zuse: identity confirmed from attached thumbdrive", 120), near=r"Artemis: |Zuse: ")
check("no genesis on the second boot", count(r"Zuse: genesis minted") == 0)
check("[zuse@Hera] ok> (second boot)", wait_for(PROMPT, 60))
n = count(PROMPT)
type_line("3 4 + .")
check("7 ok (second boot)", wait_for(PROMPT, 60, n) and re.search(r"7\s+ok", text()[-400:]) is not None)
check("no fault (second boot)", count(r"EXCEPTION|PANIC|panic") == 0)
elif m == "stick_v3":
t = text()
check("Boot disk: a StarshipOS stick", re.search(r"Boot disk: a StarshipOS stick", t) is not None)
@@ -662,8 +708,7 @@ else:
check(f"known mode {m}", False)
print(f"RESULT: {'PASS' if ok else 'FAIL'}", flush=True)
q.send(json.dumps({"execute": "quit"}).encode())
mk.wait()
shutdown()
for f in ("v3/include/version.h", "capsules/BLOCK_MAP.md"):
subprocess.run(["git", "checkout", "-q", "--", f], cwd=ROOT)
sys.exit(0 if ok else 2)