Private LightRAG retrieval — three Compass-encrypted HNSW indexes and two XorMM volume-hiding multimaps, all driven from inside an attested SEV-SNP CVM. The entire LightRAG kg_query surface runs as Ring-ORAM accesses over a fully untrusted blob store.
status shipped · private graph retrieval · benched
trust anchor SEV-SNP CVM hosts the Compass client
builds on CAPRISE + two-party HKDF + SnpTrustedExecutor
rev 2026·05·17
private LightRAG · Ring-ORAM + XorMM
measured query latency
10–58ms
in-memory backend · Local 10 ms (N=100) → 34 ms (N=2000) · Hybrid 14 → 58 ms · see §06
layer-0 ORAM reads/q
117–397
Local 117 → 212 · Hybrid 173 → 397 over N=100 → 2000 · directional filter prunes ~50 %
private-rag has three previously-shipped vertical slices — CAPRISE embedder, CAPRISE storage, GELO reranker — that together cover flat-vector RAG: documents go in as encrypted embeddings, queries come back as encrypted top-k. That vertical assumes the corpus is a flat list of chunks. This page is the graph-augmented slice. LightRAG (Guo et al. EMNLP 2025) replaces the flat list with a knowledge graph of entities + relations + chunk pointers; retrieval becomes three parallel ANN searches plus a 1-hop graph expansion plus a chunk fan-out. Done naïvely, each of those steps is a distinct side channel the storage operator can read.
The job of this slice is to compose LightRAG retrieval semantics over the Compass encrypted-ANN protocol (OSDI 2025) so the full kg_query surface — including the graph expansion and chunk fan-out — runs as oblivious primitives over an untrusted blob store. Three Compass-encrypted HNSW indexes hide which entity / relation / chunk got looked up; two XorMM volume-hiding multi-maps hide adjacency-list lengths and source-chunk fan-out widths; AES-GCM hides chunk text. Every primitive is driven from inside a SEV-SNP CVM, attestation-anchored to the same scheme_identity that already pins the embedder and storage slices.
What this unlocks in the product: a tenant can ship a structured KG (entities, relations, chunks) to the CVM over RATLS, and subsequent queries return graph-grounded context — the kind LightRAG's headline benchmarks show beats flat retrieval on multi-hop and discovery tasks — without exposing the KG topology, the lookup pattern, or the result identities to the storage operator. The retrieval primitives are general; LightRAG is the first consumer.
What this slice is not. It is not the ingest LLM (entities/relations are extracted on the client today; an in-TEE extractor is a roadmap item). It is not the generation-layer defence against AGEA-class extraction over many legitimate API calls (that is PrivGemo / DP-RAG territory, separate from the retrieval crypto here). It is not a portability story for thin browsers: 50–325 MB of in-CVM client state per tenant means we ship as a server-resident multi-tenant CVM, not as on-device code.
§02
Definitions & glossary
acronyms and load-bearing terms used on this page
Term
Definition
LightRAG
graph-augmented RAG (HKUDS/LightRAG, Guo et al. EMNLP 2025). Corpus → KG of entities + relations + chunk pointers; query → dual keyword extraction + 1-hop expansion + chunk fan-out
TEE-anchored / on-device deployments
two parallel deployments of the same Compass protocol with different trust anchors. TEE-anchored: client role inside SEV-SNP CVM (this page). On-device: paper-faithful Compass on the user's own device
kg_query
LightRAG's retrieval entry point (lightrag/operate.py:3164). Returns the assembled context for one of five modes: local · global · hybrid · mix · naive
hl / ll keywords
high-level (global, thematic) and low-level (local, specific entity) keywords extracted from the query by an LLM. Drive the relations VDB and entities VDB respectively
HNSW
Hierarchical Navigable Small World graph (Malkov & Yashunin) — the graph-based ANN index Compass wraps. Multi-layer; top layer holds the entry point
ef
HNSW dynamic candidate-list width — the beam width during greedy search. Larger ef ⇒ higher recall, more work. In Compass, the number of batched ORAM round-trips per query is n = ⌈ef / ef_spec⌉, so ef is publicly observable via RPC count
ef_spec
Compass speculation-set size — the number of candidates whose neighbours are fetched in one batched ORAM request (Speculative Neighbor Prefetch)
ef_n
Compass directional-filter size — after quantised hints prune neighbours by query direction, only the top ef_n per candidate are fetched
M
HNSW degree bound — max number of neighbours per node at each layer. Determines per-block payload size in the ORAM (D · f32 + M · u32)
Compass
Zhu, Patel, Zaharia, Popa (OSDI 2025). HNSW-over-Ring-ORAM with three optimisations: Directional Neighbor Filtering, Speculative Neighbor Prefetch, Graph-Traversal-Tailored ORAM
Ring ORAM
Ren et al. — tree-based ORAM with Z + S blocks per bucket (Z real + S dummy). Constant online bandwidth via the XOR trick; eviction is amortised every A accesses (off the user's critical path)
Z, S, A
Ring-ORAM bucket parameters — real-block slots, dummy slots, eviction rate. Tuned per index
ORAM
Oblivious RAM (Goldreich-Ostrovsky). Protocol that makes a remote storage server's view of access patterns computationally indistinguishable from random
EMM
Encrypted Multi-Map. Key → list-of-values primitive. The natural shape for an adjacency list or a source_id list
XorMM
Patel-Persiano-Yeo (CCS 2022). Non-lossy volume-hiding EMM: response size independent of the queried key. 1.5–2× storage overhead
source_id
LightRAG field on each entity / relation prop bag: a GRAPH_FIELD_SEP-delimited list of chunk IDs the entity appears in. Variable length ⇒ length leaks the entity's identity unless hidden
AP / QP / SP
SSE leakage profiles: Access Pattern (which records returned), Query Pattern (which records matched), Search Pattern (whether two queries match the same set). ORAM hides AP; XorMM hides volume / partial QP; HMAC perturbation hides SP across sessions
CVM
Confidential VM. The encrypted-memory virtual machine the trusted code runs in. AMD SEV-SNP in this codebase
Remote-Attested TLS. Client verifies the SEV-SNP attestation report inside the TLS handshake before sending plaintext. Existing transport in gelo-rag::two_party_service
VCEK
Versioned Chip Endorsement Key. The SEV-SNP attestation root, signed by AMD
HKDF
HMAC-based Key Derivation Function (RFC 5869). Used for the two-party (user_x_sk, tee_user_x_sk) → per-tenant child key chain
CAPRISE / DPE
Distance-Preserving Encryption — server can run cosine over ciphertext directly (Ye et al. 2026). In the TEE-anchored deployment, CAPRISE wraps the embedding inside the Compass block as defence in depth
DistanceDP
RemoteRAG's (n, ε)-DP construction — planar-Laplace noise on the embedding such that any two queries within radius r are formally indistinguishable. Already lives in crates/remote-rag::planar_laplace
HMAC perturbation
This page's per-session deterministic embedding perturbation — see §05 FIG. 05 inset and the search-perturb card in §04
Opal
Kaviani et al. 2026, private memory for personal AI. Runs Compass inside a TEE; cited because its security proof imports Compass's batched-access lemma into the G_att-hybrid model — formal precedent for the client-role swap on this page
AGEA
Agentic Graph Extraction Attack (arXiv 2601.14662, 2026). Reconstructs the KG over many legitimate API calls — defended at the generation layer (PrivGemo / DP-RAG), not by retrieval crypto
GRAGPOISON / LogicPoison
Integrity attacks on GraphRAG corpora — defeated by Compass's malicious-server mode (Merkle over the ORAM tree) combined with in-CVM ingest
Vec2Text
Morris et al., EMNLP 2023. Inverts embedding vectors back to near-exact source text — the canonical motivation for keeping embeddings off the storage server
PrivGemo / DP-RAG
Generation-layer defences composed on top of the retrieval layer (out of scope here): structural KG anonymisation (PrivGemo, arXiv 2601.08739) and output-side DP (DP-RAG, arXiv 2602.14374)
Same trust geometry as the CAPRISE path (storage page): the CVM is trusted, the storage server and network operator are not, the client endpoint is trusted for the duration of an RATLS session. GraphRAG introduces structural attack surfaces the flat vector path does not have — every entity, every relation, every per-node degree, every adjacency list is now a distinct side channel. The TEE-anchored deployment neutralises those by routing every server-visible operation through one of three primitives: Ring-ORAM (over an encrypted HNSW or KV blob), XorMM volume-hiding multi-map (over adjacency & source-id lists), AES-GCM blob fetch (over chunk bodies).
Departure from the Compass paper. Compass's paper places its protocol "client" — the party holding the position map, stash, treetop cache, and ORAM key — on the user's own device (laptop / phone / browser), explicitly avoiding TEEs because enclave side channels weaken the story. This design shifts that client role into the SEV-SNP CVM. The Compass protocol is unchanged; the trust anchor moves from "user owns the hardware" to "SEV-SNP attestation + AMD vendor." Opal (Kaviani et al. 2026) already does this composition and formally imports Compass's batched-access lemma into its G_att-hybrid proof — the security property holds because it's a property of the protocol, not of which silicon hosts the client. The thin-client / multi-tenant SaaS shape this prototype targets cannot put 5–500 MB of ORAM state on every user's browser, and SEV-SNP attestation is the trust anchor this codebase already relies on for CAPRISE-at-rest. Deployments that cannot accept SEV-SNP trust should use the on-device deployment from the design doc instead — paper-faithful Compass on the user's device. The two are parallel deployments of the same protocol with different trust anchors, not primary and fallback.
Aspect
Paper-faithful Compass
TEE-anchored (this design)
Trust anchor
user owns the hardware
SEV-SNP attestation + AMD vendor
Compass client state lives
device RAM
encrypted CVM RAM · hidden from host OS
Side-channel exposure
device-local
SEV-SNP side channels in-scope (CVE-2023-20593, Hertzbleed, PSP firmware)
User-facing client
thick · 5–500 MB ORAM state
thin · RATLS + plaintext requests only
State portability
per-device, user-managed sync
centralised · switch devices freely
Multi-tenant
one user per device
many tenants per CVM · HKDF-isolated
Forward security
OS-process isolation
two-party KDF Option 3 · TEE-seal break alone does not recover past sessions
Party
Trust
What it sees
What is hidden
User text + tenant secret
confidential
—
—
Client
trusted
plaintext query · user_x_sk · assembled context / top-k returned over RATLS
embedding stays inside CVM · only reaches storage as a Compass ORAM access
Access pattern over the three HNSW indexes
which entity / relation IDs got looked up
Ring-ORAM under Compass — paths random by construction · paper §4.10 proof
Per-node degree fingerprint
variable-length get_nodes_edges_batch reply uniquely identifies the queried entity
partially shipped: XorMM-padded adjacency lists hide list-length leakage at the server boundary · the "degree absorbed into encrypted prop bag" half (Risk D) is deferred until the node-props encrypted KV lands — degree currently lives in cleartext in-CVM only
source_id fan-out length
5-chunk entity vs 50-chunk entity distinguishable by reply size
lift source_id out of prop bag · serve via XorMM with bucket-padded volume (Risk E)
AGEA-class extraction over many queries
96 % node leak at T=1000 on LightRAG · KG reconstructable
orthogonal — defended at the generation layer (PrivGemo / DP-RAG); retrieval crypto is necessary, not sufficient
Cross-session linkability of identical queries
same ll_keywords ⇒ same Compass trace ⇒ linkable
per-session HMAC perturbation on the embedding (Risk F) · composes with RemoteRAG DistanceDP
any key material · plaintext entities / relations / chunks · plaintext access pattern
safe
Malicious storage server tampering with buckets
—
Compass Merkle integrity catches mutation with overwhelming probability
safe
TEE seal broken (memory dump, CVM rollback)
tee_user_x_sk · and derived OramKeys / EmmKeys / aes_chunk_key for any tenant whose store is currently resident (the LightRAG service deliberately holds them — re-decrypting the HNSW per request is not viable; the existing CAPRISE service still re-derives + wipes its two-key set per request)
user_x_sk (never persisted in CVM, RATLS forward-secret) · CAPRISE keys for the embedder path · all stored ciphertext at rest if the storage server is honest
safe — per-session HMAC perturbation on each embedding randomises the HNSW traversal across sessions; same tenant + same query in two sessions produces two different execution fingerprints
Inherited weakness. CAPRISE remains distance-preserving inside the Compass block payload — defence in depth, not a substitute. The ORAM hides which block is read; CAPRISE keeps the block contents useless if the AES-GCM key is later broken. A key-holder still recovers plaintext (Vec2Text).
§04
Components
protocol-side crates · all shipped
Each card pairs [method ↦ source ↦ what it secures ↦ threat ↦ overhead]. Crates tagged SHIPPED are end-to-end functional behind release-tested integration suites; deferred sub-features are called out in the card body and tracked in §07 Status & gaps.
SHIPPED · ORAM
ring-oramcrates/ring-oram · 34 tests
Ring-ORAM semi-honest baseline + multi-hop lazy eviction (Compass §4.7) + treetop caching. Each bucket holds Z + S blocks (Z real + S dummy). Async BlockBackend trait so the same client runs against the in-memory backend or a REST backend without code change. AES-GCM nonce = u64-LE bucket_id ‖ u32-LE write_counter.
method
read · write · admit · evict_path · flush_evictions · client-held position map + stash · reverse-lexicographic eviction cursor · treetop cache of top t bucket levels (skips backend on the hot path)
source
Ren et al. Ring ORAM (USENIX Security 2015) · port verified against Clive2312/compass
secures
access pattern over the encrypted blob store
threat
semi-honest storage server · network observer · client compromise
cost
O(log N) online · in-memory measured ~10 µs per read at N=2000 (see §06)
storage operator with full DB access · network observer reading any RPC trace
cost
10 → 34 ms p50 / query over N=100 → 2000 (in-memory, release, see §06) · sub-linear scaling — HNSW theory holds through the ORAM layer
deferred
batched ORAM reads (union-of-paths) · Speculative Neighbor Prefetch (depends on batched reads) · CAPRISE-wrap inside the block payload (defence in depth, not blocking) · paper-scale bench parity
SHIPPED · EMM
xormm-emmcrates/xormm-emm · 13 tests
Non-lossy volume-hiding encrypted multi-map. Built statically from a full key→list map; get returns the value list with response size independent of the queried key (constant volume_bound per EMM). Used for the LightRAG adjacency lists and per-entity src_chunks lists.
method
cuckoo hashing with two PRF-derived candidate buckets per key · (dummy_flag, fingerprint, value_count, padded_values) bucket layout · AES-GCM-encrypted under per-EMM HKDF child (emm_adjacency_key, emm_src_chunks_key) · in-CVM stash for cuckoo failures
LightRAG-shaped storage facade: 3× CompassIndex (entities · relations · chunks) + 2× XorMmClient (adjacency · src_chunks) + 1× AesChunkStore. Per-tenant keys derived from V2 HKDF children. build_from_kg(ExtractedKg, params, &derived) → LightKgStore is the single ingest entry point; queries are query_entities_topk, adjacency_for_entity, src_chunks_for_entity, chunk_text.
method
per-tenant key set derived once via HkdfPolicyV2 from (user_x_sk, tee_user_x_sk, tenant_id) → 8 child keys · HMAC-based logical-key derivation maps entity / relation names to LogicalKey per EMM with domain labels (ADJACENCY_ENTITY, SRC_CHUNKS_ENTITY, …) and null-byte separator against shift collisions · generic over both backends so the REST stack drops in via build_from_kg_on
source
in-house
secures
composes the three primitives behind a single LightRAG-shaped API · keeps cross-EMM key derivations safely isolated by domain label
threat
operator stitching together independent leaks across primitives · CVM-internal observer of the in-CVM cleartext name → block_id maps (Risk F — pseudonymisation is post-M9)
cost
ingest measured: 22 ms / 213 ms / 1.55 s for N=100 / 500 / 2000 entities (§06) — HNSW build dominates · ~110 MB CVM state at SIFT1M-scale projected (paper Tab. 4 ×3)
deferred
encrypted KV for node_props / edge_props (closes Risk D fully) · sub-derived ORAM keys per CompassIndex (CompassIndex::with_oram_key) · incremental ingest (currently rebuild on every ingest)
Rust port of LightRAG's kg_query orchestration. Local + Hybrid modes shipped: ll_embedding → perturb(Ll) → entities.search → adjacency fan-out → src_chunks fan-out → chunk_text decrypt → context string; Hybrid adds hl_embedding → perturb(Hl) → relations.search → endpoint fan-out merged into the entity set. Faithful to upstream modulo: (a) accepts pre-extracted keywords as input (client-side extraction today; in-TEE LLM deferred) · (b) per-session HMAC perturbation on every embedding (Risk F).
method
LightRagPrivateService::kg_query(ll, hl, params, session_key) → KgContext · KgContext::to_context_string() renders the LLM input · multi-tenant LightRagTwoPartyService wraps it with per-tenant HashMap<TenantId, LightKgStore> + persistent tee_user_x_sk
source
HKUDS/LightRAGlightrag/operate.py · Guo et al. EMNLP 2025
secures
upstream retrieval semantics, transposed onto the encrypted store
threat
regressions silently degrade either accuracy (Risk H) or security (Risk D / E)
cost
Local 10 → 34 ms / Hybrid 14 → 58 ms p50 at N=100 → 2000 (§06) · cross-session linkability test: 100/100 trials produce distinct perturbed embeddings under different session nonces
search-perturblightrag-private :: perturb · 5 unit tests
Per-session HMAC perturbation on every embedding before it enters Compass. Breaks cross-session linkability — Compass + Ring-ORAM hides the storage-side access pattern, but two sessions issuing the same query still produce identical execution fingerprints (round count · batch sizes · timing · SEV-SNP-leaky perf counters). This step randomises the start vertex of the HNSW traversal per session, closing that residual. Measured cost: 2–6 µs per embedding on the bench.
method
session_key = HMAC(search_pattern_key, session_nonce) · per-embedding direction = unit_vector(HMAC(session_key, kind ‖ 0x00 ‖ quantize(e))) · e' = normalize(e + ε · direction) · kind ∈ {q, ll, hl} · ε = 2 % default (cosine to original ≥ 0.995 pinned in unit test)
execution-pattern correlation across sessions · session-origin linkability of repeated queries
threat
network / host observer correlating timing & perf-counter fingerprints across sessions · classical search-pattern leakage in the SSE sense
cost
2–6 µs / embedding measured · invisible against the ~10 ms CompassIndex critical path · linkability test pins 100/100 trials produce distinct perturbations under different session nonces
SHIPPED · KDF
HkdfPolicy · v2crates/core :: keying · M0
Extends v1's two-info-string derivation (caprise_seed · aes_chunk_key) to an eight-child set: + oram_keys × 3 (entities · relations · chunks) + emm_keys × 2 (adjacency · src_chunks) + search_pattern_key. Same Extract, more Expands. Versioned via gelo-rag.v2.…; bumping the version yields entirely fresh keys across all primitives.
method
one Hkdf::<Sha256>::new(salt = tenant_id, ikm = user_x_sk ‖ tee_user_x_sk) · eight hk.expand("gelo-rag.v2.{role}", &mut buf) · each child Zeroizing<[u8; 32]> · SchemeParamsV2 + CompassParams + XorMmParams + LightRagParams canonically encoded into the attestation digest
source
RustCrypto hkdf 0.12 + hmac + sha2 (already in workspace; no new crypto)
Mirror of GeloRagTwoPartyService with one deliberate divergence: the LightKgStoreholds the derived OramKeys/EmmKeys/aes_chunk_key persistently per-tenant (re-decrypting the HNSW per query is not viable). user_x_sk is still consumed + wiped at ingest; tee_user_x_sk is the only residual at idle. Same loud-failure contract as the embedder service.
method
ingest_kg_for(tenant, user_x_sk, kg, params) derives V2 children + builds LightKgStore + retains it under HashMap<TenantId, Arc<Mutex<Tenant>>> · query_for(tenant, ll, hl, params, session_nonce) mints a SessionKey per request and drives kg_query · UnknownTenant → HTTP 410 Gone mapping pinned in runner test
source
in-house · realises the TEE-anchored deployment composed with two-party KDF Option 3
secures
cross-tenant isolation · loud-failure on CVM restart · all of §03's defences in a single request handler
threat
untrusted cloud operator · TEE seal break recovers resident store keys (§03 compromise table) · breaks under simultaneous TEE + client compromise
cost
HKDF + LightKgStore lookup + Compass + XorMM + AES per request · 10–58 ms p50 in-memory measured · ~ms-class HTTP overhead through axum (in next bench tier)
End-to-end stage-timed bench harness. Mirrors kg_query orchestration with Instant::now() boundaries between every stage so we can attribute query latency to perturb / entities_search / relations_search / adjacency / src_chunks / chunk_decrypt. CSV out to stdout; p50/p95/p99/mean/stddev summary to stderr; mean layer0_reads/q headlined.
method
three planned scenarios — in_memory shipped, local_rest + runner_http staged · env-configured (BENCH_SIZE, BENCH_DIM, BENCH_QUERIES, BENCH_MODE) · deterministic seed off corpus dimensions · 5-query warmup discarded from stats by default
source
in-house · pre-shipment bench infrastructure
cost
N=100 in 1 s · N=2000 in ~10 s · ingest dominates beyond ~500
deferred
REST-backed scenario · plain-hnsw_rs baseline for the ORAM-vs-cleartext multiplier · paper-fixture parity (LAION 100K, SIFT1M)
§05
Compute flow & trust boundaries
kg_query · hybrid mode · ingest path mirrors with builds instead of reads
TEE embeds (query, ll_keywords, hl_keywords) in one GELO-masked batch (see embedding page)
TEE (+ GPU under mask)
masked activations across PCIe
④
TEE applies per-session HMAC perturbation (and optionally DistanceDP) to each embedding to break cross-session linkability
TEE
—
⑤
Three Compass searches in parallel: entities_idx.search(ll_emb), relations_idx.search(hl_emb), chunks_idx.search(q_emb) (mix mode only). Each is a Ring-ORAM-mediated HNSW walk with Directional Neighbor Filtering + Speculative Prefetch — see paper §4 / our plan §3. KV reads on node / edge prop bags use the same ORAM controller (separate tree, same construction)
Adjacency expansion (local-mode 1-hop): adjacency.get_batch(entity_names) over XorMM · then source_id fan-out via src_chunks.get_batch(...) over XorMM · then chunk bodies via chunks.get_batch(chunk_ids) AES-GCM blobs
Round-robin merge across vector_chunks / entity_chunks / relation_chunks · token-budget truncation per QueryParam (constant time over the candidate list)
TEE
—
⑧
Assemble the LightRAG context string (entities + relations + chunks + reference list) and structured QueryContextResult
TEE
—
⑨
TEE deferred-flushes any pending Compass evictions (off the user's critical path); zeroizes the incoming user_x_sk and the per-request SessionKey derivative; returns the assembled context over the same RATLS session. Note: the OramKeys / EmmKeys / aes_chunk_key live inside the resident LightKgStore for the tenant — they do not wipe at request end (deliberate design choice; see §03 compromise table). tee_user_x_sk stays in CVM RAM as before.
TEE → Client / TEE ↔ storage
plain context over RATLS · eviction-path RPCs to storage in the background
What the storage server sees, end-to-end. A stream of Ring-ORAM read_path + write_path RPCs against three trees plus two KV trees, plus XorMM bucket reads against two static EMMs, plus AES-GCM blob fetches. Path IDs are uniform-random by construction. Bucket sizes are uniform per-tree. XorMM response sizes are uniform per-EMM. Request rate is constant per RATLS session. Across two distinct queries by the same tenant, the server's view is computationally indistinguishable.
Today's simplification: step ③ "Embed (GELO)" runs on the client, not in the TEE. The current wire format (/lightrag/ingest, /lightrag/query) accepts pre-computed embeddings for every chunk / entity / relation / query — the client owns the GELO + mask + GPU offload pipeline. The diagram preserves the target shape where the TEE re-runs the embedder under attestation, but the present ship trusts the client (same machine as the keyword-extraction LLM) to do this honestly. Moving embedding back into the TEE is the same plumbing as the embedder slice (embedding page) — no new crypto, just a request-path branch.
Note: ingest path. Same trust geometry, opposite direction — client streams documents + extracted KG over RATLS; TEE embeds, builds a plain hnsw_rs::Hnsw in encrypted CVM RAM, runs CompassIndex::from_plaintext_hnsw, pushes the encrypted ORAM tree to storage, builds and pushes the XorMM EMMs, writes the AES-GCM chunk blobs, then zeroizes the plaintext working set. Heavier than query — expect minutes per 100K docs — but a batch operation, not on any user-perceived path.
Inset · Step ④ per-session search perturbation
Why this step exists: Compass + Ring-ORAM randomises the storage-side path IDs, but two sessions of the same tenant issuing the same query still produce identical execution fingerprints (timing · round count · batch sizes · SEV-SNP-leaky perf counters). The perturbation randomises the HNSW start vertex per session, breaking cross-session linkability.
The Compass paper acknowledges a coarse version of this leak — "different query types use different ef, so an adversary can still distinguish the operation type" — and explicitly puts it out of scope. Concretely: Compass's batched ORAM rounds-per-query are n = ⌈ef / ef_spec⌉, so a search at ef=64 takes 4 round-trips while an insert at ef=200 takes 13. The count of round-trips is itself a fingerprint visible to any passive observer, with no decryption required. Compass openly does not try to hide it. Our HMAC perturbation closes the finer-grained content-level version of the same class of leak: same operation, same mode, but two sessions issuing the same query produce different content-adaptive execution patterns (directional-filter prune rate, speculative-prefetch hit rate, hops-to-convergence) — without the perturbation, those signatures are stable enough to link sessions to each other.
FIG. 05 — Per-session search perturbation · HMAC chain + optional DistanceDP compositionred = secret · blue = public per-session value · purple = perturbation operation
Numbers below are from the compass-rag-bench binary (crates/graphrag-bench) — release build, single-threaded, in-memory backends (no network), 50 queries per (size, mode) after 5 warmup queries dropped. The bench replicates the kg_query orchestration with Instant::now() boundaries between each stage so latency can be attributed end-to-end. The next bench tiers will swap InMemoryBlockBackend for compass-rest-backend on loopback (REST overhead) and then POST through /lightrag/query (full user-perceived latency).
Query latency · Local mode
N entities
total p50
entities_search
adjacency
src_chunks
chunk_decrypt
layer0_reads / q
ingest
100
10.0 ms
10.0 ms
18 µs
18 µs
3.5 µs
117.7
22 ms
500
19.4 ms
19.3 ms
26 µs
21 µs
9.6 µs
173.7
213 ms
2000
33.9 ms
33.7 ms
27 µs
22 µs
11.1 µs
212.0
1.55 s
Query latency · Hybrid mode
N entities
total p50
entities
relations
adjacency
src_chunks
chunk_decrypt
layer0_reads / q
100
14.3 ms
10.2 ms
4.1 ms
47 µs
47 µs
8.1 µs
172.7
500
33.3 ms
19.3 ms
13.7 ms
64 µs
57 µs
21.4 µs
312.4
2000
58.0 ms
34.1 ms
23.2 ms
68 µs
59 µs
26.4 µs
396.9
Where the time goes. CompassIndex search owns the budget: 99.4 % of Local at every size, ~71 % of Hybrid (split between the entities and relations searches, in proportion to corpus size — relations is N·0.33). EMM lookups + AES chunk decrypt + search_perturb combined stay under 0.5 % of total at every scale. This is the batched-ORAM-reads + Speculative-Neighbor-Prefetch optimization budget: at N=2000 each query does 212 sequential ORAM round-trips that in principle could be unioned into a small number of batched paths.
Sub-linear scaling. Wall time grows 10 → 19 → 34 ms for a 20× corpus (~3.4× growth) — HNSW O(log N) traversal cost is the headline, but layer-0 ORAM read count grows even more gently (117 → 174 → 212, only 1.8×). The wall-time gap is the second-order effect: n_leaves scales with corpus (256 → 1024 → 4096), Ring-ORAM tree levels go 8 → 10 → 12, and per-path-read bytes grow accordingly. Ingest cost is dominated by the HNSW build pass (O(N · ef_construction · M) distance ops); the ORAM admit phase is small.
Correctness — recall + security tests
Test
Crate
Acceptance
Status
1K-vector top-K recall ≥ 90 % vs brute-force
compass-index/tests/recall.rs
release build · cosine over 1000 unit vectors @ D=64
loud-failure contract matches the embedder service
passing
Hybrid-mode threads ll + hl axes
gelo-snp-runner (mock mode)
ll hit + hl-relation endpoint both surface in single response
passing
Test totals. ring-oram 34 · xormm-emm 13 · compass-index 13 lib + 3 recall · compass-rest-backend 5 lib + 3 integration · light-kg-store 7 lib + 1 acceptance · lightrag-private 7 lib + 2 integration · gelo-snp-runner (mock mode) 3 lightrag routes. All release-mode where needed (compass-index recall, light-kg-store acceptance, graphrag-bench); see memory/feedback_compass_recall_release.md for the rationale.
§07
Status & gaps
what landed · what's deferred
The vertical end-to-end (client → /lightrag/ingest → encrypted store → /lightrag/query → context string) is shipped, including Local and Hybrid kg_query modes and the unknown-tenant loud-failure contract. Below: feature-by-feature status with deferrals called out, then the residual gaps grouped by axis.
Feature
Status
Notes
Skeleton crates · HkdfPolicy v2 · CompassParams
shipped
8 child keys derived; scheme_identity_fragment canonically encodes CompassParams + XorMmParams + LightRagParams for the SEV-SNP report