Skip to content

Policy enforcement for AI agent tool calls — allow, approve, or deny with audit trail

License

Notifications You must be signed in to change notification settings

Runestone-Labs/gatekeeper

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

61 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Runestone Agent Gatekeeper

CI npm License

A policy-based gatekeeper service that sits between AI agents and real-world tools (shell, HTTP, filesystem), enforcing approvals, denials, and audit logging.

What Problem This Solves

AI agents need to execute actions in the real world: running shell commands, writing files, making HTTP requests. Without guardrails, an agent can accidentally (or adversarially) execute dangerous operations.

The Gatekeeper intercepts all tool requests and:

  • Allows low-risk operations immediately
  • Denies operations that match dangerous patterns
  • Requires human approval for sensitive operations

All decisions are logged to an append-only audit trail.

Threat Model

This gatekeeper protects against:

  1. Accidental damage: Agent runs rm -rf / or overwrites critical files
  2. Prompt injection execution: Malicious content tricks agent into dangerous actions
  3. Exfiltration: Agent sends secrets to external services
  4. SSRF attacks: Agent accesses internal services via HTTP

This gatekeeper does NOT protect against:

  • Malicious operator with access to the policy file
  • Attacks on the gatekeeper service itself
  • Social engineering of the human approver
  • Denial of service (no rate limiting)

Quick Start with Docker

The fastest way to try Gatekeeper:

git clone https://github.com/Runestone-Labs/gatekeeper.git
cd gatekeeper
docker-compose up

Gatekeeper is now running at http://127.0.0.1:3847 with demo mode enabled.

Test it with the quickstart script (walks through DENY, ALLOW, and APPROVE):

bash examples/quickstart.sh

Or test individual decisions:

# This will be DENIED (dangerous pattern)
curl -s -X POST http://127.0.0.1:3847/tool/shell.exec \
  -H "Content-Type: application/json" \
  -d '{"requestId":"test-001","actor":{"type":"agent","name":"test","role":"openclaw"},"args":{"command":"rm -rf /"}}'

# This will be ALLOWED
curl -s -X POST http://127.0.0.1:3847/tool/http.request \
  -H "Content-Type: application/json" \
  -d '{"requestId":"test-002","actor":{"type":"agent","name":"test","role":"openclaw"},"args":{"url":"https://httpbin.org/get","method":"GET"}}'

To customize policy:

cp policy.example.yaml policy.yaml
# Edit policy.yaml, then update docker-compose.yaml volume to use ./policy.yaml
docker-compose restart

For manual installation without Docker, see below.

Quick Start (Manual)

1. Install Dependencies

npm install

2. Configure Environment

# Required: Secret for HMAC signing (at least 32 characters)
export GATEKEEPER_SECRET="your-secret-key-at-least-32-chars-long"

# Provider selection (optional)
export APPROVAL_PROVIDER=local   # local | slack | runestone (default: local)
export AUDIT_SINK=jsonl          # jsonl | runestone (default: jsonl)
export POLICY_SOURCE=yaml        # yaml | runestone (default: yaml)

# Optional: Slack webhook for approval notifications (when using slack provider)
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/..."

# Optional: Custom port (default: 3847)
export GATEKEEPER_PORT=3847

# Optional: Bind host (default: 127.0.0.1)
# Use 0.0.0.0 when running in Docker.
export GATEKEEPER_HOST=127.0.0.1

# Optional: Base URL for approval links
export BASE_URL="http://127.0.0.1:3847"

3. Create Policy File

cp policy.example.yaml policy.yaml
# Edit policy.yaml to match your requirements

4. Start the Server

npm start
# Or for development with auto-reload:
npm run dev

Demo (2 minutes)

See all three decision types in action:

# Install dependencies
npm install

# Set a demo secret (or use your own)
export GATEKEEPER_SECRET="demo-secret-at-least-32-characters-long"

