Skip to content

Latest commit

 

History

History
1009 lines (761 loc) · 37.8 KB

File metadata and controls

1009 lines (761 loc) · 37.8 KB

Connecting Agents to SentinelGate

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 is http://sentinel-gate:8080 — and you need to add SENTINEL_GATE_ALLOWED_HOSTS=sentinel-gate to your SentinelGate service so it accepts connections from your agent container. See Section 2 for all platforms.


Table of Contents


1. Before You Begin

Prerequisites:

  • SentinelGate is running and healthy — GET /health returns 200 (this means the server is alive and healthy — upstreams connected, audit operational). Can return 503 if upstreams are degraded or the audit channel is under backpressure.
  • SentinelGate is bootstrapped and ready — GET /readyz returns 200 (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.json file, 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.


2. Find Your SentinelGate URL

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_URL and SENTINEL_GATE_KEY as user-defined environment variables — you choose the names. The SentinelGate SDK snippets (Section 10) use SENTINELGATE_SERVER_ADDR and SENTINELGATE_API_KEY — these are read automatically by the SDK libraries.


3. Claude Code

Local

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 user to install globally across all projects.

Container / Cloud

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 of localhost. You need to tell SentinelGate to accept this hostname: set SENTINEL_GATE_ALLOWED_HOSTS=sentinel-gate on the SentinelGate service (see Section 2). Without this, the connection will be rejected with a 403 error.


4. Gemini CLI

Local

gemini mcp add --transport http -s user \
  --header "Authorization: Bearer <your-api-key>" \
  sentinelgate http://localhost:8080/mcp

Or in ~/.gemini/settings.json:

{
  "mcpServers": {
    "sentinelgate": {
      "url": "http://localhost:8080/mcp",
      "type": "http",
      "headers": {
        "Authorization": "Bearer <your-api-key>"
      }
    }
  }
}

Container / Cloud

{
  "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_HOSTS on the SentinelGate service (see Section 2).


5. Codex CLI

Local

export SG_KEY="<your-api-key>"
codex mcp add sentinelgate --url http://localhost:8080/mcp \
  --bearer-token-env-var SG_KEY

Or 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

Container / Cloud

[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_HOSTS on the SentinelGate service (see Section 2).

echo 'export SG_KEY="<your-api-key>"' >> ~/.zshrc

6. Cursor / Windsurf / IDE Extensions

Local

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>"
      }
    }
  }
}

Remote / Cloud

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 set SENTINEL_GATE_ALLOWED_HOSTS (see Section 2).


7. Python MCP SDK

Install

pip install mcp httpx

Local

import 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())

Container / Cloud

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.


8. Node.js MCP SDK

Install

npm install @modelcontextprotocol/sdk

Local

Note: These snippets use ESM import syntax and top-level await. Save as .mjs or set "type": "module" in your package.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();

Container / Cloud

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 StreamableHTTPClientTransport constructor: 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.


9. Agent Frameworks

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.

9a. LangChain

Install:

pip install langchain-mcp-adapters

Connect 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".

9b. CrewAI

Install:

pip install mcp          # required — the MCP Python SDK
pip install crewai       # optional — only if using CrewAI's MCPServerHTTP wrapper

Connect 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.mcp module and MCPServerHTTP may 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.

9c. AutoGen

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())

10. SentinelGate SDKs — Policy Decision API

What is the Policy Decision API?

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.

How it differs from the MCP Proxy

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

Which agents can use which pattern?

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.

How it works in practice

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-key

Admin API access

The 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_OPEN is already set to true by 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.

PDP Response Format

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>".

SDK Reference

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.

SDK Behavior Notes

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 errors

2. 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.

10a. Go SDK

Install:

go get github.com/sentinel-gate/sdk-go
package 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)
	}
}

10b. Python SDK

Install:

pip install sentinelgate
from 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}")

10c. Node.js SDK

Install:

npm install @sentinelgate/sdk

Note: 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;
    }
  }
})();

11. cURL

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"}}}'

12. Troubleshooting

403 "host not allowed"

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.

Connection refused

What happened: The agent cannot reach SentinelGate at all — the connection is being rejected before any HTTP exchange.

Checklist:

  1. Is SentinelGate running? Check with docker compose ps or kubectl get pods
  2. Is the URL correct? Double-check against the networking table — a common mistake is using localhost from inside a container, where you should use the service name
  3. Is SentinelGate ready? Run curl http://SENTINEL_GATE_URL/readyz — it should return 200. If it returns 503, SentinelGate is still starting up
  4. Is the agent starting before SentinelGate is ready? In Docker Compose, use depends_on with condition: service_healthy to make the agent wait

401 Unauthorized

What happened: The API key is missing, malformed, or not recognized by SentinelGate.

Checklist:

  1. Is the Authorization: Bearer <key> header set correctly? The word Bearer (capital B) followed by a space and the key
  2. 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
  3. Does the identity associated with the key have the right roles for the policies you configured?

Tools list is empty

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:

  1. Check /readyz — if it says not ready: no tools discovered, SentinelGate hasn't found any tools yet (upstreams may still be connecting)
  2. Verify upstreams are configured in the Admin UI (Dashboard → Tools & Rules → Connections)
  3. If using file-based bootstrap, check that bootstrap.json includes an upstreams section with at least one server

Kill switch is active

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.

Content blocked

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.


Next Steps

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.