Framework-agnostic, self-hosted, open-source human-in-the-loop approval control plane for agent workflows.
Sphinx is the missing middle layer between agent frameworks and the humans who must supervise them. It sits between LangGraph / OpenAI SDK / CrewAI / any MCP-capable agent and the reviewer, and gives you the things every framework leaves out:
- Unified approval entry point — one web console + one approval queue for every framework, instead of one bespoke UI per framework.
- Approval SLA with auto-degradation — timeouts that escalate, auto-approve or auto-reject per policy, so "human-in-the-loop" does not decay into "blind clicking OK".
- Decision log & feedback loop — records the delta between what the agent proposed and what a human decided, and feeds governance metrics: escalation rate, error escape rate, reviewer agreement, correction rate, SLA compliance, decision latency.
- Full decision capture — intercepts every tool call, LLM inference and state change into a tamper-evident trail (SHA3-256 hash chain + Ed25519 signatures), verifiable anytime via
GET /api/capture/verify. - MCP-native — every capability is exposed as MCP tools, aligned with the 2026 protocol convergence direction. Any MCP-capable agent can request approval without new SDK code.
- Single self-contained binary — the REST API, WebSocket live-events, MCP server and web console run in one Go process (CGO-free, SQLite embedded).
.
├── cmd/sphinx/ entrypoint — REST :8001, MCP :8100, web console, policy engine
├── internal/
│ ├── api/ REST + WebSocket handlers, CORS / logging middleware
│ ├── core/ policy engine, metrics, event bus, delta diffing, capture chain, store
│ ├── db/ SQLite bootstrap (modernc.org/sqlite, CGO-free)
│ ├── mcp/ MCP streamable-HTTP server exposing sphinx_* tools
│ ├── models/ domain types + DTOs (wire-compatible with the REST contract)
│ ├── seed/ default policies + realistic demo data + a capture chain
│ ├── config/ SPHINX_* environment configuration
│ └── web/ embedded web console (React build compiled into the binary)
├── web/ React + Vite + TS console: Queue / Decisions / Metrics / Policies
├── docker/ multi-stage Dockerfile (web → go → minimal runtime)
├── docker-compose.yml one-command stack: REST+MCP+console on :8001 / :8100
└── docs/ full documentation set (see below)
| doc | contents |
|---|---|
docs/API.md |
full REST + WebSocket reference with JSON examples |
docs/MCP.md |
MCP tools, config snippets, transport options |
docs/SDK.md |
integrate your agents (Go) — REST/MCP lifecycle + capture trail |
docs/USER_GUIDE.md |
install, run, configure, deploy — the operating manual |
docs/ARCHITECTURE.md |
component breakdown, key flows, capture chain internals |
docker compose up --build| Service | URL |
|---|---|
| Web console | http://localhost:8001 |
| REST API | http://localhost:8001/api |
| MCP endpoint | http://localhost:8100/mcp |
| Live events (WS) | ws://localhost:8001/api/ws |
The stack boots with default policies and 28 rows of realistic demo data, so every page is populated immediately.
go run ./cmd/sphinx # or: make run# with demo data and a fresh database
SPHINX_SEED_DEMO_DATA=1 SPHINX_DATABASE_URL=sqlite:////tmp/sphinx.db go run ./cmd/sphinxThe web console is embedded in the binary, so no separate frontend server is needed. During frontend development use Vite's dev server instead:
cd web && npm install && npm run devVite runs on :5173 and proxies /api to :8001 and /mcp to :8100.
An agent submits an action_payload for approval with a risk_level (low/medium/high/critical). Sphinx picks a matching policy, stamps an SLA deadline, and the request lands in the queue.
lowrisk under alow-risk-autopolicy → instant auto-approve (humans only see things that matter).- otherwise it stays
pendinguntil a human approves/rejects, the agent cancels, or the SLA fires.
A background engine scans pending requests every second. When a request exceeds its timeout_seconds, the policy's on_timeout action runs:
on_timeout |
effect |
|---|---|
escalate |
marks the request escalated (stays pending, shows in console) |
auto_approve |
auto-approves with the agent's payload |
auto_reject |
auto-rejects |
This is the anti-fatigue layer: if nobody looks at a ticket in time, the policy decides deterministically instead of leaving the agent stuck forever.
Every decision is written to the decision log with:
agent_decision— what the model proposedhuman_decision— what actually happened (may be amended)delta— a path-level diff of the two payloads (add/remove/replace)agreement— did the human confirm the agent unchanged?source—human_review/policy_timeout/auto_policy/agent_feedback
The metrics endpoint turns that log into governance KPIs:
| metric | definition |
|---|---|
escalation_rate |
escalated requests / total created |
timeout_rate |
SLA auto-decided / decided |
correction_rate |
human reviews that changed the payload / human reviews |
reviewer_agreement |
human reviews confirming the agent unchanged |
error_escape_rate |
approved actions reported with a negative outcome / approved with feedback |
sla_compliance_rate |
decisions reached before the SLA deadline / decided |
| latency | avg / p50 / p95 human review decision latency |
Mutations publish to an in-process event bus; the WebSocket endpoint /api/ws fans them out (requests, decisions, policies, capture topics). The console uses this for instant badge updates (and falls back to polling).
Tools: sphinx_request_approval, sphinx_get_status, sphinx_wait_for_decision, sphinx_get_decision, sphinx_submit_feedback, sphinx_list_policies.
# agent proposes a risky action
curl -X POST http://localhost:8001/api/requests \
-H 'Content-Type: application/json' \
-d '{"agent_id":"refund-agent","title":"Approve refund of $980 for order ORD-77241",
"action_payload":{"action":"refund","order_id":"ORD-77241","amount_usd":980},
"risk_level":"medium"}'
# → {"id":"...","ref":"SPH-1A2B3C","status":"pending", ...}
# agent waits (polls status)
curl http://localhost:8001/api/requests/SPH-1A2B3C
# → {"status":"approved","decision_payload":{"action":"refund","order_id":"ORD-77241","amount_usd":980}, ...}
# agent executes with the (possibly human-amended) payload, then reports the outcome
curl -X POST http://localhost:8001/api/requests/<id>/feedback \
-H 'Content-Type: application/json' \
-d '{"outcome":"success","note":"refund completed"}'Stream every tool call, LLM inference and state change into a tamper-evident
trail — each step is hashed (SHA3-256), chained via prev_hash and Ed25519-signed,
and can be re-verified at any time:
curl -X POST http://localhost:8001/api/capture \
-H 'Content-Type: application/json' \
-d '{"agent_id":"refund-agent","session_id":"sess-1","events":[
{"event_type":"tool_call","event_name":"lookup_order",
"input_payload":{"order_id":"ORD-88231"},
"output_payload":{"order":{"amount_usd":1240}},
"metadata":{"tool":"order_db","duration_ms":41},"status":"ok"}]}'
# later: verify the trail is untampered
curl "http://localhost:8001/api/capture/verify?agent_id=refund-agent&session_id=sess-1"
# → {"valid":true,"checked":1,"chains":1,"errors":[]}Capture is fail-open: if Sphinx is unreachable, events are dropped with a log
line and the agent's main path never blocks. See docs/API.md for the full
capture endpoints.
make test # go vet + go test ./... + frontend vitestThe Go suite covers the service layer, delta diffing, metrics math, the SLA scheduler, the full REST surface, WebSocket live events, MCP tools over a real streamable-HTTP server, capture-chain integrity and seed data. The frontend suite covers the queue, decisions, metrics, policies pages and live-update hooks.
All settings are read from SPHINX_* environment variables (see internal/config/config.go):
| variable | default | purpose |
|---|---|---|
SPHINX_DATABASE_URL |
sqlite:///./sphinx.db |
SQLite URL (WAL mode, CGO-free) |
SPHINX_API_PORT |
8001 |
REST API + WS + console port |
SPHINX_MCP_PORT |
8100 |
MCP streamable HTTP port |
SPHINX_SCHEDULER_INTERVAL_SECONDS |
1.0 |
SLA engine tick interval |
SPHINX_DEFAULT_POLICY_SEED |
true |
seed the 4 default policies at startup |
SPHINX_SEED_DEMO_DATA |
false |
seed 28 demo requests at startup |
SPHINX_CORS_ORIGINS |
* |
CORS allow-list |
SPHINX_LOG_LEVEL |
info |
log level |
Open source. Built for the LANDSLIDE human-computer collaboration initiative.