Skip to content

feat(oci): convert the whole cascade to no_std — WASI-free, no-grow (OCI-P02) - #339

Merged
avrabe merged 4 commits into
mainfrom
feat/oci-p02-nostd-cascade
Aug 7, 2026
Merged

feat(oci): convert the whole cascade to no_std — WASI-free, no-grow (OCI-P02)#339
avrabe merged 4 commits into
mainfrom
feat/oci-p02-nostd-cascade

Conversation

@avrabe

@avrabe avrabe commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

All eight 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

component before after
flight 106,834 34,988
iekf 60,894 11,675
position 57,071 7,525
attitude 53,570 4,086
mixer 16,103 2,964
rate 3,908 4,026

(+ ekf 4,801 B and cascade 6,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_realloc export (wit-bindgen-rt supplies it only when std is linked), and a panic handler — roughly 60 lines of unsafe per component. Factored into crates/falcon-cm-rt with an export_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.

rate was retrofitted onto it: 214 → 95 lines, byte-identical behaviour.

Two things thread_local! was hiding

  1. It is std — the sole reason these components imported WASI at all.
  2. It allows lazy init, while a plain static needs a const one. Iekf::level() isn't 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.

Honest note on Bazel

Six Bazel targets opt into std explicitly: rules_rust can't build the no_std variant yet (no crate_universe dep, no genuine no_std config — it fails on a duplicate panic_impl lang 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 validate PASS.

Stacks on #337. Refs: SWREQ-FALCON-OCI-P02, #336.

🤖 Generated with Claude Code

@avrabe
avrabe enabled auto-merge (squash) August 7, 2026 01:29
@avrabe
avrabe disabled auto-merge August 7, 2026 02:42
@avrabe

avrabe commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Auto-merge temporarily disabled to enforce stack order.

This branch is stacked on #337 — it carries fcee587 (the WIT namespace rename) as well as its own d6602c3. If it merged first, the rename would land squashed under this PR’s title and #337 would become an empty PR, which would misattribute OCI-P04’s implementation in the trace.

Order: #337#339#340. Auto-merge goes back on here once #337 lands and this branch is updated.

🤖 Generated with Claude Code

avrabe and others added 3 commits August 7, 2026 05:48
…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
@avrabe
avrabe force-pushed the feat/oci-p02-nostd-cascade branch from ebe05f7 to fcee1fe Compare August 7, 2026 03:49
@avrabe
avrabe enabled auto-merge (squash) August 7, 2026 03:49
@avrabe

avrabe commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

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 fcee587. Resolved by rebasing and skipping that commit, after confirming main already has everything it carried:

  • wit/falcon-cascade/cascade.witpackage pulseengine:falcon-cascade@0.7.0
  • SWREQ-FALCON-OCI-P04implemented
  • stale falcon:cascade references under wit/0

Post-rebase verification on this tree:

  • no_std components: 8/8
  • export_cm_rt: 8/8
  • rivet validatePASS, exit 0 — run with v0.19.0, the version the gate pins (local rivet is 0.28.0 and disagrees: 352 vs 108 warnings, so a local 0.28 pass is not gate evidence)

🤖 Generated with Claude Code

@avrabe
avrabe merged commit 9ca1075 into main Aug 7, 2026
58 checks passed
@avrabe
avrabe deleted the feat/oci-p02-nostd-cascade branch August 7, 2026 13:15
avrabe added a commit that referenced this pull request Aug 7, 2026
…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>
avrabe added a commit that referenced this pull request Aug 7, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant