docs(adr): ADR-339 — a WebAssembly binding for ruv:// context, and its boundary - #925
Merged
Conversation
…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
5 tasks
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Records why a JavaScript consumer cannot reach
rvm-contexttoday, 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, becausecdylibis 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:
CapabilityHandleis{index: u32, generation: u32}into a live localCapabilityManagertable — 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.authorizeispub(crate), andAuthorizedRequestconstruction is private, documented as reachable only after a P1 allow record is appended. A binding that authorizes must carryContextRuntime, and therefore the witness log.ed25519lives only inrvm-proof;rvm-witnesssigns with HMAC-SHA256 — deterministic and keyed — and nogetrandomorrandexists in the workspace. The hazard is key provisioning (default_signer()/with_default_key()must not reach JS), not randomness.LogicalContextClockis 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-llmis 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
rvmbelongs 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