Payload bound and chunking -- FABRIC-3.6.md task 3.5

sk_hermes_publish() now enforces SK_HERMES_CHUNK_MAX_PAYLOAD (1024) on
every message's payload_len uniformly, chunked or not -- closing the
gap task 3.3 explicitly parked. A chunk carrier is
[SkHermesChunkHeader][content slice], slice capped at
1024 - sizeof(header) rather than 1024 itself, so every message on the
wire satisfies the same one-block bound vm_interpret()'s own drain
limit already requires -- a future chunk-aware drain never has to
special-case a carrier that can't be handed to vm_interpret() as-is.

Deliberately no chunking-sender API: building one would need
kernel-Hermes to own chunk-buffer memory with a real lifetime it has no
way to track (kept alive until every subscriber drains it). Sending is
a loop pattern a caller writes with sk_hermes_chunk_count() +
sk_hermes_publish(), demonstrated by this task's own self-test.
sk_hermes_reassemble() is pure and memory-agnostic: validates msg_id
agreement, exact seq coverage, and per-chunk slice sizes before a
single memcpy, with the total length computed once and checked against
the caller's buffer once -- never order-dependent on which chunk
happens to overflow.

Verified live on all three architectures: a 1024-byte payload as one
message, a 1025-byte send refused outright with the ledger untouched,
and a 3000-byte payload split into 3 chunks, drained, and reassembled
byte-exact against the original. dict_hash unmoved and identical
across architectures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-22 06:51:48 -04:00
co-authored by Claude Sonnet 5
parent 2b1ba031a5
commit 8a2ee0fdad
7 changed files with 28126 additions and 6 deletions
+33 -1
View File
@@ -902,10 +902,42 @@ ruling]** cannot be written precisely until Captain Bob settles the named sub-it
real disk-backed block storage, which a throwaway diagnostic has no business perturbing.
`dict_hash` for Hermes/Hestia unmoved and identical across all three architectures (this
task touches no capsule).
- [ ] **3.5** — **Payload bound and chunking** (B4) **[needs ruling 3.0d]**. Max 1024 bytes per
- [x] **3.5** — **Payload bound and chunking** (B4) **[needs ruling 3.0d]**. Max 1024 bytes per
message; larger payloads sent as ordered chunks, reassembled before drain. *Check:*
1024-byte payload single message; 3000-byte payload chunked and reassembled byte-exact;
1025-byte single-message send refused.
2026-09-22 · `logs/20260922-063122/amd64/`, `logs/20260922-063629/aarch64/`,
`logs/20260922-064147/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
`mkcapsule --lint capsules/` clean (38 files, 0 violations, unchanged -- no capsule
touched). Sizing decision (checked with `advisor()`, not itself a separate ruling -- see
the header's own note): `SK_HERMES_CHUNK_MAX_PAYLOAD` (1024) bounds every message's
`payload_len` **uniformly**, chunked or not -- a chunk carrier is
`[SkHermesChunkHeader][content slice]` where the slice is capped at
`1024 - sizeof(header)`, not 1024 itself. Rejected the literal alternative (slice up to
1024, carrier up to `1024+sizeof(header)`): under that reading a chunk carrier could never
be handed to `vm_interpret()` as-is, making a future chunk-aware drain a special case
instead of the same one-block invariant every other message already satisfies.
`sk_hermes_publish()` (task 3.3) now enforces this bound directly, closing the gap that
task's own doc comment explicitly parked ("3.3 does not enforce a payload size limit").
**Deliberately no chunking-SENDER API** (`sk_hermes_publish_chunked()` or similar) --
`advisor()` flagged that building one raises a real memory-lifetime question kernel-Hermes
has never answered (chunk buffers would need to stay alive until every subscriber drains
them, and nothing here can know when that is); sending is a loop pattern a caller writes
with `sk_hermes_chunk_count()` + `sk_hermes_publish()`, demonstrated by this task's own
self-test rather than hidden behind a new allocator. `sk_hermes_reassemble()` is pure and
memory-agnostic (caller-owned input chunks, caller-owned output buffer) -- validates
`msg_id` agreement, exact `seq` coverage `{0..n_chunks-1}` with `is_last` on exactly the
last, and every non-final slice at exactly `SK_HERMES_CHUNK_MAX_SLICE` bytes, before a
single `memcpy`; the reassembled total length is computed once from validated seq
completeness and checked against `out_buf_cap` once, so a too-small output buffer is
refused cleanly with zero partial copy rather than order-dependently on whichever chunk
happens to overflow. Self-test (kernel_main.c) proves exactly the task's three checks: a
1024-byte payload as one message; a 1025-byte single-message send refused outright, no
allocation, ledger untouched; a 3000-byte payload split into 3 chunks, drained off a
synthetic subscriber's own pending queue (task 3.3 primitives), reassembled, and confirmed
byte-exact against the original. Ledger audit and `stadium_conserved()` hold before and
after. `dict_hash` for Hermes/Hestia unmoved and identical across all three architectures
(this task touches no capsule).
- [ ] **3.6** — **ACK/NACK and private-channel negotiation** (B1) **[needs ruling 3.0a]**.
`request` → `grant`/`deny` on the common channel; a grant creates a private topic;
close tears it down. ACK/NACK are message types on the existing allocator.