Research Prototype AI Security Middleware, Guardrail Gateway & Compliance Observability System
Evaluation status: every number in this README is reproducible from
docs/ENGINEERING_ASSESSMENT.mdand the_evidence/directory — detector comparisons, fusion validation, and the end-to-end benchmark all include bootstrap confidence intervals and are re-runnable. Concurrent-throughput has not been benchmarked yet — see §9 and the roadmap (§23).
Gatekeeper is a research prototype AI Governance Platform and AI Security Gateway. Sitting between end-users and Large Language Models (LLMs), Gatekeeper intercepts, sanitizes, and evaluates incoming prompts before they hit downstream inference endpoints. By fusing deterministic symbolic rules with semantic vector search (FAISS), it enforces corporate guardrails, privacy compliance (GDPR/HIPAA), and prompt-injection defense.
+------------------+ +-----------------------+ +-------------------+
| Client App | ---> | Gatekeeper API Gateway | ---> | Local LLM |
| (Streamlit/REST) | <--- | (FastAPI Microservice) | <--- | (Ollama Engine) |
+------------------+ +-----------+-----------+ +-------------------+
|
v
+-------------------+
| Immutable JSONL |
| Audit Log Engine |
+-------------------+
Gatekeeper is engineered to address a critical security gap in modern AI adoption: the non-deterministic nature of raw LLM prompts. In enterprise environments, relying purely on LLM system prompts for safety is a known vulnerability, subject to bypass via jailbreaks, obfuscation, and prompt injection attacks.
Gatekeeper solves this by acting as a fail-closed, policy-enforcing API gateway. The platform is built on two core principles:
Gatekeeper is not a chat UI or a wrapper; it is backend governance infrastructure built for reliability and auditability.
Key engineering outcomes:
Gatekeeper utilizes a clean, decoupled microservices model to separate client concerns from high-performance machine learning workloads:
Gatekeeper Client UI (ui/login/, ui/activity/, ui/review/, ui/trace/, ui/gateways/, ui/logs/, ui/benchmarks/, ui/policy/, ui/settings/): Static pages served directly by api/main.py (mounted at /ui/, no separate container or build step) that talk to the real Gatekeeper API — sign-in, an activity feed and per-request trace over the real audit log, the human-review approval queue, and (INTERNAL capability) the model/tool gateway catalogues, raw logs, benchmark results, and a policy editor that validates before it ever deploys. This is the UI actually exercised by this project's own test suite and CI.
The client UI ships inside the gatekeeper-api image/container,
reachable at http://<gatekeeper-api host>:8000/ui/login/index.html —
no separate service required.
Gatekeeper API Gateway (api/main.py): An asynchronous FastAPI service that exposes assessment and configuration endpoints, processes payloads, and manages the execution flow.
Neuro-Symbolic Engine (core/): The core evaluation system containing distinct detection components, including normalizers, classifiers, threat vectorizers, and local semantic judges.
Vector Store (core/vector_store.py): Powered by Facebook AI Similarity Search (FAISS) for sub-millisecond similarity calculations against known threat anchors and educational safe harbors.
Local LLM Engine (Ollama): Handles judge-level arbitration for ambiguous requests — model configurable via OLLAMA_MODEL (default mistral; validated in this project's own evaluation with llama3.2) — in a self-hosted network boundary.
GENERAL, ELEVATED, INTERNAL) to allow, block, or restrict execution.HIGH risk level and blocks execution to protect core data.X-Process-Time).The flow of a user prompt through the Gatekeeper Gateway:
graph TD
User([User client]) -->|POST /api/v1/assess <br/> Authorization: Bearer key| API[FastAPI Gateway <br/> Port 8000]
API --> Auth{Capability Resolution <br/> verified API key, never <br/> client-asserted role}
subgraph Privacy Shield & Normalization
Auth --> Normalizer[Text Normalizer]
Normalizer --> PII[PII Redaction <br/> SpaCy NER / regex]
end
subgraph Symbolic Veto
PII --> Symbolic{Symbolic Veto <br/> Hardcoded Regex}
Symbolic -->|Match| BlockHigh[Block / Risk: HIGH]
end
subgraph Neuro-Symbolic Engine
Symbolic -->|Clean| CacheCheck{Semantic Cache <br/> exact hash, then fuzzy FAISS}
CacheCheck -->|Hit| CacheReturn[Return Cached Risk]
CacheCheck -->|Miss| Fusion[Learned Fusion <br/> anchors + ProtectAI + <br/> jailbreak clf + toxic-BERT]
Fusion --> Domain{Domain Gate <br/> aligned? off by default}
Domain -->|Off-Topic, enforcing mode| RiskMed[Risk: MEDIUM]
Domain -->|Aligned / off| ThresholdCheck{Fused Score vs <br/> Calibrated Threshold}
ThresholdCheck -->|>= High| RiskHigh[Risk: HIGH]
ThresholdCheck -->|< Medium| RiskLow[Risk: LOW]
ThresholdCheck -->|Ambiguous| Context{Safe Harbor <br/> Context?}
Context -->|Educational| SafeHarbor[Risk: MEDIUM / Allow]
Context -->|Adversarial| Judge{Semantic Judge <br/> local LLM via Ollama}
Judge -->|DANGEROUS| FinalHigh[Risk: HIGH]
Judge -->|SAFE| Overridden[Risk: LOW]
Judge -->|Failure/Unreachable| FailClosed[Risk: HIGH]
end
subgraph Policy Arbitration & Output
RiskHigh --> Arbiter{Policy Arbiter <br/> server-resolved capability}
RiskLow --> Arbiter
SafeHarbor --> Arbiter
FinalHigh --> Arbiter
Overridden --> Arbiter
FailClosed --> Arbiter
Arbiter -->|ALLOW| LLM[Downstream LLM <br/> Ollama Port 11434]
Arbiter -->|BLOCK / RESTRICT| BlockResponse[Gateway Block Response]
end
Arbiter -->|JSON Event| Logger[(JSON Audit Log <br/> audit.jsonl)]
If any live detector in the fusion is unavailable, the pipeline falls back to
the original anchors-only decision path rather than failing the request — see
core/fusion.py.
The core governance pipeline is orchestrated via a staged execution model in core/risk.py:
CACHE_SIMILARITY_THRESHOLD, default 0.99 — see core/cache.py for why an aggressive default like 0.95 is measurably unsafe on this kind of data).docs/ENGINEERING_ASSESSMENT.md), not hand-tuned constants. If any detector is unavailable at request time, the pipeline falls back to the original anchors-only signal rather than failing the request.OLLAMA_MODEL; default mistral, validated in evaluation with llama3.2) to determine context safety. If the model is unreachable, the system fails closed (Risk: HIGH) — this failure mode is itself covered by docs/ENGINEERING_ASSESSMENT.md's methodology section, since a benchmark run against an unreachable judge silently measures judge uptime rather than detection quality.Real, measured performance (not the single-detector, pre-fusion pipeline): on the deepset/prompt-injections benchmark with a live judge, wiring the fusion into this pipeline moved end-to-end recall from 30.0% to 63.6% (F1 0.44 → 0.71), and fixing the cache's collision behavior brought warm-cache accuracy to exactly match cold-cache. See §9.
en_core_web_sm model for NER), Sentence-Transformers (all-mpnet-base-v2 for prompt vectorization)core/detectors.py) wrapping HuggingFace transformers classifiers (ProtectAI injection detector, a jailbreak classifier, a toxicity classifier, optionally Meta Prompt Guard 2 / Llama Guard 3 where licensed) alongside the project's own anchor detectorLogisticRegression + StandardScaler, trained by scripts/train_fusion_policy.py and persisted as plain JSON (models/fusion_policy.json) — no pickle, no sklearn-version coupling at deploy timecore/auth.py), zero-trust default — capability is resolved server-side from a verified credential, never asserted by the clientpython-json-logger (Structured JSON log formats)Concurrent-throughput has not been benchmarked yet.
benchmarks/run_load_test.py is a runnable load-testing tool; publishing a
P50/P95/P99 + error-rate result from it is on the roadmap (§23).
Every figure below is measured, with sources and bootstrap confidence
intervals, and reproducible from docs/ENGINEERING_ASSESSMENT.md
and _evidence/*.json.
| Detector | AUC | Recall @ 5% FPR |
|---|---|---|
| Prompt Guard 2 (Meta, gated) | 0.949 [0.942, 0.956] | 83.9% |
| ProtectAI injection classifier | 0.909 [0.899, 0.919] | 79.7% |
| Project's own anchor detector | 0.890 [0.880, 0.900] | 70.0% |
| Learned fusion (5 detectors, out-of-fold) | 0.952 [0.945, 0.958] | 86.1% |
Every number in this table carries a 1,000-resample bootstrap confidence interval. The fusion's advantage over the single best detector is not statistically decisive on pooled AUC (overlapping intervals) — what fusion reliably buys instead is per-class coverage: the best single detector scored 91.8%/91.2% on injection/jailbreak but only 2.0% on harmful-content requests, which the pooled metric hides entirely. See §9c and the assessment doc for the full per-class breakdown.
Measured on the identical 546-prompt deepset/prompt-injections benchmark,
judge reachable (methodology gate in tests/benchmark.py aborts the run
otherwise, rather than silently measuring judge uptime):
| Recall | Precision | F1 | FPR | |
|---|---|---|---|---|
| Anchors-only pipeline (original) | 30.0% | 82.4% | 0.44 | 3.8% |
| Fusion wired into the live pipeline | 63.6% | 81.1% | 0.71 | 8.8% |
| Fusion + cache fix, cold cache | 64.0% | 81.8% | 0.72 | 8.5% |
| Fusion + cache fix, warm cache | 64.0% | 81.8% | 0.72 | 8.5% |
Two things worth calling out explicitly:
scripts/diagnose_cache_threshold.py.| Detector | Harmful-content detection @ 5% FPR |
|---|---|
| ProtectAI injection classifier | 2.0% |
| Project's own anchor detector | 22–24% |
| Toxicity classifier | 36.2% |
| Llama Guard 3 1B (Meta, gated; offline evaluation only) | 60.2% |
| Learned fusion + Llama Guard (offline evaluation only) | 62.6% |
Important scope note: the Llama Guard figures above are from an offline, stratified-sample evaluation. Llama Guard is not wired into the live pipeline — a 1B-parameter generative model takes 17–27 seconds per prompt on CPU, far too slow for synchronous request serving. Live harmful-content detection today is close to the anchor-only baseline (~24%). Wiring Llama Guard in as a judge-arbitration-stage arbiter (invoked only for the small fraction of ambiguous-zone traffic, not every request) is the identified next step — see §23.
Traditional database vector lookups often iterate linearly ($O(N)$), causing latency to scale with the number of signatures.
Gatekeeper addresses this by replacing brute-force list comparisons with an in-memory FAISS Flat Inner Product Index (faiss.IndexFlatIP). During startup, Gatekeeper pre-loads all policy signatures, vectorizes them, and builds index stores:
# core/vector_store.py
import faiss
import numpy as np
class ScalableVectorStore:
def __init__(self, dimension: int = 768):
self.dimension = dimension
self.index = faiss.IndexFlatIP(dimension)
self.texts = []
def add_texts(self, texts: list):
if not texts:
return
embeddings = [get_embedding(t) for t in texts]
vectors = np.array(embeddings).astype('float32')
# Normalize vectors for Cosine/Inner Product similarity
faiss.normalize_L2(vectors)
self.index.add(vectors)
self.texts.extend(texts)
def get_max_similarity(self, query_vector) -> float:
if self.index.ntotal == 0:
return 0.0
q_vec = np.array([query_vector]).astype('float32')
faiss.normalize_L2(q_vec)
D, I = self.index.search(q_vec, 1)
return float(D[0][0])
By normalizing vectors under IndexFlatIP, the dot product matches cosine similarity scores, allowing sub-millisecond retrieval of the closest threat vectors even as index density expands to tens of thousands of signatures.
Gatekeeper achieves audit-grade traceability by routing logs through an asynchronous structured JSON logging pipeline. Every API evaluation, cache hit, and policy bypass is logged to audit.jsonl. Note that the raw prompt is never written to the audit log — only its SHA-256 hash — so the audit trail itself cannot leak the content it is auditing:
{
"timestamp": "2026-07-31T12:05:32.148Z",
"capability": "GENERAL",
"risk": "HIGH",
"decision": "BLOCK",
"prompt_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b85",
"semantic_score": 0.91,
"source": "fusion_threat_critical",
"educational_context": false,
"domain_score": null,
"symbolic_triggered": false,
"judge_invoked": false,
"dynamic_threat_score": 0.0
}
capability reflects the server-resolved tier — resolved from a verified API
key, never from a client-supplied field (§12) — and source distinguishes
which decision system actually fired (fusion_* when the learned fusion
decided, the legacy vector_* labels if it fell back to the anchors-only
path, cache/cache_locked_high on a cache hit, symbolic_rule on a Stage-1
veto). These structured, single-line logs are designed to be ingested directly
by central log aggregators like ElasticSearch, Splunk, or Datadog
Agents for real-time alerting and historical compliance reviews.
The Gateway enforces data schemas using Pydantic, ensuring that invalid input structures are filtered out before reaching any downstream models.
Capability is resolved server-side. The request schema accepts no client-supplied role or capability field (
extra="forbid"— an unknown field returns422); a caller's capability tier is derived only from a verified API key. See core/auth.py andtests/test_auth.py.
# api/schemas.py
from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional
class AssessRequest(BaseModel):
prompt: str = Field(..., min_length=1, max_length=50_000, description="Prompt payload to assess")
# NOTE: no `role` field. A client may present a credential; it may not
# declare its own privilege. model_config extra="forbid" rejects any
# attempt to smuggle one in.
model_config = {"extra": "forbid"}
class AssessResponse(BaseModel):
decision: str = Field(..., description="Action verdict: ALLOW, BLOCK, or RESTRICT")
risk_level: str = Field(..., description="Calculated risk: LOW, MEDIUM, or HIGH")
capability: str = Field("GENERAL", description="Capability tier resolved server-side from the credential")
authenticated: bool = Field(False, description="Whether a valid API key was presented")
details: Dict[str, Any] = Field(..., description="Metadata and execution timings")
clean_prompt: str = Field(..., description="Prompt text after PII redaction")
redacted_items: List[str] = Field(default_factory=list, description="List of redacted sensitive elements")
process_time_ms: float = Field(..., description="Execution time within the API gateway layer")
Present an API key as a bearer token: Authorization: Bearer <key>. Anonymous
requests are served at GENERAL (least privilege) by default
(AUTH_MODE=optional); set AUTH_MODE=required to reject anonymous callers
with 401 instead. Keys are stored as SHA-256 hashes only — the plaintext is
shown once at issuance and is not recoverable:
python -m scripts.manage_api_keys issue --capability ELEVATED --tenant acme
python -m scripts.manage_api_keys list
python -m scripts.manage_api_keys revoke --key-id acme-elevated-01
Gatekeeper provides a containerized multi-service configuration in docker-compose.yml to ensure consistent execution environments across staging and production.
The snippet below is illustrative and has drifted from the real
docker-compose.yml (which now uses named volumes with directory-level
mounts, not the single-file bind mounts shown here, and adds redis,
model-pull, prometheus, and grafana services) — treat the real file
as authoritative. There is no gatekeeper-ui service in the real file
(removed in Phase 8 hardening, see §3 above) — the client UI ships
inside gatekeeper-api itself and needs no separate service.
version: '3.8'
services:
gatekeeper-api:
build:
context: .
dockerfile: Dockerfile.api
ports:
- "8000:8000"
environment:
- OLLAMA_API_URL=http://ollama:11434/api/generate
volumes:
- ./data:/app/data
- ./policies:/app/policies
- ./policies.json:/app/policies.json
- ./policy_rules.json:/app/policy_rules.json
- ./audit.jsonl:/app/audit.jsonl
networks:
- gatekeeper_net
ollama:
image: ollama/ollama:latest
ports:
- "11434:11434"
volumes:
- ollama_data:/root/.ollama
networks:
- gatekeeper_net
networks:
gatekeeper_net:
driver: bridge
volumes:
ollama_data:
POST /api/v1/assessMain governance endpoint. Intercepts and assesses prompt payloads. Capability
comes from the Authorization header, not the request body (§12) — an
anonymous request (no header) is evaluated at GENERAL.
{
"prompt": "Call John Doe at 555-0199 and check system health."
}
Authorization: Bearer <api-key> (optional; omit for anonymous/GENERAL)
{
"decision": "ALLOW",
"risk_level": "LOW",
"capability": "GENERAL",
"authenticated": false,
"details": {
"semantic_score": 0.09,
"source": "fusion_clean_pass",
"educational_context": false,
"domain_score": null,
"symbolic_triggered": false,
"judge_invoked": false,
"dynamic_threat_score": 0.0,
"fusion_available": true,
"anchor_threat_score": 0.11,
"policy_reason": "No policy constraints triggered for general access."
},
"clean_prompt": "Call [REDACTED_PERSON] at [REDACTED_PHONE] and check system health.",
"redacted_items": ["John Doe", "555-0199"],
"process_time_ms": 14.2
}
source: "fusion_*" means the learned fusion made the decision;
"vector_*" / "clean_pass" (no fusion_ prefix) means it fell back to
the anchors-only path because a live detector was unavailable — see
details.fusion_detail for why.POST /api/v1/updateTriggers an asynchronous sync of the local vector store and regex matrices with dynamic intelligence feeds.
{"status": "success", "signatures_added": 12}POST /api/v1/cache/flushInvalidates all entries in the local semantic vector cache.
{"status": "success"}GET /healthReturns per-dependency status; the overall status degrades if any check fails — it does not report a bare "healthy" regardless of actual state.
{
"status": "healthy",
"checks": {
"policy_files": true,
"spacy_model": true,
"embedding_model": true,
"semantic_judge": true
}
}
To boot up the entire Gatekeeper gateway stack with a single command:
git clone https://github.com/pavann19/Gatekeeper-AI-Infrastructure-and-Governance-Gateway.git
cd Gatekeeper-AI-Infrastructure-and-Governance-Gateway
cp .env.example .env
docker-compose up --build
If you prefer to run the service locally without Docker:
python -m venv venv
# Windows:
.\venv\Scripts\activate
# macOS/Linux:
source venv/bin/activate
pip install -r requirements.txt
python -m spacy download en_core_web_sm
.env file to point to your local endpoints:
OLLAMA_API_URL=http://localhost:11434/api/generate
POLICY_FILE=policies.json
uvicorn api.main:app --host 127.0.0.1 --port 8000 --reload
http://127.0.0.1:8000/ui/login/index.html.These are the tools that actually produced every number in §9. All are reproducible; none require guessing at a threshold or a result.
python -m scripts.build_eval_suite
Every detector is probed against canonical attack/benign pairs before its numbers are trusted — a detector wired backwards still returns well-formed probabilities, just inverted ones.
python -m scripts.compare_detectors --bootstrap 1000
python -m scripts.ensemble_analysis
python -m scripts.train_fusion_policy
Aborts rather than running if the judge is unreachable — a benchmark against an offline judge silently measures judge uptime, not detection quality.
PYTHONPATH=. python tests/benchmark.py
A runnable async load-testing tool. No throughput result from it is published in this README yet (see §9).
python -m benchmarks.run_load_test
ALLOWBLOCKRESTRICT (or ALLOW for ELEVATED roles)Here are the primary control layouts of the running application:


gatekeeper/
├── .github/workflows/ci.yml # CI: pytest, 1,644 tests, torch/faiss-cpu included
├── api/
│ ├── main.py # FastAPI Application Entry
│ └── schemas.py # Pydantic Schemas (no client-supplied role)
├── benchmarks/
│ ├── evaluate_accuracy.py
│ └── run_load_test.py # Load-test tool; no published result yet (§9)
├── core/
│ ├── auth.py # API-key capability resolution (zero-trust default)
│ ├── cache.py # Semantic cache: exact-hash tier + calibrated fuzzy match
│ ├── config.py # Pydantic Configuration Settings
│ ├── detectors.py # Pluggable detector registry (ProtectAI, jailbreak
│ │ # classifier, toxicity classifier, gated Prompt
│ │ # Guard 2 / Llama Guard 3)
│ ├── domain_classifier.py # Domain Verification logic (topicality, not safety)
│ ├── embeddings.py # Sentence-Transformer wrapper
│ ├── fusion.py # Applies the trained fusion policy at request time
│ ├── normalizer.py # Obfuscation Normalizer
│ ├── output_guardrails.py # Output-side toxicity/PII check
│ ├── policy.py # Access Rule Evaluator
│ ├── policy_loader.py # Policy JSON parsing
│ ├── privacy.py # Regex + SpaCy NER redaction engine
│ ├── risk.py # Governance Pipeline Orchestrator
│ ├── semantic_judge.py # Downstream Judge LLM client
│ ├── threat_centroid.py # Diagnostic centroid signal (not decision-bearing)
│ └── vector_store.py # FAISS Vector Index Wrapper
├── data/
│ └── eval_suite.jsonl # 6,933-prompt, 7-source labelled eval suite (generated)
├── docs/
│ ├── ENGINEERING_ASSESSMENT.md # Every measured finding, with evidence and caveats
│ └── EVALUATION_METHODOLOGY.md
├── evaluation/
│ └── metrics.py # AUC, recall@FPR, bootstrap CIs
├── models/
│ └── fusion_policy.json # Trained fusion weights (plain JSON, not pickled)
├── policies/
│ ├── domain_anchors.json
│ └── symbolic_rules.json
├── scripts/
│ ├── build_eval_suite.py
│ ├── calibrate_thresholds.py
│ ├── compare_detectors.py # Detector comparison with polarity self-check
│ ├── diagnose_cache_threshold.py
│ ├── ensemble_analysis.py # Out-of-fold fusion validation
│ ├── manage_api_keys.py # issue / list / revoke / verify
│ └── train_fusion_policy.py # Fits and persists models/fusion_policy.json
├── tests/ # 1,644 tests: auth bypass regression, fusion fail-closed
│ └── ... # contract, cache exact-match regression, detectors
├── ui/ # Real client UI (login, activity, review, trace,
│ └── ... # gateways, logs, benchmarks, policy, settings) —
│ # static pages served by api/main.py, see §3
├── docker-compose.yml
├── Dockerfile.api
├── requirements.txt # Production dependencies
├── requirements-ci.txt # CI dependencies (see file header for what's excluded/why)
└── README.md
Gatekeeper runs an automated workflow on every push and pull request using GitHub Actions (.github/workflows/ci.yml), currently green at 1,644 passing tests:
name: Gatekeeper CI Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.10
uses: actions/setup-python@v4
with:
python-version: '3.10'
cache: 'pip'
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-ci.txt
python -m spacy download en_core_web_sm
- name: Run Pytest
run: |
pytest tests/ -v --tb=short
requirements-ci.txt deliberately includes torch and faiss-cpu — several
tests exercise the real FAISS index and real tensor/label-resolution logic
against synthetic data rather than mocking those libraries away. Only
sentence-transformers and real HuggingFace model downloads remain excluded,
since those need network access and GB-scale weights.
Gatekeeper aligns with the following security standards:
Identified from measurement, in priority order:
benchmarks/run_load_test.py and publish P50/P95/P99 and error rate under realistic concurrency.Longer-term / aspirational:
This project is licensed under the MIT License - see the LICENSE file for details.
git checkout -b feature/AmazingFeature).pytest cases.main branch.160 commits
Python
95.2%
HTML
4.3%
Research Prototype AI Security Middleware, Guardrail Gateway & Compliance Observability System
Evaluation status: every number in this README is reproducible from
docs/ENGINEERING_ASSESSMENT.mdand the_evidence/directory — detector comparisons, fusion validation, and the end-to-end benchmark all include bootstrap confidence intervals and are re-runnable. Concurrent-throughput has not been benchmarked yet — see §9 and the roadmap (§23).
Gatekeeper is a research prototype AI Governance Platform and AI Security Gateway. Sitting between end-users and Large Language Models (LLMs), Gatekeeper intercepts, sanitizes, and evaluates incoming prompts before they hit downstream inference endpoints. By fusing deterministic symbolic rules with semantic vector search (FAISS), it enforces corporate guardrails, privacy compliance (GDPR/HIPAA), and prompt-injection defense.
+------------------+ +-----------------------+ +-------------------+
| Client App | ---> | Gatekeeper API Gateway | ---> | Local LLM |
| (Streamlit/REST) | <--- | (FastAPI Microservice) | <--- | (Ollama Engine) |
+------------------+ +-----------+-----------+ +-------------------+
|
v
+-------------------+
| Immutable JSONL |
| Audit Log Engine |
+-------------------+
Gatekeeper is engineered to address a critical security gap in modern AI adoption: the non-deterministic nature of raw LLM prompts. In enterprise environments, relying purely on LLM system prompts for safety is a known vulnerability, subject to bypass via jailbreaks, obfuscation, and prompt injection attacks.
Gatekeeper solves this by acting as a fail-closed, policy-enforcing API gateway. The platform is built on two core principles:
Gatekeeper is not a chat UI or a wrapper; it is backend governance infrastructure built for reliability and auditability.
Key engineering outcomes:
Gatekeeper utilizes a clean, decoupled microservices model to separate client concerns from high-performance machine learning workloads:
Gatekeeper Client UI (ui/login/, ui/activity/, ui/review/, ui/trace/, ui/gateways/, ui/logs/, ui/benchmarks/, ui/policy/, ui/settings/): Static pages served directly by api/main.py (mounted at /ui/, no separate container or build step) that talk to the real Gatekeeper API — sign-in, an activity feed and per-request trace over the real audit log, the human-review approval queue, and (INTERNAL capability) the model/tool gateway catalogues, raw logs, benchmark results, and a policy editor that validates before it ever deploys. This is the UI actually exercised by this project's own test suite and CI.
The client UI ships inside the gatekeeper-api image/container,
reachable at http://<gatekeeper-api host>:8000/ui/login/index.html —
no separate service required.
Gatekeeper API Gateway (api/main.py): An asynchronous FastAPI service that exposes assessment and configuration endpoints, processes payloads, and manages the execution flow.
Neuro-Symbolic Engine (core/): The core evaluation system containing distinct detection components, including normalizers, classifiers, threat vectorizers, and local semantic judges.
Vector Store (core/vector_store.py): Powered by Facebook AI Similarity Search (FAISS) for sub-millisecond similarity calculations against known threat anchors and educational safe harbors.
Local LLM Engine (Ollama): Handles judge-level arbitration for ambiguous requests — model configurable via OLLAMA_MODEL (default mistral; validated in this project's own evaluation with llama3.2) — in a self-hosted network boundary.
GENERAL, ELEVATED, INTERNAL) to allow, block, or restrict execution.HIGH risk level and blocks execution to protect core data.X-Process-Time).The flow of a user prompt through the Gatekeeper Gateway:
graph TD
User([User client]) -->|POST /api/v1/assess <br/> Authorization: Bearer key| API[FastAPI Gateway <br/> Port 8000]
API --> Auth{Capability Resolution <br/> verified API key, never <br/> client-asserted role}
subgraph Privacy Shield & Normalization
Auth --> Normalizer[Text Normalizer]
Normalizer --> PII[PII Redaction <br/> SpaCy NER / regex]
end
subgraph Symbolic Veto
PII --> Symbolic{Symbolic Veto <br/> Hardcoded Regex}
Symbolic -->|Match| BlockHigh[Block / Risk: HIGH]
end
subgraph Neuro-Symbolic Engine
Symbolic -->|Clean| CacheCheck{Semantic Cache <br/> exact hash, then fuzzy FAISS}
CacheCheck -->|Hit| CacheReturn[Return Cached Risk]
CacheCheck -->|Miss| Fusion[Learned Fusion <br/> anchors + ProtectAI + <br/> jailbreak clf + toxic-BERT]
Fusion --> Domain{Domain Gate <br/> aligned? off by default}
Domain -->|Off-Topic, enforcing mode| RiskMed[Risk: MEDIUM]
Domain -->|Aligned / off| ThresholdCheck{Fused Score vs <br/> Calibrated Threshold}
ThresholdCheck -->|>= High| RiskHigh[Risk: HIGH]
ThresholdCheck -->|< Medium| RiskLow[Risk: LOW]
ThresholdCheck -->|Ambiguous| Context{Safe Harbor <br/> Context?}
Context -->|Educational| SafeHarbor[Risk: MEDIUM / Allow]
Context -->|Adversarial| Judge{Semantic Judge <br/> local LLM via Ollama}
Judge -->|DANGEROUS| FinalHigh[Risk: HIGH]
Judge -->|SAFE| Overridden[Risk: LOW]
Judge -->|Failure/Unreachable| FailClosed[Risk: HIGH]
end
subgraph Policy Arbitration & Output
RiskHigh --> Arbiter{Policy Arbiter <br/> server-resolved capability}
RiskLow --> Arbiter
SafeHarbor --> Arbiter
FinalHigh --> Arbiter
Overridden --> Arbiter
FailClosed --> Arbiter
Arbiter -->|ALLOW| LLM[Downstream LLM <br/> Ollama Port 11434]
Arbiter -->|BLOCK / RESTRICT| BlockResponse[Gateway Block Response]
end
Arbiter -->|JSON Event| Logger[(JSON Audit Log <br/> audit.jsonl)]
If any live detector in the fusion is unavailable, the pipeline falls back to
the original anchors-only decision path rather than failing the request — see
core/fusion.py.
The core governance pipeline is orchestrated via a staged execution model in core/risk.py:
CACHE_SIMILARITY_THRESHOLD, default 0.99 — see core/cache.py for why an aggressive default like 0.95 is measurably unsafe on this kind of data).docs/ENGINEERING_ASSESSMENT.md), not hand-tuned constants. If any detector is unavailable at request time, the pipeline falls back to the original anchors-only signal rather than failing the request.OLLAMA_MODEL; default mistral, validated in evaluation with llama3.2) to determine context safety. If the model is unreachable, the system fails closed (Risk: HIGH) — this failure mode is itself covered by docs/ENGINEERING_ASSESSMENT.md's methodology section, since a benchmark run against an unreachable judge silently measures judge uptime rather than detection quality.Real, measured performance (not the single-detector, pre-fusion pipeline): on the deepset/prompt-injections benchmark with a live judge, wiring the fusion into this pipeline moved end-to-end recall from 30.0% to 63.6% (F1 0.44 → 0.71), and fixing the cache's collision behavior brought warm-cache accuracy to exactly match cold-cache. See §9.
en_core_web_sm model for NER), Sentence-Transformers (all-mpnet-base-v2 for prompt vectorization)core/detectors.py) wrapping HuggingFace transformers classifiers (ProtectAI injection detector, a jailbreak classifier, a toxicity classifier, optionally Meta Prompt Guard 2 / Llama Guard 3 where licensed) alongside the project's own anchor detectorLogisticRegression + StandardScaler, trained by scripts/train_fusion_policy.py and persisted as plain JSON (models/fusion_policy.json) — no pickle, no sklearn-version coupling at deploy timecore/auth.py), zero-trust default — capability is resolved server-side from a verified credential, never asserted by the clientpython-json-logger (Structured JSON log formats)Concurrent-throughput has not been benchmarked yet.
benchmarks/run_load_test.py is a runnable load-testing tool; publishing a
P50/P95/P99 + error-rate result from it is on the roadmap (§23).
Every figure below is measured, with sources and bootstrap confidence
intervals, and reproducible from docs/ENGINEERING_ASSESSMENT.md
and _evidence/*.json.
| Detector | AUC | Recall @ 5% FPR |
|---|---|---|
| Prompt Guard 2 (Meta, gated) | 0.949 [0.942, 0.956] | 83.9% |
| ProtectAI injection classifier | 0.909 [0.899, 0.919] | 79.7% |
| Project's own anchor detector | 0.890 [0.880, 0.900] | 70.0% |
| Learned fusion (5 detectors, out-of-fold) | 0.952 [0.945, 0.958] | 86.1% |
Every number in this table carries a 1,000-resample bootstrap confidence interval. The fusion's advantage over the single best detector is not statistically decisive on pooled AUC (overlapping intervals) — what fusion reliably buys instead is per-class coverage: the best single detector scored 91.8%/91.2% on injection/jailbreak but only 2.0% on harmful-content requests, which the pooled metric hides entirely. See §9c and the assessment doc for the full per-class breakdown.
Measured on the identical 546-prompt deepset/prompt-injections benchmark,
judge reachable (methodology gate in tests/benchmark.py aborts the run
otherwise, rather than silently measuring judge uptime):
| Recall | Precision | F1 | FPR | |
|---|---|---|---|---|
| Anchors-only pipeline (original) | 30.0% | 82.4% | 0.44 | 3.8% |
| Fusion wired into the live pipeline | 63.6% | 81.1% | 0.71 | 8.8% |
| Fusion + cache fix, cold cache | 64.0% | 81.8% | 0.72 | 8.5% |
| Fusion + cache fix, warm cache | 64.0% | 81.8% | 0.72 | 8.5% |
Two things worth calling out explicitly:
scripts/diagnose_cache_threshold.py.| Detector | Harmful-content detection @ 5% FPR |
|---|---|
| ProtectAI injection classifier | 2.0% |
| Project's own anchor detector | 22–24% |
| Toxicity classifier | 36.2% |
| Llama Guard 3 1B (Meta, gated; offline evaluation only) | 60.2% |
| Learned fusion + Llama Guard (offline evaluation only) | 62.6% |
Important scope note: the Llama Guard figures above are from an offline, stratified-sample evaluation. Llama Guard is not wired into the live pipeline — a 1B-parameter generative model takes 17–27 seconds per prompt on CPU, far too slow for synchronous request serving. Live harmful-content detection today is close to the anchor-only baseline (~24%). Wiring Llama Guard in as a judge-arbitration-stage arbiter (invoked only for the small fraction of ambiguous-zone traffic, not every request) is the identified next step — see §23.
Traditional database vector lookups often iterate linearly ($O(N)$), causing latency to scale with the number of signatures.
Gatekeeper addresses this by replacing brute-force list comparisons with an in-memory FAISS Flat Inner Product Index (faiss.IndexFlatIP). During startup, Gatekeeper pre-loads all policy signatures, vectorizes them, and builds index stores:
# core/vector_store.py
import faiss
import numpy as np
class ScalableVectorStore:
def __init__(self, dimension: int = 768):
self.dimension = dimension
self.index = faiss.IndexFlatIP(dimension)
self.texts = []
def add_texts(self, texts: list):
if not texts:
return
embeddings = [get_embedding(t) for t in texts]
vectors = np.array(embeddings).astype('float32')
# Normalize vectors for Cosine/Inner Product similarity
faiss.normalize_L2(vectors)
self.index.add(vectors)
self.texts.extend(texts)
def get_max_similarity(self, query_vector) -> float:
if self.index.ntotal == 0:
return 0.0
q_vec = np.array([query_vector]).astype('float32')
faiss.normalize_L2(q_vec)
D, I = self.index.search(q_vec, 1)
return float(D[0][0])
By normalizing vectors under IndexFlatIP, the dot product matches cosine similarity scores, allowing sub-millisecond retrieval of the closest threat vectors even as index density expands to tens of thousands of signatures.
Gatekeeper achieves audit-grade traceability by routing logs through an asynchronous structured JSON logging pipeline. Every API evaluation, cache hit, and policy bypass is logged to audit.jsonl. Note that the raw prompt is never written to the audit log — only its SHA-256 hash — so the audit trail itself cannot leak the content it is auditing:
{
"timestamp": "2026-07-31T12:05:32.148Z",
"capability": "GENERAL",
"risk": "HIGH",
"decision": "BLOCK",
"prompt_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b85",
"semantic_score": 0.91,
"source": "fusion_threat_critical",
"educational_context": false,
"domain_score": null,
"symbolic_triggered": false,
"judge_invoked": false,
"dynamic_threat_score": 0.0
}
capability reflects the server-resolved tier — resolved from a verified API
key, never from a client-supplied field (§12) — and source distinguishes
which decision system actually fired (fusion_* when the learned fusion
decided, the legacy vector_* labels if it fell back to the anchors-only
path, cache/cache_locked_high on a cache hit, symbolic_rule on a Stage-1
veto). These structured, single-line logs are designed to be ingested directly
by central log aggregators like ElasticSearch, Splunk, or Datadog
Agents for real-time alerting and historical compliance reviews.
The Gateway enforces data schemas using Pydantic, ensuring that invalid input structures are filtered out before reaching any downstream models.
Capability is resolved server-side. The request schema accepts no client-supplied role or capability field (
extra="forbid"— an unknown field returns422); a caller's capability tier is derived only from a verified API key. See core/auth.py andtests/test_auth.py.
# api/schemas.py
from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional
class AssessRequest(BaseModel):
prompt: str = Field(..., min_length=1, max_length=50_000, description="Prompt payload to assess")
# NOTE: no `role` field. A client may present a credential; it may not
# declare its own privilege. model_config extra="forbid" rejects any
# attempt to smuggle one in.
model_config = {"extra": "forbid"}
class AssessResponse(BaseModel):
decision: str = Field(..., description="Action verdict: ALLOW, BLOCK, or RESTRICT")
risk_level: str = Field(..., description="Calculated risk: LOW, MEDIUM, or HIGH")
capability: str = Field("GENERAL", description="Capability tier resolved server-side from the credential")
authenticated: bool = Field(False, description="Whether a valid API key was presented")
details: Dict[str, Any] = Field(..., description="Metadata and execution timings")
clean_prompt: str = Field(..., description="Prompt text after PII redaction")
redacted_items: List[str] = Field(default_factory=list, description="List of redacted sensitive elements")
process_time_ms: float = Field(..., description="Execution time within the API gateway layer")
Present an API key as a bearer token: Authorization: Bearer <key>. Anonymous
requests are served at GENERAL (least privilege) by default
(AUTH_MODE=optional); set AUTH_MODE=required to reject anonymous callers
with 401 instead. Keys are stored as SHA-256 hashes only — the plaintext is
shown once at issuance and is not recoverable:
python -m scripts.manage_api_keys issue --capability ELEVATED --tenant acme
python -m scripts.manage_api_keys list
python -m scripts.manage_api_keys revoke --key-id acme-elevated-01
Gatekeeper provides a containerized multi-service configuration in docker-compose.yml to ensure consistent execution environments across staging and production.
The snippet below is illustrative and has drifted from the real
docker-compose.yml (which now uses named volumes with directory-level
mounts, not the single-file bind mounts shown here, and adds redis,
model-pull, prometheus, and grafana services) — treat the real file
as authoritative. There is no gatekeeper-ui service in the real file
(removed in Phase 8 hardening, see §3 above) — the client UI ships
inside gatekeeper-api itself and needs no separate service.
version: '3.8'
services:
gatekeeper-api:
build:
context: .
dockerfile: Dockerfile.api
ports:
- "8000:8000"
environment:
- OLLAMA_API_URL=http://ollama:11434/api/generate
volumes:
- ./data:/app/data
- ./policies:/app/policies
- ./policies.json:/app/policies.json
- ./policy_rules.json:/app/policy_rules.json
- ./audit.jsonl:/app/audit.jsonl
networks:
- gatekeeper_net
ollama:
image: ollama/ollama:latest
ports:
- "11434:11434"
volumes:
- ollama_data:/root/.ollama
networks:
- gatekeeper_net
networks:
gatekeeper_net:
driver: bridge
volumes:
ollama_data:
POST /api/v1/assessMain governance endpoint. Intercepts and assesses prompt payloads. Capability
comes from the Authorization header, not the request body (§12) — an
anonymous request (no header) is evaluated at GENERAL.
{
"prompt": "Call John Doe at 555-0199 and check system health."
}
Authorization: Bearer <api-key> (optional; omit for anonymous/GENERAL)
{
"decision": "ALLOW",
"risk_level": "LOW",
"capability": "GENERAL",
"authenticated": false,
"details": {
"semantic_score": 0.09,
"source": "fusion_clean_pass",
"educational_context": false,
"domain_score": null,
"symbolic_triggered": false,
"judge_invoked": false,
"dynamic_threat_score": 0.0,
"fusion_available": true,
"anchor_threat_score": 0.11,
"policy_reason": "No policy constraints triggered for general access."
},
"clean_prompt": "Call [REDACTED_PERSON] at [REDACTED_PHONE] and check system health.",
"redacted_items": ["John Doe", "555-0199"],
"process_time_ms": 14.2
}
source: "fusion_*" means the learned fusion made the decision;
"vector_*" / "clean_pass" (no fusion_ prefix) means it fell back to
the anchors-only path because a live detector was unavailable — see
details.fusion_detail for why.POST /api/v1/updateTriggers an asynchronous sync of the local vector store and regex matrices with dynamic intelligence feeds.
{"status": "success", "signatures_added": 12}POST /api/v1/cache/flushInvalidates all entries in the local semantic vector cache.
{"status": "success"}GET /healthReturns per-dependency status; the overall status degrades if any check fails — it does not report a bare "healthy" regardless of actual state.
{
"status": "healthy",
"checks": {
"policy_files": true,
"spacy_model": true,
"embedding_model": true,
"semantic_judge": true
}
}
To boot up the entire Gatekeeper gateway stack with a single command:
git clone https://github.com/pavann19/Gatekeeper-AI-Infrastructure-and-Governance-Gateway.git
cd Gatekeeper-AI-Infrastructure-and-Governance-Gateway
cp .env.example .env
docker-compose up --build
If you prefer to run the service locally without Docker:
python -m venv venv
# Windows:
.\venv\Scripts\activate
# macOS/Linux:
source venv/bin/activate
pip install -r requirements.txt
python -m spacy download en_core_web_sm
.env file to point to your local endpoints:
OLLAMA_API_URL=http://localhost:11434/api/generate
POLICY_FILE=policies.json
uvicorn api.main:app --host 127.0.0.1 --port 8000 --reload
http://127.0.0.1:8000/ui/login/index.html.These are the tools that actually produced every number in §9. All are reproducible; none require guessing at a threshold or a result.
python -m scripts.build_eval_suite
Every detector is probed against canonical attack/benign pairs before its numbers are trusted — a detector wired backwards still returns well-formed probabilities, just inverted ones.
python -m scripts.compare_detectors --bootstrap 1000
python -m scripts.ensemble_analysis
python -m scripts.train_fusion_policy
Aborts rather than running if the judge is unreachable — a benchmark against an offline judge silently measures judge uptime, not detection quality.
PYTHONPATH=. python tests/benchmark.py
A runnable async load-testing tool. No throughput result from it is published in this README yet (see §9).
python -m benchmarks.run_load_test
ALLOWBLOCKRESTRICT (or ALLOW for ELEVATED roles)Here are the primary control layouts of the running application:


gatekeeper/
├── .github/workflows/ci.yml # CI: pytest, 1,644 tests, torch/faiss-cpu included
├── api/
│ ├── main.py # FastAPI Application Entry
│ └── schemas.py # Pydantic Schemas (no client-supplied role)
├── benchmarks/
│ ├── evaluate_accuracy.py
│ └── run_load_test.py # Load-test tool; no published result yet (§9)
├── core/
│ ├── auth.py # API-key capability resolution (zero-trust default)
│ ├── cache.py # Semantic cache: exact-hash tier + calibrated fuzzy match
│ ├── config.py # Pydantic Configuration Settings
│ ├── detectors.py # Pluggable detector registry (ProtectAI, jailbreak
│ │ # classifier, toxicity classifier, gated Prompt
│ │ # Guard 2 / Llama Guard 3)
│ ├── domain_classifier.py # Domain Verification logic (topicality, not safety)
│ ├── embeddings.py # Sentence-Transformer wrapper
│ ├── fusion.py # Applies the trained fusion policy at request time
│ ├── normalizer.py # Obfuscation Normalizer
│ ├── output_guardrails.py # Output-side toxicity/PII check
│ ├── policy.py # Access Rule Evaluator
│ ├── policy_loader.py # Policy JSON parsing
│ ├── privacy.py # Regex + SpaCy NER redaction engine
│ ├── risk.py # Governance Pipeline Orchestrator
│ ├── semantic_judge.py # Downstream Judge LLM client
│ ├── threat_centroid.py # Diagnostic centroid signal (not decision-bearing)
│ └── vector_store.py # FAISS Vector Index Wrapper
├── data/
│ └── eval_suite.jsonl # 6,933-prompt, 7-source labelled eval suite (generated)
├── docs/
│ ├── ENGINEERING_ASSESSMENT.md # Every measured finding, with evidence and caveats
│ └── EVALUATION_METHODOLOGY.md
├── evaluation/
│ └── metrics.py # AUC, recall@FPR, bootstrap CIs
├── models/
│ └── fusion_policy.json # Trained fusion weights (plain JSON, not pickled)
├── policies/
│ ├── domain_anchors.json
│ └── symbolic_rules.json
├── scripts/
│ ├── build_eval_suite.py
│ ├── calibrate_thresholds.py
│ ├── compare_detectors.py # Detector comparison with polarity self-check
│ ├── diagnose_cache_threshold.py
│ ├── ensemble_analysis.py # Out-of-fold fusion validation
│ ├── manage_api_keys.py # issue / list / revoke / verify
│ └── train_fusion_policy.py # Fits and persists models/fusion_policy.json
├── tests/ # 1,644 tests: auth bypass regression, fusion fail-closed
│ └── ... # contract, cache exact-match regression, detectors
├── ui/ # Real client UI (login, activity, review, trace,
│ └── ... # gateways, logs, benchmarks, policy, settings) —
│ # static pages served by api/main.py, see §3
├── docker-compose.yml
├── Dockerfile.api
├── requirements.txt # Production dependencies
├── requirements-ci.txt # CI dependencies (see file header for what's excluded/why)
└── README.md
Gatekeeper runs an automated workflow on every push and pull request using GitHub Actions (.github/workflows/ci.yml), currently green at 1,644 passing tests:
name: Gatekeeper CI Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.10
uses: actions/setup-python@v4
with:
python-version: '3.10'
cache: 'pip'
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-ci.txt
python -m spacy download en_core_web_sm
- name: Run Pytest
run: |
pytest tests/ -v --tb=short
requirements-ci.txt deliberately includes torch and faiss-cpu — several
tests exercise the real FAISS index and real tensor/label-resolution logic
against synthetic data rather than mocking those libraries away. Only
sentence-transformers and real HuggingFace model downloads remain excluded,
since those need network access and GB-scale weights.
Gatekeeper aligns with the following security standards:
Identified from measurement, in priority order:
benchmarks/run_load_test.py and publish P50/P95/P99 and error rate under realistic concurrency.Longer-term / aspirational:
This project is licensed under the MIT License - see the LICENSE file for details.
git checkout -b feature/AmazingFeature).pytest cases.main branch.160 commits
Python
95.2%
HTML
4.3%