RXP Module Architecture
The RXP module lives atsrc/countersignal/rxp/ and implements a retrieval poisoning validation engine. It measures whether adversarial documents achieve retrieval rank in vector similarity searches across configurable embedding models.
Module Structure
Data Flow
Avalidate run follows this pipeline:
Embedding Model Registry
Source:registry.py
The registry maps shortcut IDs (e.g. minilm-l6) to EmbeddingModelConfig dataclasses containing the full HuggingFace model name, vector dimensions, and description.
Three models are registered by default. The resolve_model() function first checks the registry; if the ID is not found, it creates an ad-hoc config using the ID as the HuggingFace model name. This allows arbitrary model names (e.g. BAAI/bge-m3) to pass through without registry changes.
Embedder Abstraction
Source:embedder.py
The Embedder class wraps sentence_transformers.SentenceTransformer with two methods:
encode(texts)— Batch-encode a list of strings into float vectorssimilarity(query, candidates)— Cosine similarity via NumPy (available but not used in the current pipeline — ChromaDB handles nearest-neighbor search internally)
get_embedder()). The cache avoids reloading models when running --model all.
RetrievalCollection
Source:collection.py
RetrievalCollection wraps a ChromaDB ephemeral client. Key design decisions:
- Ephemeral storage — Uses
chromadb.Client()(in-memory). No data persists between runs. Each validation is a clean-room test. - Pre-computed embeddings — Documents are encoded by the injected
Embedder, not by ChromaDB’s built-in embedding functions. This gives full control over which model produces the vectors. - UUID-suffixed collection names — Prevents collisions when multiple collections exist in the same process.
- Poison tracking —
is_poisonandsourcemetadata are stored as ChromaDB document metadata. Poison document IDs are tracked in a set for O(1) lookup during query result mapping.
| Method | Description |
|---|---|
ingest(documents) | Encode and store a list of CorpusDocument objects |
query(query_text, top_k) | Embed the query, run nearest-neighbor search, return RetrievalHit list |
reset() | Delete and recreate the collection |
count | Number of stored documents |
Validation Engine
Source:validator.py
The validate_retrieval() function orchestrates a single validation run:
- Resolve the model config from the registry (or create ad-hoc)
- Get or create a cached
Embedderinstance - Create a
RetrievalCollectionand ingest corpus + poison documents - Run each query and build
QueryResultobjects (recording whether the poison document was retrieved and at what rank) - Aggregate into a
ValidationResultwith retrieval rate and mean poison rank
--model all is used, it calls validate_retrieval() once per registered model and prints a comparison table.
Domain Profile Loading
Source:profiles/__init__.py
Profiles are auto-discovered by scanning subdirectories of the profiles/ package for profile.yaml files. No registration is required — drop a new directory with the right structure and it appears in list-profiles.
The loader uses yaml.safe_load (not yaml.load) for safety. Corpus and poison documents are loaded from corpus/*.txt and poison/*.txt within the profile directory, with the filename stem used as the document ID.
Optional Dependency Guard
Source:_deps.py
RXP depends on sentence-transformers and chromadb, which are heavy optional dependencies. The require_rxp_deps() function checks for their presence at runtime and raises ImportError with install instructions if either is missing.
The guard is called inside the validate command body, after argument parsing. This means --help, list-models, and list-profiles all work without the optional dependencies installed. Users only need the heavy packages when actually running validation.
Install with: