Architecture
Architecture
This document describes the internal architecture of prompt-shield, covering all components, their responsibilities, data flow, and key design decisions.
System Overview
┌───────────────────────────────────┐
│ PromptShieldEngine │
│ (engine.py) │
├───────────────────────────────────┤
│ scan() feedback() │
│ scan_batch() add_canary() │
│ export_threats() check_canary() │
│ import_threats() sync_threats() │
│ register_detector() │
└──────────┬────────────────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
┌─────▼─────┐ ┌───────▼───────┐ ┌──────▼──────┐
│ Registry │ │ Attack Vault │ │ Feedback │
│ (registry │ │ (ChromaDB + │ │ System │
│ .py) │ │ Embedder) │ │ │
├────────────┤ ├──────────────┤ ├─────────────┤
│ auto_ │ │ store() │ │ FeedbackStore│
│ discover() │ │ query() │ │ AutoTuner │
│ register() │ │ remove() │ │ │
│ list_all() │ │ import/export│ │ tune() │
└─────┬──────┘ └──────┬───────┘ └──────┬──────┘
│ │ │
┌─────▼──────┐ ┌─────▼───────┐ ┌─────▼───────┐
│ Detectors │ │ ThreatFeed │ │ Database │
│ d001-d021 │ │ Manager │ │ Manager │
│ (21 files) │ │ (import/ │ │ (SQLite) │
└────────────┘ │ export/ │ └─────────────┘
│ sync) │
└─────────────┘
┌───────────────────────────────────────────────────┐
│ Integrations │
├──────────────┬──────────────┬─────────────────────┤
│ AgentGuard │ Middleware │ LLM Framework │
│ (3-gate) │ FastAPI │ LangChain callback │
│ │ Flask │ LlamaIndex handler │
│ MCP Filter │ Django │ │
└──────────────┴──────────────┴─────────────────────┘
Core Components
PromptShieldEngine (engine.py)
The central orchestrator. All public API methods go through the engine. It coordinates detectors, vault, feedback, canary, and persistence.
Construction sequence:
1. load_config() → Merge YAML + dict + env vars
2. resolve_data_dir() → Find or create data directory
3. DatabaseManager() → SQLite with WAL mode + auto-migration
4. AttackVault() → ChromaDB PersistentClient (if enabled)
5. FeedbackStore() → SQLite feedback table (if enabled)
6. AutoTuner() → Threshold adjustment engine (if enabled)
7. CanaryTokenGenerator() → Token generation (if enabled)
8. LeakDetector() → Response scanning (if enabled)
9. ThreatFeedManager() → Import/export/sync (if vault enabled)
10. DetectorRegistry() → Auto-discover + entry points
11. Wire vault into d021 → d021.vault = vault instance
12. Run detector.setup() → Pass per-detector config
13. Compile allowlist/blocklist regex patterns
Scan pipeline:
Input Text
│
├── Allowlist check → PASS (if matched)
│
├── Blocklist check → BLOCK (if matched)
│
├── For each enabled detector:
│ ├── Get effective threshold (config or auto-tuned)
│ ├── Call detector.detect(input_text, context)
│ ├── Apply severity override from config
│ └── Include if detected=True AND confidence >= threshold
│
├── Aggregate: risk_score = max(confidence)
│
├── Determine action by highest-severity detection
│
├── Log to scan_history (SQLite)
│
├── Auto-store in vault (if confidence >= min_confidence_to_store)
│
├── Auto-tune check (every tune_interval scans)
│
└── Return ScanReport
DetectorRegistry (registry.py)
Manages detector lifecycle with three discovery methods:
| Method | How | When |
|---|---|---|
| Auto-discovery | pkgutil.iter_modules on prompt_shield.detectors package |
Engine init |
| Entry points | importlib.metadata.entry_points(group="prompt_shield.detectors") |
Engine init |
| Manual | engine.register_detector(instance) |
Runtime |
The registry stores detector instances in a dict[str, BaseDetector] keyed by detector_id. It supports register(), unregister(), get(), list_all(), list_metadata(), and __contains__().
Detectors (detectors/)
All 21 built-in detectors extend BaseDetector (abstract base class). They are stateless and independent: each receives input text and optional context, and returns a structured DetectionResult.
Detector types:
| Type | Detectors | Technique |
|---|---|---|
| Pattern-based (regex) | d001-d007, d012-d019 | Compile regex patterns, iterate finditer, collect MatchDetail objects |
| Custom logic | d008, d009, d010, d011, d020 | Decode/transform input, then analyze (base64 decode, ROT13, homoglyph normalization, etc.) |
| Context-aware | d006 | Uses context["conversation_history"] for multi-turn analysis |
| Vector similarity | d021 | Queries ChromaDB vault, no regex involved |
Attack Vault (vault/attack_vault.py)
ChromaDB-backed vector store. Core responsibilities:
- store(): Embed input → SHA-256 hash as document → store in ChromaDB with metadata
- query(): Embed input → cosine nearest-neighbor search → return
VaultMatchlist - remove(): Delete entry by UUID (used for false positive cleanup)
- import_threats(): Bulk insert from
ThreatEntryobjects with deduplication - export_threats(): Extract locally-sourced entries as
ThreatEntrylist - stats(): Count total entries grouped by source
- clear(): Delete collection and recreate empty
Key implementation detail: ChromaDB’s PersistentClient stores data on disk at <data_dir>/vault/. The collection uses hnsw:space: cosine for the distance metric. An _EmbedderBridge class adapts the Embedder interface to ChromaDB’s EmbeddingFunction protocol.
Embedder (vault/embedder.py)
Thin wrapper around sentence-transformers:
- Lazy loading: The
SentenceTransformermodel is not loaded until the firstencode()call. This keeps import time fast (~0ms vs ~2-5s). - Model:
all-MiniLM-L6-v2(384 dimensions, ~22MB, CPU-only) - Cache: Model weights saved to
<data_dir>/models/ - API:
encode(text) → list[float]andencode_batch(texts) → list[list[float]]
Feedback System (feedback/)
FeedbackStore (feedback_store.py):
- SQLite CRUD for feedback entries:
record(scan_id, detector_id, is_correct, notes) get_detector_stats(detector_id)→{total, true_positives, false_positives, fp_rate}get_all_stats()→ dict of all detectors with feedback
AutoTuner (auto_tuner.py):
- Reads stats from
FeedbackStore - Adjusts thresholds in
detector_tuningSQLite table get_effective_threshold(detector_id, default)→ float (used by engine on every scan)reset(detector_id=None)→ clear tuning data
Canary System (canary/)
CanaryTokenGenerator (token_generator.py):
- Generates random hex tokens via
secrets.token_hex(length // 2) - Injects into prompts using configurable header format:
<-@!-- {canary} --@!-> - Returns
(modified_prompt, canary_token)tuple
LeakDetector (leak_detector.py):
- Checks LLM responses for full or partial canary token presence
- Partial match: any substring >= 8 characters matching the token
- Case-insensitive comparison
Threat Feed Manager (vault/threat_feed.py)
- export_feed(): Queries vault for
source: "local"entries → serializes asThreatFeedJSON - import_feed(): Validates feed file (embedding model compatibility) → bulk imports via
vault.import_threats() - sync_feed(): Downloads remote JSON → saves locally → imports
Persistence (persistence/)
DatabaseManager (database.py):
- SQLite with WAL (Write-Ahead Logging) mode for concurrent reads during middleware use
- Auto-migration on construction
connection()context managerprune_scan_history(retention_days)for automatic cleanup
Migrations (migrations.py):
CURRENT_VERSION = 1- Creates 6 tables:
schema_version,scan_history,feedback,detector_tuning,vault_log,sync_history - Creates 2 indexes:
idx_scan_history_timestamp,idx_feedback_detector
Configuration (config/__init__.py + config/default.yaml)
Design note: The config module lives at config/__init__.py (not a standalone config.py) to avoid a Python namespace conflict with the config/default.yaml file in the same directory.
Layered configuration with four sources merged in priority order:
Priority 1: Environment variables (PROMPT_SHIELD_*)
Priority 2: config_dict parameter (Python dict)
Priority 3: YAML config file (config_path parameter)
Priority 4: Built-in defaults (config/default.yaml)
Key functions:
load_config()→ Deep-merge all sourcesresolve_data_dir()→ Explicit > CWD.prompt-shield.yaml>~/.prompt_shield/get_detector_config()→ Per-detector config with global fallbacksget_action_for_severity()→ Maps severity to action stringvalidate_config()→ Returns list of error strings (empty = valid)_deep_merge()→ Recursive dict merge_apply_env_overrides()→ ReadsPROMPT_SHIELD_*env vars
Integrations (integrations/)
| Module | Class | Purpose | Key Methods |
|---|---|---|---|
agent_guard.py |
AgentGuard |
3-gate protection for agent loops | scan_input(), scan_tool_result(), scan_tool_call(), prepare_prompt(), scan_output(), scan_multi_hop() |
mcp.py |
PromptShieldMCPFilter |
Transparent MCP server proxy | call_tool(), list_tools() |
fastapi_middleware.py |
PromptShieldMiddleware |
Starlette ASGI middleware | Scans POST/PUT/PATCH bodies |
flask_middleware.py |
PromptShieldMiddleware |
WSGI middleware | Wraps wsgi.input stream |
django_middleware.py |
PromptShieldMiddleware |
Django middleware | __call__(request) |
langchain_callback.py |
PromptShieldCallback |
LangChain callback handler | on_llm_start(), on_tool_end(), on_llm_end() |
llamaindex_handler.py |
PromptShieldHandler |
LlamaIndex handler | scan_query(), scan_retrieved_nodes(), scan_response() |
CLI (cli.py)
Click-based CLI with nested command groups:
prompt-shield
├── --version
├── -c / --config-file
├── --data-dir
├── --json-output
├── scan [TEXT] [-f FILE]
├── detectors
│ ├── list
│ └── info <DETECTOR_ID>
├── config
│ ├── init [-o OUTPUT]
│ └── validate <CONFIG_FILE>
├── vault
│ ├── stats
│ ├── search <QUERY>
│ └── clear
├── feedback --scan-id <ID> [--correct|--incorrect] [--notes TEXT]
├── threats
│ ├── export -o <OUTPUT>
│ ├── import -s <SOURCE>
│ ├── sync [--url URL]
│ └── stats
├── test
└── benchmark
Data Flow Diagram
User Input (untrusted text)
│
▼
┌───────────────────────┐
│ Allowlist Check │──── Match → Action: PASS
└───────────┬───────────┘
│ No match
▼
┌───────────────────────┐
│ Blocklist Check │──── Match → Action: BLOCK (score=1.0)
└───────────┬───────────┘
│ No match
▼
┌─────────────────────────────────────┐
│ Run Enabled Detectors (d001-d021) │
│ │
│ For each detector: │
│ threshold = auto_tuned or config │
│ result = detector.detect(input) │
│ if detected AND conf >= threshold │
│ → add to detections list │
└─────────────────┬───────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Aggregate Results │
│ │
│ risk_score = max(confidence) │
│ action = severity → action mapping │
│ vault_matched = d021 in detections │
└─────────────────┬───────────────────┘
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ Log to │ │ Store in │ │ Auto-Tune │
│ SQLite │ │ Vault │ │ (periodic) │
│ History │ │ if conf │ │ every N │
│ │ │ >= 0.7 │ │ scans │
└──────────┘ └──────────┘ └──────────────┘
│
▼
ScanReport
┌─────────────────┐
│ scan_id │
│ input_hash │
│ timestamp │
│ risk_score │
│ action │
│ detections[] │
│ total_run │
│ scan_duration │
│ vault_matched │
└─────────────────┘
Pydantic Models (models.py)
All data structures are Pydantic v2 models:
| Model | Purpose | Key Fields |
|---|---|---|
Severity |
Enum | LOW, MEDIUM, HIGH, CRITICAL |
Action |
Enum | BLOCK, FLAG, LOG, PASS |
MatchDetail |
Single pattern match | pattern, matched_text, position: tuple[int,int], description |
DetectionResult |
One detector’s output | detector_id, detected, confidence, severity, matches, explanation |
ScanReport |
Aggregated scan result | scan_id, input_hash, timestamp, overall_risk_score, action, detections, total_detectors_run, scan_duration_ms, vault_matched |
GateResult |
AgentGuard gate output | gate, action, blocked, scan_report, explanation, sanitized_text, canary_leaked |
ThreatEntry |
Single threat feed entry | id, pattern_hash, embedding, detector_id, severity, confidence, first_seen, report_count, tags |
ThreatFeed |
Complete feed document | version, generated_at, generator, embedding_model, embedding_dim, total_threats, threats |
Key Design Decisions
1. Config as Package (config/__init__.py)
The config loader lives at config/__init__.py rather than config.py because the config/ directory also contains default.yaml. In Python, a directory with __init__.py takes priority as a package, so having both config.py and config/ would create an import conflict.
2. Lazy Model Loading
The sentence-transformers model is loaded on first use (not on import). This means from prompt_shield import PromptShieldEngine is fast (~0ms) even with the vault enabled. The ~2-5 second model load only happens when the first scan hits d021 or stores a detection.
3. No Raw Text Storage
The vault stores SHA-256 hashes as documents and embedding vectors for similarity search. Raw attack text is never persisted. This is a privacy-by-design decision that protects against data breach exposure.
4. SQLite with WAL Mode
WAL (Write-Ahead Logging) allows concurrent readers with a single writer, which is critical for middleware contexts where multiple HTTP requests may scan simultaneously while the auto-tuner writes threshold updates.
5. regex Over re
The regex package provides better Unicode support (character properties like \p{Cyrillic}), which is essential for the homoglyph detector (d010) and for detecting attacks in non-Latin scripts.
6. Stateless Detectors
Detectors are designed to be stateless and independent. They don’t share state with each other or maintain internal caches. This makes them safe for concurrent use and easy to test in isolation. The one exception is d006, which reads conversation history from the context parameter (but doesn’t maintain its own state).
7. Three Registration Methods
Supporting auto-discovery (for built-in detectors), entry points (for third-party packages), and runtime registration (for application-specific detectors) maximizes flexibility. The registry handles deduplication by detector_id.
File Structure
src/prompt_shield/
├── __init__.py # Public API exports
├── engine.py # Core orchestrator (258 lines)
├── registry.py # Plugin registry
├── models.py # Pydantic models
├── exceptions.py # 9 custom exceptions
├── utils.py # Homoglyph map, invisible chars, hashing, normalization
├── cli.py # Click CLI (477 lines)
├── config/
│ ├── __init__.py # Config loader (load, resolve, validate)
│ └── default.yaml # Built-in default configuration
├── detectors/
│ ├── base.py # Abstract BaseDetector
│ ├── d001_system_prompt_extraction.py
│ ├── d002_role_hijack.py
│ ├── ... (d003-d020)
│ └── d021_vault_similarity.py
├── vault/
│ ├── embedder.py # sentence-transformers wrapper
│ ├── attack_vault.py # ChromaDB vector store
│ └── threat_feed.py # Feed import/export/sync
├── feedback/
│ ├── feedback_store.py # SQLite feedback CRUD
│ └── auto_tuner.py # Threshold adjustment engine
├── canary/
│ ├── token_generator.py # Canary token creation + injection
│ └── leak_detector.py # Response scanning for leaked tokens
├── persistence/
│ ├── database.py # SQLite connection manager
│ └── migrations.py # Schema creation + versioning
└── integrations/
├── agent_guard.py # 3-gate AgentGuard
├── mcp.py # MCP server filter proxy
├── fastapi_middleware.py # Starlette ASGI middleware
├── flask_middleware.py # WSGI middleware
├── django_middleware.py # Django middleware
├── langchain_callback.py # LangChain lifecycle hooks
└── llamaindex_handler.py # LlamaIndex query/retrieval handler