Skip to content

refactor(core,agent)!: make the WASM support matrix explicit and true - #2213

Merged
gold-silver-copper merged 18 commits into
mainfrom
wasm-support-matrix
Jul 28, 2026
Merged

refactor(core,agent)!: make the WASM support matrix explicit and true#2213
gold-silver-copper merged 18 commits into
mainfrom
wasm-support-matrix

Conversation

@gold-silver-copper

@gold-silver-copper gold-silver-copper commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

TL;DR: the WASM support matrix is now explicit and true. Browser wasm
(wasm32-unknown-unknown) is the supported wasm target, WASI is documented as
unsupported with the reason, and rmcp is native-only with a one-sentence
diagnostic instead of a wall of trait errors. Every wasm feature flag in the
workspace is gone
cargo build --target wasm32-unknown-unknown is now the
entire opt-in. Breaking only in feature surface; no behaviour changes anywhere.

Closes the last open finding from the #2197 architectural review
("Browser-WASM / WASI / rmcp matrix"); #2205, #2206, #2207, and #2208 closed
the others.

Why

The review filed one item; it is really three problems with different owners.

1. rig-agent's cfg predicates were blunt

rig-core already distinguishes precisely — target_family = "wasm" for "any
wasm", all(target_arch = "wasm32", target_os = "unknown") for things needing a
JS host — and its manifest explains why. rig-agent used a bare
cfg(target_arch = "wasm32"), so WASI builds pulled wasm-bindgen-futures
and rig-core/wasm: a browser shim on a non-JS host. The Send-relaxed stream
aliases had the same bare predicate and so disagreed with rig-core's
WasmCompat* markers on any non-browser wasm target.

2. rmcp cannot work on wasm — and not for a fixable reason

rmcp has its own maybe-Send abstraction, so enabling its local feature looks
like the fix. It is not. local relaxes the future bound
(MaybeSendFuture); the handler bound is unconditional:

// rmcp-2.2.0/src/handler/client.rs:89
pub trait ClientHandler: Sized + Send + Sync + 'static {

rig's handler owns the tool registry, whose Arc<dyn ErasedTool> is
deliberately neither Send nor Sync on wasm (WasmCompatSend/
WasmCompatSync are no-op markers there). Two maybe-Send abstractions that do
not compose, with the mismatch upstream. I verified this by enabling local:
the on_tool_list_changed error disappears and the ClientHandler bound
immediately replaces it.

3. WASI never built at all

Not "untested" — does not compile, with or without default features:

$ cargo check -p rig-core --target wasm32-wasip1
error: Only features sync,macros,io-util,rt,time are supported on wasm.
error[E0583]: file not found for module `sys`
error: Socket2 doesn't support the compile target

These are dependency-level: rig-core depends on reqwest unconditionally
(the reqwest feature only adds features, it does not gate dep:), which
pulls hyper/socket2 and a tokio feature set WASI rejects. Supporting WASI
means making reqwest optional and supplying a wasi:http client behind
rig_core::http_client — a deliberate project, out of scope here. So there was
no WASI support to drop; there was only an inaccurate implication of one.

What changed

  • Precise gating. Browser-only deps move to
    cfg(all(target_arch = "wasm32", target_os = "unknown")), matching rig-core.
    The Send-relaxed stream aliases use the same predicate, with a comment tying
    them to rig-core's WasmCompat* gate.
  • rmcp native-only. The dependency is scoped to
    cfg(not(target_family = "wasm")), every rmcp cfg site carries the native
    predicate, and a targeted compile_error! explains the situation. Requesting
    it on wasm now fails with exactly one error.
  • wasm-bindgen-futures is gone from rig-agent — its only user was the MCP
    cancellation dispatch, now unconditionally tokio::spawn.
  • CI asserts the diagnostic fires and is the only error. The count matters:
    a new ungated #[cfg(feature = "rmcp")] recompiles the rmcp surface into wasm
    and leaks the trait wall behind our message. Verified — reverting one gate
    takes the count from 1 to 14.
  • Every wasm feature flag deleted (breaking). rig-core's wasm,
    rig-agent's, and the facade's. The feature carried no information the target
    did not already have: its only dependency payload
    (wasm-bindgen-futures) was never referenced in rig-core, making it a pure
    cfg switch, and both rig-agent and rig-candle already flipped it from a
    [target.'cfg(...)'] table. WasmCompat* now keys on
    all(target_arch = "wasm32", target_os = "unknown") directly. Dependents
    passing features = ["wasm"] drop it; nothing replaces it.
  • Latent cfg bug fixed in rig-core's SSE aliases. The arms did not
    partition: not(target_arch = "wasm32") opposite a browser-scoped arm left
    WASI matching neither, so ResponseFuture/EventStream were undefined
    there. Pre-existing and invisible because WASI never built. Both arms now
    share one predicate.
  • Docs: a target-support matrix in crates/rig-agent/README.md and the crate
    docs; the root README's "Full WASM compatibility" claim corrected.

