Punch list item 4 (final): CONSOLE-ATTACH, full unattended-identity flow verified end-to-end
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s

Closes FABRIC-3.md §XXXII.2's punch list. CONSOLE-ATTACH ( name-c
name-u -- ok? ) pairs a fresh console VM to an already-live VM
registered as "<name>~user". Deliberately a plain, unconditional
primitive with no VMIdentity capability-bit check -- corrects this
session's own first-pass design (§XXXII.2 amended in the same commit):
identity.installed is 0 for Hera/Hermes/Artemis and for every
console-proxy VM, so a capability-bit gate would be unreachable for
every VM a human actually types at, Zuse included. Matches
ZUSE-ELIGIBILITY-ADD's own "no bespoke gate" precedent in this file;
real gating is `' CONSOLE-ATTACH ACL-PIN` in ACL.4th if ever wanted.

Two real bugs found and fixed via live testing, not assumed correct:
- capsule_birth_baby() never sets a VM's registry name (documented
  gap, same one capsule_runcap_birth()'s own history already hit) --
  UNATTENDED-BIRTH gained a second `name` argument and now calls
  capsule_vm_registry_set_name(new_vm_id, "<name>~user") itself.
- CONSOLE-ATTACH's first draft took an independent console name from
  the target's name. sk_repl_dispatch_line()'s pairing check (repl.c)
  reconstructs the target as console_get_vm_name()+"~user" -- a
  mismatched console name silently falls back to direct interpretation
  with no error. Caught live (typed `5 6 + .` at a mismatched console,
  got a direct `11` instead of a relay) and fixed by collapsing to one
  name argument, matching WIREBIND's own by-construction invariant.

Full end-to-end live verification on amd64: minted a test identity,
UNATTENDED-BIRTH'd it as "bob", CONSOLE-ATTACH'd a console named "bob",
USE'd it, typed `5 6 + .` -- no direct output at [zuse@bob] (relay
path taken), then `[zuse@bob~user] 11 ok>` appeared: the relayed
command executed on the target identity VM itself and printed its own
answer back through the shared console. Hera stayed healthy throughout
(2 2 + . -> 4 after switching back). CONSOLE-ATTACH also verified to
refuse cleanly on a nonexistent target with no orphaned VM. Test
capsule reverted after capture per this project's probe convention --
never committed.

FABRIC-3.md §XXXII.2 fully closed: all 4 original questions ratified,
the mid-course drive_uuid and ACL-bit corrections both recorded
plainly rather than silently folded in, and a doc-accuracy note left
for CLAUDE.md's own stale "1024-byte block limit" framing (mkcapsule's
real limits are range [2048,5120) and 16 content lines/block) --
flagged, not fixed, out of this punch list's scope.

