AI Policy Engine — validate, constrain, and audit AI tool calls.
ActionGate sits between AI agents and the tools they invoke. Every proposed action passes through a pipeline of replay protection, argument validation, rate limiting, and policy evaluation (via OPA) before the caller decides whether to execute.
This is a monorepo with three independent packages:
| Package | Description | Install |
|---|---|---|
actiongate-common |
Shared Pydantic models | pip install -e packages/common |
actiongate-server |
Policy engine, SDK, CLI, HTTP server (RES) | pip install -e packages/server |
actiongate-client |
RES client and Claude Code hook | pip install -e packages/client |
- Python 3.11+
- Docker (for OPA and PostgreSQL)
# Install all packages and start services
make setup
# Or step by step:
make install-dev # Install all packages with dev dependencies
make docker-up # Start PostgreSQL + OPA + ActionGate
make migrate # Run database migrationsactiongate create-superuser \
--email admin@example.com \
--password YourSecurePassword123!Open http://localhost:8080/management and log in with your superuser credentials. From there you can create tenants, users, actors, tool specs, and policy configs.
If you have existing YAML tool specs or MCP server configs, import them into the database:
actiongate import-yaml \
--tenant acme \
--tool-specs-dir ./packages/server/seed/tool_specsRun PostgreSQL, OPA, and the ActionGate server together:
cd packages/server
docker compose up -dThe ActionGate container auto-runs database migrations on startup. To build the image separately:
docker build -f packages/server/Dockerfile -t actiongate .from actiongate_server import ActionGate
from actiongate_server.db.repositories.tool_spec_repo import ToolSpecRepository
gate = ActionGate(
opa_url="http://localhost:8181",
registry=tool_spec_repo, # DB-backed ToolSpecLookup
)
decision = await gate.decide(
"email_send",
{"to": "alice@internal.corp", "subject": "Hi", "body": "Hello"},
actor="agent-1",
auth_context={"actor_id": "bob", "roles": ["user"], "tenant_id": "acme"},
)
if decision.allowed:
my_email_lib.send(to="alice@internal.corp", subject="Hi", body="Hello")
else:
print(f"Denied: {decision.deny_reasons}")The decide() method returns a PolicyDecision with:
allowed/denied— boolean resultdeny_reasons— list of human-readable reasonsconstraints— dict of policy-imposed constraints (e.g.{"read_only": true})
Every action carries a tenant -> user -> agent identity chain via auth_context:
decision = await gate.decide(
"db_query",
{"query": "SELECT * FROM users", "tenant_id": "acme"},
actor="agent-1",
auth_context={
"actor_id": "agent-1",
"roles": ["user"],
"tenant_id": "acme",
"user_id": "bob",
"account_type": "agent",
},
)from actiongate_server import normalize
# OpenAI function calling format
action = normalize(
{"id": "call_abc123", "type": "function", "function": {"name": "email_send", "arguments": '{"to": "a@b.com"}'}},
provider="openai",
actor="agent-1",
)
# Anthropic tool_use format
action = normalize(
{"type": "tool_use", "id": "toolu_abc123", "name": "email_send", "input": {"to": "a@b.com"}},
provider="anthropic",
actor="agent-1",
)
decision = await gate.decide_action(action)ActionGate governs MCP tool calls using the same pipeline — with server-level policies, tool specs, and rate limits on top. MCP server configs are stored in the database and managed via the CRUD API or management UI.
decision = await gate.decide(
"github.create_issue",
{"title": "Bug", "repo": "org/repo"},
actor="agent-1",
auth_context={"actor_id": "bob", "roles": ["developer"], "tenant_id": "acme"},
mcp_server="github",
operation="tool_call",
)from actiongate_server import normalize
# Claude Code PreToolUse hook format
action = normalize(
{"tool_name": "mcp__github__create_issue", "tool_input": {"title": "Bug"}},
provider="mcp",
actor="claude-code",
auth_context={"actor_id": "bob", "roles": ["developer"]},
)
# action.tool == "github.create_issue", action.mcp_server == "github"actiongate serve --database-url postgresql+asyncpg://user:pass@localhost/actiongate --port 8080See packages/server/README.md for full server documentation.
The server exposes tenant-scoped CRUD endpoints for managing configuration:
| Endpoint | Description | Auth |
|---|---|---|
GET/POST/PUT/DELETE /v1/tenants |
Tenant management | Superuser |
GET/POST/PUT/DELETE /v1/tool-specs |
Tool spec management | Tenant user |
GET/POST/PUT/DELETE /v1/actors |
Actor management | Tenant user |
GET/PUT/DELETE /v1/policy-config |
Policy config (blocklists) | Tenant user |
POST /auth/jwt/login |
JWT authentication | Public |
GET /management |
Management web UI | Superuser |
# Decision only (requires --database-url and --tenant)
actiongate decide --tenant acme '{"tool": "email_send", "args": {"to": "a@b.com", "subject": "Hi", "body": "Hello"}, "actor": "agent-1", "auth_context": {"actor_id": "bob", "roles": ["user"]}}'
# List registered tool specs for a tenant
actiongate list-tools --tenant acme
# Start HTTP server
actiongate serve --port 8080
# Import YAML tool specs into DB
actiongate import-yaml --tenant acme --tool-specs-dir ./tool_specs
# Seed default tenant, admin user, and claude-code actor
actiongate seed-defaults
# Create a superuser
actiongate create-superuser --email admin@example.com --password secretCommon development tasks are available via make:
make help # Show all available targets
make setup # Full setup: clean, install, start services, migrate
make dev # Start all Docker services
make test # Run all tests
make test-unit # Run unit tests only
make migrate # Run database migrations
make lint # Run ruff linter
make format # Format code with ruff
make docker-up # Start Docker services
make docker-down # Stop Docker services
make db-shell # Open PostgreSQL shellAI Agent
|
v
+-------------------------------------------------------+
| ActionGate (Policy Enforcement Point) |
| |
| 0. Server rate limiting (MCP only, per-server) |
| 1. Replay protection (deduplicate action IDs) |
| 2. Argument validation (DB-backed tool specs) |
| 3. Rate limiting (sliding window per tool) |
| 4. Policy evaluation --> OPA (Rego policies) |
| - - - - - - - - - - - - - - - - - - - - - - - - - |
| decide() returns here |
| process() continues |
| - - - - - - - - - - - - - - - - - - - - - - - - - |
| 5. Tool execution (with policy constraints) |
| 6. Audit logging (append-only JSONL) |
+-------------------------------------------------------+
Claude Code
| (PreToolUse hook)
v
+-----------------------------+ +-----------------------------------+
| actiongate-hook | | RES (ActionGate HTTP Server) |
| | | |
| - Parse tool names |---->| 1. Identity resolution (DB) |
| - Send to RES | | 2. Full policy pipeline |
| - Return allow/deny/ask |<----| 3. Audit logging |
| - Fail closed on errors | | 4. Return decision + correlation |
+-----------------------------------+
Policies are written in Rego and live in packages/server/policies/. All files use package actiongate and contribute to three shared partial rule sets:
| Rule | Purpose | Aggregation |
|---|---|---|
deny contains reason if { ... } |
Deny reasons | Union across all files |
constraint_set contains {...} if { ... } |
Constraints | Merged via object.union_n |
Create a .rego file in packages/server/policies/ with package actiongate:
package actiongate
import rego.v1
deny contains reason if {
input.tool == "my_tool"
input.args.dangerous == true
reason := "my_tool cannot be used with dangerous=true"
}Drop it in and restart OPA. No other Rego files need editing.
| Mode | Policy eval | Pre-policy checks | Tool execution | Audit log |
|---|---|---|---|---|
enforce (default) |
Real result returned | Raise on failure | Only when allowed | Yes |
shadow |
Evaluated but overridden to allowed=True |
Run but never raise | Always | Yes |
audit |
Real result returned as-is | Run but never raise | Never | Yes |
from actiongate_server import ActionGate
# Shadow mode
gate = ActionGate(opa_url="http://localhost:8181", registry=repo, mode="shadow")
# Per-actor overrides
gate = ActionGate(
opa_url="http://localhost:8181",
registry=repo,
mode="enforce",
actor_modes={"agent-1": "shadow", "agent-2": "audit"},
)# Unit tests (no OPA or PostgreSQL required)
make test-unit
# Integration tests (requires OPA + PostgreSQL via docker compose)
make docker-up
make test-integration
# All tests
make test
# Lint and format
make lint
make formatMakefile # Dev task runner
packages/
common/ # actiongate-common (shared models)
src/actiongate_common/
models/ # Pydantic v2 models
matching.py # Tool name matching utilities
server/ # actiongate-server (policy engine)
src/actiongate_server/
sdk.py # ActionGate class
tool_gateway.py # Pipeline orchestration
server.py # FastAPI HTTP server / RES
cli.py # Click CLI (serve, decide, import-yaml, seed-defaults, create-superuser)
normalizers.py # LLM provider adapters
policy_decider.py # OPA HTTP client
auth/ # fastapi-users auth (JWT + cookie)
db/ # SQLAlchemy async DB layer
models.py # ORM models (Tenant, User, Actor, ToolSpecRow, PolicyConfigRow)
session.py # Engine + session factory
repositories/ # Async CRUD repositories
routers/ # CRUD REST API routers
static/ # Web UIs (management, explorer)
alembic/ # Database migrations
policies/ # OPA Rego policies
seed/ # Default tool spec YAML files
docker-compose.yml # PostgreSQL + OPA + ActionGate
Dockerfile
client/ # actiongate-client (hook + RES client)
src/actiongate_client/
res_client.py # Async HTTP client for RES
hook.py # Claude Code PreToolUse hook
plugin/ # Claude Code plugin (standalone hook)
examples/ # End-to-end examples
tests/ # pytest suite
common/ # Model tests
server/ # Server tests
client/ # Client tests
plugin/ # Plugin tests