A local-first RAG security layer and deterministic containment gate that validates query-to-corpus geometric alignment across 1024 dimensions before routing to an LLM.
Three independent filters — Noise, Cosine, and Excitation — execute in a user-defined sequence. Any single failure halts execution and blocks the prompt. Built for sovereign AI deployments where data never leaves the machine.
| License | Apache-2.0 · NOTICE © 2026 Héctor Andrés Bauzán Saavedra |
| Registration | DNPI / Registro de Software Nº 226 (04/08/2026, Uruguay) |
| Version | v2.34.0 (Checkpoint / On Pause) |
| Embedder | BAAI/bge-m3 (1024D Dense + Sparse Lexical) |
| Storage | LanceDB (Serverless Vector DB + RaBitQ Hamming prefilter) |
| Runtime | Python 3.14 (uv) · React 19 (pnpm / Vite) |
| Upstream | Ollama (Local llama3.1) · OpenAI · Anthropic · Google Gemini · Groq |
| Local (macOS) | ./run_commander.sh |
About (Description):
Local-first semantic firewall & deterministic containment gate for LLMs and RAG. Validates 1024D embedding geometry to enforce data sovereignty and prevent prompt injection / data exfiltration.Topics / Tags:
ai-security·llm-security·rag·guardrails·firewall·semantic-search·embeddings·lancedb·fastapi·react19·vite·bge-m3·red-teaming·fuzzing·data-sovereignty·sentence-transformers·prompt-injection·dlp·zero-trust-ai·local-first
Note
Putting this project on temporary pause because, frankly, life has caught up with me and I just can't keep up with everything right now.
The Mystery of the "Three Heads" — Solved:
Previously, empirical benchmarks suggested that only one or two heads were doing the actual heavy lifting, as the legacy excitation filter ("la abuela") seemed to drop in-domain accuracy to 64% without rescuing attacks over cosine.
Through subsequent laboratory research in ddi-fw (RFC-003), the root cause was exposed: silent sub-epsilon decimal truncation (round(x, 4), :.4f, float16 downcasting) was obliterating coordinate micro-gaps (
The Three-Headed architecture is mathematically vindicated and ready to be re-implemented:
- Head 1 — Cosine Difference Gate: Macro-angular hypersphere orientation.
- Head 2 — Excited Coordinate Mass Counter: Coarse integer activation mass across coordinates.
-
Head 3 — Harmonic Resonance Gate: Microscopic 17-digit fine-tuning sieve across all 1024 dimensions (
$solo_b == 0 \land solo_a \ge \tau_{\text{floor}}$ ).
A deterministic, AI-actionable implementation roadmap is prepared in roadmap.md and roadmap/ for whenever execution resumes.
The Core Vision:
A local LLM working hand-in-hand with a vector database (LanceDB) enforcing everything the model is permitted to know and touch. An approach geared toward mathematical determinism for sovereign AI deployments.
For now: goodbye, so on, and thanks for all the fish.
Remember, remember, the 5th of November.
And above all: Always Look on the Bright Side of Life 🎶.
- Architecture Overview
- Prerequisites
- Installation
- Configuration
- Running the System
- Operating the Firewall
- Shell Scripts
- Testing
- Project Structure
- Transparent Proxy (OpenAI V1 Spec)
- API Reference
- Security
- License
User Prompt
│
▼
┌─────────────────────────────────────────────────┐
│ Structural Segmentation (Language-Agnostic) │
│ Split on punctuation → overflow chunk > 20w │
└──────────────────────┬──────────────────────────┘
│ per clause
▼
┌─────────────────────────────────────────────────┐
│ Sequential Pipeline (user-defined order) │
│ │
│ ┌─────────┐ ┌─────────┐ ┌──────────────┐ │
│ │ Noise │──▶│ Cosine │──▶│ Excitation │ │
│ │ Filter │ │ Filter │ │ Filter │ │
│ └─────────┘ └─────────┘ └──────────────┘ │
│ │
│ BREACH at any stage → halt, block, report │
└──────────────────────┬──────────────────────────┘
│ ALL PASS
▼
┌─────────────────────────────────────────────────┐
│ Ollama LLM (llama3.1) + Strict RAG Context │
└─────────────────────────────────────────────────┘
── OR ──
┌─────────────────────────────────────────────────┐
│ /v1/chat/completions (OpenAI-compatible proxy) │
│ Same firewall pipeline → Provider abstraction │
└─────────────────────────────────────────────────┘
Three Filters:
| # | Filter | What it checks | Blocks when |
|---|---|---|---|
| 1 | Noise Pre-Filter | Average absolute delta across all 1024 dims | avg_delta > global_noise_limit |
| 2 | Cosine Filter | Cosine similarity between query and corpus vectors | cos(Q, C) < cosine_threshold |
| 3 | Excitation Filter | Count of dimensions where ` | Q_i - C_i |
Execution order is configurable at runtime via the HUD. If Filter 1 blocks, Filters 2 and 3 never execute.
Two operating modes:
| Mode | Corpus role | First filter that detects similarity | First filter that detects divergence |
|---|---|---|---|
| Positive (default) | Allowlist — only corpus topics pass | OK, continue | BREACH, short-circuit |
| Negative | Denylist — corpus topics are blocked | BREACH, short-circuit | OK, continue |
In positive mode, the corpus defines what's allowed — queries must be similar. In negative mode, the corpus defines what's restricted — the first filter that detects similarity blocks immediately.
| Dependency | Version | Purpose |
|---|---|---|
| Python | 3.14+ | Backend (FastAPI, embeddings, vector math). Managed by uv — no manual venv needed. |
| uv | Latest | Backend toolchain (dependencies, venv, execution). Source of truth: backend/pyproject.toml. |
| Node.js | 20+ | Frontend runtime (React 19, Vite). |
| pnpm | 9+ | Frontend package manager. Source of truth: frontend/pnpm-lock.yaml. |
| Ollama | Latest | Default local LLM provider (optional if you point at a cloud provider). |
| Git | Any | Clone the repository |
Ollama must have the llama3.1 model pulled:
ollama pull llama3.1git clone https://github.com/hbauzan/semantic-firewall.git
cd semantic-firewallcd backend
uv syncuv sync reads pyproject.toml, resolves the locked dependency set from uv.lock, and creates the .venv automatically. There is no manual virtual-environment activation and no pip install — every command runs through uv run. (requirements.txt is kept as a generated artifact for non-uv consumers, not as the source of truth.)
First run note: The
BAAI/bge-m3model (~2.3 GB) will be automatically downloaded by HuggingFace on first boot. This is a one-time operation.
cd ../frontend
pnpm installcd ..
chmod +x run_commander.sh run_server.sh run_ui.sh run_tests.sh run_pack.shA single .env file in the project root configures both backend and frontend. The backend reads it via pydantic-settings at runtime; the frontend reads it via Vite at build time. No source, no export — just drop the file and start.
cp .env.example .env
# Edit .env with your values — both backend and frontend read from this single file.If a variable has an invalid type or fails validation, the backend crashes immediately with a clear error. This prevents silent misconfigurations from reaching production.
Backend variables (loaded by pydantic-settings):
| Variable | Default | Description |
|---|---|---|
ALLOWED_ORIGINS |
http://localhost:5173 |
Comma-separated CORS origins. Use * only for development. |
FIREWALL_API_KEY |
(unset) | If set, all /chat, /audit, and /galaxy/config endpoints require X-API-Key header. Stored as SecretStr — never leaked to logs. Leave unset for open local development. |
OLLAMA_BASE_URL |
http://localhost:11434 |
Ollama API endpoint. |
OLLAMA_MODEL |
llama3.1 |
LLM model name for inference. |
EMBEDDING_MODEL |
BAAI/bge-m3 |
HuggingFace embedding model ID. Change only if you reindex the corpus. |
TEI_ENABLED |
false |
If true, embeddings go to the TEI sidecar instead of in-process SentenceTransformer. |
TEI_URL |
http://127.0.0.1:8080 |
TEI HTTP base URL. |
TEI_IMAGE_DIGEST |
sha256:2614a26f… |
Must match deploy/tei/docker-compose.yml. Never a floating tag. |
TEI_TIMEOUT_S |
5.0 |
HTTP timeout for TEI embed calls (seconds). |
MAX_UPLOAD_MB |
50 |
Maximum PDF upload size in megabytes (1–500). |
CHUNK_SIZE |
2048 |
PDF chunking size in characters (100–10000). |
CHUNK_OVERLAP |
200 |
Overlap between consecutive chunks (0–2000). |
EMBEDDING_BATCH_SIZE |
10 |
Embeddings per batch during ingestion (1–100). |
RAG_TOP_K |
12 |
Corpus chunks retrieved per clause for RAG context (1–32). |
RATE_LIMIT_CHAT |
30/minute |
Rate limit for /chat and /audit endpoints per client IP. |
RATE_LIMIT_DEFAULT |
60/minute |
Rate limit for all other endpoints per client IP. |
RATE_LIMIT_UPLOAD |
10/minute |
Rate limit for /corpus/upload-pdf per client IP. |
HOST |
0.0.0.0 |
Bind address for uvicorn. Use 127.0.0.1 behind a reverse proxy. |
PORT |
8000 |
Backend listen port (1–65535). |
RELOAD |
false |
Hot-reload on code changes. Set to true for development only. |
Frontend variables (loaded by Vite — must be prefixed with VITE_):
| Variable | Default | Description |
|---|---|---|
VITE_API_BASE_URL |
http://localhost:8000 |
Backend API endpoint used by all frontend components. |
For local development, no .env file is required. All defaults work out of the box.
All frontend components import the backend URL from a single module (frontend/src/config.ts) instead of hardcoding http://localhost:8000 in each file:
// config.ts
export const API_BASE_URL: string =
import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000';Every component uses this constant:
import { API_BASE_URL } from '../config';
fetch(`${API_BASE_URL}/chat`, ...)Vite injects VITE_API_BASE_URL at build time (not runtime). This means:
- During
pnpm run dev, Vite reads the root.envand replaces the variable in-memory. - During
pnpm run build, the value is baked into the compiled JS bundle. - If the variable is not set, the fallback
http://localhost:8000is used automatically.
To point the frontend at a different backend (e.g., production), just set the variable in .env before building:
VITE_API_BASE_URL=https://api.yourdomain.com./run_commander.shInteractive menu:
| Key | Action |
|---|---|
S |
Start the FastAPI backend on http://localhost:8000 |
U |
Start the Vite frontend on http://localhost:5173 |
T |
Run the pytest test suite |
O |
Start Ollama and open a direct llama3.1 chat |
Q |
Quit |
Terminal 1 — Backend:
cd backend
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reloadTerminal 2 — Frontend:
cd frontend
pnpm run devTerminal 3 — Ollama (if not already running):
ollama serveOpen http://localhost:5173 in your browser.
Default embeddings stay in-process (BAAI/bge-m3 via SentenceTransformer). To serve the same model from Hugging Face Text Embeddings Inference, the image is pinned by digest in deploy/tei/docker-compose.yml — never :latest.
docker compose -f deploy/tei/docker-compose.yml up -d
# then set TEI_ENABLED=true in `.env`TEI_ENABLED=false keeps the ST fallback (tests without Docker). Live TEI dispersion vs L01: cd backend && RUN_TEI_INTEGRATION=1 uv run pytest -q tests/test_tei_adapter.py -m integration.
The browser UI at http://localhost:5173 is organized into two columns:
Left sidebar:
- Three Monkey Heads — one per filter (Noise, Cosine, Excitation). Each head animates when its filter is enabled and goes dark when disabled, giving a visual status of the pipeline.
- System Action — displays the current operation (
SYSTEM IDLE,STREAMING_RESPONSE...,INGESTING_CORPUS: 45%, etc.). - Metrics line — real-time CPU, RAM, and GPU utilization polled every second. GPU metric uses Apple MPS / NVIDIA CUDA memory allocation.
All firewall parameters are adjustable in real-time via sliders with -/+ step buttons. Each filter has an ON/OFF toggle and a Seq input for pipeline ordering. Changes are synced to the backend via debounced API calls.
| Slider | Default | Range | Step | Purpose |
|---|---|---|---|---|
| Noise Pre-Filter | 0.50 | 0.10 – 2.00 | 0.01 | Global delta sanity limit |
| Cosine Gate | 0.50 | 0.00 – 1.00 | 0.01 | Minimum cosine similarity |
| Excitation Threshold | 150 | 0 – 1024 | 1 | Minimum activated dimensions |
| Noise Tolerance | 0.005 | 0.001 – 0.100 | 0.001 | Per-dimension activation sensitivity |
| Adaptive Factor | 0.85 | 0.01 – 1.00 | 0.01 | Threshold reduction for short queries (< 6 words) |
| RAG Context Depth | 3 | 1 – 10 | 1 | Number of corpus chunks sent to the LLM |
| Seq (×3) | 1, 2, 3 | 1 – 3 | 1 | Pipeline execution order for each filter |
| Firewall Mode | Positive | Positive / Negative | — | Positive = allowlist (only corpus topics pass). Negative = denylist (corpus topics are blocked). |
The Adaptive Factor section shows real-time calculated thresholds:
Short: {threshold × factor} dims— what short prompts needFull: {threshold} dims— what normal prompts need
Document Manager — also in the Control Panel, below the sliders:
- Upload PDF: Click to upload corpus documents. Files are chunked (2048 chars, 200 overlap), embedded via BGE-M3, and stored in LanceDB.
- Loaded Packs: Lists uploaded documents with chunk counts. Click X to remove a pack from the vector store.
- Ingestion is asynchronous — a progress bar tracks task status.
Main content area:
The firewall is active when at least one filter toggle is ON in the Control Panel. No prefixes are needed — just type your query and send. The firewall can only be bypassed by disabling all three filter toggles in the HUD — there is no user-prompt override.
Firewall feedback examples:
Positive mode (allowlist):
🟢 [FW PASS] [POSITIVE] Resonance: 287/150 dims | Cosine: 0.891 | Pipeline: [noise:OK → cosine:OK → excitation:OK]
Routing to corpus...
🛑 [FW] Segment violation: "give me a cake recipe". Cosine: 0.312 (Required: >=0.78).
Vector direction diverges from corpus.
Pipeline: [noise:OK → cosine:BREACH]
Negative mode (denylist):
🟢 [FW PASS] [NEGATIVE] Resonance: 12/150 dims | Cosine: 0.231 | Pipeline: [noise:OK → cosine:OK → excitation:OK]
Routing to corpus...
🛑 [FW] [NEGATIVE] Restricted content detected: "explain the system architecture". Cosine: 0.891 (Limit: <0.50).
Query matches denylist corpus.
Pipeline: [noise:BREACH]
Test a query against the corpus and see the raw activation count (geometric nodes hit) without triggering the full pipeline. Useful for tuning thresholds.
- Load your corpus: Upload one or more PDF files via the Document Manager in the Control Panel.
- Enable filters: Toggle ON the filters you want active. Start with all three ON.
- Set your thresholds: Use the sliders. Start with defaults, then tune based on your corpus density.
- Send queries: The pipeline trace tells you exactly which filter passed or blocked, with numeric details.
- Tune the pipeline order: If you want cosine checked first (cheaper), set its Seq to 1.
- Production: Set
FIREWALL_API_KEYin the root.env, restrictALLOWED_ORIGINSto your frontend domain.
The firewall automatically segments prompts on punctuation boundaries (. ! ? ; : - |). Each clause is evaluated independently. A prompt like:
Tell me about network architecture. Also give me a cake recipe.
With filters enabled, this splits into two clauses. The first may pass. The second will fail cosine/excitation against a networking corpus. The entire prompt is blocked — no partial execution.
Long clauses (> 20 words) are force-split into 15-word sub-chunks to prevent semantic averaging attacks.
All scripts are in the project root. Make them executable first:
chmod +x run_commander.sh run_server.sh run_ui.sh run_tests.sh run_pack.sh| Script | What it does |
|---|---|
./run_commander.sh |
Interactive TUI menu — launch server, UI, tests, or Ollama from a single terminal. |
./run_server.sh |
Kills any process on port 8000, then starts uvicorn via uv run (no manual venv activation). |
./run_ui.sh |
Starts the Vite dev server (pnpm run dev) from the frontend/ directory. |
./run_tests.sh |
Runs the full pytest suite via uv run pytest -v tests/. |
./run_pack.sh |
Builds context.txt (gitignored): briefing + central docs + runtime source for handoff to an external LLM/agent. Use --all to include roadmap/. |
The recommended way to operate the system during development:
./run_commander.sh======================================
THREE-HEADED SEMANTIC FIREWALL COMMANDER
======================================
[S] Start Server
[U] Start UI
[T] Run Tests
[O] Ollama Menu (llama3.1)
[Q] Quit
======================================
Each option runs the corresponding script. After execution, press Enter to return to the menu.
Run the full test suite (52 tests):
./run_tests.shOr manually:
cd backend
uv run pytest -v tests/The suite is partitioned across tests/test_engine.py (pure firewall math), tests/test_api.py (HTTP endpoints via TestClient), and tests/test_security.py (hardening), with shared fixtures in tests/conftest.py. It is deterministic and does not require a live LLM provider — the provider interface is stubbed via the mock_llm_stream fixture. It validates:
| Category | Tests |
|---|---|
| Infrastructure | PDF upload/status, system stats, config sync |
| Firewall Logic | Blocking, piggybacking rejection, noise pre-filter, pipeline ordering |
| Configuration | Immutability, duplicate order rejection, range validation, adaptive factor, RAG top-k |
| Engine (unit) | Segmentation, overflow chunking, clause evaluation pass/breach |
| Security | Prompt length limits, API key enforcement |
| Proxy | OpenAI v1 spec compliance, firewall interception on proxy, SSE streaming, graceful upstream-failure handling |
| Health | Health check endpoint response shape |
| Embedder determinism (L01) | Harness with stubs in tests/test_embedder_determinism.py. Live N=100 probe is not in ./run_tests.sh. |
Live in-process BGE-M3 probe (fingerprint + vector spread + PASS/BREACH vs frozen demo chunk):
cd backend
uv run python tests/embedder_determinism.py --runs 100
# or
RUN_EMBEDDER_DETERMINISM=1 uv run pytest -q tests/test_embedder_determinism.pyReport: backend/tests/embedder_determinism_report.md. Jitter is recorded, not a CI failure. Cross-host bit-exactness is not claimed.
An async load test suite extracts hard performance metrics under concurrent load. The backend must be running before launching the suite.
# Terminal 1 — start the server
./run_server.sh
# Terminal 2 — run the load tests
cd backend
uv run python tests/load_test_suite.py| Option | Default | Description |
|---|---|---|
--base-url |
http://localhost:8000 |
Backend endpoint to test. |
--requests |
200 |
Total requests per profile/concurrency combo. |
--output |
tests/metrics_report.csv |
CSV report output path. |
The suite runs 3 payload profiles (short/long/overflow) across 3 concurrency levels (10, 50, 200) — 9 test runs total. Each run reports Min, Avg, P95, and Max latency, RPS, success/failure counts, and error rate. Results are printed as a formatted console table and saved to CSV.
Measures how LanceDB retrieval and firewall evaluation scale as the vector store grows from 1k to 50k rows. Uses a temporary isolated database — the production corpus is never touched.
cd backend
uv run python tests/db_stress_suite.py| Option | Default | Description |
|---|---|---|
--milestones |
1000 10000 50000 |
Row count milestones to benchmark at. |
--queries |
100 |
Benchmark queries per milestone. |
--batch-size |
500 |
Injection batch size. |
--output |
tests/db_scaling_metrics.md |
Markdown report output path. |
--use-embedder |
(off) | Use real BGE-M3 model instead of synthetic random vectors. |
At each milestone the script pauses injection, fires 100 queries, and isolates the timing of table.search() (retrieval) vs SemanticFirewall.evaluate_clause() (firewall math). Output is a formatted console table + a Markdown report with graph-ready tables.
semantic-firewall/
├── .env.example # Environment variable template (backend + frontend)
├── .gitignore # Excludes .env, __pycache__, node_modules, etc.
├── run_commander.sh # Interactive TUI launcher
├── run_server.sh # Backend startup script (auto-kills port 8000)
├── run_ui.sh # Frontend startup script
├── run_tests.sh # Test runner script
├── run_pack.sh # Agent handoff bundler (generates context.txt)
├── semantic_guardtrails_packager.py # Packager logic used by run_pack.sh
├── manifest.json # Version + state_schema + constraints (current state)
├── CHANGELOG.md # Notable capabilities and releases
├── architecture_spec.md # Detailed technical specification
├── CONTEXT.md # Domain glossary (ubiquitous language)
│
├── backend/
│ ├── pyproject.toml # Dependency source of truth ([project]) + pytest config
│ ├── uv.lock # Locked dependency set (uv)
│ ├── requirements.txt # Generated artifact (uv pip compile) — not the source of truth
│ ├── tests/
│ │ ├── conftest.py # Shared fixtures (TestClient, mock_llm_stream, config reset)
│ │ ├── test_engine.py # Engine unit tests (pure firewall math)
│ │ ├── test_api.py # HTTP endpoint tests via TestClient
│ │ ├── test_security.py # Security hardening tests
│ │ ├── load_test_suite.py # Async load testing (latency, RPS, error rate)
│ │ └── db_stress_suite.py # DB saturation test (retrieval scaling)
│ └── app/
│ ├── main.py # FastAPI app + CORS + security headers + rate limiting
│ ├── core/
│ │ ├── models.py # Pydantic schemas (ConfigState, etc.)
│ │ ├── state.py # Global config singleton + lock
│ │ ├── firewall.py # SemanticFirewall engine (pure math)
│ │ ├── logging_config.py # Rotating logger setup
│ │ └── settings.py # Environment-driven settings
│ ├── api/
│ │ ├── router_main.py # Aggregates the endpoint routers
│ │ └── endpoints/ # Decomposed routers: chat, config, corpus, system, _shared
│ └── modules/
│ ├── embedder.py # BGE-M3 embedding singleton
│ ├── storage.py # LanceDB vector store
│ ├── ingestor.py # PDF chunking pipeline
│ ├── sniffer.py # Real-Time Semantic Sniffer (RTSS) + trace persistence
│ ├── persistence.py # Chat history persistence
│ ├── profiles.py # Config profile save/load
│ └── providers/ # base.py (ABC) + ollama, openai, anthropic, google, groq
│
└── frontend/
├── package.json
├── pnpm-lock.yaml
└── src/
├── App.tsx # Main layout + ErrorBoundary wrappers
├── config.ts # Centralized API URL (env-driven)
├── store.ts # Zustand state management
└── components/
├── ControlPanel.tsx # Firewall sliders + pipeline ordering
├── ChatInterface.tsx # Chat with firewall toggle support
├── AuditPanel.tsx # Query audit + activation inspector
├── TelemetryHUD.tsx # System metrics display
└── ErrorBoundary.tsx # React error boundary (per-component isolation)
The firewall exposes a POST /v1/chat/completions endpoint that implements the OpenAI chat completions spec. Any tool, SDK, or agent framework that speaks the OpenAI protocol can drop in the firewall as its base URL — zero code changes required on the client side.
- The proxy receives a standard OpenAI
messagesarray. - The last message is extracted and passed through the full segmentation + firewall pipeline (same as
/chat). - If any clause fails any filter, the proxy returns a
403 Forbiddenin the OpenAI standard error format:{ "error": { "message": "🛑 [FW] Segment violation: cosine", "type": "security_breach", "code": "403" } } - If all clauses pass, the request is forwarded to the configured LLM provider and streamed back as Server-Sent Events (SSE) in the OpenAI
data: {...}\n\nformat, terminated bydata: [DONE]\n\n.
LLM routing is abstracted behind a BaseProvider interface (app/modules/providers/base.py). The initial implementation is OllamaProvider, which maps the OpenAI message format to the Ollama /api/generate endpoint and converts the response stream back to OpenAI SSE chunks. Adding a new provider (vLLM, llama.cpp server, etc.) means implementing a single stream_chat async generator.
Point any OpenAI-compatible client at the firewall:
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="your-firewall-api-key" # or "not-needed" if FIREWALL_API_KEY is unset
)
response = client.chat.completions.create(
model="llama3.1",
messages=[{"role": "user", "content": "Explain the system architecture"}],
stream=True
)
for chunk in response:
print(chunk.choices[0].delta.content, end="")If the prompt violates the firewall, the client receives a 403 with a structured error instead of a completion.
All endpoints are served at http://localhost:8000.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/v1/chat/completions |
API Key* | OpenAI-compatible proxy. Firewall intercepts the last message; streams SSE response. |
POST |
/chat |
API Key* | Send a prompt through the firewall pipeline. Streaming NDJSON response. |
POST |
/audit |
API Key* | Test a query against the corpus. Returns activation count and nearest text match. |
POST |
/galaxy/config |
API Key* | Update firewall configuration (thresholds, orders, factor). |
POST |
/corpus/upload-pdf |
API Key* | Upload a PDF for async vectorization into LanceDB. |
GET |
/corpus/task-status/{id} |
API Key* | Poll PDF ingestion task progress. |
GET |
/corpus/packs |
API Key* | List all uploaded document packs with chunk counts. |
DELETE |
/corpus/packs/{filename} |
API Key* | Remove a document pack from the vector store. |
GET |
/system/stats |
API Key* | CPU, RAM, GPU utilization metrics. |
GET |
/health |
— | Liveness probe: status and UTC timestamp. Minimal response — no internal state exposed. |
* API Key required only if FIREWALL_API_KEY environment variable is set.
Security hardening measures applied to the application infrastructure:
- CORS Hardening — Explicit origin allowlist, no wildcard credentials. Credentials are auto-disabled when
*is configured. - API Key Authentication — Opt-in header-based auth. When enabled, protects all endpoints except
/health. Comparison useshmac.compare_digest(constant-time, timing-attack safe). - Input Sanitization — 4000-character prompt limit enforced at schema level before vectorization.
- Atomic Configuration — Frozen Pydantic models + async lock prevent race conditions on concurrent config updates.
- Pipeline Order Validation — Duplicate filter priorities are rejected at the schema level.
- PDF Upload Hardening — Configurable size limit (default 50 MB), streamed read with early rejection (never loads full payload into RAM), PDF magic-byte validation, filename sanitization against path traversal.
- SQL Injection Prevention — Filename whitelist regex + quote escaping on all storage layer queries.
- Structured Logging — All modules use Python
loggingwith severity levels. Client-facing error messages are generic (no stack traces or internal URLs leaked). - Fail-Fast Configuration —
pydantic-settingsvalidates all env vars at boot. Invalid types or out-of-range values crash the app immediately.FIREWALL_API_KEYusesSecretStrto prevent accidental exposure in logs. Startup warning logged when API key is not configured. - Repository Hygiene —
.env, runtime interception data (backend/data/*.json), logs (backend/logs/), the vector store (lancedb_data/), the local_archive/tree, and the generated agent handoff bundle (context.txt) are all excluded via root.gitignore. No intercepted prompts, responses, or secrets are tracked in git. - Security Headers — All responses include
X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Content-Security-Policy: frame-ancestors 'none',Referrer-Policy,Permissions-Policy, andStrict-Transport-Security(HSTS, 2-year max-age). - DoS Mitigation — Concurrent PDF ingestion capped at 3 threads via semaphore. Ollama streaming has a 300-second read timeout. Upload size enforced during chunked read (before full allocation).
- Data Integrity — Storage ID generation is serialized via
threading.Lockto prevent duplicate IDs from concurrent uploads. - Protocol Safety — Frontend API URL fallback inherits the page's protocol (
https://in production) instead of hardcodinghttp://. - Rate Limiting — Per-IP rate limits via
slowapi:/chatand/auditat 30/min,/corpus/upload-pdfat 10/min, all other endpoints at 60/min. Configurable via.env. Returns HTTP 429 when exceeded. - LLM Response Validation — Ollama streaming validates HTTP status code and JSON format on every line before forwarding to the client. Malformed lines are dropped and logged.
- Conditional Swagger Docs —
/docsand/redocare automatically disabled whenFIREWALL_API_KEYis set, preventing schema enumeration in production. - Minimal Health Endpoint —
/healthreturns onlystatusandtimestamp. No internal state (embedder status, corpus size) is exposed to unauthenticated callers.
The FIREWALL_API_KEY variable is an opt-in authentication layer for the backend's sensitive endpoints.
Disabled (default — local development): When the variable is not set, all endpoints are open. Any process that can reach port 8000 can send queries, change firewall configuration, and upload PDFs. This is the expected behavior for local development.
Enabled (production): Add the variable to the root .env:
FIREWALL_API_KEY=your-secret-key-hereOnce set, all endpoints except /health require every request to include the header:
X-API-Key: your-secret-key-here
Requests without the header, or with an incorrect key, receive a 403 Forbidden response. Only /health remains open (for load balancer probes).
Protected endpoints:
| Endpoint | Requires API Key |
|---|---|
POST /v1/chat/completions |
Yes |
POST /chat |
Yes |
POST /audit |
Yes |
POST /galaxy/config |
Yes |
POST /corpus/upload-pdf |
Yes |
GET /corpus/packs |
Yes |
DELETE /corpus/packs/{filename} |
Yes |
GET /corpus/task-status/{id} |
Yes |
GET /system/stats |
Yes |
GET /health |
No |
The key comparison uses hmac.compare_digest (constant-time) to prevent timing side-channel attacks. The key is stored internally as a Pydantic SecretStr — if the settings object is accidentally logged or printed, the value appears as ********** instead of the real key.
To disable the key, comment out or remove the line from .env and restart the backend.
Licensed under the Apache License, Version 2.0 — see LICENSE and NOTICE.
Copyright © 2026 Héctor Andrés Bauzán Saavedra.
- Obra / Work: Three-Headed Semantic Firewall
- Titular / Author: Héctor Andrés Bauzán Saavedra
- Número de Inscripción: Nº 226
- Fecha de Inscripción: 04/08/2026 (August 4, 2026)
- Organismo / Registry: Dirección Nacional de la Propiedad Industrial (DNPI - MIEM) / Registro de Software, República Oriental del Uruguay
- Marco Legal: Ley 9.739 de 17/12/1937 (conforme Ley N° 20.212 y Decreto N° 39/2025)
Pursuant to Section 4(d) of the Apache License, Version 2.0, the NOTICE file must be retained and distributed with any reproduction or derivative work.