Clean 3-arch qemu boot (amd64/aarch64/riscv64) on the real committed
C-only change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWpNjdwPtFLuVLaAq44L9K
This commit is contained in:
Robert Allan James
2026-09-16 07:24:27 -04:00
co-authored by Claude Sonnet 5
parent 5879c8b3bc
commit af351bff92
14 changed files with 46188 additions and 34 deletions
+160 -5
View File
@@ -1615,7 +1615,7 @@ void mama_word_capsule_birth(VM *vm)
}
/**
* @brief UNATTENDED-BIRTH ( name-c name-u -- ok? )
* @brief UNATTENDED-BIRTH ( capsule-c capsule-u name-c name-u -- ok? )
* Birth an unattended identity from a named (p) capsule -- the
* punch-list item 3 call site (FABRIC-3.md §XXXII.2, 2026-09-16):
* capsule_birth_baby() unmodified (same generic build-time-capsule
@@ -1635,25 +1635,53 @@ void mama_word_capsule_birth(VM *vm)
* dictionary" idiom capsule_wirebind.c already uses for VM-NAME-REG,
* not a new mechanism.
*
* `name` becomes the VM's own registry name, suffixed "~user" exactly
* as capsule_runcap_birth()/WIREBIND already do for a real thumbdrive
* identity -- this is what CONSOLE-ATTACH (below) and sk_repl_dispatch_
* line()'s own pairing check (repl.c) look up later. capsule_birth_
* baby() itself never sets this (found live during capsule_runcap_
* birth()'s own history, repeated here rather than left silently
* missing): every caller does it as its own explicit step.
*
* Explicit invariant (FABRIC-3.md §XXXII.2's own ratified text): this
* word never touches g_wirebind_attached_username or any other
* WIREBIND/console-pairing state, and births no console VM -- an
* unattended identity stays un-promptable (§VIII.1) until a human
* pairs a console to it later via the existing VM-NAME-REG mechanism,
* a separate, already-working step this word does not perform.
* pairs a console to it later via CONSOLE-ATTACH, a separate step this
* word does not perform.
*/
static void mama_word_unattended_birth(VM *vm)
{
char capsule_name[VM_NAME_MAX];
char id_name[VM_NAME_MAX];
cell_t u, caddr;
uint32_t i;
if (vm->dsp < 1) {
console_println("UNATTENDED-BIRTH: expects S\" capsule-name\" UNATTENDED-BIRTH");
if (vm->dsp < 3) {
console_println("UNATTENDED-BIRTH: expects S\" capsule\" S\" name\" UNATTENDED-BIRTH");
vm->error = 1;
vm_push(vm, 0);
return;
}
u = vm_pop(vm);
caddr = vm_pop(vm);
if (u <= 0 || (uint32_t)u >= VM_NAME_MAX) {
console_println("UNATTENDED-BIRTH: identity name too long or empty");
vm_push(vm, 0);
return;
}
{
const uint8_t *p = vm_ptr(vm, (vaddr_t)caddr);
if (!p) {
console_println("UNATTENDED-BIRTH: invalid address on the stack");
vm->error = 1;
vm_push(vm, 0);
return;
}
for (i = 0; i < (uint32_t)u; i++) id_name[i] = (char)p[i];
}
id_name[u] = '\0';
u = vm_pop(vm);
caddr = vm_pop(vm);
if (u <= 0 || (uint32_t)u >= VM_NAME_MAX) {
@@ -1679,6 +1707,18 @@ static void mama_word_unattended_birth(VM *vm)
return;
}
char reg_name[VM_NAME_MAX + 8];
{
size_t nlen = strlen(id_name);
if (nlen + 6 > sizeof(reg_name)) {
console_println("UNATTENDED-BIRTH: identity name too long");
vm_push(vm, 0);
return;
}
memcpy(reg_name, id_name, nlen);
memcpy(reg_name + nlen, "~user", 6);
}
VMUuid new_vm_id = vm_uuid_none();
void *new_vm_ctx = (void *)0;
CapsuleRunResult result = capsule_birth_baby(
@@ -1727,11 +1767,124 @@ static void mama_word_unattended_birth(VM *vm)
return;
}
born_vm->identity = identity;
capsule_vm_registry_set_name(new_vm_id, reg_name);
console_println("UNATTENDED-BIRTH: identity installed, no console attached");
vm_push(vm, 1);
}
/**
* @brief CONSOLE-ATTACH ( name-c name-u -- ok? )
* Pair a fresh console VM to an already-live VM registered as
* "<name>~user" -- the "ACL-gated console attachment later" step
* FABRIC-3.md §XXXII.2's Q3 describes, for both an UNATTENDED-BIRTH'd
* identity and, just as validly, a WIREBIND-attached one that lost its
* console (same registry-name convention either way).
*
* Takes exactly one name, not an independently-chosen console name --
* found live, not assumed: sk_repl_dispatch_line()'s own pairing check
* (repl.c) reconstructs the target as console_get_vm_name() + "~user",
* where console_get_vm_name() is whatever name USE last switched the
* prompt to (the console's own birth name). A console named anything
* other than the identity's own base name reconstructs the wrong
* target string and silently falls back to direct interpretation --
* no error, just quietly never relays. WIREBIND's own console+user
* birth pair (capsule_wirebind_try_attach()) already enforces this by
* construction (both sides built from the same `username`); this word
* makes the same invariant explicit rather than accepting two names
* that could mismatch.
*
* Deliberately a plain, unconditional primitive -- no VMIdentity
* capability-bit check here or anywhere else in this codebase (§XXXII.2
* amended 2026-09-16, correcting the first pass of this section, which
* had proposed exactly that). Two independent reasons rule it out, not
* one: CLAUDE.md's own hard rule ("ACL policy belongs in ACL.4th, never
* in C"), and a mechanical fact traced live -- identity.installed is 0
* for Hera/Hermes/Artemis *and* for every console-proxy VM, so a
* vm_identity_has_cap() gate here would be unreachable for every VM a
* human actually types at, Zuse included. Matches ZUSE-ELIGIBILITY-ADD's
* own precedent in this same file: restricting who may call this word,
* if ever wanted, is `' CONSOLE-ATTACH ACL-PIN` in ACL.4th, not a
* bespoke check invented here.
*
* Target liveness is resolved *before* birthing anything -- a typo'd
* name refuses cleanly with no orphaned console VM left behind, rather
* than birthing first and discovering the mismatch after.
*/
static void mama_word_console_attach(VM *vm)
{
char id_name[VM_NAME_MAX];
cell_t u, caddr;
uint32_t i;
if (vm->dsp < 1) {
console_println("CONSOLE-ATTACH: expects S\" name\" CONSOLE-ATTACH");
vm->error = 1;
vm_push(vm, 0);
return;
}
u = vm_pop(vm);
caddr = vm_pop(vm);
if (u <= 0 || (uint32_t)u >= VM_NAME_MAX) {
console_println("CONSOLE-ATTACH: name too long or empty");
vm_push(vm, 0);
return;
}
{
const uint8_t *p = vm_ptr(vm, (vaddr_t)caddr);
if (!p) {
console_println("CONSOLE-ATTACH: invalid address on the stack");
vm->error = 1;
vm_push(vm, 0);
return;
}
for (i = 0; i < (uint32_t)u; i++) id_name[i] = (char)p[i];
}
id_name[u] = '\0';
char target_name[VM_NAME_MAX + 8];
{
size_t nlen = strlen(id_name);
if (nlen + 6 > sizeof(target_name)) {
console_println("CONSOLE-ATTACH: target name too long");
vm_push(vm, 0);
return;
}
memcpy(target_name, id_name, nlen);
memcpy(target_name + nlen, "~user", 6);
}
/* Resolve first -- refuse cleanly on a typo instead of birthing a
* console VM with nothing live to pair it to. */
VMRegistryEntry target_entry;
if (capsule_vm_find_by_name(target_name, &target_entry) != 0 ||
target_entry.state != VM_STATE_LIVE) {
console_println("CONSOLE-ATTACH: refused -- no live VM registered under that name");
vm_push(vm, 0);
return;
}
VMUuid console_id;
void *console_ctx = (void *)0;
if (capsule_console_birth(id_name, vm->stadium_vm_id, &console_id, &console_ctx)
!= CAPSULE_RUN_OK) {
console_println("CONSOLE-ATTACH: console birth FAILED");
vm_push(vm, 0);
return;
}
{
char reg_cmd[VM_NAME_MAX + 32];
int n = snprintf(reg_cmd, sizeof(reg_cmd), "S\" %s\" 3 VM-NAME-REG", target_name);
if (n > 0 && (size_t)n < sizeof(reg_cmd)) {
vm_interpret((VM *)console_ctx, reg_cmd);
}
}
console_println("CONSOLE-ATTACH: console paired to target -- USE the console name to attach");
vm_push(vm, 1);
}
/**
* @brief CAPSULE-RUN ( capsule-id -- )
* Run an experiment (e) capsule on Mama.
@@ -2073,6 +2226,7 @@ void register_mama_forth_words(VM *vm)
register_word(vm, "MINT-SCRATCH", mama_word_mint_scratch);
register_word(vm, "MINT-SCRATCH-EMIT", mama_word_mint_scratch_emit);
register_word(vm, "UNATTENDED-BIRTH", mama_word_unattended_birth);
register_word(vm, "CONSOLE-ATTACH", mama_word_console_attach);
register_word(vm, "ZUSE-ELIGIBILITY-ADD", mama_word_zuse_eligibility_add);
register_word(vm, "ZUSE-ELIGIBLE?", mama_word_zuse_eligible_query);
register_word(vm, "NAME>XT", mama_word_name_to_xt);
@@ -2135,6 +2289,7 @@ void register_mama_forth_words(VM *vm)
register_word(vm, "MINT-SCRATCH", mama_word_mint_scratch);
register_word(vm, "MINT-SCRATCH-EMIT", mama_word_mint_scratch_emit);
register_word(vm, "UNATTENDED-BIRTH", mama_word_unattended_birth);
register_word(vm, "CONSOLE-ATTACH", mama_word_console_attach);
register_word(vm, "ZUSE-ELIGIBILITY-ADD", mama_word_zuse_eligibility_add);
register_word(vm, "ZUSE-ELIGIBLE?", mama_word_zuse_eligible_query);
register_word(vm, "NAME>XT", mama_word_name_to_xt);