# Run the demo
npm run demo

The demo runs through:

  1. DENY - Dangerous command (rm -rf /) is blocked
  2. APPROVE - Safe command (ls -la) requires approval, then auto-approved
  3. ALLOW - HTTP request executes immediately

Recording

# Record with asciinema (creates demo.cast)
npm run demo:record

# Playback
asciinema play demo.cast

# Create GIF/MP4 with VHS (requires: brew install vhs)
npm run demo:gif

Outputs

  • demo.cast - Terminal recording (asciinema format)
  • demo.gif - Animated GIF for sharing
  • demo.mp4 - Video file
  • data/audit/YYYY-MM-DD.jsonl - Audit log with all demo actions

Provider Architecture

The gatekeeper uses a pluggable provider system for flexibility:

Approval Providers

  • local (default): Logs approval URLs to console
  • slack: Sends interactive approval requests via Slack webhook
  • runestone: Enterprise control plane (coming soon)

Audit Sinks

  • jsonl (default): Writes to daily JSONL files in data/audit/
  • runestone: Stream to cloud for search and compliance (coming soon)

Policy Sources

  • yaml (default): Load from local YAML file
  • runestone: Managed policies with version control (coming soon)

Example Requests

All tool requests must include actor.role to enforce principal policies. For safe retries, include an idempotencyKey.

Execute a Tool (Allow Decision)

curl -X POST http://127.0.0.1:3847/tool/http.request \
  -H "Content-Type: application/json" \
  -d '{
    "requestId": "550e8400-e29b-41d4-a716-446655440000",
    "actor": {
      "type": "agent",
      "name": "my-agent",
      "role": "openclaw",
      "runId": "run-123"
    },
    "args": {
      "url": "https://api.example.com/data",
      "method": "GET"
    }
  }'

Response (200):

{
  "decision": "allow",
  "requestId": "550e8400-e29b-41d4-a716-446655440000",
  "reasonCode": "POLICY_ALLOW",
  "humanExplanation": "Policy allows \"http.request\".",
  "policyVersion": "sha256:abc123...",
  "success": true,
  "result": {
    "status": 200,
    "headers": {"content-type": "application/json"},
    "body": "{...}"
  },
  "executionReceipt": {
    "startedAt": "2024-01-15T10:30:00.000Z",
    "completedAt": "2024-01-15T10:30:00.120Z",
    "durationMs": 120
  }
}

Execute a Tool (Approve Decision)

curl -X POST http://127.0.0.1:3847/tool/shell.exec \
  -H "Content-Type: application/json" \
  -d '{
    "requestId": "550e8400-e29b-41d4-a716-446655440001",
    "actor": {
      "type": "agent",
      "name": "my-agent",
      "role": "openclaw"
    },
    "args": {
      "command": "ls -la /tmp"
    }
  }'

Response (202):

{
  "decision": "approve",
  "requestId": "550e8400-e29b-41d4-a716-446655440001",
  "approvalId": "abc123...",
  "expiresAt": "2026-01-31T13:00:00.000Z",
  "reasonCode": "POLICY_APPROVAL_REQUIRED",
  "humanExplanation": "Policy requires human approval before running \"shell.exec\".",
  "message": "Approval required. Check local for approval links.",
  "approvalRequest": {
    "approvalId": "abc123...",
    "expiresAt": "2026-01-31T13:00:00.000Z",
    "reasonCode": "POLICY_APPROVAL_REQUIRED",
    "humanExplanation": "Policy requires human approval before running \"shell.exec\"."
  },
  "policyVersion": "sha256:abc123..."
}

Execute a Tool (Deny Decision)

curl -X POST http://127.0.0.1:3847/tool/shell.exec \
  -H "Content-Type: application/json" \
  -d '{
    "requestId": "550e8400-e29b-41d4-a716-446655440002",
    "actor": {
      "type": "agent",
      "name": "my-agent",
      "role": "openclaw"
    },
    "args": {
      "command": "rm -rf /"
    }
  }'

Response (403):

{
  "decision": "deny",
  "requestId": "550e8400-e29b-41d4-a716-446655440002",
  "reasonCode": "TOOL_DENY_PATTERN",
  "humanExplanation": "Request matches a deny pattern configured for this tool.",
  "policyVersion": "sha256:abc123..."
}

Capability Tokens (Pre-Approved)

If a tool is configured with decision: approve, you can pre-authorize a specific call with a capability token scoped to tool + args hash:

npm run capability:create -- --tool shell.exec --args /tmp/args.json --ttl 3600

Include the capabilityToken in the tool request. Gatekeeper will allow the call without manual approval if the token is valid.

Health Check

curl http://127.0.0.1:3847/health

Response:

{
  "version": "0.3.0",
  "policyHash": "sha256:abc123...",
  "uptime": 3600,
  "pendingApprovals": 2,
  "providers": {
    "approval": "local",
    "policy": "yaml"
  },
  "database": {
    "available": true,
    "healthy": true,
    "latencyMs": 2
  },
  "memory": {
    "enabled": true
  }
}

Policy Configuration

See policy.example.yaml for a complete example.

tools:
  shell.exec:
    decision: approve           # allow | approve | deny
    deny_patterns:
      - "rm -rf"               # Regex patterns to block
    allowed_cwd_prefixes:
      - "/tmp/"                # Allowed working directories
    allowed_commands:
      - "ls"
      - "git"
    sandbox_command_prefix:
      - "firejail"
      - "--noprofile"
      - "--"
    max_output_bytes: 1048576
    max_timeout_ms: 30000

  files.write:
    decision: approve
    allowed_paths:
      - "/tmp/"
    deny_extensions:
      - ".env"
    max_size_bytes: 10485760

  http.request:
    decision: allow
    allowed_methods: ["GET", "POST"]
    allowed_domains:
      - "api.example.com"
    deny_domains:
      - "pastebin.com"
    deny_ip_ranges:            # SSRF protection
      - "127.0.0.0/8"
      - "169.254.0.0/16"
    max_body_bytes: 1048576
    max_redirects: 3

For a complete policy writing tutorial, see docs/POLICY_GUIDE.md.

Approval Flow

  1. Agent submits tool request
  2. Gatekeeper evaluates against policy
  3. If approve: Creates pending approval, sends notification via configured provider
  4. Human clicks Approve or Deny link
  5. If Approved: Tool executes, result returned
  6. All actions logged to audit trail

Approval links are:

  • HMAC-signed (tamper-proof)
  • Single-use (prevents replay)
  • Time-limited (1 hour expiry)

For a detailed approval workflow guide, see docs/APPROVALS.md.

Audit Logs

All requests are logged via the configured audit sink. Default (jsonl) writes to data/audit/YYYY-MM-DD.jsonl:

{
  "timestamp": "2026-01-31T12:00:00.000Z",
  "requestId": "550e8400-...",
  "tool": "shell.exec",
  "decision": "approve",
  "actor": {"type": "agent", "name": "my-agent", "role": "openclaw"},
  "argsSummary": "{\"command\":\"ls -la\"}",
  "riskFlags": [],
  "policyHash": "sha256:abc123...",
  "gatekeeperVersion": "0.1.0"
}

Logs are:

  • Append-only (never modified)
  • One file per day (easy rotation)
  • Include policy hash (for forensics)
  • Secrets are redacted

For a complete audit log reference with querying examples, see docs/AUDIT_LOGS.md.

Client Library

Install the TypeScript client for integrating your agent with Gatekeeper:

npm install @runestone-labs/gatekeeper-client
import { GatekeeperClient } from '@runestone-labs/gatekeeper-client';

const client = new GatekeeperClient({
  baseUrl: 'http://127.0.0.1:3847',
  role: 'openclaw',
});

