Commit graph

1 commit

Author SHA1 Message Date
rUv
684d21a354
docs(adr): ADR-339 — a WebAssembly binding for ruv:// context, and its boundary (#925)
* docs(adr): ADR-339 — a WebAssembly binding for ruv:// context, and its 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

* docs(adr): revise ADR-339 after reading the real capability API

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
2026-08-23 18:28:45 -04:00