Skip to content

docs(adr): ADR-339 — a WebAssembly binding for ruv:// context, and its boundary - #925

Merged
ruvnet merged 2 commits into
mainfrom
docs/adr-339-context-js-binding
Aug 23, 2026
Merged

docs(adr): ADR-339 — a WebAssembly binding for ruv:// context, and its boundary#925
ruvnet merged 2 commits into
mainfrom
docs/adr-339-context-js-binding

Conversation

@ruvnet

@ruvnet ruvnet commented Aug 23, 2026

Copy link
Copy Markdown
Owner

Records why a JavaScript consumer cannot reach rvm-context today, and what a binding may and may not carry. Tracks ruvnet/rvm#45.

The crate type settles the first half

crate-type = ["rlib"] links Rust to Rust. No JS, no wasm, no C ABI — and no feature flag produces one, because cdylib is a crate-type, not a feature.

Reading the API changed the second half

Four facts, each verified in source, two of which contradict what the release notes imply:

  • Capability handles are not portable. CapabilityHandle is {index: u32, generation: u32} into a live local CapabilityManager table — not a bearer token, not signed, not serializable. Two integers handed from a Rust service to a JS host index a different table and mean something else. This is the load-bearing constraint.
  • An allow decision cannot be separated from its witness record. authorize is pub(crate), and AuthorizedRequest construction is private, documented as reachable only after a P1 allow record is appended. A binding that authorizes must carry ContextRuntime, and therefore the witness log.
  • There is no entropy requirement. ed25519 lives only in rvm-proof; rvm-witness signs with HMAC-SHA256 — deterministic and keyed — and no getrandom or rand exists in the workspace. The hazard is key provisioning (default_signer() / with_default_key() must not reach JS), not randomness.
  • No host clock is needed. LogicalContextClock is a counter from zero.

The boundary moved, and sharpened

An earlier draft excluded authorization entirely. The handle representation shows that was the wrong line: the danger was never that JavaScript might mint a capability, because a capability minted in the module grants nothing outside it. The danger is the illusion of authority — a gateway provisioning its own scopes, rendering a decision, and reporting it as though it said something about a separate Rust-side authority.

So the binding widens to four layers — URI, scope, runtime, verification — and the claim gets more precise: the wasm module is a faithful, deterministic policy simulator. Handles are not portable; a decision binds only to the scope table the host provisioned. Exactly right for shadow-mode evaluation, where nothing is enforced and the question is whether two policies agree. Not evidence about another authority unless that authority provisioned the same scopes.

Worth noting for the motivating consumer: scope containment alone answers the shadow-mode question — no capability, no runtime, no key — so meta-llm is unblocked at layer 2, well before the runtime layer lands.

Gates

Include that no default-key signer crosses the boundary, that getrandom/rand/a host clock appearing in the tree is a blocking review failure rather than a dependency bump, byte-identical cross-implementation determinism for witness and receipt bytes, and that the cross-tenant negative test must place the violating segment last — a containment check inside a short-circuiting loop is green at position 1 while broken for positions 2..n.

Also records that npm's rvm belongs to an unrelated project (Ruff Version Manager, ruffjs/rvm), so this is a new scoped package rather than an update.

Docs only. Implementation lands in ruvnet/rvm.

🤖 Generated with claude-flow

https://claude.ai/code/session_016QSCkKnxDjqU49NVVpWMK5

…s boundary

Records why a JavaScript consumer cannot reach rvm-context today, and what a
binding may and may not carry.

The manifest settles the first half: crate-type = ["rlib"] links Rust to Rust,
so there is no JS, wasm, or C ABI surface and no feature flag that produces
one. Three adjacent facts were verified rather than assumed, each plausible
enough to guess wrong about: crates/rvm-wasm is a WebAssembly GUEST runtime for
partitions, not a binding; ed25519-dalek is a dev-dependency, so the runtime
crypto surface is sha2 and sha3, both wasm32-clean; and a std feature already
exists, which wasm-bindgen requires.

The decision is a cdylib wrapper crate publishing to npm as
@ruvnet/rvm-context, exposing canonical parsing, canonical re-formatting, and
the specific UriError variant on rejection -- pure computation over a string.

It deliberately does not expose the runtime or resolver paths. Those require an
authenticated PartitionId bound at construction and a runtime-owned clock,
which exist so a caller cannot supply its own actor or timestamp. Projecting
them into JavaScript would mean inventing a JS-side actor -- the forgery the
design prevents -- or shipping something that looks like authorization and is
not.

So a JavaScript consumer gets the naming layer, not the trust layer. That is
not a limitation to lift later by adding bindings; it is the separation the
namespace is built on, holding at one more boundary.

Also records that npm's `rvm` belongs to an unrelated project (Ruff Version
Manager, ruffjs/rvm), so any JS distribution here is a new scoped package
rather than an update to that one.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_016QSCkKnxDjqU49NVVpWMK5
Sizing the binding against origin/main turned up four facts that change the
decision, two of which contradict what the release notes imply.

Capability handles are not portable. CapabilityHandle is {index, generation}
into a LIVE LOCAL CapabilityManager table -- not a bearer token, not signed,
not serializable. Two integers handed from a Rust service to a JS host index a
different table and mean something else. This is the load-bearing constraint.

An allow decision cannot be separated from its witness record: authorize is
pub(crate) and AuthorizedRequest construction is private, documented as
reachable only after a P1 allow record is appended. A binding that authorizes
must carry ContextRuntime and therefore the witness log.

There is no entropy requirement. ed25519 lives only in rvm-proof; rvm-witness
signs with HMAC-SHA256, deterministic and keyed, and no getrandom or rand
exists in the workspace. The hazard is key provisioning -- default_signer() and
with_default_key() must not reach JS -- not randomness. No host clock is needed
either: LogicalContextClock is a counter from zero.

The consequence is that the earlier draft drew the boundary in the wrong place.
It excluded authorization entirely; the handle representation shows why that
was wrong. The danger was never that JS might mint a capability, because a
capability minted in the module grants nothing outside it. The danger is the
ILLUSION of authority -- a gateway provisioning its own scopes, rendering a
decision, and reporting it as though it said something about a separate
Rust-side authority.

So the binding widens to four layers (URI, scope, runtime, verification) and
the claim sharpens: the module is a faithful deterministic policy SIMULATOR,
handles are not portable, a decision binds only to host-provisioned scopes.
Correct for shadow mode, not evidence about another authority.

Scope containment alone answers the shadow-mode question with no capability, no
runtime and no key, so the motivating consumer is unblocked at layer 2.

Gates revised accordingly, including that the cross-tenant negative test must
place the violating segment LAST -- a containment check inside a
short-circuiting loop is green at position 1 while broken for 2..n.

Tracks ruvnet/rvm#45.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_016QSCkKnxDjqU49NVVpWMK5
@ruvnet
ruvnet merged commit 684d21a into main Aug 23, 2026
49 of 50 checks passed
@ruvnet
ruvnet deleted the docs/adr-339-context-js-binding branch August 23, 2026 22:28
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