Subtlety worth review attention

tool/server.rs had two complementary #[cfg(feature = "rmcp")] /
#[cfg(not(feature = "rmcp"))] pairs. Narrowing only the positive arms left
wasm+rmcp binding neither, producing cannot find value errors. Both arms
now carry matching predicates. This is exactly the failure the CI error-count
assertion is designed to catch.

Compatibility

One breaking change, and it is a feature-surface break with no behaviour behind
it: any dependent listing features = ["wasm"] on rig, rig-core, or
rig-agent must drop it.
Cargo rejects an unknown feature outright, so this
fails loudly at resolution rather than silently changing behaviour. Nothing
replaces it — build for wasm32-unknown-unknown and the bounds relax.

Relaxing those bounds cannot break implementors: the relaxed markers are
blanket-implemented (impl<T> WasmCompatSend for T {}), so every type that
satisfied the strict form satisfies the relaxed one. The one exception is a
generic consumer on browser wasm that wrote T: WasmCompatSend and then
relied on T: Send internally — and only if it was previously building with the
feature off, which neither rig-agent nor rig-candle did.

Otherwise nothing that worked stops working. rmcp never compiled for any wasm
target; WASI never built. What changes is the failure mode (one sentence instead
of ~34 errors), the honesty of the gates, and one fewer dependency in the wasm
graph.

Also fixed: candle_wasm_chat never built standalone

Pre-existing on main, fixed here because it is the only consumer exercising the
browser-wasm facade path this PR documents.

The example imports rig::agent::{Agent, AgentBuilder} and
rig::completion::Chat while declaring default-features = false without
agent:

$ cargo check -p candle_wasm_chat --target wasm32-unknown-unknown
error[E0432]: unresolved imports `rig::agent`, `rig::completion::Chat`

CI missed it because cargo unifies features across packages built in one
invocation
. The only job that compiles workspace members is the doctest job's
cargo test --doc --workspace --all-features, which enables agent on the
shared rig build and supplies what this manifest forgot:

Invocation Errors
cargo check -p candle_wasm_chat (its own features) 4
cargo check -p candle_wasm_chat -p rig --all-features (unified) 0

Both halves are fixed: the manifest declares agent, and candle_wasm_chat
joins the wasm matrix so every package is checked on its own. Matrix entries
are separate parallel jobs and this is the cheapest of the three —
default-features = false trims its tree to 544 crates against rig's 3089.
Verified the lane bites: reverting the manifest fix takes it from 0 errors to 4.

Found in review

From a review pass over the finished branch.

The feature deletion broke a test fixture. tests/fixtures/tool_facade/
declares an all_root feature enumerating every facade feature, and it still
named rig/wasm. tests/tool_facade_features.rs builds that fixture under
--all-features, so it failed before compiling anything:

package `rig-tool-facade-fixture` depends on `rig` with feature `wasm`
but `rig` does not have that feature.

That leg runs in CI (workspace cargo nextest run --all-features turns on
facade-build-tests), so the branch was red from the feature-deletion commit
until this fix. Some irony: the lane this PR adds to catch an under-declared
manifest was broken by an over-declared one, and for the same underlying
reason — a feature list nothing checks against reality.

Stale prose. prompt_request/streaming.rs still told the reader to keep its
predicate "in step with that target table" and named rig-core/wasm — both
deleted two commits later. The crate docs had two sentences merged into one
carrying three em-dash clauses. The CHANGELOG grew a second ### Changed
heading inside [Unreleased]. And "relaxing the bounds cannot break user code"
overstated the case; it is now the accurate version quoted under Compatibility.

Drift guard: added, then removed

