# SafeClaw — Offline-First, RAG-First, MCP-Exposed Stack

Production-grade Python system for `.md`-corpus RAG with:

- **LangGraph controller** enforcing RAG-first via graph topology (not prompts)
- **FastAPI gateway** with user confirmation flow for gated Grok fallback
- **Hybrid retrieval** (ChromaDB semantic + BM25 keyword) with RRF fusion
- **MCP server** (retrieval-only, no sampling capability)
- **sentence-transformers** for CPU-only local embeddings (no Ollama)

> **Platform**: Windows 10/11 with Python 3.13  
> **Shell**: Commands shown for both PowerShell and CMD where they differ.

## Architecture

```
User Query
    │
    ▼
┌─────────────────────────┐
│  FastAPI Gateway :8787  │ ◄── Input sanitization (prompt filter)
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│  LangGraph Controller   │ ◄── Topology = Enforcement
│  ┌───────────────────┐  │
│  │ 1. retrieve       │──┼── Always first (hybrid search)
│  │ 2. route_by_score │──┼── score >= 0.75 → local_llm
│  │ 3. local_llm      │  │   score < 0.75 → user_gate
│  │ 4. user_gate      │──┼── needs_confirm → client
│  │ 5. grok_fallback  │  │   confirmed + hybrid → Grok
│  │ 6. offline_best   │  │   declined/offline → local best effort
│  │ 7. audit_logger   │──┼── ALL paths end here
│  └───────────────────┘  │
└─────────────────────────┘
```

## Invariants (Enforced by Code, Not Prompts)

1. Every query passes through retrieval first
2. No LLM is called before the score gate
3. No Grok without explicit user confirmation AND hybrid mode
4. Every response passes through audit logging

## Quick Start

### 0. Verify Python 3.13

```powershell
# PowerShell — use the Python Launcher
py -3.13 --version
# Expected: Python 3.13.x

# If py launcher isn't installed, use the full path:
# "C:\Users\<you>\AppData\Local\Programs\Python\Python313\python.exe" --version
```

### 1. Install

```powershell
# Create venv with Python 3.13
py -3.13 -m venv venv

# Activate (PowerShell)
.\venv\Scripts\Activate.ps1

# Activate (CMD)
# venv\Scripts\activate.bat

# Verify you're in the venv
python --version
# Should show Python 3.13.x

# Install dependencies
pip install -r requirements.txt

# Verify critical packages resolved (no "Building wheel" on torch/hnswlib)
pip install --dry-run -r requirements.txt
```

> **If PowerShell blocks the activate script**: Run `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser` once, then retry.

### 2. Index Corpus

Place `.md` or `.txt` files in `data\corpus\`, then:

```powershell
python -m retrieval.indexer
```

This builds both ChromaDB (semantic) and BM25 (keyword) indices using
sentence-transformers `all-MiniLM-L6-v2` on CPU. First run downloads
the model (~80MB) to `.emb_cache\`.

### 3. Start LM Studio

Load a GGUF model (e.g., Qwen 2.5 7B Instruct) in LM Studio.
Ensure the server is running on `http://127.0.0.1:1234`.

### 4. Run Gateway

```powershell
python gate.py
```

Gateway binds to `127.0.0.1:8787` (localhost only).

### 5. Query

```powershell
# PowerShell — High-confidence query (local LLM answers directly)
Invoke-RestMethod -Uri "http://127.0.0.1:8787/query" `
  -Method POST `
  -ContentType "application/json" `
  -Body '{"query": "What is Veeam immutability?"}'

# PowerShell — Low-confidence query (triggers confirmation flow)
Invoke-RestMethod -Uri "http://127.0.0.1:8787/query" `
  -Method POST `
  -ContentType "application/json" `
  -Body '{"query": "Explain quantum physics basics"}'
# Response includes: needs_confirm = True, confirm_message = "Vault miss..."

# PowerShell — Re-submit with confirmation (decline online)
Invoke-RestMethod -Uri "http://127.0.0.1:8787/query" `
  -Method POST `
  -ContentType "application/json" `
  -Body '{"query": "Explain quantum physics basics", "user_confirmed_online": false}'
```

If you prefer `curl` (ships with Windows 10+):

```cmd
:: CMD — single-line curl (Windows curl uses double quotes for JSON, escaped inner quotes)
curl -X POST http://127.0.0.1:8787/query -H "Content-Type: application/json" -d "{\"query\": \"What is Veeam immutability?\"}"

:: CMD — confirmation flow
curl -X POST http://127.0.0.1:8787/query -H "Content-Type: application/json" -d "{\"query\": \"Explain quantum physics basics\", \"user_confirmed_online\": false}"
```

## Hybrid Mode (Grok Fallback)

To enable Grok fallback:

1. Set `app.mode: "hybrid"` and `models.grok.enabled: true` in `config.yaml`
2. Set your API key:

```powershell
# PowerShell (session only)
$env:GROK_API_KEY = "your_key_here"

