The complete reference for SentinelGate — the universal MCP proxy/firewall for AI agents.
What is MCP? The Model Context Protocol (MCP) is the standard way AI agents (Claude, Gemini, Codex, etc.) interact with external tools — reading files, querying databases, calling APIs. SentinelGate sits between the agent and those tools as a security proxy: every tool call passes through SentinelGate, which enforces policies, scans for sensitive data, logs everything, and provides an emergency kill switch. The agent connects to SentinelGate instead of directly to the tool servers.
For installation, see the README.
Table of Contents
- Architecture
- Quick Start — Bootstrap, Profiles, Kill Switch, Container Setup
- Policy Engine — CEL Rules, Templates, Session-Aware Policies, Quotas, Transforms
- Agent Configuration
- Security Features — Content Scanning, Evidence, Tool Security, HITL, Drift Detection, Red Team, Cost Tracking, Agent Health
- Admin UI
- Configuration Reference — YAML Reference, Environment Variables
- CLI Reference
- Admin API Reference
- Multi-Agent Sessions
- Troubleshooting
- FAQ
- Threat Model and Limitations
- Policy Decision API (PDP) — Govern non-MCP agents
graph LR
A[AI Agent] --> B[SentinelGate MCP Proxy]
B --> CA[CanonicalAction]
CA --> IC[Interceptor Chain]
IC -->|Allow| U[Upstream MCP Servers]
IC -->|Deny| BL[Blocked]
SentinelGate sits between AI agents and upstream MCP servers. Every MCP tool call passes through SentinelGate's policy engine before reaching the upstream server.
- Agents connect to SentinelGate as if it were an MCP server
- SentinelGate discovers tools from all configured upstream MCP servers
- Every tool call is evaluated against your policies before forwarding
- When upstream servers change, connected clients are automatically notified via
notifications/tools/list_changed
Every MCP tool call is converted to a CanonicalAction — a unified representation containing the action type, name, arguments, identity, destination, and protocol. The policy engine evaluates all actions identically.
Every CanonicalAction passes through an ordered chain:
| # | Interceptor | What it does |
|---|---|---|
| 0 | Kill Switch | Emergency stop — blocks tool_call actions when active (protocol actions like initialize, ping, tools/list pass through) |
| 1 | Validation | Well-formed JSON-RPC? |
| 2 | IP Rate Limit | Too many requests from this IP? |
| 3 | Auth | Valid identity and API key? Session management |
| 4 | Audit | Wraps interceptors below it (#5–#14) — logs tool-call decisions, latency, scan results. Kill switch, validation, IP rate limit, and auth denials (#0–#3) are logged to stdout, not the audit trail. |
| 5 | Budget Block | Monthly budget exceeded? Denies all tool calls when Cost Tracking budget is exceeded with action "block" |
| 6 | Quota | Session/tool quotas exceeded? (calls, writes, deletes) |
| 7 | User Rate Limit | Too many requests from this identity? |
| 8 | Quarantine | Tool flagged by integrity drift detection? |
| 9 | Policy (CEL) | Evaluate rules — highest priority match wins |
| 10 | Approval (HITL) | Human approval required? Blocking wait with timeout |
| 11 | Transform | Response transforms (redact, truncate, inject, mask, dry_run) |
| 12 | Content Scan (Input) | PII/secret scanning in tool arguments |
| 13 | Response Scan (Output) | Prompt injection detection in upstream responses |
| 14 | Route | Forward to correct upstream via tool cache |
:8080/mcp MCP Proxy (multi-upstream, tool discovery, policy enforcement)
:8080/admin Admin UI (policy CRUD, audit log, config, dashboard)
:8080/admin/api REST API for programmatic management
:8080/health Liveness probe (component health)
:8080/readyz Readiness probe (bootstrap complete, tools discovered, kill switch off)
:8080/metrics Prometheus metrics
Default:
127.0.0.1:8080(localhost only). Docker images use0.0.0.0:8080(all interfaces viaPORTenv var).
For installation instructions, see the README.
This section covers: starting the server, adding upstreams, connecting agents, automated bootstrap (file + API), sandbox profiles, kill switch, and container deployment. Skip to any subsection using the sidebar navigation.
sentinel-gate startThe Admin UI is available at http://localhost:8080/admin. On first launch (no upstreams configured), a guided onboarding wizard walks you through the setup: Add Server → Connect Agent → Set Rules. You can also apply a policy template directly from the onboarding page.
In the Admin UI, go to Tools & Rules and click Add Upstream. Add one or more MCP servers:
# Example: filesystem server
npx @modelcontextprotocol/server-filesystem /path/to/dir
# Example: remote MCP server
https://my-mcp-server.example.com/mcp
SentinelGate discovers tools from all upstreams automatically.
In the Admin UI, go to Connections:
- Create an identity (name + roles)
- Create an API key for that identity
- Save the
cleartext_key— it is shown only once
Configure your AI agent to use SentinelGate as its MCP server. The Connections page in the Admin UI has a Connect Your Agent section with ready-to-use configuration snippets for 7 agent types:
- Claude Code — CLI command or
~/.claude/settings.json - Gemini CLI —
~/.gemini/settings.json - Codex CLI —
~/.codex/config.toml - Cursor / IDE — MCP server settings in IDE
- Python — MCP client library
- Node.js — MCP client library
- cURL — Direct HTTP calls
See Agent Configuration for detailed instructions per agent.
When you add, remove, or restart upstream MCP servers, SentinelGate sends a notifications/tools/list_changed notification to all connected MCP clients. Agents that support this notification automatically refresh their tool list — no reconnection needed.
You can add or remove upstream MCP servers at any time from the Admin UI. No restart needed — SentinelGate discovers tools immediately and the agent sees them on its next request.
In the Admin UI, go to Tools & Rules and create rules. Rules have a priority — the highest priority matching rule wins.
| Priority | Rule | Action |
|---|---|---|
| 0 | Match all tools | Deny (baseline) |
| 10 | Match all tools | Allow (override) |
| 20 | CEL: action_arg_contains(arguments, "secret") |
Deny |
Everything is allowed (priority 10 beats 0), except actions involving "secret" (priority 20 beats 10). Test rules before deploying with the built-in Policy Test playground.
For automated deployments (Docker, Kubernetes, CI/CD), use file-based bootstrap instead of the Admin UI wizard. Drop a bootstrap.json file in one of these locations (checked in order):
- Path in
SENTINEL_GATE_BOOTSTRAP_FILEenvironment variable /etc/sentinelgate/bootstrap.json- Same directory as
state.json
Example bootstrap.json:
{
"profile": "strict",
"upstreams": [
{
"name": "filesystem",
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
},
{
"name": "remote-api",
"type": "http",
"url": "https://my-mcp-server.example.com/mcp"
}
],
"identities": [
{"name": "agent-1", "roles": ["user"]},
{"name": "agent-2", "roles": ["admin"]}
]
}On first boot, SentinelGate reads the file, creates upstreams, discovers tools, creates identities, generates API keys, applies the profile policies, and configures content scanning. API keys are written to bootstrap-keys.json (same directory as state.json) and logged to stdout for container log capture.
The bootstrap-keys.json file format:
[
{"identity_name": "agent-1", "identity_id": "uuid", "cleartext_key": "sg_..."},
{"identity_name": "agent-2", "identity_id": "uuid", "cleartext_key": "sg_..."}
]The bootstrap file is consumed once (renamed to bootstrap.json.consumed). On subsequent starts, the system loads from state.json.
Bootstrap fields:
| Field | Required | Description |
|---|---|---|
profile |
No | Security profile: strict, standard, or permissive (see below) |
upstreams |
Yes | MCP servers to connect |
identities |
Yes | Agent identities with roles |
default_policy |
No | System-wide default policy (allow or deny); overrides profile default |
policies |
No | Additional policies with custom rules |
content_scanning |
No | Override content scanning settings |
Upstream fields:
| Field | For type | Description |
|---|---|---|
name |
both | Unique upstream name (required) |
type |
both | stdio or http (required) |
command |
stdio |
Executable path (required for stdio) |
args |
stdio |
Arguments array |
url |
http |
Remote MCP server URL (required for http) |
env |
both | Environment variables (key-value map) |
Content scanning override:
"content_scanning": {
"enabled": true,
"mode": "enforce",
"input_scan_enabled": true
}If profile is omitted, the system starts with no rules — configure everything via the dashboard or API.
API bootstrap is also available via POST /admin/api/v1/bootstrap with the same request format (requires CSRF token). Returns 409 if already bootstrapped.
Three built-in profiles provide pre-configured security stances:
| Strict | Standard | Permissive | |
|---|---|---|---|
| Default policy | deny | deny | allow |
| Read (read_, list_, search_, get_) | allow | allow | allow |
| Write (write_, create_, edit_, update_) | deny | allow | allow |
| Delete (delete_, remove_, drop_*) | deny | deny | allow |
| Sensitive paths (.env, .ssh, /etc/shadow, credentials) | deny | deny | deny |
| Exfiltration domains (pastebin, ghostbin, transfer.sh) | deny | deny | deny |
| Content scanning | enforce | monitor | monitor |
| Input scan (PII/secrets in arguments) | on | on | off |
Profiles are starting points — after bootstrap, customize everything from the dashboard or API.
The kill switch is an emergency stop that instantly blocks all MCP tool calls. It is the outermost interceptor in the chain — when active, nothing gets through.
From the dashboard: Click Activate Kill Switch on the main dashboard page.
From the API:
# First, get a CSRF token (needed for all write operations)
COOKIE_JAR=$(mktemp)
curl -s -c "$COOKIE_JAR" http://localhost:8080/admin/ > /dev/null
CSRF_TOKEN=$(grep sentinel_csrf_token "$COOKIE_JAR" | awk '{print $NF}')
# Activate
curl -X POST http://localhost:8080/admin/api/v1/system/kill \
-H "Content-Type: application/json" \
-b "$COOKIE_JAR" -H "X-CSRF-Token: $CSRF_TOKEN" \
-d '{"reason":"suspicious activity detected"}'
# Check status
curl http://localhost:8080/admin/api/v1/system/kill -b "$COOKIE_JAR"
# Resume
curl -X POST http://localhost:8080/admin/api/v1/system/resume \
-b "$COOKIE_JAR" -H "X-CSRF-Token: $CSRF_TOKEN"Behavior when active:
- All tool calls return a generic "Access denied by policy" error (intentionally vague for security)
/readyzreturns HTTP 503 with"kill_switch": "not ready: kill switch active"- The dashboard shows the kill switch banner with reason and denied call count
- State is persisted — survives restarts
Note: the denied call count shown in the banner is in-memory only. After clicking Resume Operations, the count is hidden from the UI but remains accessible via
GET /admin/api/v1/system/kill. It is reset to zero on the next activation and on every process restart (it is not persisted instate.json).
Agents can detect the kill switch by polling /readyz and checking for HTTP 503.
SentinelGate provides deployment examples for 13 platforms and an interactive demo (examples/playground/) in the examples/ directory:
| Platform | Directory | Description |
|---|---|---|
| Docker Compose | examples/docker/ |
Standalone container |
| Docker + Agent | examples/docker-demo/ |
SentinelGate + MCP server + agent simulator |
| Docker + Claude | examples/claude-code/ |
Claude Code in isolated container with SentinelGate as gateway |
| Kubernetes | examples/kubernetes/ |
Namespace, deployment, service, configmap |
| AWS ECS Fargate | examples/ecs-fargate/ |
Task definition + service |
| Fly.io | examples/fly-io/ |
fly.toml + volume |
| Modal | examples/modal/ |
Serverless Python app |
| E2B | examples/e2b/ |
Firecracker microVM sandbox |
| Firecracker | examples/firecracker/ |
Manual microVM setup |
| Podman | examples/podman/ |
Rootless container + Quadlet |
| LXC/LXD | examples/lxc-lxd/ |
System container |
| Daytona | examples/daytona/ |
Dev container |
| systemd | examples/systemd/ |
Linux service |
Each example includes a README with step-by-step instructions.
The most secure pattern is running the AI agent inside a Docker container with SentinelGate as the only gateway to the filesystem:
┌─── Docker ──────────────────────────────────────┐
│ │
│ AI Agent (Claude, etc.) │
│ │ brain │ hands │
│ │ │ │
└────┼───────────────────────────┼──────────────────┘
│ │
▼ ▼
Model API SentinelGate
(internet) (policy + audit)
│
▼
MCP Servers
(your files)
The agent talks to its model API over the internet (how it thinks). For file operations, it must go through SentinelGate (how it acts). The container has no direct filesystem access — SentinelGate is the only door.
See examples/claude-code/ for a complete working example with Claude Code.
| Variable | Default | Description |
|---|---|---|
SENTINEL_GATE_ADMIN_OPEN |
true (Docker image) |
Allow admin API access from any IP (required for Docker — port forwarding doesn't preserve localhost). Binary default is false. |
SENTINEL_GATE_ALLOWED_HOSTS |
(none) | Comma-separated hostnames allowed for /mcp DNS rebinding protection (e.g., sentinel-gate for Docker Compose) |
SENTINEL_GATE_BOOTSTRAP_FILE |
(none) | Path to bootstrap.json file |
SENTINEL_GATE_STATE_PATH |
./state.json |
Path to state persistence file |
PORT |
(none) | Set listen address to :<PORT> when no http_addr is configured (cloud platforms) |
Embedded (simplest): SentinelGate spawns the MCP server as a subprocess. Agent connects via HTTP. Used in docker-demo and claude-code.
Sidecar (Kubernetes): SentinelGate runs in the same pod as the agent. Agent connects to localhost:8080/mcp. Network policies block direct access to upstreams.
Gateway (multi-agent): Single SentinelGate instance serves multiple agents. Each agent has its own identity, API key, and policies.
SentinelGate listens on HTTP by design. In production, terminate TLS at a reverse proxy in front of SentinelGate. This is the standard pattern for Go services — it keeps the application simple and lets you manage certificates in one place.
sentinelgate.example.com {
reverse_proxy localhost:8080
}Caddy automatically provisions and renews TLS certificates via Let's Encrypt. No manual certificate management required.
server {
listen 443 ssl;
server_name sentinelgate.example.com;
ssl_certificate /etc/ssl/certs/sentinelgate.crt;
ssl_certificate_key /etc/ssl/private/sentinelgate.key;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
}
}services:
sentinelgate:
image: ghcr.io/sentinel-gate/sentinel-gate:latest
expose:
- "8080"
caddy:
image: caddy:2-alpine
ports:
- "443:443"
- "80:80"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
depends_on:
- sentinelgateWith the Caddyfile:
sentinelgate.example.com {
reverse_proxy sentinelgate:8080
}Why no built-in TLS? SentinelGate is a security proxy for AI agents, not a web server. Delegating TLS to a reverse proxy follows the principle of separation of concerns: the reverse proxy handles transport security, SentinelGate handles tool-call security. This also lets you share TLS termination across multiple services.
Policies contain rules. Each rule has:
- A name (human-readable identifier)
- A priority (integer — higher priority wins)
- A condition (tool pattern or CEL expression)
- An action (
allow,deny, orapproval_required)
All matching rules are sorted by priority. The highest-priority match wins. If no rule matches, the default action is allow.
Important
When using sandbox profiles (strict or standard), the profile creates a baseline deny rule at priority 0 that blocks everything, then allow rules at higher priority for permitted operations. The effective behavior is "deny by default" even though the engine's built-in fallback is "allow". Without any profiles or rules, the system allows all tool calls.
Note
When creating rules via the API, tool_match defaults to "*" (match all tools) if omitted.
| Pattern | Matches |
|---|---|
read_file |
Exactly read_file |
read_* |
Any tool starting with read_ |
*_file |
Any tool ending with _file |
* |
All tools |
For advanced conditions, use CEL (Common Expression Language) — the same engine used by Kubernetes, Firebase, and Envoy.
Action variables (always available):
| Variable | Type | Example values |
|---|---|---|
action_type |
string | "tool_call" |
action_name |
string | "read_file", "bash", "list_directory" |
arguments |
map | {"path": "/etc/passwd"} |
Identity variables:
| Variable | Type | Example values |
|---|---|---|
identity_name |
string | "claude-prod", "test-agent" |
identity_id |
string | "id-1" |
identity_roles |
list | ["admin", "reader"] |
user_roles |
list | Alias for identity_roles (backward-compatible) |
session_id |
string | Current session identifier |
request_time |
timestamp | When the request was received |
Context variables:
| Variable | Type | Example values |
|---|---|---|
protocol |
string | "mcp" |
framework |
string | "crewai", "langchain", "autogen" |
gateway |
string | "mcp-gateway" |
framework_attrs |
map(string,string) | Additional framework-specific attributes (e.g., framework_attrs["crewai.role"]) |
Destination variables (when the action has a target):
| Variable | Type | Description |
|---|---|---|
dest_url |
string | Full destination URL |
dest_domain |
string | Destination domain only |
dest_ip |
string | Resolved destination IP |
dest_port |
int | Destination port number |
dest_scheme |
string | "http", "https" |
dest_path |
string | URL path or file path |
dest_command |
string | Command being executed |
Backward-compatible aliases (MCP):
| Alias | Equivalent to |
|---|---|
tool_name |
action_name when action_type == "tool_call" |
tool_args |
arguments when action_type == "tool_call" |
Argument & pattern functions:
| Function | Description |
|---|---|
action_arg_contains(arguments, "pattern") |
Search all argument values for a substring |
action_arg(arguments, "key") |
Get a specific argument value by key |
glob(pattern, name) |
Glob pattern match (e.g., glob("read_*", action_name)) |
dest_domain_matches(dest_domain, "*.evil.com") |
Glob match on destination domain |
dest_ip_in_cidr(dest_ip, "10.0.0.0/8") |
CIDR range check on destination IP |
Session history functions (require session_action_history, session_action_set, or session_arg_key_set):
| Function | Description |
|---|---|
session_count(session_action_history, "read") |
Count actions by call type |
session_count_for(session_action_history, "tool_name") |
Count actions by tool name |
session_count_window(session_action_history, "tool", 60) |
Count actions in last N seconds |
session_has_action(session_action_set, "tool") |
Tool used in session? |
session_has_arg(session_arg_key_set, "key") |
Arg key used in session? |
session_has_arg_in(session_action_history, "key", "tool") |
Arg key used with specific tool? |
session_sequence(session_action_history, "a", "b") |
Check A occurred before B |
session_time_since_action(session_action_history, "tool") |
Seconds since last call (-1 if never) |
Standard CEL operators: ==, !=, &&, ||, !, .contains(), .startsWith(), .endsWith(), .matches() (regex), in (list membership), has() (key existence).
- Expression length: 1,024 characters maximum
- Cost limit: 100,000 (prevents expensive expressions)
- Nesting depth: 50 levels maximum
- Evaluation timeout: 5 seconds per expression
MCP tools typically use these argument field names:
| Field | Examples |
|---|---|
path |
File path for read/write operations |
source, destination |
Source and destination for copy/move operations |
command |
Command to execute |
url |
URL for web requests |
query |
Search query |
Tip
Use action_arg_contains(arguments, "pattern") to search across all fields regardless of tool. For field-specific checks, use has():
(has(arguments.path) && arguments.path.contains("secret"))
|| (has(arguments.command) && arguments.command.contains("secret"))
# Block access to files containing "secret"
action_arg_contains(arguments, "secret")
# Only admins can execute shell commands (set as deny rule)
action_name == "bash" && !("admin" in identity_roles)
# Block data exfiltration
dest_domain_matches(dest_domain, "*.pastebin.com") || dest_domain_matches(dest_domain, "*.ngrok.io")
# Restrict writes to a specific directory
action_name == "write_file"
&& !action_arg_contains(arguments, "/safe/workspace/")
# Block untrusted domains
dest_domain_matches(dest_domain, "*.untrusted.com")
Via Admin UI: Tools & Rules → Policy Test sandbox.
Via API:
curl -X POST http://localhost:8080/admin/api/v1/policy/evaluate \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: test-token" \
-H "Cookie: sentinel_csrf_token=test-token" \
-d '{
"action_type": "tool_call",
"action_name": "read_file",
"arguments": {"path": "/etc/passwd"},
"identity_name": "test-agent",
"identity_roles": ["agent"],
"protocol": "mcp"
}'Note
The Policy Evaluate API generates audit records for each evaluation.
Seven pre-built security profiles you can apply with one click from the Admin UI (Tools & Rules → Use Template) or via API:
| Template | What it does |
|---|---|
| Safe Coding | Allows read operations and writes to non-sensitive paths. Ideal for AI coding assistants. |
| File Server — Read Only | Permits only file-system read operations (read_file, read_text_file, read_multiple_files, read_media_file, list_directory, list_directory_with_sizes, list_allowed_directories, search_files, list_files, get_file_info). Blocks all other tools including those from other servers. |
| Research Mode | Allows reading, web searches, and writing to temporary directories. Blocks all other modifications. |
| Full Lockdown | Blocks all tool calls unconditionally. Use when you need to completely disable agent activity. |
| Audit Only | Allows all tool calls but logs everything for monitoring. No blocking, full visibility. |
| Data Protection | Blocks writes to sensitive paths (.env, credentials, .ssh, /etc). Allows reads and other writes. |
| Anti-Exfiltration | Detects and blocks data exfiltration patterns: reading sensitive files followed by sending data externally. |
Templates create independent policies — modify or delete them without affecting the template.
Via API:
# List available templates
curl http://localhost:8080/admin/api/v1/templates
# Apply a template
curl -X POST http://localhost:8080/admin/api/v1/templates/read-only/applyCEL functions that use session history for context-dependent decisions. These evaluate the sequence and frequency of actions within the current session.
| Variable / Function | Returns | Use case |
|---|---|---|
session_call_count |
int | Total tool calls in session |
session_write_count |
int | Write operations in session |
session_delete_count |
int | Delete operations in session |
session_duration_seconds |
int | Session duration in seconds |
session_cumulative_cost |
double | Cumulative cost of all actions in session |
session_sequence(session_action_history, "tool_a", "tool_b") |
bool | True if tool_a was called before tool_b in this session |
session_count_window(session_action_history, "tool_name", seconds) |
int | Calls to tool_name in the last N seconds |
session_count_for(session_action_history, "tool_name") |
int | Total calls to a specific tool in session |
session_time_since_action(session_action_history, "tool_name") |
int | Seconds since last call to a specific tool |
session_has_action(session_action_set, "tool_name") |
bool | True if tool was called in this session |
session_has_arg(session_arg_key_set, "key") |
bool | True if any call in session had this argument key |
session_count(session_action_history, "read") |
int | Count actions by call type ("read", "write", "delete", "other") in the full session history |
session_has_arg_in(session_action_history, "key", "tool") |
bool | True if a specific argument key was used with a specific tool in this session |
# Deny send_email if read_file was called earlier in the session
session_sequence(session_action_history, "read_file", action_name)
Apply this as a deny rule with tool_match: "send_*" to block any send operation after a file read.
These variables reflect the calling agent's behavioral health metrics, enabling adaptive policies that respond to anomalous behavior.
| Variable | Type | Description |
|---|---|---|
user_deny_rate |
double | Agent's deny rate (0.0–1.0) over recent history |
user_drift_score |
double | Behavioral drift score (0.0 = stable, 1.0 = highly anomalous) |
user_violation_count |
int | Total policy violations by this agent |
user_total_calls |
int | Total lifetime tool calls by this agent |
user_error_rate |
double | Agent's error rate (0.0–1.0) |
# Block writes for agents with high deny rate
user_deny_rate > 0.15 && tool_name.contains("write")
# Require approval for drifting agents
user_drift_score > 0.3 && tool_name.contains("delete")
# Rate-limit agents with many violations
user_violation_count > 50 && session_call_count > 10
In the Policy Test playground, expand the Session Context section to add simulated previous actions. Each action has a tool name, call type (read/write/delete/other), and a "seconds ago" value.
Per-identity usage limits enforced at the interceptor level. Configure via Connections → Identity → Quota button, or via API.
| Field | Description |
|---|---|
enabled |
Must be true for the quota to be enforced. Defaults to false if omitted. |
max_calls_per_session |
Maximum total tool calls per session |
max_writes_per_session |
Maximum write operations per session |
max_deletes_per_session |
Maximum delete operations per session |
max_calls_per_minute |
Rate limit (calls per minute) |
tool_limits |
Per-tool call limits (map of tool name → max calls, e.g. {"write_file": 10, "delete_file": 5}) |
action |
What happens when a limit is reached: deny (block) or warn (log only) |
# Set quota for an identity
curl -X PUT http://localhost:8080/admin/api/v1/quotas/{identity_id} \
-H "Content-Type: application/json" \
-d '{"enabled": true, "max_calls_per_session": 100, "max_writes_per_session": 20, "tool_limits": {"delete_file": 5, "execute_command": 10}, "action": "deny"}'
# Get quota
curl http://localhost:8080/admin/api/v1/quotas/{identity_id}
# Remove quota
curl -X DELETE http://localhost:8080/admin/api/v1/quotas/{identity_id}Live quota usage is visible in the Dashboard Active Sessions widget with color-coded progress bars.
Transform tool responses before they reach the agent. Configure via Tools & Rules → Transforms tab, or via API.
| Type | What it does | Key fields |
|---|---|---|
| redact | Replace regex matches with a placeholder | patterns (regex list), replacement (default: [REDACTED]) |
| truncate | Limit response size | max_bytes, max_lines, suffix |
| inject | Add text before/after the response | prepend, append |
| dry_run | Replace the real response with a mock | response (JSON template) |
| mask | Partially reveal matched values | mask_patterns, visible_prefix, visible_suffix, mask_char |
Each transform rule has a name, type, tool_match (glob pattern), priority, and enabled toggle.
The Transforms tab includes a Test Transform sandbox. Paste sample text, set a tool name, and run against saved rules or custom JSON rules to see the transformed output.
# Create a redact transform
curl -X POST http://localhost:8080/admin/api/v1/transforms \
-H "Content-Type: application/json" \
-d '{
"name": "redact-api-keys",
"type": "redact",
"tool_match": "*",
"priority": 1,
"enabled": true,
"config": {
"patterns": ["sk-[a-zA-Z0-9]{20,}"],
"replacement": "[REDACTED]"
}
}'
# Test a transform
curl -X POST http://localhost:8080/admin/api/v1/transforms/test \
-H "Content-Type: application/json" \
-d '{"text": "My key is sk-abc123xyz456def789ghi", "rules": [{"name": "test", "type": "redact", "tool_match": "*", "enabled": true, "config": {"patterns": ["sk-[a-zA-Z0-9]{20,}"]}}]}'Record every tool call with full context for replay and analysis. Configure via Sessions → Recording Configuration.
| Setting | Description |
|---|---|
| Enable Recording | Master toggle for session recording |
| Record Payloads | When off (privacy mode), only metadata is recorded — no request args or response body |
| Max File Size | Maximum size in bytes for a single recording JSONL file |
| Retention Days | Auto-delete recordings older than N days |
| Redact Patterns | Regex patterns to redact from recorded payloads |
| Storage Directory | Directory where recording files are stored (default: recordings) |
Click a session in the list to see a vertical timeline of every event:
- Sequence number, timestamp, tool name, decision badge, latency
- Click to expand: rule ID, reason, request arguments, response, transforms applied, quota state
- Deny events highlighted with red border
- JSON — Full structured export of all events
- CSV — Tabular export for spreadsheet analysis
# Enable recording
curl -X PUT http://localhost:8080/admin/api/v1/recordings/config \
-H "Content-Type: application/json" \
-d '{"enabled": true, "record_payloads": true, "retention_days": 30, "storage_dir": "recordings"}'
# List recordings
curl http://localhost:8080/admin/api/v1/recordings
# Get recording events
curl http://localhost:8080/admin/api/v1/recordings/{id}/events
# Export
curl http://localhost:8080/admin/api/v1/recordings/{id}/exportAll agents connect to SentinelGate the same way: by configuring SentinelGate as an MCP server in their settings. Each agent has its own configuration format.
Option A: CLI command
claude mcp add --transport http sentinelgate http://localhost:8080/mcp \
--header "Authorization: Bearer <your-api-key>"Tip
Add -s user to install globally across all projects (e.g., claude mcp add -s user sentinelgate ...).
Option B: Settings file (~/.claude/settings.json)
{
"mcpServers": {
"sentinelgate": {
"type": "http",
"url": "http://localhost:8080/mcp",
"headers": {
"Authorization": "Bearer <your-api-key>"
}
}
}
}Option A: CLI command
gemini mcp add --transport http -s user \
--header "Authorization: Bearer <your-api-key>" \
sentinelgate http://localhost:8080/mcpOption B: Settings file (~/.gemini/settings.json)
{
"mcpServers": {
"sentinelgate": {
"url": "http://localhost:8080/mcp",
"type": "http",
"headers": {
"Authorization": "Bearer <your-api-key>"
}
}
}
}Important
At least one upstream MCP server must be configured in SentinelGate (Admin UI → Tools & Rules → Add Upstream) for Gemini to have access to tools.
Option A: CLI command
export SG_KEY="<your-api-key>"
codex mcp add sentinelgate --url http://localhost:8080/mcp \
--bearer-token-env-var SG_KEYOption B: Settings file (~/.codex/config.toml)
[mcp_servers.sentinelgate]
url = "http://localhost:8080/mcp"
bearer_token_env_var = "SG_KEY"Then launch with: SG_KEY="<your-api-key>" codex
Note
Codex does not persist the API key. Unlike Claude Code and Gemini CLI (which save the key in their settings files), Codex only stores the name of an environment variable. The variable must be set in your terminal each time. To make it permanent, add it to your shell profile:
echo 'export SG_KEY="<your-api-key>"' >> ~/.zshrc # macOS/Linux (zsh)
echo 'export SG_KEY="<your-api-key>"' >> ~/.bashrc # Linux (bash)Alternative: You can also use direct headers instead of environment variables:
[mcp_servers.sentinelgate.headers]withAuthorization = "Bearer <your-key>". This avoids the env var requirement but embeds the key in the config file.
Add SentinelGate as an MCP server in your IDE's MCP settings (e.g. .cursor/mcp.json):
{
"mcpServers": {
"sentinelgate": {
"type": "http",
"url": "http://localhost:8080/mcp",
"headers": {
"Authorization": "Bearer <your-api-key>"
}
}
}
}The exact configuration location depends on the IDE. Cursor uses .cursor/mcp.json, Windsurf uses its own MCP settings panel.
For Python agents that use an MCP client library, point the client at SentinelGate:
import httpx
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client
http_client = httpx.AsyncClient(
headers={"Authorization": "Bearer <your-api-key>"}
)
async with streamable_http_client(
"http://localhost:8080/mcp", http_client=http_client
) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
result = await session.call_tool("read_file", {"path": "/tmp/test.txt"})For Node.js agents that use an MCP client library:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport(
new URL("http://localhost:8080/mcp"),
{ requestInit: { headers: { "Authorization": "Bearer <your-api-key>" } } }
);
const client = new Client({ name: "my-client", version: "1.0.0" });
await client.connect(transport);
const { tools } = await client.listTools();
const result = await client.callTool({ name: "read_file", arguments: { path: "/tmp/test.txt" } });For testing or scripting, call the MCP endpoint directly:
# List available tools
curl -X POST http://localhost:8080/mcp \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}'
# Call a tool
curl -X POST http://localhost:8080/mcp \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "read_file", "arguments": {"path": "/tmp/test.txt"}}}'Note: These examples skip the MCP
initializehandshake for brevity. A compliant MCP client should first sendinitialize, receive the session ID, then sendinitializedbefore making tool calls.
Complete reference: For Docker Compose networking, Kubernetes service DNS, cloud platforms, IDE setup, framework integration, and the Policy Decision API — see Connecting Agents.
Two scanning engines protect against different threats:
Response scanning (IPI defense) — Scans tool responses for prompt injection patterns before forwarding to agents. Detects system prompt overrides, role hijacking, instruction injection, delimiter escapes, model delimiter escapes, hidden instructions, context switches, tool poisoning directives, system tag injection, DAN/jailbreak attempts, and instruction delimiter escapes.
| Setting | Behavior |
|---|---|
enabled: true, mode: "enforce" |
Block responses containing detected prompt injection patterns |
enabled: true, mode: "monitor" |
Allow but log a warning (recommended for initial deployment) |
enabled: false |
No scanning |
curl -X PUT http://localhost:8080/admin/api/v1/security/content-scanning \
-H "Content-Type: application/json" \
-d '{"enabled": true, "mode": "enforce"}'Input scanning (PII/secrets) — Scans tool call arguments for sensitive data before forwarding to upstream servers:
| Pattern Type | Action | Examples |
|---|---|---|
| Email, Credit Card, SSN, UK NI | mask |
Replaced with type-specific labels: [REDACTED-EMAIL], [REDACTED-CC], [REDACTED-SSN], [REDACTED-NINO] |
| Phone numbers | mask |
Replaced with [REDACTED-PHONE] |
| AWS/GCP/Azure/Stripe/GitHub keys, generic secrets | block |
Request rejected |
Configure via Admin UI (Security → Input Scanning) or API:
# Toggle input scanning
curl -X PUT http://localhost:8080/admin/api/v1/security/input-scanning \
-H "Content-Type: application/json" \
-d '{"enabled": true}'
# Add whitelist exception (skip email detection for a specific tool)
curl -X POST http://localhost:8080/admin/api/v1/security/input-scanning/whitelist \
-H "Content-Type: application/json" \
-d '{"pattern_type": "email", "scope": "tool", "value": "send_email"}'
# Whitelist scopes:
# "tool" — skip for a specific tool (any agent)
# "agent" — skip for a specific agent identity (any tool)
# "path" — skip for files matching a path patternConfigurable pattern actions — Each pattern type can be individually configured to off, alert, mask, or block via the Admin UI (Security → Input Scanning → Pattern Types dropdown) or API:
# Change email detection from mask to block
curl -X PUT http://localhost:8080/admin/api/v1/security/input-scanning \
-H "Content-Type: application/json" \
-d '{"pattern_actions": {"email": "block"}}'
# Disable phone number detection entirely
curl -X PUT http://localhost:8080/admin/api/v1/security/input-scanning \
-H "Content-Type: application/json" \
-d '{"pattern_actions": {"phone_number": "off"}}'| Action | Behavior |
|---|---|
off |
Pattern is not scanned |
alert |
Detect and log, but allow the request through |
mask |
Replace matched content with [REDACTED-xxx] placeholder |
block |
Reject the entire tool call |
Pattern action overrides are persisted in state.json and survive restarts.
Input scanning events (content.pii_detected, content.secret_detected) appear in the Notification Center.
Every tool call decision is recorded as a tamper-proof cryptographic evidence chain. Each record is ECDSA P-256 signed with a hash chain linking it to the previous record. Evidence is enabled by default — the signing key is auto-generated on first boot.
YAML configuration (optional — defaults are sensible):
evidence:
enabled: true
key_path: "evidence-key.pem" # Auto-generated if missing
output_path: "evidence.jsonl"
signer_id: "my-server"Verify the evidence chain:
# Verify with private key
sentinel-gate verify --evidence-file evidence.jsonl --key-file evidence-key.pem
# Verify with public key (preferred for external auditors)
sentinel-gate verify --evidence-file evidence.jsonl --pub-key evidence-key.pub.pemEvidence records include: identity, tool name, decision, policy matched, reason (omitempty), latency_micros (int64, microseconds), timestamp, chain hash, and ECDSA signature. Used by the Compliance module for EU AI Act Art. 13-15 evidence requirements.
Continuously monitors upstream MCP server tools for unauthorized changes (tool poisoning). Fully automatic — no manual setup required.
How it works:
- Auto-baseline at first boot — When SentinelGate starts and discovers tools for the first time, the baseline is captured automatically. No admin action needed.
- Auto-baseline on upstream changes — When you add or remove an MCP server via the Admin UI or API, the baseline is updated automatically. You can also click Capture Baseline to refresh it manually at any time.
- Periodic re-discovery (every 5 minutes) — SentinelGate re-calls
tools/liston ALL active upstreams periodically. If a tool's definition has changed since the baseline, drift is detected immediately. - Drift check on restart — When an upstream is restarted, its tools are re-discovered and checked against the baseline.
- Auto-quarantine on schema change — If a tool's definition (description or input schema) has changed, the tool is automatically quarantined and blocked from execution until an admin reviews and accepts the change.
Drift types:
| Type | Severity | Auto-quarantine | Action required |
|---|---|---|---|
added |
Warning | Yes | Review and un-quarantine if trusted |
removed |
Warning | No | Investigate removal |
changed |
Warning | Yes | Accept change or keep quarantined |
Notifications — drift events (tool.changed, tool.new, tool.removed) appear in the Notification Center with upstream name and tool details.
Admin workflow for quarantined tools:
# Check current drift
curl http://localhost:8080/admin/api/v1/tools/drift
# Accept a legitimate change (updates baseline, does NOT remove quarantine)
curl -X POST http://localhost:8080/admin/api/v1/tools/accept-change \
-H "Content-Type: application/json" \
-d '{"tool_name": "updated_tool"}'
# Remove quarantine after accepting the change
curl -X DELETE http://localhost:8080/admin/api/v1/tools/quarantine/updated_tool
# Or quarantine manually if you spot something suspicious
curl -X POST http://localhost:8080/admin/api/v1/tools/quarantine \
-H "Content-Type: application/json" \
-d '{"tool_name": "suspicious_tool"}'
# List all quarantined tools
curl http://localhost:8080/admin/api/v1/tools/quarantine
# Re-capture baseline manually (rarely needed — baseline updates automatically)
curl -X POST http://localhost:8080/admin/api/v1/tools/baselineHigh-risk actions can require human approval. When a policy returns approval_required, the action is held pending until approved via Admin UI or API.
Note: The
approval_requiredaction is available only through the Admin UI or API. YAML config files support onlyallowanddeny.
Example via API:
# Create a policy with approval_required action
# (uses $COOKIE_JAR and $CSRF_TOKEN from the CSRF pattern in the Kill Switch section above)
curl -X POST http://localhost:8080/admin/api/policies \
-b "$COOKIE_JAR" -H "X-CSRF-Token: $CSRF_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "require-approval-for-deletes",
"rules": [{
"name": "approve-deletes",
"tool_match": "delete_*",
"action": "approval_required",
"priority": 100
}]
}'# List pending
curl http://localhost:8080/admin/api/v1/approvals
# Get decision context (session trail, agent history, assessment)
curl http://localhost:8080/admin/api/v1/approvals/{id}/context
# Approve with audit note
curl -X POST http://localhost:8080/admin/api/v1/approvals/{id}/approve \
-d '{"note":"approved per cleanup procedure"}'
# Deny with reason and audit note
curl -X POST http://localhost:8080/admin/api/v1/approvals/{id}/deny \
-d '{"reason":"suspicious activity","note":"blocked per policy"}'When an approval is pending, the Admin UI Notification Center shows a notification with Review/Approve/Deny buttons. Clicking "Review" opens the Decision Context panel with:
- Request Detail — tool, arguments, which policy triggered the hold, the CEL condition
- Session Trail — the agent's recent actions in chronological order
- Agent History — how many times this agent has used this tool (last 30 days)
- Contextual Assessment — deterministic notes (target is staging, agent consulted docs, etc.)
- Audit Note — free-text field included in the cryptographic evidence (EU AI Act Art. 14)
Events emitted: approval.hold, approval.approved, approval.rejected, approval.timeout.
Warning
Stdio-based upstream MCP servers (e.g., npx) may timeout while waiting for approval.
SentinelGate analyzes agent behavior over time and detects when an agent deviates from its baseline. No ML — purely statistical (KL divergence, threshold-based comparisons, distribution shift detection).
# Get drift reports for all agents
curl http://localhost:8080/admin/api/v1/drift/reports
# Get drift profile for a specific agent
curl http://localhost:8080/admin/api/v1/drift/profiles/{identity_id}
# Reset baseline
curl -X POST http://localhost:8080/admin/api/v1/drift/profiles/{identity_id}/reset
# Get drift config
curl http://localhost:8080/admin/api/v1/drift/configAnomaly types detected:
- tool_shift — Tool usage distribution changed significantly (e.g., bash went from 8% to 41%)
- deny_rate — Denial rate increased beyond threshold
- error_rate — Error rate increased beyond threshold
- latency — Average latency changed significantly
- temporal — Activity pattern shifted (e.g., nighttime activity where baseline shows none)
- arg_shift — Argument keys changed (e.g., tool started receiving different parameters)
Drift score: 0.0 (no drift) to 1.0 (severe drift). Visible in Agent View KPI strip. The detail view shows a Score Components breakdown (deny rate, error rate, total calls, avg latency, temporal pattern, argument shift) comparing historical (14-day) vs current values, plus a tool distribution comparison.
Default thresholds (configurable): 14-day baseline window, 1-day current window, 20% tool shift, 10% deny/error rate change, 50% latency change, 0.30 KL divergence.
Events emitted: drift.anomaly (with anomaly details), drift.baseline_reset.
SentinelGate observes actual tool usage and compares it against policy-granted permissions to identify over-privileged agents. Four modes:
- disabled — No analysis
- shadow — Report only (no notifications)
- suggest — Report + notify admin of permission gaps
- auto — Apply auto-tighten suggestions after grace period
Permission Gap Types:
- never_used — Tool permitted but zero calls in observation window (default 14 days)
- rarely_used — Tool used 1-2 times in window
- temporal_excess — Tool active only in narrow time window (consider time-based restriction)
Least Privilege Score: (used tools / permitted tools) × 100. Higher = better. Agents with score < 50% are flagged with warning severity.
Auto-Tighten Suggestions: Generated CEL deny rules for over-privileged tools, scoped to the specific identity. Can be applied individually or in batch. Whitelisted tools (configurable) are never suggested for removal.
Configuration (runtime via API):
{
"mode": "suggest",
"learning_days": 14,
"grace_period_days": 7,
"whitelist_tools": ["health_check", "auth_verify"]
}Events emitted: permissions.gap_detected (with score, gap count), permissions.auto_tighten_applied (with tool list).
API endpoints:
GET /admin/api/v1/permissions/health— All agents health reportsGET /admin/api/v1/permissions/health/{identity_id}— Single agent healthGET /admin/api/v1/permissions/suggestions/{identity_id}— Auto-tighten suggestionsPOST /admin/api/v1/permissions/apply— Apply suggestions (body:{identity_id, suggestion_ids})GET /admin/api/v1/permissions/config— Shadow mode configPUT /admin/api/v1/permissions/config— Update shadow mode config
Filter the tools/list response based on the caller's roles. An agent with the "marketing" role sees only marketing tools — finance tools don't exist in their universe. This is stronger than policy deny (which blocks but reveals the tool exists).
Configure from the admin UI via API — no YAML config needed. Default: disabled (all tools visible to all roles).
Modes per role:
- Whitelist (
visible_tools): only listed tools are visible. Supports glob patterns (read_*). - Blacklist (
hidden_tools): listed tools are hidden, everything else visible. - No rule: role has no restrictions (all tools visible).
When an identity has multiple roles, visibility is the union — if any role grants visibility, the tool is shown.
API endpoints:
GET /admin/api/v1/namespaces/config— Current namespace configurationPUT /admin/api/v1/namespaces/config— Update config (body:{enabled, rules})
Example config:
{
"enabled": true,
"rules": {
"marketing": {"visible_tools": ["search", "read_file", "analytics_*"]},
"intern": {"hidden_tools": ["delete_*", "exec_command"]},
"admin": {}
}
}Export traces and metrics to stdout in OpenTelemetry format. Enable/disable from the admin UI — no YAML config needed.
Traces: One span per tool call with attributes: sg.identity_id, sg.tool_name, sg.decision, sg.drift_score.
Metrics:
sg.tool_calls.total— Counter by tool, decision, identitysg.tool_calls.duration— Histogram in millisecondssg.tool_calls.denied— Counter of denied callssg.approvals.total— Counter by identity, tool, outcome
Traces and metrics are written to stdout in OTel JSON format when enabled. Use log collection (Fluentd, Vector, etc.) to forward to your observability stack.
API endpoints:
GET /admin/api/v1/telemetry/config— Current telemetry configurationPUT /admin/api/v1/telemetry/config— Update config (body:{enabled, service_name})
Configure a webhook URL to receive event notifications via HTTP POST:
webhook:
url: "https://hooks.slack.com/services/..."
secret: "my-hmac-secret" # optional, HMAC-SHA256 signing
events: ["approval.hold", "drift.anomaly"] # optional, empty = all eventsThe webhook receives JSON payloads with type, source, severity, timestamp, requires_action, and payload fields. When secret is set, payloads are signed with HMAC-SHA256 in the X-Signature-256 header.
Built-in attack simulation that tests your policies against 30 MCP-specific attack patterns across 6 categories:
| Category | Patterns | Tests |
|---|---|---|
| Tool Misuse | 7 | Unauthorized access, path traversal in names, role violations |
| Argument Manipulation | 7 | Command injection, SQL injection, SSRF, template injection |
| Prompt Injection (Direct) | 5 | Instruction override, system prompt, base64 encoding |
| Prompt Injection (Indirect) | 5 | Model delimiters, hidden instructions, context switching |
| Permission Escalation | 4 | Role claiming, impersonation, config modification |
| Multi-Step Attack | 2 | Recon-then-exploit, data exfiltration chains |
Each vulnerability includes a suggested CEL remediation policy that can be applied with one click.
API endpoints:
POST /admin/api/v1/redteam/run— Run full suite or by category (body:{target_identity, roles, category})POST /admin/api/v1/redteam/run/single— Run single pattern (body:{pattern_id, target_identity, roles})GET /admin/api/v1/redteam/corpus— List available attack patternsGET /admin/api/v1/redteam/reports— Recent scan reportsGET /admin/api/v1/redteam/reports/{id}— Specific report
Cost estimation and budget guardrails for tool calls. Estimates cost per call based on configurable per-tool rates, tracks cumulative costs by identity and tool, and enforces budgets when approached or exceeded.
Retroactive tracking: Costs include all calls made during the current month, even before enabling Cost Tracking. Historical data is calculated from audit logs when the feature is activated.
Features:
- Per-tool cost configuration (default $0.01/call, customizable)
- Per-identity monthly budgets with configurable action: Notify (alert only) or Block (deny all tool calls when exceeded)
- Threshold alerts at 70%, 85%, and 100% of budget
- Cost drill-down: by identity → by tool
- Linear projection to end of period
API endpoints:
GET /admin/api/v1/finops/costs— Cost report for period (query:start,end)GET /admin/api/v1/finops/costs/{identity_id}— Identity cost detailGET /admin/api/v1/finops/budgets— Budget statuses (triggers alert check)GET /admin/api/v1/finops/config— Current Cost Tracking configurationPUT /admin/api/v1/finops/config— Update config (body:{enabled, default_cost_per_call, tool_costs, budgets, budget_actions, alert_thresholds}). Budget entries acceptaction: "notify"(default) oraction: "block"(deny calls when exceeded). Thebudget_actionsmap overrides per-identity budget actions (values:"notify"or"block").
Per-agent health metrics with trend analysis and baseline comparison. The health dashboard fuses into the Agent View (no separate page), providing deny rate, drift score, error rate, and violation tracking with 30-day sparklines and baseline comparison.
Health Metrics:
- deny_rate — Percentage of denied calls (0.0 to 1.0)
- drift_score — Behavioral drift score from Drift Detection (0.0 to 1.0)
- error_rate — Percentage of error calls (0.0 to 1.0)
- violation_count — Total policy violations (denials + scan blocks)
Health Status Classification:
- healthy — All metrics below warning thresholds
- attention — At least one metric above warning but below critical
- critical — At least one metric above critical threshold
Default Thresholds (configurable via API):
- Deny rate: warning 10%, critical 25%
- Drift score: warning 0.30, critical 0.60
- Error rate: warning 5%, critical 15%
CEL Variables for policy rules: user_deny_rate, user_drift_score, user_violation_count, user_total_calls, user_error_rate. See Agent health variables for full documentation and examples.
Health Trend: 30-day sparklines for deny rate, error rate, violations, and call volume visible in the Agent View detail.
Cross-Agent Health Overview: Standalone table comparing all agents sorted by health status severity, accessible via "Health Overview" button on the agent list page.
API endpoints:
GET /admin/api/v1/agents/{identity_id}/health— Agent health trend + baseline comparisonGET /admin/api/v1/health/overview— Cross-agent health overviewGET /admin/api/v1/health/config— Current health thresholdsPUT /admin/api/v1/health/config— Update thresholds (body:{deny_rate_warning, deny_rate_critical, ...})
Events emitted: health.alert (when status is attention or critical).
Available at http://localhost:8080/admin when the server is running.
Dashboard — Real-time stats: upstream count, tool count, allowed/denied/rate-limited counts, protocol distribution chart, framework activity widget, active sessions with live quota progress bars, upstream status, recent activity feed. Auto-refreshes via SSE.
Expandable use-case cards (MCP Proxy, Connect Your Agent) with numbered steps and copyable code snippets, plus feature cards (Policy Templates, Response Transforms, Budget & Quota, Session Recording) linking to the relevant pages.
Four tabs:
- Tools & Rules — Tool list grouped by upstream (with allow/deny badges you can click to create/edit rules), policy rules (create/edit/delete with priority and CEL), "Use Template" for one-click policy templates. The Visual Policy Builder modal offers a condition builder with typed operators, a variable catalog (35+ variables across 8 categories), smart suggestions, real-time policy linter, and bidirectional CEL ↔ Builder sync.
- Transforms — Response transform rules (redact, truncate, inject, dry_run, mask) with a test sandbox.
- Policy Test — Policy evaluation playground with optional session context for testing session-aware rules.
- Simulation — Run "what-if" analysis on policy changes: simulates candidate rules against recent audit records and shows allow→deny / deny→allow impact, impacted agents, and impacted tools.
Identity management (name + roles), API key management (cleartext shown once at creation), per-identity quota configuration (calls, writes, deletes, rate limits). Connect Your Agent section with 7 tabs: Claude Code, Gemini CLI, Codex CLI, Cursor/IDE, Python, Node.js, cURL — each with ready-to-use configuration snippets.
Unified timeline of all intercepted actions. Filter by decision (allow/deny), protocol (MCP, HTTP, WebSocket), tool, identity, time period (including custom date range). Click entries for full detail panel. CSV export.
Session recording configuration (enable/disable, privacy mode, retention, redact patterns). Session list with filters (identity, date range, denies). Click a session for timeline replay with expandable event cards. Export to JSON or CSV.
Content scanning (response: monitor/enforce modes; input: PII/secret detection with configurable pattern actions and whitelist), tool security baseline/drift/quarantine with diff viewer.
Shadow mode analysis of agent permissions vs actual usage. Heat matrix with least privilege scores per agent, gap analysis (never used, rarely used, temporal excess), auto-tighten suggestions with one-click apply or edit in Policy Builder. Shadow mode config (disabled/shadow/suggest/auto) with configurable learning window.
Interactive red team report with scorecard per attack category, vulnerability cards with expand/collapse, attack explanation, suggested CEL remediation policy, one-click apply or edit in Policy Builder, and re-test button for immediate verification. Configurable target identity and roles.
Cost explorer with budget progress bars, cost drill-down by identity and tool, budget status tracking, and linear projection. Configurable per-tool costs and per-identity budgets with Notify or Block actions. Retroactive: includes all calls from the current month even before enabling.
Unified agent view with drill-down per identity. Header card (name, roles, session, health status badge), KPI strip (calls, denied, deny%, drift, violations), 30-day health trend sparklines (deny rate, error rate, violations, call volume), tool usage breakdown with proportional bars, drift score components breakdown (deny rate, error rate, calls, latency, temporal, arg shift), and chronological activity timeline. "Health Overview" button shows cross-agent comparison table sorted by health severity.
Action queue for events requiring attention (tool drift, content scan detections, permission gaps, red team vulnerabilities, budget alerts) and informational events. Real-time SSE updates, badge counter in sidebar. Actions: accept change, quarantine, view diff, navigate, apply suggestions.
Coverage map for regulatory framework packs (EU AI Act Art. 13-15). Overall score bar, per-requirement cards with pass/fail evidence checks, and evidence bundle generator (JSON download). Disclaimer always visible.
SentinelGate works with zero configuration. Everything can be managed from the Admin UI. YAML is optional for advanced tuning.
Config loaded from (first found wins): ./sentinel-gate.yaml, $HOME/.sentinel-gate/sentinel-gate.yaml, /etc/sentinel-gate/sentinel-gate.yaml (Linux/macOS) or %ProgramData%\sentinel-gate\sentinel-gate.yaml (Windows). Both .yaml and .yml extensions are accepted at each path.
# Server
server:
http_addr: "127.0.0.1:8080" # Listen address (default: "127.0.0.1:8080")
log_level: "info" # debug, info, warn, error (default: "info")
session_timeout: "30m" # Admin session timeout (default: "30m")
allowed_hosts: [] # Additional hostnames for DNS rebinding protection on /mcp
# (default: [] — localhost/127.0.0.1/::1 always allowed)
# Also settable via SENTINEL_GATE_ALLOWED_HOSTS env var (comma-separated)
# Rate limiting
rate_limit:
enabled: true # (default: true)
ip_rate: 100 # Per-IP requests/minute (default: 100)
ip_burst: 100 # Per-IP burst size (default: same as ip_rate)
user_rate: 1000 # Per-identity requests/minute (default: 1000)
user_burst: 1000 # Per-identity burst size (default: same as user_rate)
cleanup_interval: "5m" # (default: "5m")
max_ttl: "1h" # (default: "1h")
# Audit
audit:
output: "stdout" # "stdout" or "file:///path" (default: "stdout")
channel_size: 1000 # Async buffer size (default: 1000)
batch_size: 100 # Flush batch size (default: 100)
flush_interval: "1s" # (default: "1s")
send_timeout: "100ms" # (default: "100ms")
warning_threshold: 80 # Warn at N% full (default: 80)
buffer_size: 1000 # In-memory ring buffer for UI (default: 1000)
# Audit file rotation (when output is file)
audit_file:
dir: ""
retention_days: 7 # (default: 7)
max_file_size_mb: 100 # (default: 100)
cache_size: 1000 # (default: 1000)
# Cryptographic evidence (optional)
evidence:
enabled: true # Enable signed evidence chain (default: true)
key_path: "evidence-key.pem" # ECDSA P-256 key (auto-generated if missing)
output_path: "evidence.jsonl" # Append-only evidence file
signer_id: "" # Signer identifier (default: hostname)
# Webhook notifications (optional)
webhook:
url: "" # HTTP endpoint to POST events to
secret: "" # HMAC-SHA256 secret for signing payloads
events: [] # Event types to send (empty = all)
# Upstream MCP server (optional, can also configure via Admin UI)
upstream:
command: "" # MCP executable path
args: [] # Arguments
http: "" # URL for remote MCP server
http_timeout: "30s" # (default: "30s")
# Auth (optional, can also configure via Admin UI)
auth:
identities:
- id: "id-1"
name: "my-agent"
roles: ["agent"]
api_keys:
- key_hash: "sha256:abc..." # Use `sentinel-gate hash-key` to generate
identity_id: "id-1"
# Policies (optional, can also configure via Admin UI)
# YAML rules only support name, condition, action. Priority is determined by
# rule order (first rule = highest priority). tool_match is always "*".
# For full control over tool_match and priority, use the Admin UI or API.
policies:
- name: "deny-secrets"
rules:
- name: "block-secret-files"
condition: 'action_arg_contains(arguments, "secret")'
action: "deny"Override any YAML key with SENTINEL_GATE_ prefix, underscores for nesting:
SENTINEL_GATE_SERVER_HTTP_ADDR=:9090 sentinel-gate start
SENTINEL_GATE_RATE_LIMIT_ENABLED=true sentinel-gate startSpecial variables:
SENTINEL_GATE_STATE_PATH— override state file location (default:./state.json)
State (policies, identities, API keys, upstreams from Admin UI) persists in state.json in the working directory. YAML loads first, then state.json overlays on top. Runtime changes via Admin UI always go to state.json.
Note
Policy IDs are preserved across restarts. You can reference policies by either ID or name.
Start the proxy server.
sentinel-gate startStop the running server. Reads PID from ~/.sentinelgate/server.pid and sends a graceful shutdown signal. Waits up to 10s, then force-kills if needed.
Print version, commit hash, build date, Go version, OS/architecture.
Generate a SHA-256 hash for use in YAML config. Output format: sha256:<hex>. Deterministic.
sentinel-gate hash-key <api-key>Reset to a clean state, removing all runtime configuration created via the Admin UI or API.
By default, removes state.json and its backup. This clears all upstreams, policies, identities, API keys, content scanning config, and quotas. After a reset, the next start boots clean — as if it were the first launch.
If you have a
sentinel-gate.yamlconfig file, settings defined there will still be loaded. The reset only affects runtime state, not your YAML configuration.
| Flag | Default | Description |
|---|---|---|
--force |
false |
Skip the confirmation prompt |
--include-audit |
false |
Also remove audit log files (if using file-based audit output) |
--include-certs |
false |
Also remove TLS inspection CA certificates (~/.sentinelgate/) |
| Scope | Files | Content |
|---|---|---|
| Default | ./state.json, ./state.json.bak |
All runtime state |
--include-audit |
Audit log file / directory | As configured in audit.output and audit_file.dir |
--include-certs |
~/.sentinelgate/ |
CA certificate and private key for TLS inspection |
sentinel-gate reset # Interactive confirmation
sentinel-gate reset --force # Skip confirmation
sentinel-gate reset --include-audit --include-certs --force # Full cleanup
sentinel-gate reset --state /etc/sentinel-gate/state.json --force # Custom state pathNote
The server must be stopped before resetting. If no state files exist, the command prints "Nothing to reset" and exits cleanly.
If the server is running and you want to reset without stopping it, use the Admin API:
curl -X POST http://localhost:8080/admin/api/system/factory-reset \
-H "Content-Type: application/json" \
-d '{"confirm": true}'This is also accessible from the Admin UI via the Command Palette (Cmd+K → type "reset").
The live factory reset removes all runtime state while the server keeps running:
| Cleared | Preserved |
|---|---|
| MCP servers (upstream connections stopped) | Audit logs (compliance) |
| Policies and rules | YAML config defaults |
| Identities and API keys | Read-only resources from YAML |
| Quotas and transforms | |
| Active sessions | |
| Pending HITL approvals | |
| Tool baseline and quarantine | |
| All feature configs (scanning, recording, drift, telemetry, namespaces, Cost Tracking, health, permissions, evidence) | |
| Policy evaluation history | |
| Stats and notifications |
After the reset, the system is in the same state as a fresh start — ready to be configured from the Admin UI or API.
Note
Connected agents are immediately disconnected. Concurrent resets are prevented (returns HTTP 409).
Verify the integrity of a cryptographic evidence chain file.
| Flag | Default | Description |
|---|---|---|
--evidence-file |
(required) | Path to the JSONL evidence file |
--key-file |
— | Path to the private key PEM file (mutually exclusive with --pub-key) |
--pub-key |
— | Path to PEM public key file (preferred for external verification) |
One of --key-file or --pub-key is required.
sentinel-gate verify --evidence-file evidence.jsonl --key-file evidence-key.pem
sentinel-gate verify --evidence-file evidence.jsonl --pub-key evidence-key.pub.pemChecks: hash chain integrity (each record links to the previous), ECDSA signature verification for every record. Exits 0 if valid, 1 if tampered.
| Flag | Default | Description |
|---|---|---|
--config |
./sentinel-gate.yaml |
Config file path |
--state |
./state.json |
State file path |
Admin endpoints are under http://localhost:8080/admin/api/. Health probes (/health, /readyz) are at the server root. Write operations require CSRF protection.
Important
CSRF required on all POST/PUT/DELETE calls. Get the token first:
COOKIE_JAR=$(mktemp)
curl -s -c "$COOKIE_JAR" http://localhost:8080/admin/api/auth/status > /dev/null
CSRF_TOKEN=$(grep sentinel_csrf_token "$COOKIE_JAR" | awk '{print $NF}')Then add -b "$COOKIE_JAR" -H "X-CSRF-Token: $CSRF_TOKEN" to every write request. Most curl examples below omit these headers for brevity.
Note
Core CRUD endpoints (upstreams, policies, identities, keys, audit) use /admin/api/. Newer feature endpoints (policy evaluation, approvals, security, tool security) use /admin/api/v1/. Both prefixes are stable. The path for each endpoint is shown in the sections below.
All admin API errors return a JSON body:
{"error": "descriptive error message"}Common HTTP status codes:
| Code | Meaning |
|---|---|
| 400 | Bad request (invalid JSON, missing required field, validation failure) |
| 403 | CSRF token mismatch or admin access denied |
| 404 | Resource not found |
| 409 | Conflict (already bootstrapped, reset in progress) |
| 429 | Rate limited — check Retry-After header for seconds until next allowed request |
| 503 | Kill switch active or system unhealthy |
GET /admin/api/auth/status Get auth status + CSRF token (no auth required)
# Get auth status + CSRF token
curl -c cookies.txt http://localhost:8080/admin/api/auth/status
# Use CSRF token for write operations
CSRF_TOKEN=$(grep sentinel_csrf_token cookies.txt | awk '{print $NF}')
curl -b cookies.txt -H "X-CSRF-Token: $CSRF_TOKEN" \
-X POST http://localhost:8080/admin/api/policies \
-H "Content-Type: application/json" \
-d '{"name": "my-policy", ...}'GET /admin/api/upstreams List upstreams
POST /admin/api/upstreams Add upstream
PUT /admin/api/upstreams/{id} Update upstream
DELETE /admin/api/upstreams/{id} Remove upstream
POST /admin/api/upstreams/{id}/restart Restart upstream
Create upstream request:
{"name": "my-server", "type": "http", "url": "http://localhost:3000/mcp"}{"name": "local-server", "type": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]}GET /admin/api/tools List discovered tools (includes conflicts)
POST /admin/api/tools/refresh Force re-discovery
GET /admin/api/policies List policies
POST /admin/api/policies Create policy
PUT /admin/api/policies/{id} Update policy
DELETE /admin/api/policies/{id} Delete policy
DELETE /admin/api/policies/{id}/rules/{ruleId} Delete a single rule from a policy
POST /admin/api/policies/test Test policy (sandbox)
Create policy example:
curl -X POST http://localhost:8080/admin/api/policies \
-H "Content-Type: application/json" \
-d '{
"name": "deny-secrets",
"rules": [{
"name": "block-secret-files",
"condition": "action_arg_contains(arguments, \"secret\")",
"action": "deny",
"priority": 20,
"tool_match": "*"
}]
}'POST /admin/api/v1/policy/evaluate Evaluate policy (note: v1 in path)
GET /admin/api/v1/policy/evaluate/{request_id}/status Check evaluation status
GET /admin/api/identities List identities
POST /admin/api/identities Create identity
PUT /admin/api/identities/{id} Update identity
DELETE /admin/api/identities/{id} Delete identity
Create identity request:
{"name": "my-agent", "roles": ["user"]}GET /admin/api/keys List all keys
POST /admin/api/keys Create key (response: cleartext_key)
DELETE /admin/api/keys/{id} Delete key
Create key request:
{"identity_id": "<identity-uuid>", "name": "my-key"}Create key response (cleartext shown only once):
{"id": "uuid", "identity_id": "uuid", "name": "my-key", "cleartext_key": "sg_..."}GET /admin/api/audit Query audit log (?limit=200)
GET /admin/api/audit/stream SSE event stream
GET /admin/api/audit/export CSV export
GET /admin/api/v1/approvals List pending approvals
GET /admin/api/v1/approvals/{id}/context Decision context (session trail, history, assessment)
POST /admin/api/v1/approvals/{id}/approve Approve (body: {"note":"..."})
POST /admin/api/v1/approvals/{id}/deny Deny (body: {"reason":"...","note":"..."})
GET /admin/api/v1/drift/reports All drift reports
GET /admin/api/v1/drift/profiles/{identity_id} Drift profile for identity
POST /admin/api/v1/drift/profiles/{identity_id}/reset Reset baseline
GET /admin/api/v1/drift/config Current thresholds
PUT /admin/api/v1/drift/config Update drift detection configuration
GET /admin/api/v1/security/content-scanning Get response scan mode
PUT /admin/api/v1/security/content-scanning Update response scan mode
GET /admin/api/v1/security/input-scanning Get input scan config
PUT /admin/api/v1/security/input-scanning Toggle input scanning
POST /admin/api/v1/security/input-scanning/whitelist Add whitelist exception
DELETE /admin/api/v1/security/input-scanning/whitelist/{id} Remove whitelist exception
POST /admin/api/v1/security/content-scanning/patterns Add custom pattern
GET /admin/api/v1/security/content-scanning/patterns List all patterns (builtin + custom)
DELETE /admin/api/v1/security/content-scanning/patterns/{id} Remove custom pattern
POST /admin/api/v1/tools/baseline Create baseline snapshot
GET /admin/api/v1/tools/baseline Get baseline
GET /admin/api/v1/tools/drift Get drift report
POST /admin/api/v1/tools/accept-change Accept a drift change
POST /admin/api/v1/tools/quarantine Quarantine a tool
DELETE /admin/api/v1/tools/quarantine/{tool_name} Un-quarantine a tool
GET /admin/api/v1/tools/quarantine List quarantined tools
POST /admin/api/policies/lint Lint a CEL expression
GET /admin/api/v1/notifications List active notifications
GET /admin/api/v1/notifications/count Pending count (badge)
GET /admin/api/v1/notifications/stream SSE real-time stream
POST /admin/api/v1/notifications/{id}/dismiss Dismiss notification
POST /admin/api/v1/notifications/dismiss-all Dismiss all
GET /admin/api/v1/agents/{identity_id}/summary Agent detail summary
GET /admin/api/v1/agents/{identity_id}/health Agent health trend + baseline
POST /admin/api/v1/agents/{identity_id}/acknowledge Acknowledge agent health alert
GET /admin/api/v1/health/overview Cross-agent health overview
GET /admin/api/v1/health/config Health alert thresholds
PUT /admin/api/v1/health/config Update health thresholds
GET /admin/api/v1/compliance/packs List compliance packs
GET /admin/api/v1/compliance/packs/{id} Get pack details
POST /admin/api/v1/compliance/packs/{id}/coverage Analyze coverage
POST /admin/api/v1/compliance/bundles Generate evidence bundle
GET /admin/api/v1/compliance/evidence Get evidence configuration
PUT /admin/api/v1/compliance/evidence Update evidence configuration
POST /admin/api/v1/simulation/run Run policy simulation
POST /admin/api/v1/redteam/run Run attack suite (full or by category)
POST /admin/api/v1/redteam/run/single Run single attack pattern
GET /admin/api/v1/redteam/corpus List available attack patterns
GET /admin/api/v1/redteam/reports Recent scan reports
GET /admin/api/v1/redteam/reports/{id} Specific scan report
GET /admin/api/v1/finops/costs Cost report for period
GET /admin/api/v1/finops/costs/{identity_id} Identity cost detail
GET /admin/api/v1/finops/budgets Budget statuses
GET /admin/api/v1/finops/config Cost Tracking configuration
PUT /admin/api/v1/finops/config Update Cost Tracking configuration
GET /admin/api/v1/quotas List all quotas
GET /admin/api/v1/quotas/{identity_id} Get quota for identity
PUT /admin/api/v1/quotas/{identity_id} Set/update quota
DELETE /admin/api/v1/quotas/{identity_id} Remove quota
GET /admin/api/v1/sessions/active List active sessions
DELETE /admin/api/v1/sessions/{id} Terminate an active session
GET /admin/api/v1/recordings List recordings
GET /admin/api/v1/recordings/{id} Get recording detail
GET /admin/api/v1/recordings/{id}/events Get recording events
GET /admin/api/v1/recordings/{id}/export Export recording (CSV/JSON)
DELETE /admin/api/v1/recordings/{id} Delete recording
GET /admin/api/v1/recordings/config Get recording config
PUT /admin/api/v1/recordings/config Update recording config
GET /admin/api/v1/templates List policy templates
GET /admin/api/v1/templates/{id} Get template detail
POST /admin/api/v1/templates/{id}/apply Apply template (creates policies)
GET /admin/api/v1/transforms List transform rules
POST /admin/api/v1/transforms Create transform rule
PUT /admin/api/v1/transforms/{id} Update transform rule
GET /admin/api/v1/transforms/{id} Get a single transform rule
DELETE /admin/api/v1/transforms/{id} Delete transform rule
POST /admin/api/v1/transforms/test Test transform in sandbox
GET /admin/api/v1/namespaces/config Get namespace config
PUT /admin/api/v1/namespaces/config Update namespace config
GET /admin/api/v1/permissions/health All agents permission gap analysis
GET /admin/api/v1/permissions/health/{identity_id} Single agent permission gaps
GET /admin/api/v1/permissions/suggestions/{identity_id} Least privilege suggestions
POST /admin/api/v1/permissions/apply Apply suggestion as policy
GET /admin/api/v1/permissions/config Shadow mode config
PUT /admin/api/v1/permissions/config Update shadow mode config
GET /admin/api/v1/telemetry/config Get OTel config
PUT /admin/api/v1/telemetry/config Update OTel config
GET /admin/api/stats Dashboard stats
GET /admin/api/system System info
POST /admin/api/system/factory-reset Reset all runtime state to clean
POST /admin/api/v1/bootstrap One-shot bootstrap (upstreams, identities, policies)
POST /admin/api/v1/system/kill Activate kill switch (requires {"reason": "..."})
GET /admin/api/v1/system/kill Get kill switch status
POST /admin/api/v1/system/resume Deactivate kill switch
Factory reset request body:
{"confirm": true}Response:
{
"success": true,
"upstreams_removed": 3,
"policies_removed": 2,
"identities_removed": 4,
"keys_removed": 6,
"quotas_removed": 2,
"transforms_removed": 1,
"recordings_deleted": 2,
"sessions_cleared": 3,
"approvals_cancelled": 0,
"quarantine_cleared": 1,
"stats_reset": true,
"notifications_reset": true,
"skipped_read_only": ["identity:admin-from-yaml"]
}Returns HTTP 409 if a reset is already in progress. Audit logs are intentionally preserved.
GET /health Liveness probe (component health)
GET /readyz Readiness probe (bootstrap, tools, kill switch)
/health — Liveness probe. Returns HTTP 200 when core components are operational.
Response:
{
"status": "healthy",
"checks": {
"session_store": "ok",
"rate_limiter": "ok",
"audit": "ok: 0/1000 (0%)",
"upstreams": "ok",
"goroutines": "16"
},
"version": "2.1.3"
}When audit records are dropped, an additional audit_drops key appears:
{
"checks": {
"audit_drops": "42 dropped"
}
}Returns HTTP 503 with "unhealthy" when audit buffer exceeds 90% capacity.
/readyz — Readiness probe. Returns HTTP 200 when the system is ready to serve traffic. Returns HTTP 503 with details when not ready. Use this for Kubernetes readinessProbe. For Docker HEALTHCHECK and other container restart-triggers, use /health instead — this prevents a persistent kill switch from causing a restart loop.
Response:
{
"ready": true,
"checks": {
"tools_discovered": "ok: 14 tools",
"upstreams": "ok",
"kill_switch": "ok: inactive",
"bootstrapped": "ok"
}
}When the kill switch is active, ready is false and kill_switch shows "not ready: kill switch active". Agents should poll for "ready": false to detect kill switch activation.
| Endpoint | Authentication method |
|---|---|
MCP proxy (/mcp) |
Authorization: Bearer <key> (API key generated at bootstrap or via Admin UI) |
| Admin API | Session cookie from GET /admin/api/auth/status + X-CSRF-Token header |
The Admin API and dashboard have no native authentication (no username/password). Access control is network-level:
- By default (binary): the admin API is bound to
localhostonly. Only processes on the same machine can reach it. - In Docker (
SENTINEL_GATE_ADMIN_OPEN=true): the localhost check is disabled because Docker port forwarding doesn't preserve the original client IP. Access control is delegated to Docker network isolation. - Cloud platforms (Cloud Run, Smithery, Modal): When the
PORTenvironment variable is set, SentinelGate binds to all interfaces (0.0.0.0:<PORT>) instead of localhost. The Docker image setsPORT=8080by default. - In production: place a reverse proxy (Caddy, nginx) with authentication in front of the admin API, or use Kubernetes NetworkPolicy to restrict access.
Warning
SENTINEL_GATE_ADMIN_OPEN=true makes the admin API and dashboard accessible from ANY IP that can reach the port. Docker's -p 8080:8080 binds to 0.0.0.0 by default (all interfaces), not just localhost. On shared networks, use -p 127.0.0.1:8080:8080 to restrict access.
For production on public servers: use a firewall, a reverse proxy with authentication, or SSH tunnel (ssh -L 8080:localhost:8080 yourserver).
Risk matrix:
| Environment | ADMIN_OPEN | Port Exposed | Risk | Action |
|---|---|---|---|---|
| Local machine, bare metal | not set | localhost only | None | None |
| Local machine, Docker | true | 0.0.0.0:8080 by default |
Low | Use -p 127.0.0.1:8080:8080 on shared networks |
| Private server, Docker | true | internal port | Low | Firewall recommended |
| Public server, Docker | true | public port | High | Firewall mandatory or reverse proxy with auth |
This is consistent with how most infrastructure tooling (Prometheus, Redis, Traefik) approaches the problem — the network is the primary security boundary. API keys protect the MCP proxy endpoint (/mcp), not the admin interface.
Multiple agents can connect to the same SentinelGate instance simultaneously. Each agent uses its own identity and API key.
# Terminal 1: Start the server
sentinel-gate start
# Terminal 2: Agent 1 (e.g., Claude Code) connects with key-1
# Terminal 3: Agent 2 (e.g., Gemini CLI) connects with key-2
# Terminal 4: Agent 3 (e.g., Python script) connects with key-3Each agent connects with a separate API key linked to a distinct identity. This gives you:
- Separate audit trails — every action is tagged with the agent's identity
- Per-agent policies — use
identity_nameoridentity_rolesin CEL rules to apply different policies to different agents - Per-agent quotas — set different usage limits for each identity
- Independent sessions — each agent has its own session context for session-aware policies
All agents see the same set of upstream tools (unless filtered by namespace role-based visibility rules). When an upstream is added or removed, all connected agents receive a notifications/tools/list_changed notification.
Port in use: SENTINEL_GATE_SERVER_HTTP_ADDR=:9090 sentinel-gate start
Corrupt state file: SentinelGate automatically falls back to state.json.bak if the primary file is corrupt. If both files are corrupt, delete them and re-bootstrap: rm state.json state.json.bak && sentinel-gate start
Windows — permission denied on evidence-key.pem or state.json: state files and the evidence signing key default to the current working directory. Running sentinel-gate.exe from a read-only location — e.g. C:\Program Files\... without administrator rights, or as a Windows Service with no WorkingDirectory set — fails at startup with permission denied. Fix: run the binary from a writable folder (your user directory works), or set explicit paths via state_path, evidence.key_path, and evidence.output_path in sentinel-gate.yaml. When running as a Windows Service, always set WorkingDirectory to a writable path.
- Agent can't connect: Verify the MCP URL is
http://localhost:8080/mcpand theAuthorization: Bearer <key>header is set correctly. - Tools not appearing: Wait a few seconds after adding an upstream for discovery. Check Admin UI → Tools & Rules to verify tools are listed.
- Upstream timeout: Stdio servers (npx) are single-threaded. Too many parallel requests overwhelm the pipe. Restart the upstream from Admin UI.
- Tool list not refreshing: Ensure your agent supports
notifications/tools/list_changed. If not, disconnect and reconnect the agent.
- MCP proxy:
Authorization: Bearer <key> - Admin API: The CSRF token is set as a
sentinel_csrf_tokencookie on any GET request. Read this cookie and send it back as theX-CSRF-Tokenheader on POST/PUT/DELETE requests.
Tip
API key not working? Use cleartext_key from creation response (not the hash). Keys are loaded at boot and on creation.
Check that traffic is flowing through the MCP proxy. The Policy Evaluate API also generates audit records.
- Set
tool_match: "*"(when creating rules via the API,tool_matchdefaults to"*"if omitted; YAML config rules always match all tools — the tool_match field is only available in the API) - Check priority — higher wins
- Use Policy Test sandbox in Admin UI
Policy IDs are UUIDs generated at creation time and preserved across restarts in state.json. You can reference policies by either ID or name.
Auto-recovers with adaptive flushing (4x faster at >80%). If audit_drops is non-zero, increase audit.channel_size.
How do I connect my agent to SentinelGate?
Configure SentinelGate as an MCP server in your agent's settings. The Admin UI Connections page has ready-to-use configuration snippets for Claude Code, Gemini CLI, Codex CLI, Cursor/IDE, Python, Node.js, and cURL. See Agent Configuration for details.
My agent uses an MCP client library. How do I protect it?
Point the MCP client at http://localhost:8080/mcp instead of the real MCP server. Add the real server as upstream in Admin UI → Tools & Rules → Add Upstream. Create an identity + API key for authentication.
Can I connect multiple agents at once?
Yes. Each agent connects with its own API key and identity. All agents share the same upstream tools (unless filtered by namespace role-based visibility rules). See Multi-Agent Sessions.
What about Codex?
Codex supports MCP — configure SentinelGate as an MCP server in ~/.codex/config.toml. See Agent Configuration.
Are native agent tools (Read, Write, Bash) intercepted?
SentinelGate intercepts MCP tool calls only. If an agent has native tools that do not go through MCP, those are not intercepted by SentinelGate. The protection applies to all tools routed through the MCP proxy. For full isolation, run the agent inside a Docker container with no filesystem mount — the agent's native tools find nothing, and it must use MCP through SentinelGate. See examples/claude-code/ for this pattern.
How do I run Claude Code through SentinelGate in Docker?
Use examples/claude-code/. Start SentinelGate with docker compose up -d sentinel-gate, then launch Claude with docker compose run -e ANTHROPIC_API_KEY=... claude. Claude runs in an isolated container and can only access files through SentinelGate. See the example README for details.
How do I bootstrap SentinelGate without the UI?
Create a bootstrap.json file and mount it at /etc/sentinelgate/bootstrap.json in your container. SentinelGate reads it on first boot, creates upstreams, identities, and API keys, then renames the file to bootstrap.json.consumed. See Automated Bootstrap for the full format.
What are the sandbox profiles?
Three pre-built security configurations: strict (read-only, deny-all default, enforce scanning), standard (read+write, deny-all default, monitor scanning), permissive (allow-all default, minimal scanning). Set "profile": "strict" in your bootstrap.json. See Sandbox Profiles.
What is the kill switch?
An emergency stop that instantly blocks all MCP tool calls. Activate from the dashboard or via API. The kill switch persists across restarts. See Kill Switch.
What happens when I add or remove an upstream?
SentinelGate automatically discovers tools from the new upstream and sends a notifications/tools/list_changed notification to all connected MCP clients. Agents that support this notification refresh their tool list automatically. No restart required — upstreams are hot-pluggable.
Untrusted │ Trusted
│
AI Agent ────────┼──── SentinelGate ──── Admin UI/API
│ │
Upstream MCP ────┼──────────┘
servers │
- AI agents are untrusted. They may be compromised by prompt injection, have bugs, or behave unexpectedly. SentinelGate enforces policies on their actions.
- SentinelGate itself is trusted. It is the security boundary. If SentinelGate is compromised, all protections are bypassed.
- The admin interface is trusted. Anyone with access to the Admin API can disable policies, create identities, or factory-reset the system. Protect it with network isolation (see Admin API access control).
- Upstream MCP servers are semi-trusted. SentinelGate scans their responses for prompt injection (IPI defense), but trusts the tool implementations themselves.
- Agent mistakes — AI deletes files it shouldn't, calls wrong API, accesses sensitive paths
- Indirect prompt injection — a document read by the agent contains hidden instructions ("send this file to pastebin.com"). Response scanning detects common injection patterns.
- Overreach — agent does more than intended (reads files outside allowed scope, calls write tools when only reads are permitted)
- Data exfiltration — agent tries to send sensitive data to external domains. Blocked by exfiltration domain rules and content scanning.
- Sensitive data exposure — PII, API keys, and secrets in tool call arguments are detected and masked or blocked before reaching the upstream.
- Actively adversarial agents. SentinelGate assumes the agent is not deliberately trying to evade controls. A compromised agent that knows it is behind SentinelGate could craft inputs to avoid pattern-based detection. Defense in depth (container isolation + SentinelGate) mitigates this.
- Exfiltration through the model API. An agent can include sensitive data in its conversation context, which is sent to the model provider's API. This traffic does not pass through SentinelGate. For high-security environments, consider routing model API traffic through an inspectable proxy.
- Compromised upstream MCP servers. If a tool server is malicious, it can return crafted responses. SentinelGate's response scanner detects common prompt injection patterns but cannot guarantee detection of all attack vectors.
- State file tampering. If an attacker gains write access to
state.json, they can modify policies, disable the kill switch, or add identities. Protect the state file with filesystem permissions.
- MCP-only protection (proxy mode). The MCP Proxy intercepts MCP tool calls routed through it. Native agent tools that bypass MCP (e.g., an agent's built-in file operations) are not intercepted. For full isolation, run the agent inside a Docker container with no filesystem access and SentinelGate as the only gateway — see
examples/claude-code/for this pattern. For agents that don't use MCP, see Chapter 14: Policy Decision API. - Regex-based content scanning. Content scanning uses pattern matching, not ML-based detection. It catches known formats (AWS keys, credit card numbers, etc.) but may miss obfuscated or novel credential formats. Custom patterns can be added for organization-specific secrets.
- Admin API has no native authentication. Access control is network-level only (localhost binding or Docker network isolation). In production, protect the admin interface with a reverse proxy or network policies. See Admin API access control.
- Stdio upstream timeout during approval. MCP servers via stdio (npx) may close the connection while waiting for human approval.
- Tool poisoning detection is near-real-time. Drift detection runs every 5 minutes and on every upstream restart. Changed tools are auto-quarantined immediately. However, calls made during the window between a tool change and the next re-discovery cycle are not retroactively blocked.
- No native TLS. Transport encryption requires a reverse proxy (see Production Deployment section).
- Audit logs in the Admin UI are not cryptographically protected. Only the evidence chain (ECDSA P-256 + hash chain) provides tamper-proof records. See Cryptographic Evidence.
- API keys created via UI or API are stored as Argon2id hashes in
state.json(OWASP-recommended parameters: 47 MiB memory, 1 iteration, 1 parallelism). Keys seeded from YAML config use SHA-256 hashes for fast O(1) lookup at startup. The cleartext key is shown only once at creation time. Keys do not expire automatically — revoke unused keys via the Admin UI or API.
SentinelGate's MCP Proxy (Chapter 4) is powerful: it intercepts every tool call, applies policies, scans content, enforces rate limits, and provides a complete audit trail. But it only works for MCP traffic. If your agent calls REST APIs, runs SQL queries, executes shell commands, or uses any protocol that isn't MCP, the proxy can't help — it never sees that traffic.
SentinelGate also operates as a Policy Decision Point (PDP) — a well-established security pattern used by Open Policy Agent (OPA), AWS Verified Permissions, and Google's Zanzibar. Instead of intercepting traffic, it answers a simple question: "Is this action allowed?"
Your agent calls SentinelGate's evaluate endpoint before executing an action. SentinelGate evaluates the action against your CEL policies (the same policies used by the MCP Proxy) and returns allow, deny, or approval_required. The agent then decides whether to proceed.
MCP Proxy mode (Chapter 4):
Agent ──traffic──> SentinelGate ──traffic──> MCP Server
(intercepts, evaluates, enforces)
Policy Decision API mode (this chapter):
Agent ──"can I?"──> SentinelGate ──"yes/no"──> Agent ──traffic──> Any tool
(evaluates only)
| MCP Proxy | Policy Decision API | |
|---|---|---|
| How it works | Agent connects to SentinelGate as its MCP server. All tool calls are intercepted. | Agent asks SentinelGate "can I do this?" before each action. Traffic never flows through SentinelGate. |
| Security stack | Full: policies + content scanning + rate limiting + kill switch + session tracking + audit | Policy evaluation + audit only |
| Enforcement | Physical — the agent cannot bypass it | Cooperative — the agent must choose to obey |
| Protocol | MCP only | Any — REST, gRPC, SQL, shell, custom |
| Agent code changes | None | You must add SDK calls before each action |
Only agents whose code you control. CLI agents and IDE extensions are closed applications — you can't inject policy-check code into their tool-calling logic.
| Agent | MCP Proxy | Policy Decision API |
|---|---|---|
| Claude Code, Gemini CLI, Codex CLI, Cursor | Yes | No — closed applications, can't modify their code |
| Python/Node.js MCP SDK | Yes | Yes, but MCP Proxy gives full security stack |
| LangChain, CrewAI, AutoGen | Yes | Yes, but MCP Proxy gives full security stack |
| Custom agent (non-MCP) | No — doesn't use MCP | Yes — this is the primary use case |
Recommendation: If your agent uses MCP, use the MCP Proxy. It's more secure (physical enforcement, full security stack) and requires zero code changes. Use the Policy Decision API only for agents that operate outside the MCP protocol.
A Python agent that calls a REST API — before executing, it asks SentinelGate:
from sentinelgate import SentinelGateClient, PolicyDeniedError
import requests
client = SentinelGateClient()
# Reads SENTINELGATE_SERVER_ADDR and SENTINELGATE_API_KEY from environment
# Before calling the REST API, ask SentinelGate
try:
client.evaluate("api_call", "delete_user",
arguments={"user_id": "123"},
destination={"domain": "api.myapp.com"})
# Allowed — proceed
requests.delete("https://api.myapp.com/users/123")
except PolicyDeniedError as e:
# Denied — don't execute
print(f"Blocked by rule: {e.rule_name} — {e.reason}")The policies are the same CEL rules you use with the MCP Proxy:
action_name.startsWith("delete_") → deny
"admin" in identity_roles → allow
dest_domain_matches(dest_domain, "*.production.com") → deny
- Deploy SentinelGate (same as always)
- Create policies via the Admin UI or bootstrap — the same CEL rules work for both the MCP Proxy and the PDP
- No upstreams needed — SentinelGate is not in the traffic path
- Install an SDK in your agent:
pip install sentinelgate(Python),npm install @sentinelgate/sdk(Node.js), orgo get github.com/sentinel-gate/sdk-go(Go) - Set environment variables on your agent process:
SENTINELGATE_SERVER_ADDR=http://sentinel-gate:8080 SENTINELGATE_API_KEY=your-api-key
- Add evaluate calls before each action in your agent code
The evaluate endpoint lives on the admin API (/admin/api/v1/policy/evaluate), which is restricted to localhost by default. If your agent runs in a separate container, set SENTINEL_GATE_ADMIN_OPEN=true on the SentinelGate service. This is already the default in Docker.
CSRF required. All POST requests to the admin API require a CSRF token. Send a matching token in both the
X-CSRF-Tokenheader and thesentinel_csrf_tokencookie (double-submit pattern). Generate any random token and include it in both places — see the curl example in Quick Start.
| Field | Type | Description |
|---|---|---|
decision |
string | "allow", "deny", or "approval_required" |
request_id |
string | Unique ID for this evaluation |
reason |
string | Why the decision was made |
rule_id |
string | ID of the matched rule (empty on default deny/allow) |
rule_name |
string | Name of the matched rule (empty on default deny/allow) |
help_text |
string | Guidance for the agent (deny only) |
help_url |
string | Link to policy documentation (deny only) |
latency_ms |
integer | Evaluation time in milliseconds |
- Same CEL policy engine as the MCP Proxy — all your existing rules work
- Approval workflow — the evaluate endpoint can return
approval_required, enabling human-in-the-loop. The SDKs poll for approval automatically (30 polls, 2-second intervals) - Audit trail — every evaluate call is logged in the audit trail, visible in the Admin UI alongside MCP Proxy audit events
- Caching — the SDKs cache
allowdecisions (default: 5 seconds TTL, up to 1000 entries) to minimize latency - Fail-open / fail-closed — configurable behavior when SentinelGate is unreachable:
open(allow, default) orclosed(deny)
Be aware of what the Policy Decision API does NOT do:
- No content scanning — SentinelGate cannot detect PII, secrets, or credentials in your agent's data because it never sees the actual traffic
- No rate limiting — rate limits are enforced in the proxy; the PDP is a lightweight check
- No kill switch — the emergency stop only blocks MCP Proxy traffic
- No physical enforcement — if your agent has a bug and skips the evaluate call, the action executes without governance
For the complete SDK reference (constructors, methods, environment variables, error handling), see connecting-agents.md — Section 10.
SentinelGate is licensed under the GNU Affero General Public License v3.0.