air-gate
Policy engine + human-in-the-loop tool gating + tamper-evident HMAC-SHA256 audit chain for AI agents. Helps satisfy EU AI Act Article 14 (Human Oversight) and Article 12 (Record-Keeping).
Overview
air-gate sits between your AI agent and its tools. Every tool call is checked
against a policy and recorded to an append-only, cryptographically signed audit
chain. Low-risk actions run automatically; risky ones (send email, delete data,
execute SQL) pause and wait for a human decision before the tool runs.
Two ways to use it:
- Local mode — everything runs in-process. Zero servers. Great for a single agent, tests, or embedding the audit chain directly in your app.
- Server mode — run the Gate proxy; approvals happen in Slack (or via the HTTP API). Use this for multi-agent setups and real human-in-the-loop review.
Install
pip install air-gate # core: GateClient + policy + audit chain
pip install "air-gate[server]" # adds the FastAPI proxy + Slack approvals
pip install "air-gate[langchain]" # adds the LangChain tool wrapper
Quick Start (local mode)
from air_gate import GateClient
# Local mode: no server. Events are policy-checked, signed, and chained on disk.
gate = GateClient(
signing_key="use-a-real-secret",
storage_path="gate_events.db",
policy_config={
"default": "require_approval",
"rules": [
{"name": "search", "action_type": "search", "decision": "auto_allow"},
{"name": "emails", "action_type": "email", "decision": "require_approval"},
{"name": "deletes", "action_type": "db_delete", "decision": "block"},
],
},
)
result = gate.check(
agent_id="recruiting-agent",
action_type="email",
tool_name="send_email",
payload={"to": "jane@example.com", "subject": "Hello"},
input_context="Agent matched Jane as a 92% fit",
)
# check() returns one of: "auto_allowed", "pending_approval", "blocked"
if result["decision"] == "auto_allowed":
send_the_email()
elif result["decision"] == "blocked":
print("Blocked by policy:", result["reason"])
else: # pending_approval
# A human approves out-of-band (Slack, API, another process), then:
gate.approve(result["event_id"], authorized_by="alice@company.com")
# Verify the audit chain at any time
print(gate.verify()) # {"valid": True, "events_checked": N, "errors": []}
Policy configuration
Rules are evaluated in order — first match wins. If nothing matches, the
default decision applies. A rule's decision is one of auto_allow,
require_approval, or block. Match on any combination of agent_id,
action_type, and tool_name; a field left unset matches anything.
# gate_config.yaml
policy:
default: require_approval # safest default: humans approve everything
rules:
- name: allow-read-only
action_type: db_read
decision: auto_allow
- name: block-delete
action_type: db_delete
decision: block
- name: approve-emails
action_type: email
decision: require_approval
max_per_hour: 50 # optional rate limit (blocks over the cap)
max_payload_size: 100000 # optional payload byte cap
gate = GateClient(config_path="gate_config.yaml")
Human-in-the-loop (blocking)
When you wrap tools with an integration, a require_approval action blocks
the tool call until a human decides, then runs the tool only if approved. If
it is rejected — or no decision arrives before the timeout — the wrapper fails
closed and the tool never runs.
LangChain
from langchain_community.tools import DuckDuckGoSearchRun
from air_gate.integrations.langchain import GatedTool
gated_search = GatedTool(
tool=DuckDuckGoSearchRun(),
agent_id="research-agent",
gate_url="http://localhost:8000", # server mode; omit for local mode
action_type="search",
wait=True, # block on pending approval (default)
timeout=300, # seconds to wait before failing closed
)
# Use gated_search anywhere a LangChain tool is expected.
OpenAI Agents / plain function tools
from air_gate import GateClient
from air_gate.integrations.openai_agents import gated_tool
gate = GateClient(server_url="http://localhost:8000")
@gated_tool(gate=gate, agent_id="assistant-v1", action_type="email", wait=True)
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email."""
... # runs only after a human approves
You can also wait manually without a wrapper:
r = gate.check("agent", "email", "send_email", payload={"to": "x@y.com"})
if r["decision"] == "pending_approval":
outcome = gate.wait_for_decision(r["event_id"], timeout=300) # blocks
if outcome == "approved":
send_the_email()
Server mode + Slack approvals
uvicorn air_gate.proxy:app --host 0.0.0.0 --port 8000
# or: docker compose up
When an action needs approval, Gate posts a message to Slack with Approve / Reject buttons; the click is recorded to the signed chain. Key endpoints:
| Endpoint | Purpose |
|---|---|
POST /actions |
Submit an action for policy check + audit |
POST /actions/{id}/approve · /reject |
Human decision (requires approver token) |
GET /actions/{id}/status |
Poll a pending action's effective result |
GET /verify |
Verify audit chain integrity |
GET /report?format=html |
Compliance report (HTML/JSON/Markdown) |
POST /slack/interact |
Slack button handler (Slack-signature verified) |
Configuration (environment variables)
| Variable | Purpose |
|---|---|
GATE_SIGNING_KEY |
Required. HMAC key for signing the audit chain. |
GATE_APPROVAL_TOKEN |
Bearer token required to call approve/reject. If unset, those endpoints are unauthenticated (logged as a warning). |
SLACK_SIGNING_SECRET |
Slack app signing secret. Required for /slack/interact; without it, Slack approvals are rejected. |
SLACK_WEBHOOK_URL / SLACK_BOT_TOKEN |
Where approval requests are sent. |
GATE_STORAGE_PATH |
.db → SQLite, .jsonl → JSONL file. |
GATE_CONFIG_PATH |
Path to gate_config.yaml. |
Approve/reject over HTTP with the token:
curl -X POST http://localhost:8000/actions/$ID/approve \
-H "Authorization: Bearer $GATE_APPROVAL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"authorized_by": "alice@company.com"}'
Audit chain & tamper-evidence
Every action and every human decision is a signed entry, chained to the one
before it (previous_hash). The chain is append-only: approvals and
rejections are recorded as new signed decision entries, never edits, so a
resolution can never rewrite or break earlier history. Editing, deleting, or
reordering any entry fails verification.
gate.verify() # {"valid": True/False, "events_checked": N, "errors": [...]}
Note: HMAC uses a shared secret, so anyone holding GATE_SIGNING_KEY can both
sign and verify. It protects against tampering by parties without the key; it is
not a substitute for asymmetric signatures or an external anchor if you need to
prove integrity to a party who must not hold the signing key.
CLI
air-gate demo # self-contained demo (no server needed)
air-gate verify PATH # re-verify an existing .db or .jsonl chain
air-gate version
PII redaction (optional)
The server can redact PII from payloads before they enter the audit chain and
attach a GDPR Article 30 processing manifest. Enable with GATE_PII_REDACTION=true
(default) and choose a method via GATE_PII_METHOD (hash_sha256, mask,
remove, tokenise). See air_gate/pii.py for the multi-vertical detectors
(recruiting, finance/PCI, healthcare/HIPAA, legal) and Article 17 erasure lookup.
Part of AIR Blackbox
air-gate is one component of the AIR Blackbox ecosystem for EU AI Act
compliance:
- air-gate — tool gating + audit chain (Articles 12, 14)
- air-trust — trust layers and compliance tooling
- air-compliance — automated Article 9–15 scanning
- air-docs — model cards, consent logs, audit trails
License
Apache License 2.0. See LICENSE.
Questions? Open an issue on GitHub.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file air_gate-0.3.0.tar.gz.
File metadata
- Download URL: air_gate-0.3.0.tar.gz
- Upload date:
- Size: 1.3 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fed9746c24fd83f92550da7e8b5773f659a799c0a89f8e8c9b40e35067044f00
|
|
| MD5 |
67b449285f0ea6bd6e975a70e8dedcb5
|
|
| BLAKE2b-256 |
8df1fd2fa4dbd8e40f0283b7feb13cc1f0b138ed3621fde6b992b85458e7b473
|
File details
Details for the file air_gate-0.3.0-py3-none-any.whl.
File metadata
- Download URL: air_gate-0.3.0-py3-none-any.whl
- Upload date:
- Size: 54.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
68aa318a87c3a53268175dc0163c14ceb314bef09c1c33323979ee7af3560321
|
|
| MD5 |
7df56d116e03624c8e143aae3d93c734
|
|
| BLAKE2b-256 |
cc47b04ce52bddfc96db1b3ebab426d1adfb61a28804852259605fe7e5ef7b97
|