Earlier commits on this branch added a grep check asserting that every
target_arch = "wasm32" names a target_os. It has been removed — it cost
more than it caught, in three ways:

  • It rejected correctly written cfg_attr predicates. \bcfg!?\( does not
    match cfg_attr(, so a correct
    cfg_attr(all(target_arch = "wasm32", target_os = "unknown"), ...) fell
    through to the outside-any-group fallback and failed — with an error advising
    the exact spelling already on the line. There was no way to write a passing
    wasm cfg_attr.
  • It flagged /* */ prose. Only // was stripped, so the "comments are exempt"
    property held for one of Rust's two comment syntaxes.
  • It flagged commented-out TOML tables starting with #[.

The commits are left in history rather than squashed away, so the reasoning is
still readable; the final tree has no such step.

What it was standing in for is covered by jobs that compile rather than
pattern-match: check-wasm and check-wasm-runtimes build rig-core,
rig-agent, rig, and candle_wasm_chat for wasm32-unknown-unknown
individually, and check-rmcp-native-only asserts the rmcp diagnostic is the
only error. The residual gap is a bare target_arch = "wasm32" that builds fine
on browser wasm but misbehaves on WASI — a target that does not build and is
documented unsupported, so nothing exercised it either way. The convention stays
documented in crates/rig-agent/README.md.

Verification

  • cargo fmt --all --check; cargo clippy -p rig-core -p rig-agent --all-features --all-targets -- -D warnings — clean
  • cargo test -p rig-agent --all-features --lib — 485 pass; -p rig-core --lib — 932 pass
  • doctests + cargo doc --no-deps -p rig-agent --all-features — clean
  • Builds: rig-agent native (default / rmcp / derive,test-utils,audio,image),
    rig-agent + rig on wasm32-unknown-unknown, rig + rmcp native — all pass
  • cargo check -p rig-agent --features rmcp --target wasm32-unknown-unknown
    fails with exactly one error, the native-only diagnostic
  • cargo check -p rig --no-default-features --features rmcp --target wasm32-unknown-unknown
    also exactly one error, so the facade path behaves the same as rig-agent
    direct (CI asserts the count only for rig-agent)
  • cargo check -p rig-core --target wasm32-wasip1 — still fails at the
    dependency level, unchanged in kind by this PR
  • cargo check --package {rig-agent,rig,candle_wasm_chat} --target wasm32-unknown-unknown — all clean (the new matrix, run verbatim)
  • cargo metadata --manifest-path tests/fixtures/tool_facade/Cargo.toml --all-features — resolves (this is what the fixture fix restores)
  • Each commit compiles standalone

…ve-only

`rig-core` already distinguishes browser wasm precisely — `target_family =
"wasm"` for "any wasm", `all(target_arch = "wasm32", target_os = "unknown")` for
things that need a JS host — and documents why. `rig-agent` discarded that
precision: a bare `cfg(target_arch = "wasm32")` pulled `wasm-bindgen-futures`
and forced `rig-core/wasm` on WASI too, where a browser shim is
wrong-by-construction. The `Send`-relaxed stream aliases had the same bare
predicate, so they disagreed with `rig-core`'s `WasmCompat*` markers on any
non-browser wasm target.

MCP is native-only, and cannot be otherwise from this side. rmcp's
`ClientHandler` is declared `Sized + Send + Sync + 'static` unconditionally; its
`local` feature relaxes the future bounds (`MaybeSendFuture`) but not the
handler. This crate's handler owns the tool registry, whose `Arc<dyn ErasedTool>`
is deliberately neither `Send` nor `Sync` on wasm. Two maybe-`Send` abstractions
that do not compose. Requesting `rmcp` on wasm previously produced a wall of
`dyn ErasedTool` trait errors; it now produces one sentence naming the cause and
the fix.

Every `rmcp` cfg site carries the native predicate, including the two
complementary `not(feature = "rmcp")` arms in `tool/server.rs` — with only the
positive arms narrowed, wasm builds bound neither arm. That is also why the
follow-up CI check asserts the error *count*, not just the message.

`wasm-bindgen-futures` leaves the dependency graph entirely: its only user was
the MCP cancellation dispatch, which is now unconditionally `tokio::spawn`.

Not a breaking change: `rmcp` never compiled for any wasm target and WASI never
built at all, so no working configuration changes behaviour.
The `check-wasm-runtimes` comment claimed the lane guarded "the target-gated MCP
cancellation dispatch", which a bare default-feature check never exercised —
`rmcp` is not a default feature. Drop the false claim and add a job that
actually pins the contract.

The assertion checks three things, because each has its own failure mode: the
build must fail (a success means the gate is gone), the failure must be our
`compile_error!` (not some unrelated breakage), and it must be the *only* error.
The count matters — a newly added `#[cfg(feature = "rmcp")]` without the target
predicate compiles the rmcp surface back into the wasm build and leaks the
trait-error wall behind our message. Verified: with one such gate reverted the
count goes from 1 to 14.
The root README claimed "Full WASM compatibility for the portable core and
classic runtime". Browser wasm is supported; WASI is not, and `rmcp` is
native-only on every target. State the matrix once, with the reasons, so the
next person does not re-derive it from compiler output.

WASI is unsupported because the dependency graph does not build for it, not
because it is untested: `rig-core` depends unconditionally on `reqwest`, which
pulls `hyper`/`socket2` and a tokio feature set WASI rejects. Supporting it means
making `reqwest` optional and supplying a `wasi:http` client behind
`rig_core::http_client` — a deliberate project, not a `cfg` fix. Recording that
distinction is the point of the section.
`rig-agent`'s `wasm` feature did nothing in every configuration that builds:

- browser wasm already enables `rig-core/wasm` through this crate's target
  table, so passing the feature was redundant;
- `rig-core`'s gate is `all(feature = "wasm", target_arch = "wasm32")`, so on
  native the feature was inert;
- WASI is the one target where it would have had an effect, and WASI does not
  build.

Worse than useless, in fact: on WASI it would relax `rig-core`'s `WasmCompat*`
markers while this crate's stream aliases still require `Send`, so the two
halves would disagree. Narrowing the target table to `target_os = "unknown"`
removed the feature's last theoretical purpose, so retire it. Building for
`wasm32-unknown-unknown` is the entire opt-in.

The root `rig` facade keeps its `wasm` feature and now forwards only
`rig-core/wasm`: an agent-less facade build has no target table of its own and
still needs a way to ask for the relaxed bounds. That path is exercised by
`examples/candle_wasm_chat`, which depends on the facade with
`default-features = false, features = ["candle", "wasm"]`.

Breaking only for direct `rig-agent` dependents that list `features = ["wasm"]`;
they should drop it. No behaviour changes anywhere.
@gold-silver-copper gold-silver-copper changed the title refactor(agent): make the WASM support matrix explicit and true refactor(agent)!: make the WASM support matrix explicit and true Jul 27, 2026
…it in CI

`candle_wasm_chat` imports `rig::agent::{Agent, AgentBuilder}` and
`rig::completion::Chat` while declaring `default-features = false` without
`agent`, so it never built standalone:

    cargo check -p candle_wasm_chat --target wasm32-unknown-unknown
    error[E0432]: unresolved imports `rig::agent`, `rig::completion::Chat`

CI missed it because cargo unifies features across packages built in one
invocation. The only job compiling workspace members is the doctest job's
`cargo test --doc --workspace --all-features`, which enables `agent` on the
shared `rig` build and supplies what this manifest forgot. Standalone: 4 errors.
Alongside `-p rig --all-features`: zero. No job ever built the example with only
its own features.

So fix both halves. The manifest declares `agent`, and the wasm matrix gains
`candle_wasm_chat` so each package is checked on its own and an under-declared
manifest fails instead of being masked. The matrix entries are separate parallel
jobs, and this one is the cheapest of the three — `default-features = false`
trims its tree to 544 crates against `rig`'s 3089.

Pre-existing on main; fixed here because this is the only consumer exercising
the browser-wasm facade path the rest of this PR documents.
…asm gates

Two loose ends from review.

`candle_wasm_chat` listed `wasm` alongside `agent`, but with `agent` enabled
`rig-agent`'s browser target table already supplies `rig-core/wasm`, so the
feature did nothing — the same no-op this release removed from `rig-agent`
itself. Drop it and record why, so it does not look like an oversight.

The rest of this PR normalizes every rig-agent wasm gate onto
`all(target_arch = "wasm32", target_os = "unknown")`, and nothing enforced that.
Add a grep guard beside the existing CWD-relative-fixture check: in rig-agent,
any `target_arch = "wasm32"` must name a `target_os`. rig-core is out of scope —
it uses bare `not(target_arch = "wasm32")` for native-only exclusions and
`all(feature = "wasm", target_arch = "wasm32")` for its `WasmCompat` markers,
both correct there.

One trap worth the comment it now carries: the exemption for prose cannot simply
skip lines beginning with `#`. That is a TOML comment *and* the first character
of a Rust `#[cfg(...)]` attribute, so the obvious pattern silently ignores every
gate the guard exists to catch. Caught it by testing the guard against a
reintroduced bare gate in both a `.rs` and a `.toml` file; the first version
passed the `.rs` case while seeing nothing.
Browser wasm now needs no feature flags at all. `cargo build --target
wasm32-unknown-unknown` is the entire opt-in.

`rig-core`'s `wasm` feature turned out to carry no information the target did
not already have. Its only dependency payload, `wasm-bindgen-futures`, was
never referenced anywhere in `rig-core` — it was a pure `cfg` switch. And every
consumer already flipped that switch purely from the target: `rig-agent` and
`rig-candle` each enabled it from a `[target.'cfg(...)']` table, leaving the
facade's `wasm` as a manual escape hatch that, after the previous commit, had no
consumers at all.

So key the `WasmCompat*` markers on `all(target_arch = "wasm32", target_os =
"unknown")` directly and delete the feature from all three crates, along with
the two target tables that existed only to set it and the unused optional
dependency. Relaxing bounds cannot break user code: the relaxed markers are
blanket-implemented (`impl<T> WasmCompatSend for T {}`), so anything satisfying
the strict form satisfies the relaxed one.

Doing this surfaced a latent bug in `rig-core`'s SSE aliases, where the arms did
not partition — `not(target_arch = "wasm32")` opposite a browser-scoped arm left
WASI matching neither, so `ResponseFuture` and `EventStream` were undefined
there. Pre-existing (the old feature-based arm had the same hole) and invisible
because WASI never built, but it is exactly the failure mode the bare-predicate
guard exists to prevent, so both arms now share one predicate. With rig-core
uniformly browser-scoped, that guard extends to cover it.
WASI was mentioned 17 times across the README, CHANGELOG, and CI comments — far
past what the reader needs. It earns exactly two mentions: one row in the
support matrix saying it is unsupported, and one sentence explaining why wasm
gates name a `target_os` at all (a bare `target_arch = "wasm32"` matches WASI,
which has no JS host). Everything else was repetition.

Also folds three overlapping CHANGELOG entries for this change into four
distinct ones, so a reader gets the outcome — no wasm features, rmcp is
native-only, the matrix is documented, one `cfg` fix — instead of the same
rationale three times.

Down to 8 mentions, most of them the unavoidable ones in the matrix table.
`wasm-bindgen-futures` appears nowhere in this crate's source, so it reads as an
unused dependency — I removed it on exactly that reasoning and the build broke.
`#[wasm_bindgen]` on an `async fn` expands to code that references
`wasm_bindgen_futures`, so the reference is macro-generated and invisible to a
grep for the crate name.

Record that where the next person will look, since the surrounding commits
removed this same crate from `rig-core` (genuinely unused) and `rig-agent` (its
only user became native-only), which makes the remaining copy look like an
oversight.
@gold-silver-copper gold-silver-copper changed the title refactor(agent)!: make the WASM support matrix explicit and true refactor(core,agent)!: make the WASM support matrix explicit and true Jul 27, 2026
This PR deletes the facade's `wasm` feature, but the fixture's `all_root`
list still named it, so `tests/tool_facade_features.rs` failed before
compiling anything:

    package `rig-tool-facade-fixture` depends on `rig` with feature `wasm`
    but `rig` does not have that feature.

That test's `--all-features` leg runs in CI — the root package has no
`default-members` exclusion, and the workspace `cargo nextest run
--all-features` lane turns on `facade-build-tests`. So the lane this PR
adds to catch under-declared manifests was itself broken by an
over-declared one.

Nothing to note in the CHANGELOG: the feature this removes was only ever
referenced by a feature introduced and deleted within this same PR.
Two holes in the guard added earlier in this PR.

It matched only the space-normalized `target_arch = "wasm32"`, justified
by the `cargo fmt --check` step above it. That reasoning holds for `.rs`
files and not for manifests: `cargo fmt` does not format `Cargo.toml` and
there is no TOML formatter in CI, so `cfg(target_arch="wasm32")` in a
`[target.'cfg(...)']` table sailed straight through the guard whose whole
purpose is catching that table. Match tolerates whitespace now.

Its path list was also just rig-core and rig-agent, leaving rig-candle,
the other member crates, the facade's own `src`/`Cargo.toml`, and every
`tests/` directory unguarded. None of them names `target_arch` today —
they all use `target_family = "wasm"` — which is exactly when widening
the scope is free.

Tested in four directions: silent on the clean tree, fires on a spaced
bare gate in a `.rs` file, fires on an unspaced bare gate in a
`Cargo.toml` (the case that previously passed), and still exempts prose
in both `//` and `#` comments.
Leftovers from rewriting this PR in place.

`prompt_request/streaming.rs` still told the reader to keep its predicate
"in step with that target table" and named `rig-core/wasm` — both deleted
two commits later. The advice was right, the referent was gone; point it
at the predicate `rig-core` actually keys on.

`rig-agent`'s crate docs had two sentences merged into one carrying three
em-dash clauses and a 110-column line in a file that wraps at 78. Split
and rewrapped.

The CHANGELOG grew a second `### Changed` heading inside `[Unreleased]`
when this PR's entries were prepended; folded back into one.

Also softened "relaxing the bounds cannot break user code". True for
implementors, since the relaxed markers are blanket-implemented, but a
generic *consumer* that wrote `T: WasmCompatSend` and then relied on
`T: Send` internally does break — on browser wasm, and only if it was
building with the feature off. Narrow enough to be a parenthetical,
not narrow enough to state the absolute.
The `rmcp rejected on wasm` job asserts the native-only `compile_error!`
is the *only* error, counting lines that match `^error(\[|:)`. But the
workflow sets `CARGO_TERM_COLOR: always`, so rustc prefixes every
diagnostic with SGR escapes:

    ^[[1m^[[91merror^[[0m^[[1m: the `rmcp` feature is native-only: ...

The anchor never matched, the count came out 0, and the job failed with
"expected exactly 1 error, got 0 — an ungated #[cfg(feature = "rmcp")]
is leaking follow-on errors" while the log directly above it showed
exactly one error and nothing else. The gate was fine; the parser was
reading past a color code.

Parse a decolorized copy instead of turning color off for the step, so
the check does not silently depend on the ambient `CARGO_TERM_COLOR`.
The echoed log keeps its colors.

Verified by running the step verbatim under `bash -e` with
`CARGO_TERM_COLOR=always`: exits 0, "rmcp on wasm fails with exactly one
actionable error". Before this it exited 1.
The guard was a line-scoped grep: find `target_arch = "wasm32"`, drop
lines that also say `target_os`. But `cargo fmt` wraps an attribute wider
than 100 columns across several lines, which splits `target_arch` from
the `target_os` that answers for it:

    #[cfg(all(
        target_arch = "wasm32",
        target_os = "unknown",
        feature = "...",
    ))]

The guard flags line 2 of that and advises adding a `target_os` that is
already on line 3. Worse, the `cargo fmt --check` step above it *demands*
the wrapped form, so once a gate grows past the column limit the two
steps contradict each other and no source text satisfies both. No gate in
the tree is long enough to hit this today; the first one that grows a
third predicate is.

Scope the check to the whole `cfg(...)` group via balanced-paren
recursion, so wrapping is invisible to it. That also makes whitespace
around the `=` structurally irrelevant rather than allowed for by regex,
and brings `cfg!(...)` into scope. Comments are stripped per language
first (`//` in Rust, `#` not followed by `[` in TOML) — keeping the two
syntaxes separate is what preserves the prose exemption without
swallowing every `#[cfg(...)]`, the trap the previous version documented.

Group-anchoring could become its own blind spot, so a `target_arch =
"wasm32"` outside any `cfg`/`cfg!` group — a `cfg_attr` predicate, say —
is reported separately. Only `.rs` and `.toml` are read: the only places
a gate can live, and it keeps the walk off the cassette fixtures under
`tests/`.

Tested against the real tree, six directions: silent on the clean tree
and on a rustfmt-canonical wrapped gate (which the old guard flagged
while `rustfmt --check` called the same file canonical); fires on a bare
spaced gate in a `.rs` file, on a bare unspaced gate in a `Cargo.toml`,
and on a `cfg_attr` predicate; still exempts prose in `//`, `//!`, and
`#` comments. 0.02s over 763 files.
The guard's path list was the part that kept rotting. The first version
named only rig-core and rig-agent, leaving rig-candle, the other member
crates, the facade's own sources and every `tests/` directory unguarded.
Widening it to `crates/*/{src,tests,Cargo.toml}` plus the facade fixed
that instance and left `examples/` out — which is where `candle_wasm_chat`
lives, the one package this PR adds to the wasm CI matrix precisely
because it is the only consumer exercising the browser-wasm facade path.
The guard did not cover the crate it was written alongside.

Enumerating directories is the failure mode; stop enumerating. `git
ls-files -- '*.rs' '*.toml'` sweeps the repo, inherits `.gitignore` so
`target/` stays out, and the extension filter keeps the walk off the
cassette YAMLs under `tests/`. Nothing outside the normalized gates names
`target_arch`, so the breadth costs nothing: 932 files in 0.02s, against
763 before.

Only tracked files are listed — right for CI, where every file in a PR is
committed, but it means a local run before `git add` will not see a new
file. Noted in the step comment.

Verified: silent on the clean tree; fires on a bare gate planted in
`examples/candle_wasm_chat/src/` and in `crates/rig-mongodb/examples/`,
neither of which the previous scope read. The existing cases still hold —
fires on a bare gate in `crates/rig-agent/src`, silent on a
rustfmt-wrapped correct gate and on prose in both comment syntaxes.
`pub use rig_agent::tool::rmcp` was the one `rmcp` site left on a bare
`feature = "rmcp"` after every other gained `not(target_family = "wasm")`.
The module it names does not exist on wasm.

Nothing breaks today: reaching this re-export requires `rig-agent` to
have compiled, which its own `compile_error!` prevents, so the build
stops one crate earlier. Confirmed — `cargo check -p rig --features rmcp
--target wasm32-unknown-unknown` still reports exactly one error, the
native-only diagnostic.

Fixing it anyway, because the CI error-count assertion only builds
`-p rig-agent`. If this predicate ever became load-bearing, no lane would
notice. The uniform gate is also what makes "every `rmcp` cfg site
carries the native predicate" a claim a reader can check by grepping.

Verified: `cargo check -p rig --features rmcp` (native) and
`cargo check -p rig --target wasm32-unknown-unknown` both clean.
Removing the `wasm` feature also changed what these two expand to, and
that deserves its own entry rather than riding along in the feature
removal, because the two break differently.

`if_wasm!`/`if_not_wasm!` are `#[macro_export]`ed, and a `cfg` inside a
macro expansion is evaluated in the *calling* crate. The old expansion
therefore tested whether the **caller** had a feature named `wasm` — not
`rig-core`. A downstream crate without one took the `if_not_wasm!` branch
on every target, browser wasm included. Keying on the target is the fix,
but it is a silent one: unlike the feature removal, which Cargo rejects
at resolution, a caller that did define a `wasm` feature and expected it
to drive these macros now gets the target's answer with nothing to fail
on.
The grep guard cost more than it caught. It rejected correctly written
`cfg_attr` predicates outright — `\bcfg!?\(` does not match `cfg_attr(`,
so a correct `cfg_attr(all(target_arch = "wasm32", target_os =
"unknown"), ...)` fell to the outside-any-group fallback and failed with
an error advising the exact spelling already on the line. It also flagged
`/* */` prose (only `//` was stripped) and commented-out TOML tables
starting with `#[`.

What it guarded remains covered by jobs that compile rather than
pattern-match: `check-wasm` and `check-wasm-runtimes` build rig-core,
rig-agent, rig, and candle_wasm_chat for wasm32-unknown-unknown
individually, and `check-rmcp-native-only` asserts rmcp on wasm fails
with exactly one error. The residual gap is a bare `target_arch =
"wasm32"` that builds fine on browser wasm but misbehaves on WASI —
a target that does not build and is documented as unsupported, so
nothing exercised it either way.

The convention itself stays documented in crates/rig-agent/README.md.
@gold-silver-copper
gold-silver-copper added this pull request to the merge queue Jul 28, 2026
Merged via the queue into main with commit fb3b347 Jul 28, 2026
10 checks passed
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