b0f12710bb211ef7f81f61dedee47b32a0b43d33
23
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b0f12710bb |
Console-VM + user-VM pair: real async message-passing relay
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 |
||
|
|
cc9521d2cc |
Retire emergency CLI: Zuse goes thumbdrive-resident, ACL.4th activated
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 |
||
|
|
6fd87923a5 |
MINT: parameterize with full name, username, email, phone
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 |
||
|
|
f6e2737f1e |
Phase E: MINT -- real keypair, Zuse-signed DER cert, working default identity
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 |
||
|
|
e1e839258d |
Phase D: RUNCAP -- runtime capsule construction from thumbdrive content
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 |
||
|
|
21bca315ff |
Phase C: distributed messaging capsule + idle-loop pump
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 |
||
|
|
0ec91b517a |
Implement CERTVERIFY -- real DER cert verification, tested against OpenSSL
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 |
||
|
|
c07184e984 |
Add VMIdentity type, relocate homeblocks_sig_t off devblock 0
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
|
||
|
|
030a3e6dc0 |
Milestone 6: hard-refuse flip, scoped to INVALID only -- closes Milestone 6
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 |
||
|
|
2fc55f47e1 |
Milestone 6: mkcapsule signing + capsule_birth.c wiring, WARN-only
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 |
||
|
|
3e0966095c |
Stadium: real VM-patron admission + real COOL dispatch at KILL (FABRIC-3.md §B)
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 |
||
|
|
8d90538801 |
item 4.6: fix Stadium quota-grant ordering; fix aarch64 PSCI SYSTEM_RESET function ID
Artemis's 30-rep surface stress campaign was failing 100% of trials on all three architectures: stadium_grant_quota() ran after IDENTITY exec in capsule_birth.c, but Artemis's init.4th auto-runs the stress campaign as part of that same IDENTITY exec, so every STADIUM-ADMIT call during it hit a nonexistent quota slot and refused unconditionally. Moved the grant call before IDENTITY exec. Verified 30/30 reps PASS on amd64, aarch64, and riscv64 post-fix (was 30/30 FAIL on all three pre-fix). Also fixed an independent, real bug found during the same acceptance pass: aarch64's arch_cold_reset() issued PSCI SYSTEM_RESET using the SMC64 calling convention (0xC4000009), which is not a valid PSCI function ID -- SYSTEM_RESET has no SMC64 variant. Corrected to the SMC32 encoding (0x84000009). This did not resolve the separate aarch64 BYE cold-restart exception also found in this pass (root cause not yet found, tested and refuted an interrupt-race hypothesis, documented in FABRIC-2.md Section I for follow-up) but is a genuine spec fix worth keeping regardless. Full writeup, evidence, and the still-open aarch64 crash investigation in FABRIC-2.md Sections H and I. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
89d8c08582 |
stadium: wire STADIUM_CAPACITY_TICK in as a flat threshold, not a scheduler
Closes FABRIC-2.md's last open §12 Q5 question. fleet_heartbeat_tick_count is fed by every live VM's own vm_tick(), not one VM's, so it was reaching HEARTBEAT_INFERENCE_FREQUENCY (shared/borrowed from the per-VM inference gate) several times faster than intended with more than one VM live - backwards from FABRIC.md §22.4's required ~1000:1 separation. What's actually gated turned out to be low-stakes: vm_physics_tick() (capsule_vm_physics.c:397) is a passive statistics refit - re-sorts a window of past heat-transfer samples and recomputes a median rate estimate. It doesn't move heat or arbitrate capacity. Firing too often just meant a noisier statistic recomputed more frequently than planned, not incorrect behavior. Considered and explicitly rejected: scaling the threshold by live VM count at the check site. That's the first brick of a scheduler - reading fleet state to adjust a rate dynamically - which this project has deliberately avoided building. Implemented instead: STADIUM_CAPACITY_TICK (existing Kconfig symbol, defined but never read by any code path) now gates vm_physics_heartbeat_tick()'s call directly, replacing the borrowed HEARTBEAT_INFERENCE_FREQUENCY. Default bumped 1000 -> 4000, a flat constant picked once for Tripod's known 4-VM topology, same kind of placeholder as every other frequency knob in Kconfig.kernel - not computed from anything at runtime. Renamed fleet_last_inference_tick -> fleet_last_capacity_tick to match. Still one clock, one counter (fleet_heartbeat_tick_count) - just a bigger flat divisor on it. Three-arch QEMU acceptance: all clean to ok>, identical Stadium conservation invariant on all three (resident_sum=43691 reservoir=21845 sum=65536). logs/20260815-093425/amd64, logs/20260815-093521/aarch64, logs/20260815-093641/riscv64. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
00e657019e |
stadium: make VM population bound RAM-derived, not a static array of 4
Replaces STADIUM_MAX_VM_COUNT (Kconfig, hardcoded default 4) with a boot-time computation, mirroring the pattern stadium_boot_init() already used for the cell pool. New Kconfig STADIUM_VM_MEMORY_PERCENT (default 50): max_vm_count = (kmalloc_get_stats().free_bytes after the cell array * STADIUM_VM_MEMORY_PERCENT / 100) / VM_MEMORY_SIZE, floored to 1, no ceiling (population is not knowable in advance - could be 4, could be 4000). stadium_quotas and word_slots (plus stat_promotions/stat_evictions) are now kmalloc'd to the computed count instead of declared with a macro. New accessor stadium_max_vm_count() replaces every STADIUM_MAX_VM_COUNT reference, including capsule_birth.c's birth-refusal gate. Two things found and fixed along the way: - The existing cell-pool budget was sourced from pmm_get_stats(), which reflects physical pages PMM hasn't handed to any subsystem yet - but the actual allocation is kmalloc(), which draws from the separate, fixed-size heap kmalloc_init() (M6) already carved out of PMM before stadium_boot_init() ever runs. Budgeting against PMM's leftover and allocating from the kmalloc heap are two different pools. Both the cell budget and the new VM-count budget now source from kmalloc_get_stats() instead. - stadium_owner[] (which VM's quota owns each cell) was uint8_t, capped at 255 slots by a compile-time assert tied to the old macro. Widened to uint16_t (65535 slots of headroom) with a runtime clamp + log if the computed count ever exceeds that, since there's no ceiling anymore. Three-arch QEMU acceptance: all clean to ok>, computed VM count genuinely differs by actual available RAM (amd64/riscv64: 50 slots at -m 1024, aarch64: 101 slots), Stadium conservation invariant identical across all three (resident_sum=43691 reservoir=21845 sum=65536). logs/20260815-080526/amd64, logs/20260815-080826/aarch64, logs/20260815-080952/riscv64. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
db6640682b |
mama_forth_words: KILL must not physics-retire Hera before refusing her
S" Hera" KILL called vm_physics_retire(hera_id) before capsule_vm_kill()'s own Hera guard ever ran. Hera's self-referential parent_vm_id makes vm_physics_find_root_id() return her own id immediately, so vm_physics_retire() treated her as an unreachable root and zeroed the fleet's entire execution_heat_q48 sum -- silently, with only the harmless-looking "cannot kill Hera" message as output. Guard the retire call the same way capsule_vm_kill() already guards the actual kill. Three-arch acceptance boot, all clean to ok>: logs/20260813-083429/amd64, logs/20260813-083551/aarch64, logs/20260813-083738/riscv64. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
5a28458b21 |
starkernel: item 4.2 -- Hermes native on the Stadium (complete)
Migrates Hermes's message/channel lifecycle onto the Stadium's unified heat/capacity economy: MSG-ALLOC/FREE-NODE and CH-ALLOC/FREE-NODE now route entirely through stadium_admit()/stadium_evict(), replacing the old local free-list + independent heat-field mechanism. Eight kernel-only STADIUM-* FORTH primitives (ADMIT, EVICT, RES@, RES-PULL, RES-PUSH, HEAT@, HEAT!, WORD-HEAT), VM.stadium_vm_id threaded through all three vm_core.c dispatch sites (replacing item 4.1's hardcoded vm_uuid_hera()), and the stadium_owner[idx] fix so evict-credit lands in the VM that actually admitted a patron, not whoever owned cell 0. This session's own contribution, on top of that pre-existing implementation: found and fixed two bugs blocking the item's own K≡1.0 conservation self-check (HERMES-K was reading 0, not 65536): - Q.SLOT admission-heat fix (capsules/hermes/init.4th): MSG-SEND/ CH-ACCEPT admitted with Q.1 (the entire fleet-wide "1.0" unit) per item, a leftover from before the Stadium migration when each message/channel had its own unconstrained heat field. Instantly drained the shared, finite reservoir. - Reservoir floor for word-execution admission (stadium_words.c): stadium_word_dispatch() (item 4.1) pulls STADIUM_WORD_HEAT_QUANTUM on every word dispatch, not just first admission -- exhausts a VM's entire reservoir in ~32 dispatches, starving any application-level economy sharing that VM's reservoir before it gets a chance to pull anything. word_dispatch_pull() now clamps word-execution's own pulls to leave a Q48_ONE/3 floor (same fair-share figure COMMON-CH's own floor already uses); application-level pulls are unaffected. - STADIUM-WORD-HEAT primitive + stadium_words_resident_heat(): the floor deliberately leaves word-execution residents holding real heat, invisible to HERMES-K's original formula (MSG+CH+reservoir, no term for word patrons). Adding this term closes K to exactly 65536 on all three architectures. Also rules on two open scope questions in FABRIC.md: MBR-ALLOC/ MBR-FREE-NODE stay off the Stadium (membership records have no heat field, never did -- the acceptance bullet's inclusion of them was a completeness gesture predating a check of the actual layout), and records the effort number (12 implementation files, +759/-120 lines). Verified: all three architectures boot clean, full self-test passes, Stadium conservation closes exactly (resident_sum + reservoir = Q48_ONE) at both the C/Stadium level and the FORTH-level HERMES-K check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
2981ada2a5 |
starkernel: item 4.1a -- quota granting, Hermes's one-time birth grant
Punch list §25 item 4.1a complete. New prerequisite item, found while scoping 4.2: no quota-granting mechanism existed at all. Adds stadium_grant_quota(new_vm_id, from_vm_id) -- a one-time initial grant at birth, distinct from item 1.3's still-unbuilt recurring capacity-transfer arbitration. Splits the donor's free list evenly by cell count, reassigns stadium_owner[] for every moved cell, and grants the new VM a fresh Q48_ONE reservoir (not a split of the donor's -- per-VM conservation, same pattern as Hera's own boot grant). Wired into every baby VM's birth in capsule_birth.c. Verified via a boot-time self-test in kernel_main.c using a synthetic identity (not the real UUID pool, not a real capsule birth -- item 0.1's Hera-alone pruning stays intact). All three architectures booted to ok> with identical output: grant OK, Hera reservoir=0 (already fully committed to resident words, correctly unchanged), test-vm reservoir=65536 (fresh Q48_ONE). dict_hash identical across all three and unchanged from item 4.1's baseline (0x3d4e1daf289da94f) -- confirms no dictionary word was added. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
9b305a5be7 |
starkernel: item 3.8 -- VM identifiers as UUID/GUID
Punch list §25 item 3.8 complete. Added after starting item 4.1
surfaced the need to thread a vm_id into stadium_admit()'s new quota
parameter; Captain Bob ruled UUID/GUID rather than keeping the
narrower uint32_t.
New VMUuid type (vm_uuid.h/vm_uuid.c): two uint64_t halves, RFC-4122-
shaped for logging. Not real randomness -- checked directly against
QEMU 10.2.1's actual CPU feature set: amd64 RDRAND and riscv64 Zkr are
both real, available features here; aarch64 has no RNG property on any
CPU model including "max" (verified exhaustively via QMP
query-cpu-model-expansion). Captain Bob ruled a uniform fallback
across all three ISAs rather than a per-architecture split.
Fallback is a deterministic PRNG (splitmix64) seeded from the Mama
capsule's content hash, pre-filling a 16-entry FIFO pool at boot and
refilling with another batch of the same stream when exhausted --
exactly the shape requested. Same capsule booted twice produces the
same id sequence, preserving the dict_hash reproducibility this
session has relied on throughout.
Hera keeps a fixed, reserved all-zero id, not drawn from the pool --
capsule_birth.c uses vm_id == 0 as a load-bearing sentinel in three
places (KILL protection x2, fleet heat-fanout parent-chain
terminator), found by reading before writing any code.
Two real sentinel-collision bugs caught before shipping, same class as
STADIUM_CONTAINS_NONE: vm_uuid_none() (all-ones, not all-zero) for
"not yet assigned"/"no VM" placeholders; confirmed item 3.7's quota
table already used an in_use boolean rather than a vm_id sentinel, so
no second collision was actually possible there -- the dead,
never-referenced STADIUM_QUOTA_SLOT_EMPTY macro was removed.
Blast radius larger than first scoped, flagged mid-work rather than
silently absorbed: capsule_vm_physics.c/.h (the fleet heat-transfer
layer item 2.1 modified earlier this session) has its own vm_id-keyed
node table and walks parent_vm_id chains through the same identity
space, so it needed the same change, plus its callers in
mama_forth_words.c and sk_vm_bootstrap.c.
One live FORTH word contract changed, by explicit ruling: CAPSULE-BIRTH
was ( capsule-id -- vm-id ), a single cell -- can't hold 128 bits.
Captain Bob picked pushing two cells ("there is doubles support in the
FORTH std word set anyway"): ( capsule-id -- vm-id-hi vm-id-lo ).
MAMA-VM-ID changed the same way: ( -- 0 0 ).
Verified: full (not standalone-file) kernel rebuild to catch cross-file
breakage given the size of this change -- it surfaced the
capsule_vm_physics.c blast radius a narrower check would have missed.
Three-architecture boot (amd64, aarch64, riscv64), all reaching ok>
with identical dict_hash=0x3d4e1daf289da94f matching the item-3.7
baseline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
b0416794de |
starkernel: item 2.2 complete -- bound the VM registry
Wires the birth-refusal check into capsule_birth_baby(): calls vm_registry_live_count() (added in the prior WIP commit) between capsule validation and vm_registry_alloc(), returning the new CAPSULE_RUN_ERR_FLEET_FULL and logging via capsule_parity_log_birth_failed() before any EMBRYO registry slot is consumed. Also fixes a gap in that WIP commit: STADIUM_MAX_VM_COUNT was only ever defined via a Kconfig .config-driven -D flag, with no fallback default the way every sibling knob in starforth_config.h has -- a build with no .config present (this one) failed with the macro undeclared. Added STARFORTH_CONFIG_STADIUM_MAX_VM_COUNT_DEFAULT (4, matching Kconfig.kernel) following the existing HEARTBEAT_INFERENCE_FREQUENCY pattern exactly. All three architectures boot clean to ok> with dict_hash=0x3d4e1daf289da94f, matching the item-0.10/2.1 baseline. FABRIC.md item 2.2 checked off. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
5572e5e429 |
WIP: item 2.2 -- bound the VM registry (not complete, do not check off)
Partial work toward FABRIC.md punch list item 2.2. Adds the STADIUM_MAX_VM_COUNT Kconfig symbol (default 4, per item 1.5) wired through Makefile.starkernel, a new CAPSULE_RUN_ERR_FLEET_FULL result code, and a vm_registry_live_count() helper in capsule_birth.c that counts LIVE VMs only (distinct from the existing monotonic vm_registry_count, which never decrements on death). NOT YET DONE: nothing calls vm_registry_live_count() yet -- the actual birth-refusal check is not wired into capsule_birth_baby(). Not built, not boot-tested. FABRIC.md's item 2.2 checkbox is deliberately left unchecked; this commit exists only to save in-progress work before a pause, not to claim the item complete. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
542d7dbf0d |
starkernel: restate VM fleet heat transfer on the virtual tick
Punch list §25 item 2.1 complete. vm_physics_touch() no longer takes a wall-clock timestamp -- it reads fleet_heartbeat_tick_count internally, which is execution-paced (vm_runtime.c:143), not wall-clock. VMPhysics.last_active_ns -> last_active_tick, VMFleetTouchSample.elapsed_us -> elapsed_ticks, and a new explicit `touched` flag replaces the old `> 0` sentinel (tick 0 is a legitimate value a first touch can land on, unlike wall-clock ns). Verified: three-architecture boot (amd64 x2, aarch64, riscv64), all reaching ok> with identical dict_hash=0x3d4e1daf289da94f matching the item-0.10 baseline. No new compiler warnings in the touched files. Honestly flagged, not fixed: with Tripod pruned to Hera alone (item 0.1), vm_physics_touch()'s fan-out has no other live VM to pull heat from, so the fleet-heat-sum acceptance criterion is trivially satisfied rather than genuinely stress-tested -- a real check needs Phase 4's multi-VM fleet. fleet_transfer_slope_q48's seed (65536/3) was calibrated for elapsed microseconds and has not been re-fit for elapsed ticks; left as-is rather than guessed, deferred to item 5.1's DoE work. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
cc6c8c43f3 |
Fix ABORT to actually unwind to QUIT instead of one level
ABORT is documented and tested in this codebase as standard FORTH-79 behavior -- system_words_test.c:63: "Should clear stacks and return to QUIT" -- meaning it should unwind all the way back to the outermost interpreter loop, abandoning whatever's left of the current line/block. The implementation only unwound one level: every place that checked vm->abort_requested cleared it the instant it saw it, so it never survived to propagate past the first nested frame. This surfaced via Artemis's ART-HALT-UNRECOG (capsules/artemis/init.4th): on an unrecognized disk it correctly printed "ARTEMIS HALT: unrecognized disk content" and called ABORT, but WELCOME (the next line in the same block) ran anyway, and Artemis announced ready to Hermes and joined the fleet normally -- contradicting .claude/ARTEMIS.md's "Refuse to mount... do not overwrite it" requirement. Root cause is general, not Artemis-specific, and present identically in both the hosted and kernel VM cores. Fixed at every level execution can nest through, verified by exhaustively grepping every !vm->error-gated continuation loop and adding the parallel !vm->abort_requested check: - execute_colon_word (src/vm.c, src/starkernel/vm/vm_core.c): stop clearing the flag on return -- every colon-word call is a recursive call to this same function, so leaving it set lets every enclosing frame's own check also unwind. - vm_interpret (src/vm.c, src/starkernel/vm/vm_core.c): stop parsing further words in the current input string once the flag is set. - exec_block_with_retry (src/starkernel/capsule/capsule_loader.c): capsule birth's line-by-line block executor -- stop processing further lines in the current block, but return 0 (not -1), so capsule_exec_payload still loads later blocks in the same capsule payload. Returning -1 here would have silently broken word definitions in blocks that come after the aborting one for reasons unrelated to why it aborted (concretely, Artemis's ART-PING/LOAD-DOE in blocks 4851/4852, which follow the entry block 4133). - THRU and --> (src/word_source/block_words.c): stop processing further blocks/lines in their own loops. - DODOES (src/word_source/defining_words.c): the CREATE...DOES> runtime has its own hand-rolled execution loop, separate from execute_colon_word -- same bug class, same fix. Also guarded the post-loop "if (vm->rsp < base_rsp) vm->rsp = base_rsp" clamp so it doesn't fire on an abort exit -- ABORT's own reset_vm_state() already set rsp; restoring it to base_rsp would have partially undone that. - Both REPL loops (src/repl.c, src/starkernel/repl.c x2 call sites): clear the flag after each line, mirroring the existing vm->error pattern, so a mid-line abort doesn't silently freeze subsequent interactive input. Verified directly: ": AB-TEST 1 2 3 ABORT 999 . ; AB-TEST 42 . CR 777 . CR" -- 999 never prints (stops mid-colon-word), 42 never prints (stops the rest of the same line), 777 prints fine (next line unaffected). Artemis: WELCOME/"Artemis ready" no longer fires after the halt message. No regression: all three architectures still show PASS: persist-read, PASS: E2E msg flow, and matching dict_hash on the normal (non-aborted) boot path; hosted test suite 965 passed / 0 failed. Known follow-up, not fixed here (see memory for details): Artemis still announces ready to Hermes via a separate call path (CD-INIT, block 4141) that never went through capsule_exec_payload's block chain in the first place, and the disk file still picks up incidental writes even on a correctly-halted boot -- likely generic block-subsystem housekeeping, not traced yet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
a5ed8c3d87 | Initial commit — LithosAnanke kernel |