Corpus semantic embedding + prove-local weight-hash cache (+ S5 on real KVM, verified) #34

Merged
dcharlot merged 2 commits from corpus-embedder-s5 into main 2026-07-20 19:51:33 -04:00
Owner

Three follow-ons from PR #31. Two are code (in this diff); the third is an operational verification on real hardware (no code change).

1. jouleclaw-corpus: semantic embedding path

The corpus encoded receipt structure — intent class, retrieval depth, counter honesty. Useful, but blind to meaning: two differently-worded questions of the same shape embedded identically. This adds the semantic path, and the obstacle is why it is not a one-liner: a receipt records a BLAKE3 of its input, never the input. An embedder that captures what a query means cannot read it off the receipt — it needs the text, which only the deployment that logged both the receipts and the queries can supply. So the type system now says so:

  • TextEmbedder — the semantic counterpart to the structural Embedder.
  • CorpusBuilder::build_with_text(&[(Receipt, String)]) — takes the (receipt, query) join explicitly. The join is the caller's and is not silently verifiable (the receipt hash is over a normalisation this crate does not know).
  • Rows are [semantic ‖ structural] by default (meaning and shape are complementary signals); with_append_structural(false) gives a pure-semantic vector. Width is constant across the corpus either way.

The real embedder is behind the candle feature so the default build stays light and offline: CandleTextEmbedder wraps the fleet's own on-device BERT/BGE embedder (jouleclaw-embed-candle) — the same L2-tier backend the cascade already uses, now feeding the corpus that trains the router above it.

Verified end to end with a real model, not a mock: MiniLM (384-dim) over five real queries → a 399-dim composite corpus (384 semantic + 15 structural), one L1Lawful receipt skipped, and train_logreg + train_mf both fit a router on the result. 19 tests (5 new, driven by a deterministic mock so the logic stays offline-testable); the real model runs in emit_corpus_semantic behind the feature.

2. sandbox-proof: weight-hash cache

prove-local streams the full weight file into a BLAKE3 to bind the proof to the exact bytes — a ~13 GiB read on every run. This adds an opt-in cache keyed on (size, mtime).

The honesty cost is stated plainly, because this is a proof input. A cache that skips the re-hash decides the bytes are unchanged without reading them, exactly as build systems do — a freshness heuristic, not a proof. So the library default is no cache (full stream, strong guarantee); the cache is something the operator opts into; it is never a correctness dependency (a corrupt cache degrades to a full hash); and the prover prints [hashed] or [cache hit: trusts size+mtime] on every run so a reader always knows which guarantee held. The CLI turns it on by default at a temp path so the every-run pain is solved out of the box; SANDBOX_WEIGHTS_HASH_CACHE=off forces the full stream.

Verified on the real Devstral-Small-2-24B Q4_K_M (13.35 GiB): run 1 cold = 15.72s wall [hashed]; run 2 warm = 6.58s wall [cache hit] — the eliminated ~9s is exactly the 13 GiB BLAKE3 stream, and both runs bind to the identical hash b3:60fc77b9fcc1…, so the proof is unchanged. 4 tests.

3. S5 microVM proof on real KVM — verified, no code change

Not in this diff — this ran unmodified origin/main on real hardware to close out the third follow-on, and the finding matters.

A real Firecracker v1.16.1 microVM booted on real KVM on jetson-hub (i9-13900H, Ubuntu 24.04): Hypervisor detected: KVM, guest ran /bin/true and exited cleanly. The sealed cell: substrate MicroVm, linux/x86_64, tier S5, conformance 10/10, energy provenance HwShunt (RAPL-measured), 19.54 J (confirmed not the ModelBased fallback constant), receipt b3:s92SN…, signature verified (verify → 1/1 cells).

Honest scope caveat: this proves the S5 substrate on real KVM. It does not prove inference inside the guest — the current S5 path boots /bin/true and runs no workload in the VM. Making local inference execute inside the guest so the S5 tier is earned for the inference itself (custom rootfs + in-guest engine + result channel) is a further, larger lift, deliberately not attempted here rather than faked by stamping S5 on a host subprocess.