// Execute a shell command through the gatekeeper
const result = await client.shellExec({ command: 'ls -la' });
console.log(result.decision); // 'allow' | 'approve' | 'deny'

See the full client README for all available methods.

Using with Real Agents

Gatekeeper is designed to be agent-agnostic. Any agent that can route tool calls over HTTP can integrate with Gatekeeper. See INTEGRATING_AGENTS.md for the integration pattern.

Enterprise Control Plane

Runestone Control Plane provides:

  • Managed Policies: Version-controlled policy configuration with templates
  • Searchable Audit: Full-text search across all audit logs with compliance exports
  • Web-based Approvals: Modern approval UI with mobile notifications
  • Team Workflows: Approval routing, escalation, and delegation

Contact: enterprise@runestone.dev

Security Decisions

Feature Implementation Rationale
Approval signing HMAC-SHA256 of full payload Prevents parameter tampering
Single-use approvals Status field + atomic update Prevents replay attacks
Expiry 1 hour default Limits approval window
Input validation Zod with .strict() Rejects unknown fields
Shell constraints cwd allowlist, timeout caps Limits blast radius
SSRF protection DNS resolution + IP checks Blocks internal access
Audit logging Append-only via pluggable sink Tamper-evident trail

Development

# Type check
npm run typecheck

# Run tests
npm run test:run

# Run with auto-reload
npm run dev

# Run production
npm start

Memory Module (Optional)

The memory module provides graph-based knowledge storage (entities, episodes, evidence) via PostgreSQL + Apache AGE. It is an optional module — Gatekeeper works as a standalone policy engine without it.

Without a database: Only core tools (shell.exec, files.write, http.request) are registered. Policy enforcement, approvals, and JSONL audit logging work normally.

With a database: Memory tools are additionally registered, providing a knowledge graph for AI assistants.

Configuration

# Set DATABASE_URL to enable the memory module
export DATABASE_URL="postgresql://user:pass@localhost:5432/memory"

# Or explicitly control (overrides DATABASE_URL detection)
export ENABLE_MEMORY=true   # or false to disable even with a DATABASE_URL

Database Setup

# Generate migration SQL from schema changes
npm run db:generate

# Apply migrations to a running database
npm run db:migrate

# Or push schema directly (dev only)
npm run db:push

Memory Tools

Tool Description
memory.upsert Create/update entities (people, projects, concepts)
memory.link Create relationships between entities
memory.unlink Remove relationships between entities
memory.query Query entities (with full-text search) and traverse relationships
memory.episode Log decisions, events, and observations
memory.evidence Attach evidence/provenance to entities or episodes
# Create an entity
curl -X POST http://127.0.0.1:3847/tool/memory.upsert \
  -H "Content-Type: application/json" \
  -d '{"requestId":"...","actor":{"type":"agent","name":"test","role":"openclaw"},"args":{"type":"person","name":"Alice"}}'

# Link two entities
curl -X POST http://127.0.0.1:3847/tool/memory.link \
  -H "Content-Type: application/json" \
  -d '{"requestId":"...","actor":{"type":"agent","name":"test","role":"openclaw"},"args":{"sourceId":"<id1>","targetId":"<id2>","relation":"knows"}}'

Schema Architecture

The database schema is split into two modules:

  • src/db/schema/audit.ts — Audit logs table (core gatekeeper, always available)
  • src/db/schema/memory.ts — Knowledge graph tables: entities, episodes, evidence (optional module)

The KG schema is a generic entity/episode/evidence model. Application-specific ontology (entity types, facet types, edge relations) is defined by the consuming application, not by gatekeeper.

See docs/MEMORY.md for setup and full API reference.

Documentation

Guides

Reference

Contributing

License

Apache-2.0 - See LICENSE for details.

About

Policy enforcement for AI agent tool calls — allow, approve, or deny with audit trail

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages