Channel-open policy hook -- FABRIC-3.6.md task 3.7

Added HERMES-CHANNEL-OPEN? ( req-hi req-lo -- allow? ) at
capsules/ACL.4th block 4008 (default: approve everything) -- the one
word policy authors edit. sk_hermes_channel_open_policy(VM*, VMUuid)
(kernel_hermes.h/.c) is the C-side query that calls it via plain
word-dispatch against the target VM's own dictionary/stack, never
vm_interpret() (avoids task 3.4's input-buffer cursor hazard entirely)
and never decides the answer itself. Fails closed: no policy word,
a policy error, or stack underflow all deny, matching CLAUDE.md's
posture that absence of policy must never mean "always allow."

Two real bugs found and fixed before this was called done:
missing current_executing_entry assignment before calling the word's
func pointer (colon words silently no-op without it, vm_core.c:730 --
no crash, just a wrong answer); and a second FAIL with debug
instrumentation still in place whose precise cause isn't
reconstructable, since no intermediate commit exists for that attempt.

Self-test proves the task's check four ways against the same
unchanged C function: default approve, live redefinition to deny
(zero C change), restore, and a VM with no ACL.4th loaded at all
(fail closed). A fifth check wires the result into task 3.6's
sk_hermes_channel_respond() end to end: a denied policy produces a
NACK and no channel, ledger/stadium_conserved() holding throughout.

Scope, per Captain Bob's ruling: closes with the query built and
proven; sk_hermes_channel_respond() still takes a caller-supplied
approved bool rather than calling the policy internally. Wiring a
real channel-open call site to only this query is deferred to
whichever later task first needs a live decision.

dict_hash identical across amd64/aarch64/riscv64 for every VM, zero
UNKNOWN WORD, mkcapsule --lint clean (38 files, 0 violations).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-22 09:08:48 -04:00
co-authored by Claude Sonnet 5
parent 205a49ecd0
commit f37aa0fb17
11 changed files with 46618 additions and 12 deletions
+61 -1
View File
@@ -984,9 +984,69 @@ ruling]** cannot be written precisely until Captain Bob settles the named sub-it
task touches no capsule). Artemis's own `dict_hash` differs run-to-run as already
documented (task 3.1's own finding) -- shared `disk/artemis.img` state accumulated across
this session's many prior boots, not an architecture divergence.
- [ ] **3.7** — **Channel-open policy hook** **[needs ruling 3.0b]**. Kernel-Hermes asks
- [x] **3.7** — **Channel-open policy hook** **[needs ruling 3.0b]**. Kernel-Hermes asks
`ACL.4th`, never decides in C, never gates on `zuse_session`. *Check:* a denied open is
denied by FORTH policy, with the C unchanged when the policy changes.
2026-09-22 · `logs/20260922-075138/amd64/`, `logs/20260922-075649/aarch64/`,
`logs/20260922-080212/riscv64/` (the successful runs; `logs/20260922-074119/amd64/` and
`logs/20260922-074628/amd64/` are two genuine prior attempts that hit real bugs, caught by
the self-test itself before this task was called done -- kept, not deleted, per the
never-delete-logs convention) — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
`mkcapsule --lint capsules/` clean (38 files, 0 violations — one block added inside the
existing `ACL.4th`, no file added). `dict_hash` identical across all three architectures
for every VM (Hera `0xbb2b51e7595464ce`, Hermes `0xc95ef2d92fa0781f`, Hestia
`0xfbde9fe105fd3b3d`, Artemis `0x7f18214489036b39`).
Added `HERMES-CHANNEL-OPEN? ( req-hi req-lo -- allow? )` at `capsules/ACL.4th` block 4008
(default: approve everything) — the one word policy authors edit; `sk_hermes_channel_open_policy(VM *target_vm, VMUuid requester)`
(`kernel_hermes.h`/`.c`) is the C-side query that calls it, and never itself decides. Uses
plain word-dispatch (`vm_find_word()` + the entry's own `func` pointer) against the
*target* VM's own dictionary and data stack, not `vm_interpret()` — deliberately, since
`HERMES-CHANNEL-OPEN?` is a fixed known name, and this avoids task 3.4's input-buffer
cursor-preservation hazard entirely (this touches the target's data stack only, never its
input buffer). **Fails closed, not open**: no policy word present (e.g. a VM that never
loaded `ACL.4th`), a policy-word error, or stack underflow all return "denied," matching
`.claude/CLAUDE.md`'s own posture that absence of policy must never mean "always allow."
A broken policy word's own error state is cleared on the target VM before returning, so it
cannot leak into whatever else that VM is doing.
**Two real bugs found and fixed before this was called done, not a clean first pass:**
1. (`logs/20260922-074119/`) The first draft called `entry->func(target_vm)` directly
without setting `target_vm->current_executing_entry` first — colon words dispatch
through `execute_colon_word()`, which reads its own body address from that field and
silently no-ops if it's `NULL` (`vm_core.c:730`). Every call silently did nothing,
leaving the pushed requester args untouched on the stack rather than erroring — no
crash, no obvious symptom, just a wrong answer, which is why it needed the self-test
(not a crash) to catch. Fixed by setting `current_executing_entry` before the call and
restoring the caller's own value after (in case the target VM is ever called into
mid-dispatch elsewhere later).
2. (`logs/20260922-074628/`) Still `FAIL`, with debug instrumentation left in from
diagnosing bug 1 (`entry=... func=...`, `error=0 dsp=1`/`error=0 dsp=2` printed, then
`FAIL`). **No intermediate commit exists for this attempt, so its precise defect is
not reconstructable now** — the log records the symptom and nothing more. Debug prints
were removed; the third attempt (`075138`/`075649`/`080212`) passed clean on all three
architectures with no debug output.
Self-test in `kernel_main.c` proves the task's check four ways against the **same
unchanged C function**: (a) Hera's default `HERMES-CHANNEL-OPEN?` approves; (b)
redefining that same word live on Hera to deny flips the answer with zero C change; (c)
restoring the approve-default flips it back; (d) Hermes — who never loads `ACL.4th` at
all (grep-confirmed: only `init.4th`/`ACL.4th`/`zuse.4th`/`block-acl.4th` reference it) —
is correctly refused closed, not silently approved, proving the fail-closed behaviour on a
VM with no policy at all. A fifth check wires the policy result straight into task 3.6's
`sk_hermes_channel_respond()` end to end: a denied policy really produces a NACK and no
channel, with the ledger audit and `stadium_conserved()` holding before and after —
exactly task 3.6's own deny-path shape, reused rather than re-derived.
**Not yet done, by scope**: the actual call site inside `sk_hermes_channel_respond()`'s
caller still takes a plain `approved` bool (task 3.6's own signature, unchanged, as
promised) — this task builds and proves the query function itself; wiring it as the
*only* source of that bool at a real call site is deferred to whichever later task first
needs a live (not self-test-only) channel-open decision, consistent with every other
Phase 3 task before Stage C being self-test-only. `dict_hash` for Hermes/Hestia unmoved
from task 3.6's values, as expected (neither VM loads `ACL.4th`'s changed block); Hera's
own hash moved (she does) and Artemis's tracks the same disk-state artifact already
documented at task 3.1.
- [ ] **3.8** — **Stage C: cut over `BLK-ATTACH-EVENT` alone** (§XXXIV.3). One layer owns it;
FORTH Hermes still routes every other type. *Check:* the real Hera↔Artemis attach path
works end to end on all three ISAs; ledger and `stadium_conserved()` true before/after;