Skip to content

Repository files navigation

Synapse

Docker build (app image)

Agentic SRE incident co-pilot. Fully offline, no API keys.

Synapse investigates production incidents on its own. When an alert fires it queries your logs, searches a runbook knowledge base with hybrid vector + full-text retrieval, runs diagnostic commands through a guardrailed tool-use loop, and comes back with a structured resolution (root cause, exact commands, prevention, MTTR), then stays around for follow-up questions.

Every tool call goes through an in-process guardrail gateway first. Destructive commands (drops, deletes, force flags, scale-to-zero) never run automatically, they pause and wait for a human to approve, dry-run, or deny. More on that in Guardrails below.

Runs entirely in Docker on your machine. Nothing leaves your network.

Quickstart

git clone https://github.com/joshuabvarghesearghese/synapse && cd synapse
./start.sh

Opens at http://localhost:8501

First run pulls two models (roughly 2-5GB total depending on size, cached in a Docker volume so this only happens once):

  • qwen2.5-coder:3b for inference + tool-use (default; set OLLAMA_MODEL=qwen2.5-coder:7b if you've got the RAM for stronger reasoning)
  • nomic-embed-text for 768-dim embeddings
make up       # start everything
make status   # health check all services
make logs     # follow app logs
make shell    # psql into the database
make down     # stop (data preserved)
make reset    # wipe all volumes, start fresh

Running on Hugging Face Spaces

This also runs as a single-container HF Space, Postgres + Ollama + Streamlit baked into one image (see Dockerfile / start_hf.sh). The README frontmatter (sdk: docker, app_port: 7860) is what tells HF Spaces to actually build and run that Dockerfile. If it's missing or gets edited out, the Space silently falls back to a default template and nothing starts. Don't remove it.

Two things worth knowing if you're deploying there:

Storage is ephemeral by default. Every restart/sleep-wake re-runs start_hf.sh, which re-bootstraps Postgres from init.sql and re-seeds the KB from kb/*.md. That's fine for a stateless demo, but add an HF persistent storage volume if you want incident history or custom runbooks to survive restarts.

You can also point it at an external managed Postgres instead of the baked-in container: set POSTGRES_HOST / POSTGRES_PORT / POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB / POSTGRES_SSLMODE as Space secrets for Neon/Supabase/RDS. Just remember managed providers usually need POSTGRES_SSLMODE=require, the container default is disable.

If the sidebar shows "Postgres unreachable" it now shows the actual driver error underneath (auth failure, TLS required, connection refused, etc), which is usually enough to tell you what's wrong.

How it works

Alert fires
    │
    ▼
Synapse Agent Loop  (qwen2.5-coder:7b + tool-use, up to 8 steps)
    ├─→ query_logs(service, level)      → recent errors from Postgres
    ├─→ search_kb(query)                → hybrid pgvector + FTS → RRF fusion
    ├─→ run_diagnostic(kubectl/redis/aws) → simulate against live env
    └─→ run_diagnostic(verify fix)
    │
    ▼
Structured resolution: root cause · runbook · prevention · MTTR
    │
    ▼
Multi-turn follow-up conversation

Guardrails: a real MCP Gateway, not just the phrase

This is the same problem an MCP Gateway solves for LLM tool use in production: a model deciding to call a tool isn't the same thing as that call being safe to run, and the only place that distinction can be reliably enforced is in the infrastructure the call passes through, not the prompt, not the model's own judgment.

The gateway is a standalone module with zero Streamlit/Postgres/MCP-SDK dependencies (app/core/mcp_gateway.py): a Gateway class with pluggable risk policies, per-tool rate limiting, and an injectable audit sink. Synapse's specific policy (the destructive-command list, which tools are read-only, how decisions get persisted) is configured on top of it in app/core/guardrails.py. That one configured instance, SYNAPSE_GATEWAY, is shared by two front doors:

  1. The in-app agent loop (app/core/agent.py): Streamlit UI, a human clicks approve/dry-run/deny.
  2. A standalone MCP server (app/mcp_server.py): any external MCP client, no session to pause, so it requires an explicit confirm=true re-call instead. See MCP Server below.

What the gateway enforces regardless of which door the call came through:

Classification. Every proposed tool call gets inspected before execution. Read-only tools (query_logs, search_kb) are always safe. run_diagnostic commands get pattern-matched against a destructive-operation list: DROP/TRUNCATE/DELETE FROM, kubectl delete, Redis FLUSHALL/FLUSHDB, nodetool decommission, RDS instance deletion, --force flags, scale-to-zero, rm -rf, etcd member removal, Kafka topic deletion.

Interception. A destructive call never runs automatically. In the Streamlit UI the agent loop pauses (AgentSession.pending) and shows an approval card with three choices: approve & run, dry-run only (preview with zero side effects), or deny. From an external MCP client the same call just comes back refused with the policy's reason and a note to re-call with confirm=true.

Rate limiting. The gateway can cap calls-per-tool in a sliding window, independent of risk level, so a runaway loop can't hammer an expensive or destructive tool even when every individual call looked fine on its own.

Audit trail. Every decision, including auto-approved safe calls, gets written to guardrail_audit_log in Postgres, independent of the browser session or MCP client. There's a record of what was tried and what was allowed, from either surface.

Idempotency. Tool calls from the agent loop are cached by (name, canonical arguments) for the life of the investigation. A Streamlit rerun, a slow human clicking "approve," or the model asking for the same evidence twice all replay the cached result instead of re-executing, so a destructive action can't silently run twice just because the page re-rendered.

Fail-safe execution. Every tool call is wrapped so a failure (DB down, embedding service unreachable) degrades to an error string the agent can reason about, instead of an unhandled exception killing the whole investigation.

Toggle interception off with GUARDRAILS_ENABLED=false (not recommended outside local testing) and tune the step budget with AGENT_MAX_STEPS (default 8). The MCP server always enforces the gateway, there's no toggle there since there's no human session to fall back on.

MCP Server: use Synapse from any MCP client

app/mcp_server.py runs Synapse's tools as a real Model Context Protocol server over stdio, so any MCP-aware client can call search_kb, query_logs, and run_diagnostic directly, no Streamlit UI needed. Backed by the same Postgres hybrid search and the same SYNAPSE_GATEWAY policy as the in-app agent, so a command blocked in the UI is blocked here too.

Two ways to run it depending on your MCP client:

Option A: client spawns the process itself. Most desktop MCP clients start the server as a subprocess and talk over stdin/stdout, so they need a local Python environment with the app's deps:

docker compose up -d postgres ollama   # backing services only, Streamlit isn't required
cd app && pip install -r requirements.txt
python mcp_server.py                    # POSTGRES_HOST defaults to localhost

Point the client's config at that command. Most MCP clients use the same stdio server config shape, roughly:

{
  "mcpServers": {
    "synapse": {
      "command": "python",
      "args": ["/absolute/path/to/Synapse/app/mcp_server.py"],
      "env": {
        "POSTGRES_HOST": "localhost",
        "OLLAMA_HOST": "http://localhost:11434"
      }
    }
  }
}

Option B: everything in Docker. The app image already bundles mcp_server.py and its dependencies (built from the same requirements.txt as Streamlit), so once the stack is up you can exec straight into it, no separate install step, POSTGRES_HOST/OLLAMA_HOST already wired to postgres/ollama by docker-compose.yml:

make up               # or: docker compose up -d
make mcp-server        # docker compose exec -T app python mcp_server.py

Mainly useful for testing the server manually (echo '...' | make mcp-server against the raw JSON-RPC) or for an MCP client willing to shell out through docker compose exec as its spawn command, most desktop clients expect to launch a local process directly, which is what Option A is for.

Tools exposed:

Tool Risk Behaviour
list_scenarios safe Lists valid scenario_key values for the other tools
search_kb(query, top_k) safe Hybrid pgvector + FTS search, returns fused score plus contributing vec_rank/fts_rank
query_logs(service, level, limit) safe Recent log rows from Postgres
run_diagnostic(command, reason, scenario_key, confirm) gated Safe commands run immediately; ones the gateway classifies as destructive are refused with the policy's reason unless re-called with confirm=true

Demo scenarios

Scenario Severity Service What the agent finds
CPU runaway: nginx fork storm P1 nginx worker_processes=512 in ConfigMap; rolling restart + HPA
DB replica lag: checkout 500s P1 postgres Aurora failover lag; promotes replica, rotates secret
Memory leak: auth-svc OOM P2 auth-svc Unbounded JWTCache; sets eviction policy + restarts
Bad deploy: ConfigMap missing key P2 checkout DB_REPLICA_HOST missing from v2.4.1; rollback to v2.4.0
Redis cluster fail: cascade timeout P2 redis Disk-full node; sentinel failover + circuit breaker reset

Stack

Layer Technology Notes
LLM Ollama qwen2.5-coder:3b (default) Runs fine on CPU-only Spaces; swap in 7b/14b for stronger reasoning
Embeddings nomic-embed-text (768-dim) Runs in Ollama, no extra process
Vector search PostgreSQL + pgvector HNSW m=16, ef_construction=64
Full-text search Postgres tsvector + GIN Auto-maintained by trigger
Search fusion Reciprocal Rank Fusion (RRF) Beats either signal alone; explain=True exposes per-signal ranks
Agent Ollama tool-use loop Up to 8 steps, idempotent, full audit trail
Gateway Standalone core/mcp_gateway.py Pluggable risk policies, rate limiting, audit sink, no framework deps
MCP server mcp_server.py (stdio) Same tools + same gateway, callable from any MCP client
Conversation Multi-turn with full context Incident + resolution in system prompt
KB watcher watchdog file observer Drop .md files → indexed in seconds
UI Streamlit Custom dark-mode terminal aesthetic
Infra Docker Compose Postgres 16, Ollama, Streamlit

Adding your own runbooks

Drop .md files into kb/, they get embedded and indexed automatically, no restart needed:

## INC-2025-0099 · P1 · redis · cache, eviction

**Title:** Redis maxmemory hit: cache miss storm

**Resolution:**
Root cause: maxmemory-policy was set to noeviction. Under memory pressure,
all writes returned OOM errors, cascading to checkout timeouts.

Steps:
1. `redis-cli CONFIG SET maxmemory-policy allkeys-lru`
2. `redis-cli BGREWRITEAOF`
3. Verify hit rate recovers: `redis-cli INFO stats | grep keyspace`

MTTR: 4 minutes.
Prevention: Set maxmemory-policy in redis.conf before deployment.

Changing the model

Set OLLAMA_MODEL in .env (copy from .env.example):

OLLAMA_MODEL=qwen2.5-coder:14b   # better reasoning, needs ~10 GB RAM
OLLAMA_MODEL=mistral-small        # alternative, strong structured output
OLLAMA_MODEL=llama3.1:8b          # general purpose, good tool-use

Configuration

All settings via environment variables. Copy .env.example to .env:

cp .env.example .env
Variable Default Description
OLLAMA_MODEL qwen2.5-coder:3b Inference model
EMBED_MODEL nomic-embed-text Embedding model (must produce 768-dim vectors)
POSTGRES_HOST localhost Postgres host, point at an external managed instance if you want
POSTGRES_PORT 5432 Postgres port
POSTGRES_USER ops Database user
POSTGRES_PASSWORD ops Database password
POSTGRES_DB synapse Database name
POSTGRES_SSLMODE disable Set to require for managed providers (Neon/Supabase/RDS)
GUARDRAILS_ENABLED true Require human approval for destructive tool calls
AGENT_MAX_STEPS 8 Tool-use loop step budget
APP_PORT 8501 Streamlit port

GPU (NVIDIA): uncomment the deploy block in docker-compose.yml.

Architecture

See ARCHITECTURE.md for the full technical write-up: design decisions, the hybrid search implementation, why HNSW over IVFFlat, RRF math, and what's next.

License

MIT

Releases

Packages

Contributors

Languages