The complete reference for connecting any AI agent, CLI tool, framework, or SDK to SentinelGate — whether it runs on your laptop or inside a container.
Quick start: Running SentinelGate locally? Your URL is
http://localhost:8080. Running in Docker Compose? Your URL ishttp://sentinel-gate:8080— and you need to addSENTINEL_GATE_ALLOWED_HOSTS=sentinel-gateto your SentinelGate service so it accepts connections from your agent container. See Section 2 for all platforms.
- 1. Before You Begin
- 2. Find Your SentinelGate URL
- 3. Claude Code
- 4. Gemini CLI
- 5. Codex CLI
- 6. Cursor / Windsurf / IDE Extensions
- 7. Python MCP SDK
- 8. Node.js MCP SDK
- 9. Agent Frameworks
- 10. SentinelGate SDKs — Policy Decision API
- 11. cURL
- 12. Troubleshooting
- Next Steps
Prerequisites:
- SentinelGate is running and healthy —
GET /healthreturns200(this means the server is alive and healthy — upstreams connected, audit operational). Can return503if upstreams are degraded or the audit channel is under backpressure. - SentinelGate is bootstrapped and ready —
GET /readyzreturns200(this means upstreams are connected, tools are discovered, and the system is ready to serve requests) - You have an API key (from the bootstrap response, the
bootstrap-keys.jsonfile, or the Admin UI at/admin)
Two connection patterns:
| Pattern | Endpoint | When to use |
|---|---|---|
| MCP Proxy | POST /mcp |
Your agent connects to SentinelGate as if it were a normal MCP server. SentinelGate intercepts every tool call and applies the full security stack: policy evaluation, content scanning, rate limiting, kill switch, session tracking, and audit logging. Allowed calls are forwarded to the real MCP servers behind it. The agent discovers tools automatically — no extra configuration needed. This is the recommended approach (Sections 3-9). |
| Policy Decision API | POST /admin/api/v1/policy/evaluate |
A lightweight policy-check-only endpoint. Your agent asks SentinelGate "am I allowed to do this?" and gets back an allow/deny decision — but traffic does not flow through SentinelGate. This means no content scanning, no rate limiting, and no kill switch protection. It follows the standard PDP (Policy Decision Point) architecture used by tools like OPA and AWS Verified Permissions. Useful for agents that don't use MCP or can't route traffic through a proxy (Section 10). |
Most agents should use the MCP Proxy pattern — it provides the full security stack. The Policy Decision API is for advanced use cases where you need policy checks but cannot use the proxy.
The URL depends on where SentinelGate and the agent run.
SentinelGate protects the /mcp endpoint with a hostname security check (DNS rebinding protection) — it only accepts requests from hostnames it knows about. By default, localhost, 127.0.0.1, and ::1 are always trusted. If your agent connects using a different hostname (e.g. a Docker service name like sentinel-gate, or a cloud URL like app.fly.dev), you must tell SentinelGate to trust that hostname by setting SENTINEL_GATE_ALLOWED_HOSTS. If you skip this step, your agent will get a 403 "host not allowed" error.
| Platform | URL | SENTINEL_GATE_ALLOWED_HOSTS |
|---|---|---|
| Local (no container) | http://localhost:8080 |
(not needed) |
| Docker Compose (separate services) | http://sentinel-gate:8080 |
sentinel-gate |
| Kubernetes (same namespace) | http://sentinel-gate:8080 |
sentinel-gate |
| Kubernetes (cross-namespace) | http://sentinel-gate.NAMESPACE.svc.cluster.local:8080 |
sentinel-gate.NAMESPACE.svc.cluster.local |
| Kubernetes (sidecar) | http://localhost:8080 |
(not needed) |
| E2B / Daytona | http://localhost:8080 |
(not needed) |
| Fly.io | https://APP-NAME.fly.dev |
APP-NAME.fly.dev |
| Modal | https://USERNAME--sentinel-gate-serve.modal.run |
USERNAME--sentinel-gate-serve.modal.run |
| ECS Fargate (sidecar) | http://localhost:8080 |
(not needed) |
| LXC/LXD | http://CONTAINER-IP:8080 |
CONTAINER-IP |
| Firecracker | http://GUEST-IP:8080 |
GUEST-IP |
| Podman Compose (separate containers) | http://sentinel-gate:8080 |
sentinel-gate |
| systemd (same host) | http://localhost:8080 |
(not needed) |
Set SENTINEL_GATE_ALLOWED_HOSTS on the SentinelGate container:
# docker-compose.yml
services:
sentinel-gate:
environment:
- SENTINEL_GATE_ALLOWED_HOSTS=sentinel-gate# kubernetes deployment
env:
- name: SENTINEL_GATE_ALLOWED_HOSTS
value: "sentinel-gate"Multiple hosts are comma-separated: SENTINEL_GATE_ALLOWED_HOSTS=sentinel-gate,sg.internal.
Throughout this guide, replace SENTINEL_GATE_URL with the URL from this table.
Docker Compose — running an agent alongside SentinelGate:
services:
sentinel-gate:
image: ghcr.io/sentinel-gate/sentinel-gate:latest
environment:
- SENTINEL_GATE_ALLOWED_HOSTS=sentinel-gate
volumes:
- sg-state:/data
- ./bootstrap.json:/etc/sentinelgate/bootstrap.json:ro
healthcheck:
test: ["CMD", "/usr/bin/wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
interval: 10s
timeout: 5s
retries: 3
my-agent:
build: ./agent
environment:
- SENTINEL_GATE_URL=http://sentinel-gate:8080
- SENTINEL_GATE_KEY=${API_KEY}
depends_on:
sentinel-gate:
condition: service_healthy
volumes:
sg-state:Env var conventions: The MCP Proxy snippets (Sections 3-9) use
SENTINEL_GATE_URLandSENTINEL_GATE_KEYas user-defined environment variables — you choose the names. The SentinelGate SDK snippets (Section 10) useSENTINELGATE_SERVER_ADDRandSENTINELGATE_API_KEY— these are read automatically by the SDK libraries.
claude mcp add --transport http sentinelgate http://localhost:8080/mcp \
--header "Authorization: Bearer <your-api-key>"Or in ~/.claude/settings.json:
{
"mcpServers": {
"sentinelgate": {
"type": "http",
"url": "http://localhost:8080/mcp",
"headers": {
"Authorization": "Bearer <your-api-key>"
}
}
}
}Add
-s userto install globally across all projects.
Replace localhost:8080 with the URL from the networking table:
claude mcp add --transport http sentinelgate http://sentinel-gate:8080/mcp \
--header "Authorization: Bearer <your-api-key>"Or in settings.json:
{
"mcpServers": {
"sentinelgate": {
"type": "http",
"url": "http://sentinel-gate:8080/mcp",
"headers": {
"Authorization": "Bearer <your-api-key>"
}
}
}
}When the agent runs in a different container, it reaches SentinelGate by service name (
sentinel-gate) instead oflocalhost. You need to tell SentinelGate to accept this hostname: setSENTINEL_GATE_ALLOWED_HOSTS=sentinel-gateon the SentinelGate service (see Section 2). Without this, the connection will be rejected with a 403 error.
gemini mcp add --transport http -s user \
--header "Authorization: Bearer <your-api-key>" \
sentinelgate http://localhost:8080/mcpOr in ~/.gemini/settings.json:
{
"mcpServers": {
"sentinelgate": {
"url": "http://localhost:8080/mcp",
"type": "http",
"headers": {
"Authorization": "Bearer <your-api-key>"
}
}
}
}{
"mcpServers": {
"sentinelgate": {
"url": "http://sentinel-gate:8080/mcp",
"type": "http",
"headers": {
"Authorization": "Bearer <your-api-key>"
}
}
}
}At least one upstream MCP server must be configured in SentinelGate (Admin UI -> Tools & Rules -> Add Upstream) for Gemini to have access to tools. If your agent runs in a container, remember to set
SENTINEL_GATE_ALLOWED_HOSTSon the SentinelGate service (see Section 2).
export SG_KEY="<your-api-key>"
codex mcp add sentinelgate --url http://localhost:8080/mcp \
--bearer-token-env-var SG_KEYOr in ~/.codex/config.toml:
[mcp_servers.sentinelgate]
url = "http://localhost:8080/mcp"
bearer_token_env_var = "SG_KEY"Then launch: SG_KEY="<your-api-key>" codex
[mcp_servers.sentinelgate]
url = "http://sentinel-gate:8080/mcp"
bearer_token_env_var = "SG_KEY"Codex does not persist the API key. It stores only the name of the environment variable, not its value. You must set the variable each time you open a new terminal, or add it to your shell profile to make it permanent. If your agent runs in a container, also set
SENTINEL_GATE_ALLOWED_HOSTSon the SentinelGate service (see Section 2).echo 'export SG_KEY="<your-api-key>"' >> ~/.zshrc
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>"
}
}
}
}Point the URL to wherever SentinelGate is running (see the networking table):
{
"mcpServers": {
"sentinelgate": {
"type": "http",
"url": "http://sentinel-gate:8080/mcp",
"headers": {
"Authorization": "Bearer <your-api-key>"
}
}
}
}The exact config location depends on the IDE. Cursor uses
.cursor/mcp.json, Windsurf uses its own MCP settings panel. If SentinelGate runs in a container or on a remote server, remember to setSENTINEL_GATE_ALLOWED_HOSTS(see Section 2).
pip install mcp httpximport asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client
import httpx
async def main():
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()
result = await session.list_tools()
print(f"Tools available: {len(result.tools)}")
result = await session.call_tool(
"read_file", {"path": "/tmp/test.txt"}
)
print(f"Content: {result.content[0].text}")
asyncio.run(main())The code is the same — the only difference is reading the URL and API key from environment variables instead of hardcoding them, so the same code works on your laptop and in a container:
import asyncio
import os
import httpx
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client
async def main():
sg_url = os.environ["SENTINEL_GATE_URL"] # e.g. http://sentinel-gate:8080
api_key = os.environ["SENTINEL_GATE_KEY"]
http_client = httpx.AsyncClient(
headers={"Authorization": f"Bearer {api_key}"}
)
async with streamable_http_client(
f"{sg_url}/mcp", http_client=http_client
) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# Discover tools from all upstream MCP servers
result = await session.list_tools()
print(f"Tools available: {len(result.tools)}")
for tool in result.tools:
print(f" - {tool.name}")
# Call a tool (policy: ALLOW)
result = await session.call_tool(
"read_file", {"path": "/tmp/test.txt"}
)
print(f"Content: {result.content[0].text}")
# Call a tool (policy: DENY — SentinelGate blocks it)
result = await session.call_tool(
"delete_file", {"path": "/tmp/test.txt"}
)
if result.content and hasattr(result.content[0], "text"):
print(f"Response: {result.content[0].text}")
asyncio.run(main())For a Docker Compose example with an agent service, see the Docker Compose fragment in Section 2.
npm install @modelcontextprotocol/sdkNote: These snippets use ESM
importsyntax and top-levelawait. Save as.mjsor set"type": "module"in yourpackage.json.
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-agent", version: "1.0.0" });
await client.connect(transport);
const { tools } = await client.listTools();
console.log(`Tools available: ${tools.length}`);
const result = await client.callTool({
name: "read_file",
arguments: { path: "/tmp/test.txt" }
});
console.log("Content:", result.content[0].text);
await client.close();import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const sgUrl = process.env.SENTINEL_GATE_URL; // e.g. http://sentinel-gate:8080
const apiKey = process.env.SENTINEL_GATE_KEY;
const transport = new StreamableHTTPClientTransport(
new URL(`${sgUrl}/mcp`),
{
requestInit: {
headers: {
"Authorization": `Bearer ${apiKey}`
}
}
}
);
const client = new Client({ name: "my-agent", version: "1.0.0" });
await client.connect(transport);
// Discover tools from all upstream MCP servers
const { tools } = await client.listTools();
console.log(`Tools available: ${tools.length}`);
for (const tool of tools) {
console.log(` - ${tool.name}`);
}
// Call a tool (policy: ALLOW)
const result = await client.callTool({
name: "read_file",
arguments: { path: "/tmp/test.txt" }
});
console.log("Content:", result.content[0].text);
// Call a tool (policy: DENY — SentinelGate blocks it)
const denied = await client.callTool({
name: "delete_file",
arguments: { path: "/tmp/test.txt" }
});
console.log("Response:", denied.content[0]?.text);
// Clean up
await client.close();Note on
StreamableHTTPClientTransportconstructor: it takes exactly two arguments —(url, options). All options (requestInit,sessionId,fetch) go in a single object as the second argument. Passing three arguments silently drops the third.
These frameworks connect to SentinelGate as an MCP client under the hood — you configure SentinelGate as the MCP server, and the framework handles the rest. Your agent gets tools from SentinelGate with security policies enforced transparently, without any changes to your agent logic. No LLM is required to test the connection.
The snippets below use environment variables (SENTINEL_GATE_URL, SENTINEL_GATE_KEY) with a fallback to localhost, so the same code works on your laptop and in containers. In a container, set these env vars on your agent service and set SENTINEL_GATE_ALLOWED_HOSTS on the SentinelGate service.
Install:
pip install langchain-mcp-adaptersConnect and get tools:
import asyncio
import os
from langchain_mcp_adapters.client import MultiServerMCPClient
async def main():
sg_url = os.environ.get("SENTINEL_GATE_URL", "http://localhost:8080")
api_key = os.environ["SENTINEL_GATE_KEY"]
client = MultiServerMCPClient(
{
"sentinelgate": {
"transport": "http",
"url": f"{sg_url}/mcp",
"headers": {
"Authorization": f"Bearer {api_key}",
},
}
}
)
tools = await client.get_tools()
print(f"Tools available: {len(tools)}")
for tool in tools:
print(f" - {tool.name}")
# Use with a LangGraph agent:
# from langgraph.prebuilt import create_react_agent
# agent = create_react_agent(model, tools)
asyncio.run(main())Transport naming: all three variants work:
"streamable_http"(underscore),"streamable-http"(hyphen), and"http".
Install:
pip install mcp # required — the MCP Python SDK
pip install crewai # optional — only if using CrewAI's MCPServerHTTP wrapperConnect via MCP Python SDK (recommended):
import os
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
sg_url = os.environ.get("SENTINEL_GATE_URL", "http://localhost:8080")
api_key = os.environ["SENTINEL_GATE_KEY"]
async with streamablehttp_client(
f"{sg_url}/mcp",
headers={"Authorization": f"Bearer {api_key}"},
) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("read_file", {"path": "/tmp/test.txt"})Alternative: CrewAI native integration (if available):
import os
from crewai import Agent
from crewai.mcp import MCPServerHTTP
sg_url = os.environ.get("SENTINEL_GATE_URL", "http://localhost:8080")
api_key = os.environ["SENTINEL_GATE_KEY"]
agent = Agent(
role="Assistant",
goal="Help the user with file operations",
backstory="An agent that uses SentinelGate-governed tools",
mcps=[
MCPServerHTTP(
url=f"{sg_url}/mcp",
headers={"Authorization": f"Bearer {api_key}"},
streamable=True,
cache_tools_list=True,
),
],
)
# Use in a Crew:
# from crewai import Crew, Task
# crew = Crew(agents=[agent], tasks=[...])
# crew.kickoff()Note: The
crewai.mcpmodule andMCPServerHTTPmay not be available in all CrewAI versions (especially with Python 3.14+). If it is not available, use the MCP SDK pattern above. SentinelGate works identically with both approaches since CrewAI uses the MCP SDK under the hood.
Install:
pip install "autogen-ext[mcp]"Connect via McpWorkbench:
import asyncio
import os
from autogen_ext.tools.mcp import McpWorkbench, StreamableHttpServerParams
async def main():
sg_url = os.environ.get("SENTINEL_GATE_URL", "http://localhost:8080")
api_key = os.environ["SENTINEL_GATE_KEY"]
server_params = StreamableHttpServerParams(
url=f"{sg_url}/mcp",
headers={"Authorization": f"Bearer {api_key}"},
timeout=30.0,
sse_read_timeout=300.0,
terminate_on_close=True,
)
async with McpWorkbench(server_params) as workbench:
# workbench provides all tools from SentinelGate upstreams
# Use with an AutoGen agent:
# from autogen_ext.models.openai import OpenAIChatCompletionClient
# from autogen_agentchat.agents import AssistantAgent
# agent = AssistantAgent(
# name="my-agent",
# model_client=OpenAIChatCompletionClient(model="gpt-4"),
# workbench=workbench,
# )
print("Connected to SentinelGate via McpWorkbench")
asyncio.run(main())The MCP Proxy (Sections 3-9) is SentinelGate's primary mode: it sits in the middle of all traffic, intercepts every tool call, and enforces security. But what if your agent doesn't use MCP? What if it calls REST APIs, runs SQL queries, or uses a custom protocol that can't go through a proxy?
That's where the Policy Decision API comes in. It implements a Policy Decision Point (PDP) — a well-established security architecture pattern also used by Open Policy Agent (OPA), AWS Verified Permissions, and Google's Zanzibar. The idea is simple: before your agent executes an action, it asks SentinelGate "Am I allowed to do this?" and gets back an allow or deny decision. The agent then obeys the decision — but the actual traffic never flows through SentinelGate.
Think of it this way:
- MCP Proxy = a locked gate. The agent can't pass without SentinelGate opening it. SentinelGate physically controls the traffic.
- Policy Decision API = a security guard you ask for permission. The guard says yes or no, but you're the one who decides to obey. It's a cooperative model.
| MCP Proxy (Sections 3-9) | Policy Decision API (this section) | |
|---|---|---|
| How it works | Agent connects to SentinelGate as its MCP server. All tool calls are intercepted and evaluated. | Agent calls SentinelGate's evaluate endpoint before each action. Traffic goes directly from agent to tool. |
| Security stack | Full: policies + content scanning + rate limiting + kill switch + session tracking + audit | Policy evaluation + audit only — no content scanning, no rate limiting, no kill switch |
| Enforcement | Physical — the agent cannot bypass SentinelGate | Cooperative — the agent must choose to obey the decision |
| Protocol | MCP only (agent must be an MCP client) | Any protocol — REST, gRPC, SQL, shell commands, custom APIs |
| Agent code changes | None — the agent thinks it's talking to a normal MCP server | You must modify your agent code to add SDK calls before each action |
Not all agents can use both patterns. CLI agents and IDE extensions are closed applications — you can't inject policy-check code into their tool-calling logic. Only agents whose code you control can use the Policy Decision API.
| Agent | MCP Proxy | Policy Decision API | Why |
|---|---|---|---|
| Claude Code | Yes | No | Closed application — you can't modify its tool-calling code |
| Gemini CLI | Yes | No | Same reason |
| Codex CLI | Yes | No | Same reason |
| Cursor / Windsurf | Yes | No | Same reason |
| Python MCP SDK | Yes | Yes (but MCP Proxy is better) | You control the code, but MCP Proxy gives full security stack |
| Node.js MCP SDK | Yes | Yes (but MCP Proxy is better) | Same |
| LangChain / CrewAI / AutoGen | Yes | Yes (but MCP Proxy is better) | Same — they already have MCP support |
| Custom agent (non-MCP) | No | Yes — this is the use case | Agent calls REST APIs, SQL, shell commands — can't go through MCP proxy |
The bottom line: If your agent uses MCP (most do), use the MCP Proxy — it's more secure and requires zero code changes. The Policy Decision API exists for custom agents that operate outside the MCP protocol.
Step 1: Deploy SentinelGate and create policies (same as always — CEL rules in the Admin UI or via bootstrap).
Step 2: You do NOT need to add upstream MCP servers. SentinelGate is not in the traffic path — it only answers policy questions.
Step 3: In your agent code, before each action, call the SDK:
from sentinelgate import SentinelGateClient, PolicyDeniedError
client = SentinelGateClient() # reads from env vars
# Agent wants to call a REST API — ask SentinelGate first
try:
client.evaluate("api_call", "delete_user",
arguments={"user_id": "123"},
destination={"domain": "api.myapp.com"})
# SentinelGate said allow — proceed
requests.delete("https://api.myapp.com/users/123")
except PolicyDeniedError as e:
# SentinelGate said deny — don't execute
print(f"Blocked by rule: {e.rule_name} — {e.reason}")The policies are the same CEL rules you already know:
action_name.startsWith("delete_") → deny
"admin" in identity_roles → allow
dest_domain_matches(destination.domain, "*.production.com") → deny
Step 4: Set environment variables on your agent:
SENTINELGATE_SERVER_ADDR=http://sentinel-gate:8080
SENTINELGATE_API_KEY=your-api-keyThe Policy Decision API lives on the admin API (/admin/api/v1/policy/evaluate), which is restricted to localhost by default for security. If your agent runs in a different container or host from SentinelGate, you must set SENTINEL_GATE_ADMIN_OPEN=true on the SentinelGate service. Without this, the SDK will get a connection error.
When SentinelGate runs in Docker,
SENTINEL_GATE_ADMIN_OPENis already set totrueby default in the Dockerfile.
CSRF protection: The admin API requires a CSRF token on all POST requests, even with SENTINEL_GATE_ADMIN_OPEN=true. For raw HTTP calls, generate a random token and send it in both the X-CSRF-Token header and the sentinel_csrf_token cookie.
POST /admin/api/v1/policy/evaluate returns a JSON response with these fields:
| Field | Type | Always present | Description |
|---|---|---|---|
decision |
string | yes | "allow", "deny", or "approval_required" |
reason |
string | yes | Why the decision was made |
request_id |
string | yes | Unique ID for this evaluation (useful for audit correlation) |
latency_ms |
number | yes | Evaluation time in milliseconds |
rule_id |
string | only on rule match | ID of the matching rule |
rule_name |
string | only on rule match | Name of the matching rule |
help_url |
string | on deny or approval_required | Link to the relevant policy in the admin dashboard |
help_text |
string | on deny or approval_required | Human-readable explanation for the denial |
Default deny vs explicit deny: When using the strict or standard sandbox profiles (which set DefaultPolicy: "deny"), tools that don't match any explicit allow or deny rule are denied by the default policy. In this case, rule_id and rule_name are absent from the response, and reason is "no matching rule (default deny)". For explicit rule matches, reason is "matched rule <rule-name>".
All three SDKs read configuration from environment variables:
| Variable | Description | Default |
|---|---|---|
SENTINELGATE_SERVER_ADDR |
SentinelGate URL (e.g. http://sentinel-gate:8080) |
(required) |
SENTINELGATE_API_KEY |
API key for authentication | (required) |
SENTINELGATE_FAIL_MODE |
What to do when SentinelGate is unreachable: open means allow the action anyway, closed means deny it |
open |
SENTINELGATE_PROTOCOL |
Protocol identifier sent with evaluation requests | sdk |
SENTINELGATE_TIMEOUT |
HTTP request timeout in seconds | 5 |
SENTINELGATE_CACHE_TTL |
Cache duration for policy decisions, in seconds | 5 |
SENTINELGATE_CACHE_MAX_SIZE |
Maximum number of cached policy decisions | 1000 |
SENTINELGATE_IDENTITY_NAME |
Identity name for policy evaluation | sdk-client |
SENTINELGATE_IDENTITY_ROLES |
Comma-separated roles | agent |
The same env vars work locally and in containers — only the value of SENTINELGATE_SERVER_ADDR changes.
1. check() error behavior.
check() never raises on a policy deny — it returns false. However, it does propagate two exceptions: ApprovalTimeoutError and ServerUnreachableError. All other errors — including HTTP 4xx/5xx responses — are caught and treated as false (deny). This is consistent across all three SDKs. Callers should handle the propagated exceptions:
// Go: Check() returns an error only for ApprovalTimeout and ServerUnreachable
allowed, err := client.Check(ctx, req)
if err != nil { /* handle ApprovalTimeoutError or ServerUnreachableError */ }# Python: check() re-raises ApprovalTimeoutError and ServerUnreachableError
try:
allowed = client.check("tool_call", "read_file", arguments={"path": "/tmp/x"})
except (ApprovalTimeoutError, ServerUnreachableError):
# handle non-policy errors2. Default decision is "deny" when the field is missing.
If the server response lacks a decision field, the SDKs default to "deny" (Python, Node). This prevents accidental allow on malformed responses.
3. HTTP redirects are disabled. The Go and Python SDKs explicitly block HTTP redirects to prevent Bearer token leakage to unexpected hosts. Node.js does not follow redirects by default (stdlib behavior). If you need to place SentinelGate behind a reverse proxy, ensure the proxy does not redirect API requests.
4. Cache key includes identity.
All three SDKs include identity_name and identity_roles in the cache key. In multi-agent setups where different agents have different roles, each agent gets its own cache entries — a decision cached for agent-admin will not be served to agent-reader.
5. failMode accepts only "open" or "closed".
If an invalid value is provided, all three SDKs default to "open" and emit a warning (Go: logger.Warn, Python: warnings.warn, Node: console.warn). Valid values: "open" (allow on unreachable) or "closed" (deny on unreachable).
6. Approval polling fails fast on server errors.
When waiting for human approval, all three SDKs poll the status endpoint every 2 seconds for up to 60 seconds. If the server becomes unreachable during polling (3 consecutive connection errors), the SDK raises ServerUnreachableError immediately instead of continuing to poll silently. HTTP errors (4xx/5xx) during polling are propagated immediately as SentinelGateError.
Install:
go get github.com/sentinel-gate/sdk-gopackage main
import (
"context"
"errors"
"fmt"
"log"
sentinelgate "github.com/sentinel-gate/sdk-go"
)
func main() {
// Reads SENTINELGATE_SERVER_ADDR and SENTINELGATE_API_KEY from env
client := sentinelgate.NewClient()
ctx := context.Background()
// Check if an action is allowed (returns bool)
allowed, err := client.Check(ctx, sentinelgate.EvaluateRequest{
ActionType: "tool_call",
ActionName: "read_file",
Arguments: map[string]interface{}{"path": "/tmp/test.txt"},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("read_file allowed: %v\n", allowed)
// Full evaluation with decision details
resp, err := client.Evaluate(ctx, sentinelgate.EvaluateRequest{
ActionType: "tool_call",
ActionName: "delete_file",
Arguments: map[string]interface{}{"path": "/tmp/test.txt"},
})
if err != nil {
var denied *sentinelgate.PolicyDeniedError
if errors.As(err, &denied) {
fmt.Printf("Denied by rule: %s — %s\n", denied.RuleName, denied.Reason)
} else {
log.Fatal(err)
}
} else {
fmt.Printf("Decision: %s, Rule: %s, Reason: %s\n",
resp.Decision, resp.RuleName, resp.Reason)
}
}Install:
pip install sentinelgatefrom sentinelgate import SentinelGateClient, PolicyDeniedError
# Reads SENTINELGATE_SERVER_ADDR and SENTINELGATE_API_KEY from env
client = SentinelGateClient()
# Boolean check (never raises on deny)
allowed = client.check("tool_call", "read_file",
arguments={"path": "/tmp/test.txt"})
print(f"read_file allowed: {allowed}")
# Full evaluation (raises PolicyDeniedError on deny by default)
try:
result = client.evaluate("tool_call", "delete_file",
arguments={"path": "/tmp/test.txt"})
print(f"Decision: {result['decision']}")
except PolicyDeniedError as e:
print(f"Denied by rule: {e.rule_name} — {e.reason}")Install:
npm install @sentinelgate/sdkNote: Uses CommonJS
require. Works in any Node.js 18+ project.
const { SentinelGateClient, PolicyDeniedError } = require("@sentinelgate/sdk");
// Reads SENTINELGATE_SERVER_ADDR and SENTINELGATE_API_KEY from env
const client = new SentinelGateClient();
(async () => {
// Boolean check (never throws on deny)
const allowed = await client.check("tool_call", "read_file", {
arguments: { path: "/tmp/test.txt" }
});
console.log(`read_file allowed: ${allowed}`);
// Full evaluation (throws PolicyDeniedError on deny by default)
try {
const result = await client.evaluate("tool_call", "delete_file", {
arguments: { path: "/tmp/test.txt" }
});
console.log(`Decision: ${result.decision}`);
} catch (e) {
if (e instanceof PolicyDeniedError) {
console.log(`Denied by rule: ${e.ruleName} — ${e.reason}`);
} else {
throw e;
}
}
})();Useful for testing or scripting. Works the same locally and in containers — just change the URL.
# Step 1: Initialize the MCP session
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": "initialize", "params": {"protocolVersion": "2025-11-25", "clientInfo": {"name": "curl", "version": "1.0"}, "capabilities": {}}}'
# Step 2: 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": 2, "method": "tools/list"}'
# Step 3: 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": 3, "method": "tools/call", "params": {"name": "read_file", "arguments": {"path": "/tmp/test.txt"}}}'What happened: Your agent tried to connect to SentinelGate using a hostname (like sentinel-gate or app.fly.dev) that SentinelGate doesn't recognize. This is the hostname security check (DNS rebinding protection) doing its job.
Fix: Add the hostname your agent uses to SENTINEL_GATE_ALLOWED_HOSTS on the SentinelGate container. See the networking table for the correct value for your platform. For example, in Docker Compose: SENTINEL_GATE_ALLOWED_HOSTS=sentinel-gate.
What happened: The agent cannot reach SentinelGate at all — the connection is being rejected before any HTTP exchange.
Checklist:
- Is SentinelGate running? Check with
docker compose psorkubectl get pods - Is the URL correct? Double-check against the networking table — a common mistake is using
localhostfrom inside a container, where you should use the service name - Is SentinelGate ready? Run
curl http://SENTINEL_GATE_URL/readyz— it should return200. If it returns503, SentinelGate is still starting up - Is the agent starting before SentinelGate is ready? In Docker Compose, use
depends_onwithcondition: service_healthyto make the agent wait
What happened: The API key is missing, malformed, or not recognized by SentinelGate.
Checklist:
- Is the
Authorization: Bearer <key>header set correctly? The wordBearer(capital B) followed by a space and the key - Is the key from the current bootstrap? If you ran
docker compose down -v(which deletes all data), new keys are generated on the next bootstrap — the old ones no longer work - Does the identity associated with the key have the right roles for the policies you configured?
What happened: SentinelGate returned an empty tools list. This usually means no upstream MCP servers are configured, or tool discovery hasn't finished yet.
Checklist:
- Check
/readyz— if it saysnot ready: no tools discovered, SentinelGate hasn't found any tools yet (upstreams may still be connecting) - Verify upstreams are configured in the Admin UI (Dashboard → Tools & Rules → Connections)
- If using file-based bootstrap, check that
bootstrap.jsonincludes anupstreamssection with at least one server
What happened: Someone activated the kill switch (emergency stop), which blocks all tool calls. This is a safety feature. The /readyz endpoint returns 503 when the kill switch is active.
Fix: Deactivate the kill switch via the Admin UI dashboard (the red "Kill Switch" button) or by calling POST /admin/api/v1/system/resume.
What happened: A tool call was blocked because its arguments contained sensitive content (like API keys, credit card numbers, email addresses, or other PII) and content scanning is set to enforce mode.
What to do: Check the audit log in the Admin UI (/admin → Audit) to see which content pattern matched and in which tool call. If the block was a false positive, you can adjust the content scanning patterns in the Admin UI or via the API.
Once your agent is connected:
- Configure security policies — Define what tools each agent can use and under what conditions. See Guide.md — Chapter 3: Policy Engine.
- Review the Admin UI — Monitor tool calls, audit logs, agent health, and drift detection at
http://SENTINEL_GATE_URL/admin. - Set up content scanning — Detect and block PII, secrets, and credentials in tool call arguments. See Guide.md — Chapter 5: Security Features.
- Explore deployment examples — Platform-specific configurations are in the
examples/directory.