# PowerShell (persistent for current user)
[Environment]::SetEnvironmentVariable("GROK_API_KEY", "your_key_here", "User")
```

```cmd
:: CMD (session only)
set GROK_API_KEY=your_key_here

:: CMD (persistent for current user)
setx GROK_API_KEY "your_key_here"
```

3. Restart the gateway.

Grok is only called when ALL conditions are met:

- `app.mode == "hybrid"`
- `models.grok.enabled == true`
- `GROK_API_KEY` is set
- User explicitly confirms online escalation

## MCP Server (Retrieval Only)

```powershell
# PowerShell — pipe JSON-RPC to MCP server via stdio
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25"}}' | python mcp_hybrid_server.py
```

```cmd
:: CMD
echo {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25"}} | python mcp_hybrid_server.py
```

The MCP server exposes `hybrid_search` only. `sampling: null` is set at
protocol level — the MCP server **cannot** invoke an LLM.

## Testing

```powershell
# Run all tests (mocked — no live services required)
pytest tests\ -v

# Run specific test categories
pytest tests\test_stemmer.py -v          # Stemmer unit tests
pytest tests\test_sanitizer.py -v        # Prompt filter tests
pytest tests\test_audit.py -v            # Audit logging tests
pytest tests\test_graph.py -v            # LangGraph path tests
pytest tests\test_gate.py -v             # FastAPI endpoint tests
pytest tests\test_hybrid_search.py -v    # RRF fusion math tests
```

## Metrics

```powershell
python metrics.py
```

Parses `logs\audit.jsonl` and reports hit rate, score distribution,
model usage, and query volume.

## Configuration

All behavior controlled via `config.yaml`. Key settings:

| Setting | Description | Default |
|---|---|---|
| `app.mode` | `offline` or `hybrid` | `offline` |
| `retrieval.min_score` | Score threshold for local_llm path | `0.75` |
| `models.grok.enabled` | Enable Grok fallback | `false` |
| `policy.prompt_filter.enabled` | Input sanitization | `true` |
| `api.host` | Gateway bind address | `127.0.0.1` |
| `api.port` | Gateway port | `8787` |

## Security

- **Localhost only**: Gateway and LM Studio bind to `127.0.0.1`
- **No Ollama**: Embeddings are local sentence-transformers (no extra server)
- **Prompt filter**: Banned patterns stripped from input and corpus
- **Privacy redaction**: Emails, IPs, secrets redacted from audit logs
- **Query hashing**: Audit log stores SHA256 hashes, not raw queries
- **MCP sampling disabled**: Protocol-level guarantee of no LLM in MCP
- **No third-party tools**: Only `hybrid_search` exposed, hardcoded

## Windows-Specific Notes

**Path separators**: Python handles `/` fine on Windows, but if you see path errors in config.yaml, use forward slashes (`data/corpus`) or escaped backslashes (`data\\corpus`). The YAML parser handles both.

**Long path support**: If your project is nested deep, enable long paths:
```powershell
# Run as Administrator (one-time)
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force
```

**Firewall**: Windows Defender Firewall may prompt when LM Studio or the gateway starts listening. Allow on "Private networks" only — both bind to localhost so external access isn't needed.

**sentence-transformers first run**: The model download (`all-MiniLM-L6-v2`, ~80MB) goes to `.emb_cache\` in the project root. If your antivirus quarantines `.bin` files, add an exclusion for the project directory.

## Project Structure

```
safeclaw\
├── gate.py                     # FastAPI gateway (HTTP entry point)
├── graph.py                    # LangGraph state machine (controller)
├── mcp_hybrid_server.py        # MCP server (retrieval-only, stdio)
├── config.yaml                 # Controller-grade configuration
├── requirements.txt
├── metrics.py                  # Audit log analysis
├── retrieval\
│   ├── embeddings.py           # sentence-transformers wrapper (CPU)
│   ├── hybrid_search.py        # ChromaDB + BM25 + RRF fusion
│   ├── indexer.py              # Corpus ingestion + index builder
│   └── stemmer.py              # Enhanced Porter stemmer
├── llm\
│   └── client.py               # LM Studio + Grok clients
├── schemas\
│   └── api.py                  # Pydantic request/response models
├── utils\
│   ├── errors.py               # Typed exception hierarchy
│   ├── health.py               # Dependency health checks
│   ├── logger.py               # Audit logging (JSONL + hashing)
│   └── sanitizer.py            # Prompt injection filter
├── data\corpus\                # Your .md\.txt files
├── index\                      # ChromaDB + BM25 indices
├── logs\                       # Audit and application logs
└── tests\
    ├── conftest.py             # Shared mocks and fixtures
    ├── test_stemmer.py
    ├── test_sanitizer.py
    ├── test_audit.py
    ├── test_hybrid_search.py
    ├── test_graph.py           # LangGraph integration tests
    └── test_gate.py            # FastAPI endpoint tests
```
