Status: Proposed
Date: 2026-06-25
Author: Nightly Research Agent
Branch: research/nightly/2026-06-25-capability-gated-ann
Crate: crates/ruvector-capgated
Related: ADR-227 (Proof-Gated Writes), ADR-240 (Coherence-HNSW), ADR-256 (Hybrid Search)
RuVector stores vectors that represent memory, knowledge, and agent state. In multi-agent deployments — and increasingly in enterprise RAG pipelines — the right to retrieve is not uniform. An agent managing user A's memory should not be able to recall user B's embeddings, even if they are geometrically close. A security-cleared retrieval pipeline should not surface classified embeddings to uncleared queriers.
Current state of the ecosystem:
- ADR-227 (proof-gated writes): vectors can only be written if the writer presents a valid proof token. There is no symmetric read-time access control.
- ACORN / Milvus RBAC / Qdrant payload-filter: these systems filter after ANN search — meaning the ANN engine first retrieves candidates from the full corpus, then drops unauthorised ones. This wastes distance computations and, more importantly, leaks the existence of nearby vectors to any observer who can count results or measure latency.
- pgvector / Weaviate class-level permissions: access control is at the collection or class level, not per-vector.
None of these systems embed access control into the retrieval index at the per-vector granularity. For agent memory, this matters: a single index may hold memories belonging to hundreds of different agent sessions, each of which should be isolated.
Introduce crates/ruvector-capgated as a standalone Rust crate implementing
capability-gated ANN search: each stored vector carries a CapMask (64-bit
bitset of required capabilities) and each query presents a CapMask (held
capabilities). A result is returned only if the querier holds all required bits.
Three retrieval variants are implemented and benchmarked:
(Numbers from n=5,000 × 64-dim, 37.5% access ratio, 200 queries, release build on x86_64.)
The public API is the CapGatedIndex trait:
pub trait CapGatedIndex {
fn insert(&mut self, id: usize, vector: Vec<f32>, required: CapMask);
fn search(&self, query: &[f32], k: usize, holder: CapMask) -> Vec<SearchResult>;
fn name(&self) -> &'static str;
}This trait shape should survive into production.
- Per-vector read access control: the only RuVector system to enforce capability checks at vector granularity during retrieval.
- EagerMask is strictly better than PostFilter at low access ratios: at 12.5% access ratio, EagerMask is 7.9× faster than PostFilter while achieving identical recall.
- CapGraph achieves sub-linear recall potential: graph-based traversal visits only ef=300 nodes out of 5,000 to find 90.6% of ground-truth results.
- No external dependencies: zero-dep pure Rust, WASM-safe.
- Composable with existing RuVector primitives:
CapMaskcan wrap any inner ANN backend (HNSW, LSM-ANN, PQ-ADC).
- CapGraph build is O(n²·d): the brute-force k-NN graph build in this PoC takes 2.3 s for n=5,000. Production requires incremental insertion (HNSW- style) or an approximate k-NN graph (RNG, random projection).
- Graph traversal leaks access patterns: in the "transparent traversal" model, unauthorised node coordinates are implicitly traversed (but not returned). A timing side-channel could reveal existence. A "strict isolation" model (only expand authorised nodes) closes this but degrades recall further.
- CapGraph recall degrades with low access ratio: at 12.5% access, recall@10 is 0.869; at 37.5% it is 0.906. This is expected — the graph is built on the full corpus and the authorised subgraph may be disconnected.
- 64-bit cap space only: the current
CapMaskis a fixed 64-bit integer. For >64 roles, a variable-length bitset or hierarchical capability tree is needed.
All major vector databases (Qdrant, pgvector, Milvus, Weaviate) perform access control as a post-retrieval filter. This is simple but: (a) wastes distance computation on unauthorised vectors, (b) requires k' >> k candidates to guarantee k authorised results, (c) leaks the existence of unauthorised neighbours via timing.
Rejected: misses the EagerMask speedup at low access ratios; no index-level isolation.
Maintain one HNSW/LSM index per capability group. A querier with 3 caps holds 3 separate indices.
Rejected: memory cost is O(n × 2^caps) in the worst case; search requires merging multiple indices; impractical for fine-grained capabilities.
Encrypt vectors before storing; query against encrypted corpus.
Rejected: 3–6 orders of magnitude slower; no practical Rust SEAL binding with HNSW; out of scope for this nightly. Worth a separate ADR as a long-term research direction.
Embed capability bits directly into PQ codes; filter at the codebook-lookup stage.
Interesting future direction: would enable compressed + access-controlled retrieval in one pass. Documented as a "next" item.
crates/ruvector-capgated: trait, CapMask, Oracle, PostFilter, EagerMask, CapGraph with batch_build.- Benchmark binary with acceptance tests.
- ADR, research document, gist.
- Replace O(n²) graph build with incremental HNSW-style insertion.
- Variable-length CapMask for >64 capabilities.
- Integrate EagerMask as a filter layer in
ruvector-core's query pipeline. - MCP tool wrapper:
vector_memory_search(query, capability_token). - Proof-gate integration: writes require proof, reads require capability token.
- Capability-aware PQ quantization.
- Strict isolation mode with recall characterisation.
- Timing side-channel analysis and mitigation.
- Distributed capability-gated search with ruFlo workflow.
Hardware: x86_64 Linux
Rust: 1.94.1
Command: cargo run --release -p ruvector-capgated --bin benchmark
Scenario: high-access (37.5% authorised)
| Variant | Mean(μs) | p50(μs) | p95(μs) | QPS | Recall@10 | Pass |
|---|---|---|---|---|---|---|
| PostFilter | 494 | 493 | 552 | 2,023 | 1.000 | ✓ |
| EagerMask | 175 | 167 | 206 | 5,728 | 1.000 | ✓ |
| CapGraph | 289 | 281 | 329 | 3,466 | 0.906 | ✓ |
Scenario: low-access (12.5% authorised)
Key observation: EagerMask provides the best latency when access ratio is low, as it skips distance computation for unauthorised vectors entirely.
- Access ratio → 0%: all variants degrade gracefully; PostFilter and EagerMask return empty results immediately; CapGraph returns empty after exhausting ef budget.
- Capability token forgery:
CapMaskis a plainu64in this PoC. Production requires cryptographic signing (integrate with ADR-227 proof-gate) to prevent queriers from self-asserting capabilities. - Graph disconnection: if authorised vectors form isolated clusters in the proximity graph, CapGraph may miss them entirely. Mitigation: increase ef_multiplier; ensure n_entry_points covers the authorised subgraph.
- Capability inflation: if a querier holds all 64 bits (
CapMask::ALL), all vectors are accessible — equivalent to no access control. Applications must not hand out ALL-capable tokens unless intentional.
- This PoC does not provide cryptographic access control. Any caller can
construct any
CapMaskand pass it tosearch. Production must gate token creation behind a proof-gate (ADR-227) or a signed JWT / capability cert. - Timing side-channels: EagerMask and PostFilter latency is proportional to the authorised fraction, leaking the fraction to a timing observer. For high- sensitivity deployments, constant-time execution is required.
- CapGraph traversal leakage: the graph walk visits unauthorised nodes as bridge nodes. Strict isolation (expand only authorised nodes) prevents this at a recall cost.
- Add
ruvector-capgatedas a dependency inruvector-core. - Wrap existing
HnswIndexwith aCapGatedWrapper<HnswIndex>using the EagerMask strategy. - Expose via
ruvector-serverasPOST /searchwithcapability_tokenfield. - Wire into MCP tool surface as
vector_memory_search(query, token).
- Should
CapMaskbe variable-length (e.g.Vec<u64>) to support >64 roles? - Should proof-gate verification (ADR-227) be composable with CapMask in a single call?
- Is the strict-isolation CapGraph worth implementing as a separate variant for high-security deployments?
- Can capability bits be embedded in PQ codebooks for compressed + gated search?
- What is the right ef_multiplier trade-off for production use (latency vs recall)?