Two incidental findings from that run: origin/main pins Rust 1.96.0 (the 1.97.0 pin was uncommitted local work on dev), and the repo still has Actions disabled — so this PR, like #31, carries no CI checks.


🤖 Generated with Claude Code

Three follow-ons from PR #31. Two are code (in this diff); the third is an operational verification on real hardware (no code change). ## 1. `jouleclaw-corpus`: semantic embedding path The corpus encoded receipt *structure* — intent class, retrieval depth, counter honesty. Useful, but blind to meaning: two differently-worded questions of the same shape embedded identically. This adds the semantic path, and the obstacle is why it is not a one-liner: **a receipt records a BLAKE3 of its input, never the input.** An embedder that captures what a query *means* cannot read it off the receipt — it needs the text, which only the deployment that logged both the receipts and the queries can supply. So the type system now says so: - `TextEmbedder` — the semantic counterpart to the structural `Embedder`. - `CorpusBuilder::build_with_text(&[(Receipt, String)])` — takes the `(receipt, query)` join explicitly. The join is the caller's and is not silently verifiable (the receipt hash is over a normalisation this crate does not know). - Rows are `[semantic ‖ structural]` by default (meaning *and* shape are complementary signals); `with_append_structural(false)` gives a pure-semantic vector. Width is constant across the corpus either way. The real embedder is behind the `candle` feature so the default build stays light and offline: `CandleTextEmbedder` wraps the fleet's own on-device BERT/BGE embedder (`jouleclaw-embed-candle`) — the same L2-tier backend the cascade already uses, now feeding the corpus that trains the router above it. **Verified end to end with a real model, not a mock:** MiniLM (384-dim) over five real queries → a 399-dim composite corpus (384 semantic + 15 structural), one L1Lawful receipt skipped, and `train_logreg` + `train_mf` both fit a router on the result. 19 tests (5 new, driven by a deterministic mock so the logic stays offline-testable); the real model runs in `emit_corpus_semantic` behind the feature. ## 2. `sandbox-proof`: weight-hash cache `prove-local` streams the full weight file into a BLAKE3 to bind the proof to the exact bytes — a ~13 GiB read on **every** run. This adds an opt-in cache keyed on `(size, mtime)`. The honesty cost is stated plainly, because this is a proof input. A cache that skips the re-hash decides the bytes are unchanged *without reading them*, exactly as build systems do — a freshness heuristic, not a proof. So the **library default is no cache** (full stream, strong guarantee); the cache is something the operator opts into; it is never a correctness dependency (a corrupt cache degrades to a full hash); and the prover prints `[hashed]` or `[cache hit: trusts size+mtime]` on every run so a reader always knows which guarantee held. The CLI turns it on by default at a temp path so the every-run pain is solved out of the box; `SANDBOX_WEIGHTS_HASH_CACHE=off` forces the full stream. **Verified on the real Devstral-Small-2-24B Q4_K_M (13.35 GiB):** run 1 cold = 15.72s wall `[hashed]`; run 2 warm = 6.58s wall `[cache hit]` — the eliminated ~9s is exactly the 13 GiB BLAKE3 stream, and both runs bind to the identical hash `b3:60fc77b9fcc1…`, so the proof is unchanged. 4 tests. ## 3. S5 microVM proof on real KVM — verified, no code change **Not in this diff** — this ran unmodified `origin/main` on real hardware to close out the third follow-on, and the finding matters. A real Firecracker v1.16.1 microVM booted on real KVM on **jetson-hub** (i9-13900H, Ubuntu 24.04): `Hypervisor detected: KVM`, guest ran `/bin/true` and exited cleanly. The sealed cell: substrate `MicroVm`, `linux/x86_64`, **tier S5**, conformance 10/10, **energy provenance `HwShunt` (RAPL-measured), 19.54 J** (confirmed not the ModelBased fallback constant), receipt `b3:s92SN…`, signature verified (`verify` → 1/1 cells). **Honest scope caveat:** this proves the **S5 substrate** on real KVM. It does **not** prove inference *inside* the guest — the current S5 path boots `/bin/true` and runs no workload in the VM. Making local *inference* execute inside the guest so the S5 tier is earned *for the inference itself* (custom rootfs + in-guest engine + result channel) is a further, larger lift, deliberately not attempted here rather than faked by stamping S5 on a host subprocess. Two incidental findings from that run: `origin/main` pins **Rust 1.96.0** (the 1.97.0 pin was uncommitted local work on dev), and the repo still has **Actions disabled** — so this PR, like #31, carries no CI checks. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The corpus so far encoded receipt *structure* — intent class, retrieval
depth, counter honesty. Useful, but blind to meaning: two differently
worded questions of the same shape embedded identically. This adds the
semantic path.

The obstacle is real and is why this is not a one-liner: a receipt
records a BLAKE3 of its input, never the input. An embedder that
captures what a query *means* cannot read it off the receipt — it needs
the text, which only the deployment that logged both the receipts and
the queries can supply. So the type system now says so:

  - `TextEmbedder` — embeds query text, the semantic counterpart to the
    structural `Embedder`.
  - `CorpusBuilder::build_with_text(&[(Receipt, String)])` — takes that
    (receipt, query) join explicitly. The join is the caller's; it is
    not silently verifiable here (the receipt hash is over a
    normalisation this crate does not know).
  - rows are `[semantic ‖ structural]` by default — meaning and shape
    are complementary signals, so a row carries both; the
    `with_append_structural(false)` toggle gives a pure-semantic vector.
    Either way the width is constant across the corpus, as the trainer
    requires.

The real embedder is behind the `candle` feature so the default build
stays light and offline: `CandleTextEmbedder` wraps the fleet's own
on-device BERT/BGE embedder (jouleclaw-embed-candle) — the same L2-tier
backend the cascade already uses, now feeding the corpus that trains the
router above it. The skip/negative semantics are unchanged: L1Lawful
still has no EOC analogue and is counted, not guessed.

Verified end to end with a real model, not a mock: MiniLM (384-dim) over
five real queries → a 399-dim composite corpus (384 semantic + 15
structural), one L1Lawful receipt skipped, and `train_logreg` +
`train_mf` both fit a router on the result. 19 tests (5 new for the text
path, driven by a deterministic mock so the logic stays offline-testable);
the real model runs in the `emit_corpus_semantic` example behind the
`candle` feature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
prove-local streams the full weight file into a BLAKE3 to bind the proof
to the exact bytes. Correct, but on a 13 GiB model that is a ~13 GiB read
on every single run — pure waste when the file has not changed.

Add an opt-in cache keyed on the file's (size, mtime) fingerprint. A hit
returns the stored hash and skips the stream; any miss (absent entry,
changed size, changed mtime) re-hashes and updates the cache.

The honesty cost is stated plainly, because this is a proof input. The
cell binds to the BLAKE3 of the bytes; a cache that skips the re-hash
decides the bytes are unchanged *without reading them*, exactly as build
systems do — an unchanged (size, mtime) is taken as evidence the contents
are unchanged. That is a freshness heuristic, not a proof. So:

  - The library default is `hash_cache: None` — full stream every run,
    preserving the strong bytes-bound guarantee. Caching is something the
    operator opts into knowingly.
  - The cache is never a correctness dependency: an unreadable or corrupt
    cache file degrades to a full hash, it never fails the proof.
  - The prover is loud about which applied: it prints `[hashed]` or
    `[cache hit: trusts size+mtime]` on every run, so a reader always
    knows which guarantee held.

The CLI turns the cache on by default at a temp path (the ergonomics
live here, not in the library) so the every-run pain is solved out of the
box; `SANDBOX_WEIGHTS_HASH_CACHE=off` forces the full stream.

Verified on the real Devstral-Small-2-24B Q4_K_M (13.35 GiB): run 1 cold
= 15.72s wall, printed `[hashed]`; run 2 warm = 6.58s wall, printed
`[cache hit]` — the eliminated ~9s is exactly the 13 GiB BLAKE3 stream,
and both runs bind to the identical hash b3:60fc77b9fcc1…, so the proof
is unchanged. 4 tests: hit-matches-full-hash, miss-on-change,
no-cache-always-streams, corrupt-cache-degrades. Test temp dirs are
process+counter unique so they never collide under parallelism.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Transaction-Science/open-standards!34
No description provided.