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;
+10
View File
@@ -98,6 +98,16 @@ Block 4007
( rewired: physics-style regression belongs to the kernel )
( exclusively, and this file is deliberately host-portable. )
Block 4008
( HERMES-CHANNEL-OPEN? -- FABRIC-3.6.md task 3.7 hook. )
( Stack: req-hi req-lo -- allow? Kernel-Hermes asks )
( this word, never decides in C, never gates on )
( zuse_session (CLAUDE.md). Default: approve every )
( request. Policy authors edit THIS word's body only -- )
( kernel_hermes.c's own query function never changes. )
: HERMES-CHANNEL-OPEN? ( req-hi req-lo -- allow? )
2DROP 1 ;
Block 4015
( Self-activation - runs after all ACL words are defined )
ACL-BOOT
+12 -11
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-09-20T09:57:21Z -->
<!-- Generated by mkcapsule --manifest 2026-09-22T12:01:35Z -->
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
<!-- Hand-written justifications and immutability notes live -->
<!-- in MANIFEST.md alongside this auto-generated index. -->
@@ -8,7 +8,7 @@
| Capsule | Blocks claimed | xxHash64 | Signed |
|---------|----------------|----------|--------|
| `ACL.4th` | 4000, 4001, 4002, 4003, 4004, 4005, 4006, 4007, 4015 | `0xd781d22148ff171d` | yes |
| `ACL.4th` | 4000, 4001, 4002, 4003, 4004, 4005, 4006, 4007, 4008, 4015 | `0xf8890c05c0d8f921` | yes |
| `acl-std79.4th` | 4023, 4024, 4025, 4026, 4027, 4028, 4029, 4030, 4031, 4032, 4033, 4034, 4035, 4036, 4037, 4038, 4039, 4040, 4041, 4042, 4043, 4044, 4045, 4046, 4047, 4048 | `0x773bf9209df191d1` | yes |
| `artemis:init.4th` | 4110, 4111, 4112, 4113, 4122, 4123, 4124, 4125, 4126, 4127, 4128, 4129, 4130, 4131, 4132, 4133, 4134, 4135, 4136, 4137, 4138, 4139, 4140, 4141, 4160, 4161, 4162, 4163, 4164, 4165, 4166, 4167, 4168, 4169, 4170, 4171, 4172, 4173, 4174, 4177, 4178, 4179, 4180, 4181, 4182, 4851, 4852, 4853, 4854, 4856, 4857, 4858, 4860 | `0xf55edc4e76a8c294` | yes |
| `block-acl.4th` | 4019, 4020 | `0xf6cc2a59e3a6734e` | yes |
@@ -101,15 +101,16 @@
| 2160 | `workload-8.4th` | `0x56b7f2f0efa000df` | ok |
| 2200 | `workload-0.4th` | `0x93f86f60aeba8feb` | ok |
| 2201 | `workload-0.4th` | `0x93f86f60aeba8feb` | ok |
| 4000 | `ACL.4th` | `0xd781d22148ff171d` | ok |
| 4001 | `ACL.4th` | `0xd781d22148ff171d` | ok |
| 4002 | `ACL.4th` | `0xd781d22148ff171d` | ok |
| 4003 | `ACL.4th` | `0xd781d22148ff171d` | ok |
| 4004 | `ACL.4th` | `0xd781d22148ff171d` | ok |
| 4005 | `ACL.4th` | `0xd781d22148ff171d` | ok |
| 4006 | `ACL.4th` | `0xd781d22148ff171d` | ok |
| 4007 | `ACL.4th` | `0xd781d22148ff171d` | ok |
| 4015 | `ACL.4th` | `0xd781d22148ff171d` | ok |
| 4000 | `ACL.4th` | `0xf8890c05c0d8f921` | ok |
| 4001 | `ACL.4th` | `0xf8890c05c0d8f921` | ok |
| 4002 | `ACL.4th` | `0xf8890c05c0d8f921` | ok |
| 4003 | `ACL.4th` | `0xf8890c05c0d8f921` | ok |
| 4004 | `ACL.4th` | `0xf8890c05c0d8f921` | ok |
| 4005 | `ACL.4th` | `0xf8890c05c0d8f921` | ok |
| 4006 | `ACL.4th` | `0xf8890c05c0d8f921` | ok |
| 4007 | `ACL.4th` | `0xf8890c05c0d8f921` | ok |
| 4008 | `ACL.4th` | `0xf8890c05c0d8f921` | ok |
| 4015 | `ACL.4th` | `0xf8890c05c0d8f921` | ok |
| 4016 | `zuse.4th` | `0x490ded9be257a90b` | ok |
| 4017 | `zuse.4th` | `0x490ded9be257a90b` | ok |
| 4018 | `zuse.4th` | `0x490ded9be257a90b` | ok |
+43
View File
@@ -727,6 +727,49 @@ int sk_hermes_channel_respond(VMUuid target, VMUuid requester, int approved);
* @return 0 on success, -1 on refusal. */
int sk_hermes_channel_close(VMUuid closer, int channel_id);
/*
* Channel-open policy hook (FABRIC-3.6.md task 3.7, B1, ruling 3.0b:
* "kernel-Hermes asks ACL.4th, never decides in C, never gates on
* zuse_session"). sk_hermes_channel_open_policy() is the CALL SITE task
* 3.6's own doc comment named as replacing sk_hermes_channel_respond()'s
* caller-supplied `approved` bool with a real query -- this function IS
* that query; sk_hermes_channel_respond()'s own signature stays
* unchanged, as promised.
*
* Deliberately NOT vm_interpret() (contrast task 3.4's drain, which
* genuinely needs to run arbitrary payload text): HERMES-CHANNEL-OPEN?
* is a FIXED, known word name, so this uses the plain-word-dispatch
* pattern FABRIC-3.md SXX already established for Hera's own MSG-TICK
* self-drain ("a plain word dispatch in her own dictionary, not a
* VM-EXEC dispatch into anyone else's input buffer -- no reentrancy
* risk") -- vm_find_word() + calling the entry's own func pointer
* directly, with the requester passed via the target's own data stack.
* This touches neither target_vm->input_buffer nor input_pos, so it
* carries none of task 3.4's cursor-preservation hazard; it DOES still
* touch target_vm's own data stack (dsp), which is the correct and
* expected thing for a word call to do. Calling this from within a live
* checkpoint on a VM that is itself mid-dispatch is deferred, undecided
* territory -- this task (like every Phase 3 task before Stage C/3.8+)
* is self-test only, not wired into any real checkpoint yet.
*
* Fails CLOSED, not open: if target_vm has no HERMES-CHANNEL-OPEN? word
* (e.g. ACL.4th was never loaded into it) or the word itself sets
* target_vm->error or leaves too few stack items, this returns 0
* (denied) -- absence of policy must never silently mean "always
* allow" (CLAUDE.md's own posture on ACL policy). A policy-word bug's
* own error state is cleared on target_vm before returning, so a
* broken policy word cannot leak an error into whatever else target_vm
* is doing.
*
* @param target_vm The VM being asked -- its own dictionary is
* consulted, its own data stack is used for the call.
* @param requester Passed to HERMES-CHANNEL-OPEN? as (hi, lo) on
* target_vm's own stack.
* @return 1 if HERMES-CHANNEL-OPEN? approved (returned a nonzero top of
* stack), 0 otherwise (denied, missing, or errored).
*/
int sk_hermes_channel_open_policy(VM *target_vm, VMUuid requester);
#endif /* __STARKERNEL__ */
#endif /* STARKERNEL_VM_KERNEL_HERMES_H */
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+86
View File
@@ -1749,6 +1749,92 @@ static void kernel_main_deep(BootInfo *boot_info) {
}
}
/* FABRIC-3.6.md task 3.7 self-test: the channel-open policy hook.
* Proves "a denied open is denied by FORTH policy, with the C
* unchanged" three ways against the SAME unchanged C function
* (sk_hermes_channel_open_policy()): Hera's default
* HERMES-CHANNEL-OPEN? (capsules/ACL.4th block 4008, "approve
* everything") approves; redefining that same word live on Hera to
* deny flips the answer with no C change; and 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 (no policy present), not silently approved. A fourth check
* wires the policy result straight into sk_hermes_channel_respond()
* (task 3.6) end to end: a denied policy really produces a NACK and
* no channel, exactly like task 3.6's own deny path. */
{
VMRegistryEntry hera_pol_entry, hermes_pol_entry;
VMUuid requester_pol;
int pol_ok = 1;
requester_pol.hi = 0; requester_pol.lo = 17;
console_puts("Kernel-Hermes channel-open policy self-test: ");
if (capsule_vm_find_by_name_nocase("Hera", &hera_pol_entry) != 0 || !hera_pol_entry.vm_ptr ||
capsule_vm_find_by_name_nocase("Hermes", &hermes_pol_entry) != 0 ||
hermes_pol_entry.state != VM_STATE_LIVE || !hermes_pol_entry.vm_ptr) {
console_println("SKIPPED (Hera/Hermes not available)");
} else {
VM *hera_vm = (VM *)hera_pol_entry.vm_ptr;
VM *hermes_vm = (VM *)hermes_pol_entry.vm_ptr;
/* Default: approve. */
if (sk_hermes_channel_open_policy(hera_vm, requester_pol) != 1) pol_ok = 0;
/* Same C function, policy redefined on Hera alone -- deny. */
if (pol_ok) {
vm_interpret(hera_vm, ": HERMES-CHANNEL-OPEN? ( a b -- f ) 2DROP 0 ;");
if (hera_vm->error) { hera_vm->error = 0; pol_ok = 0; }
}
if (pol_ok && sk_hermes_channel_open_policy(hera_vm, requester_pol) != 0) pol_ok = 0;
/* Restore the default before this self-test's own later use
* of respond()/negotiation against Hera, and for whatever
* runs after this block. */
if (pol_ok) {
vm_interpret(hera_vm, ": HERMES-CHANNEL-OPEN? ( a b -- f ) 2DROP 1 ;");
if (hera_vm->error) { hera_vm->error = 0; pol_ok = 0; }
}
if (pol_ok && sk_hermes_channel_open_policy(hera_vm, requester_pol) != 1) pol_ok = 0;
/* No policy word at all (Hermes never loads ACL.4th) --
* fail closed, not open. */
if (pol_ok && sk_hermes_channel_open_policy(hermes_vm, requester_pol) != 0) pol_ok = 0;
/* End to end with task 3.6: a denied policy really produces
* a NACK and no channel. Funds requester/target fresh so
* this sub-test doesn't depend on the negotiation self-test
* above having left any particular ledger state. */
if (pol_ok) {
VMUuid pub_id5, sub_id5;
int grant_rc3, grant_rc4;
pub_id5.hi = 0; pub_id5.lo = 19;
sub_id5.hi = 0; sub_id5.lo = 20;
grant_rc3 = stadium_grant_quota(pub_id5, vm_uuid_hera());
grant_rc4 = stadium_grant_quota(sub_id5, vm_uuid_hera());
if (grant_rc3 != 0 || grant_rc4 != 0) {
pol_ok = 0;
} else {
int approved = sk_hermes_channel_open_policy(hermes_vm, pub_id5); /* Hermes: no policy -> denied */
int ch5 = sk_hermes_channel_respond(sub_id5, pub_id5, approved);
if (approved != 0 || ch5 != -1) pol_ok = 0;
if (pol_ok && sk_hermes_pending_count(pub_id5) != 1) pol_ok = 0; /* NACK only */
if (pol_ok) {
SkHermesMessage *m5 = sk_hermes_pending_peek(pub_id5);
if (!m5 || m5->type != SK_HERMES_MSG_TYPE_NACK) pol_ok = 0;
if (pol_ok && sk_hermes_release(m5) != 0) pol_ok = 0;
if (pol_ok && sk_hermes_pending_pop(pub_id5) != 0) pol_ok = 0;
}
if (pol_ok && !stadium_conserved(pub_id5)) pol_ok = 0;
if (pol_ok && !stadium_conserved(sub_id5)) pol_ok = 0;
}
}
console_println(pol_ok ? "PASS" : "FAIL");
}
}
/* Decided 2026-09-05: no console for the running system unless a
* thumbdrive is present -- headless by default (EMERGENCY_CONSOLE_
* ENABLED off), reusing that flag's own existing "does this build
+39
View File
@@ -713,4 +713,43 @@ int sk_hermes_channel_close(VMUuid closer, int channel_id) {
return sk_hermes_channel_destroy(channel_id);
}
int sk_hermes_channel_open_policy(VM *target_vm, VMUuid requester) {
static const char word_name[] = "HERMES-CHANNEL-OPEN?";
DictEntry *entry;
DictEntry *saved_entry;
cell_t result;
if (!target_vm) return 0;
entry = vm_find_word(target_vm, word_name, sizeof(word_name) - 1);
if (!entry || !entry->func) return 0; /* fail closed: no policy, no open */
/* Colon words dispatch through execute_colon_word(), which reads its
* own body address from vm->current_executing_entry and no-ops
* silently if it is NULL (vm_core.c:730) -- vm_interpret_word() always
* sets this immediately before calling entry->func(); matched here
* for the same reason (found live: without it, every call silently
* did nothing, leaving the pushed requester untouched on the stack
* rather than erroring, which is why this needed a debug session to
* catch rather than showing up as an obvious crash). Saved and
* restored, not just set, in case target_vm is ever called into
* mid-dispatch (not exercised by this task's own self-test, but no
* reason to assume it away). */
saved_entry = target_vm->current_executing_entry;
target_vm->current_executing_entry = entry;
vm_push(target_vm, (cell_t)requester.hi);
vm_push(target_vm, (cell_t)requester.lo);
entry->func(target_vm);
target_vm->current_executing_entry = saved_entry;
if (target_vm->error || target_vm->dsp < 0) {
target_vm->error = 0; /* a broken policy word must not leak into
* whatever else target_vm is doing */
return 0;
}
result = vm_pop(target_vm);
return result != 0;
}
#endif /* __STARKERNEL__ */