feat(oci): convert the whole cascade to no_std — WASI-free, no-grow (OCI-P02) - #339
Conversation
|
Auto-merge temporarily disabled to enforce stack order. This branch is stacked on #337 — it carries Order: #337 → #339 → #340. Auto-merge goes back on here once #337 lands and this branch is updated. 🤖 Generated with Claude Code |
…OCI-P02, v1.133)
All eight Component-Model components now build `no_std` for
wasm32-unknown-unknown: ZERO `wasi:*` imports and ZERO `memory.grow` each.
`memory.grow` is what makes `meld fuse --memory shared --address-rebase` reject
a component (gale#89, meld#299), so this is what lets the fused cascade lower to
a single-address-space MCU image. Previously only `rate` was converted, and the
others' WASI imports were publicly visible on their wasm.directory listings
(falcon-iekf and falcon-attitude carried 14 each).
Measured, per published component:
flight 106,834 -> 34,988 B iekf 60,894 -> 11,675 B
position 57,071 -> 7,525 B attitude 53,570 -> 4,086 B
mixer 16,103 -> 2,964 B rate 3,908 -> 4,026 B
(+ ekf 4,801 B and cascade 6,321 B, bundled but not published)
SHARED RUNTIME rather than six copies. The conversion needs the same three
pieces each time — a bounded work-memory arena, the canonical-ABI
`cabi_realloc` export (wit-bindgen-rt supplies it only when std is linked), and
a panic handler — which is ~60 lines of `unsafe` per component. Factored into
crates/falcon-cm-rt with an `export_cm_rt!()` macro, because
`#[global_allocator]`/`#[no_mangle]`/`#[panic_handler]` must be defined in the
final artifact while the logic can live in one audited place. `rate` was
retrofitted onto it: 214 -> 95 lines, byte-identical behaviour (4,026 B,
0 grow, 0 wasi, closed-loop proof unchanged at 0.193 s / |err| 0.0059 rad/s).
Two things `thread_local!` was hiding, now explicit:
- it is `std`, and was the sole reason these components imported WASI at all;
- it allows LAZY initialisation, while a plain `static` needs a const one.
`Iekf::level()` is not const (it builds a NavState), so iekf now
lazily initialises through an `Option`. Making the constructor const would
have meant editing relay-iekf — a Kani-verified flight crate — for the sake
of a static; not worth it.
Bazel targets opt into `std` EXPLICITLY (6 of them): rules_rust cannot build the
no_std variant yet — it lacks the crate_universe dep and a genuine no_std
config, and would otherwise fail on a duplicate `panic_impl` lang item. The two
build paths therefore still differ, as documented; converging them stays the
open follow-up.
Verified per component: header 0061736d0d000100, `wasm-tools`-measured 0 wasi
imports and 0 memory.grow, and the through-wasm closed-loop proof unchanged.
rivet validate PASS.
Stacks on #337 (the pulseengine:falcon-* rename).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
Per the two-commit rule an implementation PR sets at most `implemented`; the promotion to `verified` is a separate code-free verify PR after the release exercises it. Precedent: OCI-P01 landed at `implemented` in 4d454f2 and was promoted in bb5b810. Caught before tagging: all three v1.133 artifacts were still `proposed` on their own implementation branches, which would have put three unimplemented- looking artifacts in the release scope at tag time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
The push that would have triggered CI landed during the outage (Actions was in major_outage 15:22Z-00:0xZ; zero runs were created repo-wide for hours), so no run exists for this branch. Empty commit to generate a fresh synchronize event. Squash-merge drops it from main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
ebe05f7 to
fcee1fe
Compare
|
Rebased onto main after #337 landed, and auto-merge re-enabled — stack order held (#337 → #339 → #340). The update hit the expected squash-merge stacked-PR conflict: main now carries the WIT rename as a squashed commit, while this branch still carried the original
Post-rebase verification on this tree:
🤖 Generated with Claude Code |
…erifiers (#346) All three v1.133 requirements sat at `implemented` with NO verification artifact pointing at them. The release-execution traceability gate blocks a tag in that state, and it should: `implemented` with no verifier is a claim with no evidence. This was my own gap. I bumped the three to `implemented` citing OCI-P01 as precedent — but OCI-P01 landed at `implemented` WITH a verifier (FV-FALCON-OCI-001) attached. I replicated the status and not the evidence. Adds one verifier each, with CI-runnable assertions rather than prose: FV-FALCON-REL-001 -> SWREQ-FALCON-REL-P01 both `gh release create` paths pass --draft; one terminal publish step; the malformed-tag guard exists. FV-FALCON-OCI-002 -> SWREQ-FALCON-OCI-P02 8/8 components are no_std AND export the shared bounded-arena runtime. Asserted as EIGHT, not "at least one": v1.129 shipped a partially converted cascade, and one std stage re-introduces WASI and memory.grow into the fused image. FV-FALCON-OCI-003 -> SWREQ-FALCON-OCI-P04 the package declares pulseengine:falcon-cascade AND there are ZERO remaining `falcon:cascade` references. The negative assertion carries the weight — a PARTIAL rename is worse than none. These are real verifiers, not decoration — each was observed to FAIL before its implementation merged and PASS after: OCI-003 PASS once #337 landed OCI-002 1/8 -> 8/8 the moment #339 landed REL-001 0/2 -> 2/2 the moment #340 landed A step that cannot fail is not evidence; these can, and did. Also caught by running the gate rather than trusting the draft: an `gh release view falcon-v1.133.0 ... # bench-only` step really EXECUTED and failed rc=1. The gate detects bench-only by COMMAND SHAPE (BENCH_PATTERNS) — rivet strips shell comments at the YAML->JSON boundary, so the marker is inert. Removed, with the reasoning recorded in the artifact: a step whose result depends on WHEN it runs is not evidence. All three stay `implemented`, not `verified`. The binding evidence — published blobs, and an actual draft->published transition — only exists once falcon-v1.133.0 has run. Promotion is the post-tag verify PR, per the two-commit rule. Verified locally: gate scoped to these artifacts: 7/7 steps PASS, exit 0, 5 bench-only skipped rivet validate (v0.19.0 — the version the GATE pins, not local 0.28): PASS Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
On 2026-08-06/07 the self-hosted fleet degraded three times: one wedged runner, then 4 of 7 offline, then 6 of 7. Each time CI simply stopped progressing and looked IDENTICAL to a slow build — no error, no alert, no failed check. One incident burned ~3 hours with a PR sitting at 5/57 checks. Every diagnosis was a human hand-querying the runners API. Nothing here was broken in a way any existing check could see, because nothing was broken: jobs queue forever against a label with no live runner. TWO SIGNALS, IN THIS ORDER: 1. STARVATION (always on, no secret required). Any run queued past 30 minutes. Repo scope only, and it watches the SYMPTOM — so it fires for causes we have not thought of, not just the ones we have. This is the load-bearing check. Validated against today's incident: PR #339 was queued 08:46->11:38 = 172 minutes. This would have alerted at the 30-minute mark, ~2.5 hours before a human noticed. 2. FLEET INVENTORY (optional). Per-runner online/offline/busy, plus WEDGE detection: `offline AND busy`. A runner that drops while HOLDING a job keeps that job assigned forever and no other runner can take it — a naive online-count misses this entirely, which is why a required Format check once "ran" for 55 minutes and then cancelled. Needs /orgs/{org}/actions/runners, which requires admin:org — the default GITHUB_TOKEN CANNOT read it. Set ORG_RUNNER_READ_TOKEN to enable. Without the secret the job still runs and reports starvation only: it degrades, it does not fail. PLACEMENT RULE, NON-NEGOTIABLE: runs on `light`, never `rust-cpu`. A monitor scheduled on the fleet it monitors goes down with that fleet — which is exactly how the outage stayed invisible. Output: a status table in the job summary, and ONE ROLLING ISSUE on alert (comment, not a new issue per tick). Security: no `${{ }}` is expanded inside any `run:` block; every input is bound through `env:` and dereferenced as a quoted shell variable. Verified with a parser over the file, not by eye. Tested locally against the live API: found the queued runs, parsed timestamps (GNU and BSD `date` both), computed ages, and correctly did NOT alert at 4 min. Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
All eight components now build
no_stdforwasm32-unknown-unknown: zerowasi:*imports and zeromemory.groweach.memory.growis what makesmeld fuse --memory shared --address-rebasereject a component (gale#89, meld#299), so this is what lets the fused cascade lower to a single-address-space MCU image.Previously only
ratewas converted — and the others' WASI imports were publicly visible on their wasm.directory listings (falcon-iekfandfalcon-attitudecarried 14 each).Measured
(+
ekf4,801 B andcascade6,321 B, bundled but not published.)Shared runtime, not six copies
Each conversion needs the same three pieces — bounded work-memory arena, the canonical-ABI
cabi_reallocexport (wit-bindgen-rt supplies it only when std is linked), and a panic handler — roughly 60 lines ofunsafeper component. Factored intocrates/falcon-cm-rtwith anexport_cm_rt!()macro, since#[global_allocator]/#[no_mangle]/#[panic_handler]must be defined in the final artifact while the logic can live in one audited place.ratewas retrofitted onto it: 214 → 95 lines, byte-identical behaviour.Two things
thread_local!was hidingstd— the sole reason these components imported WASI at all.staticneeds a const one.Iekf::level()isn't const (it builds aNavState), soiekfnow lazily initialises through anOption. Making the constructor const would have meant editing relay-iekf, a Kani-verified flight crate, for the sake of a static — not worth it.Honest note on Bazel
Six Bazel targets opt into
stdexplicitly: rules_rust can't build the no_std variant yet (no crate_universe dep, no genuine no_std config — it fails on a duplicatepanic_impllang item). The two build paths still differ, as documented; converging them remains the open follow-up.Verified per component
Header
0061736d0d000100,wasm-tools-measured 0 wasi / 0 memory.grow, and the through-wasm closed-loop proof unchanged — 0.193 s, |err| 0.0059 rad/s.rivet validatePASS.Stacks on #337. Refs:
SWREQ-FALCON-OCI-P02, #336.🤖 Generated with Claude Code