Skip to content

Repository files navigation

ActionGate

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.

Packages

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

Quick Start

Prerequisites

  • Python 3.11+
  • Docker (for OPA and PostgreSQL)

Install & Setup

# 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 migrations

Create a Superuser

actiongate create-superuser \
  --email admin@example.com \
  --password YourSecurePassword123!

Access the Management UI

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.

Import Existing YAML 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_specs

Docker Deployment

Run PostgreSQL, OPA, and the ActionGate server together:

cd packages/server
docker compose up -d

The ActionGate container auto-runs database migrations on startup. To build the image separately:

docker build -f packages/server/Dockerfile -t actiongate .

Usage

SDK / Library

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 result
  • deny_reasons — list of human-readable reasons
  • constraints — dict of policy-imposed constraints (e.g. {"read_only": true})

Identity Binding

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

LLM Provider Normalization

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)

MCP Governance

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",
)

MCP Normalizer

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"

HTTP Server (Remote Enforcement Service)

actiongate serve --database-url postgresql+asyncpg://user:pass@localhost/actiongate --port 8080

See packages/server/README.md for full server documentation.

Management API

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

CLI

# 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 secret

Makefile

Common 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 shell

How It Works

SDK / Server Pipeline

AI 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 Hook + Remote Enforcement Service

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

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

Adding a New Policy

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.

Operating Modes

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

Testing

# 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 format

Project Structure

Makefile                         # 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

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages