Developer Guide
Build third-party enforcer applications that integrate with the Airlock Integrations Gateway for human-in-the-loop approval.
Overview
Airlock enables human-in-the-loop approval for AI agent operations. An enforcer intercepts agent actions (commands, code changes, API calls) and routes them through Airlock for human review before execution.
Third-party enforcers communicate with the Integrations Gateway
at https://igw.airlocks.io.
To build a third-party enforcer, you typically:
- Join the Developer Programme
- Register an Enforcer App
- Integrate using the Gateway SDK
- Implement the enforcer lifecycle (PAT or OAuth, consent, pairing, presence, artifacts, decision ack)
Step 1 — Join the Developer Programme
Apply via the Airlock Platform under Developer Programme. Your developer profile details (name/org, contact email, and optional web page) may be shown to end users during the consent flow.
Step 2 — Register an Enforcer App
Create an app entry under Developer Programme → My Apps. When created, you'll receive an App ID, Client ID, and (for confidential apps) a Client Secret (shown only once).
App kind reference
| Kind | Client Type | Auth Method | Typical Use Case |
|---|---|---|---|
| Web | Public | Client ID + Origin validation | Browser-based enforcer UIs, dashboards |
| Mobile | Public | Client ID + Origin validation | Mobile enforcer apps |
| Agent | Confidential | Client ID + Client Secret | AI agent plugins, autonomous enforcers |
| Desktop | Confidential | Client ID + Client Secret | Desktop IDE plugins, CLI tools |
| VsCodeExtension | Confidential | Client ID + Client Secret | VS Code / IDE extension enforcers |
Authentication methods
Public clients use X-Client-Id plus
Origin validation. Confidential clients use both
X-Client-Id and
X-Client-Secret.
Many flows are also user-scoped and include a user Bearer token.
Public client example:
GET /v1/exchanges/req-123 HTTP/1.1
Host: igw.airlocks.io
X-Client-Id: ABCDEFGHJKLMNPRSTUVWXYZabc
Origin: https://myapp.com
Authorization: Bearer <user-jwt> Confidential client example:
POST /v1/artifacts HTTP/1.1
Host: igw.airlocks.io
X-Client-Id: ABCDEFGHJKLMNPRSTUVWXYZabc
X-Client-Secret: abcdEFGH1234567890abcdEFGH1234567890abcd
Authorization: Bearer <user-jwt> Dual authentication (app + user)
Most enforcer operations are user-scoped.
The gateway accepts user identity via
X-PAT (Personal Access Token — recommended)
or Authorization: Bearer <jwt>
(Device Authorization Grant). Auth priority:
the gateway checks X-PAT first; if absent,
it falls back to the Bearer JWT. App credentials
(X-Client-Id /
X-Client-Secret or Origin) identify your app.
Step 3 — Integrate Using the Gateway SDK
The Gateway SDKs provide a consistent client surface for the Integrations Gateway. Initialize the client with the gateway URL and your app credentials.
SDK source and packages
Authentication quick start
Configure app credentials, then authenticate the user with a PAT (recommended) or a Bearer token after Device Auth Grant sign-in.
Personal Access Token (Python)
# Recommended: set a Personal Access Token (X-PAT header)
client.set_pat("airlock_pat_...") Personal Access Token (TypeScript)
// Recommended: PAT from Platform or Mobile Approver (X-PAT header)
client.setPat("airlock_pat_..."); Bearer token (Python)
from airlock_gateway import AirlockGatewayClient
async with AirlockGatewayClient(
"https://igw.airlocks.io",
token="your-jwt-token",
) as client:
echo = await client.echo() App credentials (TypeScript)
import { AirlockGatewayClient } from "@airlockapp/gateway-sdk";
const client = new AirlockGatewayClient({
baseUrl: "https://igw.airlocks.io",
clientId: "your-client-id",
clientSecret: "your-client-secret",
}); Dual auth — Bearer after OAuth (TypeScript)
// After user login (Device Auth Grant or Auth Code + PKCE)
client.setBearerToken(accessToken); Initialize the client
Python
from airlock_gateway import AirlockGatewayClient
client = AirlockGatewayClient(
"https://igw.airlocks.io",
client_id="your-client-id",
client_secret="your-client-secret",
) .NET
var httpClient = new HttpClient { BaseAddress = new Uri("https://igw.airlocks.io") };
httpClient.DefaultRequestHeaders.Add("X-Client-Id", "your-client-id");
httpClient.DefaultRequestHeaders.Add("X-Client-Secret", "your-client-secret");
var client = new AirlockGatewayClient(httpClient); TypeScript
import { AirlockGatewayClient } from "@airlockapp/gateway-sdk";
const client = new AirlockGatewayClient({
baseUrl: "https://igw.airlocks.io",
clientId: "your-client-id",
clientSecret: "your-client-secret",
}); Go
client := airlock.NewClient(
"https://igw.airlocks.io",
airlock.WithClientCredentials("your-client-id", "your-client-secret"),
) Step 4 — Implement the Enforcer Lifecycle
A complete enforcer typically follows this lifecycle:
Discovery → Set PAT (or Sign In) → Consent Check → Pair → Heartbeat ↺
↓
Submit Artifact → Wait for Decision → Ack delivery
↓
Unpair → Sign Out 1. Discovery
Discover the gateway’s IdP configuration (and whether PAT auth is supported):
GET /v1/integrations/discovery Response:
{
"idp": {
"baseUrl": "https://auth.airlocks.io/realms/airlock",
"clientId": "airlock-integrations"
},
"auth": {
"patSupported": true
}
} 2a. PAT authentication (recommended)
The simplest user identity for enforcers: create a Personal Access Token from the
Platform (Settings → Access Tokens) or the
Mobile Approver. The SDK sends it as
X-PAT on every request — no OIDC polling or
token refresh.
# Set the PAT on the client (recommended for CLIs / scripts)
client.set_pat("airlock_pat_...") 2b. User authentication (Device Authorization Grant — fallback)
Use the OAuth 2.0 Device Authorization Grant (RFC 8628) when you need interactive
browser-based login. Use OIDC client ID airlock-integrations.
After obtaining an access token, set it on the SDK client:
# After obtaining the access token
client.set_bearer_token(access_token) 3. Consent check
Check whether the user has consented to your app:
GET /v1/consents/check 4. Workspace pairing
Initiate pairing, display the pairing code, and poll until completed:
response = client.initiate_pairing(
device_id="dev-my-machine",
enforcer_id="my-enforcer",
enforcer_label="My Custom Enforcer",
workspace_name="default",
)
# Display the pairing code to the user
print(f"Pairing code: {response.pairing_code}")
print("Enter this code in the Airlock mobile app.") Poll for completion:
status = client.get_pairing_status(response.pairing_nonce)
if status.state == "completed":
routing_token = status.routing_token
# Save this token — you'll need it for all subsequent requests 4b. Pre-generated pairing code
Alternatively, an approver can pre-generate a pairing code from the Mobile Approver. The enforcer claims that code instead of initiating a new session.
Approver creates a session (Mobile Approver):
POST /v1/pairing/pre-generate Enforcer claims the code (Python SDK):
response = client.claim_pairing(
pairing_code="ABCD-1234",
enforcer_id="my-enforcer",
enforcer_label="My Custom Enforcer",
workspace_name="default",
)
routing_token = response.routing_token 5. Presence heartbeat
While paired, send heartbeats periodically (e.g. every 30 seconds):
client.send_heartbeat(
enforcer_id="my-enforcer",
enforcer_label="My Custom Enforcer",
workspace_name="default",
) 6. Artifact submission & decision polling
Submit an artifact and long-poll for a decision:
request_id = client.submit_artifact(
enforcer_id="my-enforcer",
artifact_type="command.review",
artifact_hash="sha256-of-content",
ciphertext={
"alg": "xchacha20-poly1305",
"data": "<base64-encrypted-payload>",
"nonce": "<base64-nonce>",
"tag": "<base64-auth-tag>",
},
metadata={
"routingToken": routing_token,
"workspaceName": "default",
},
) Wait for a decision:
decision = client.wait_for_decision(request_id, timeout_seconds=25)
if decision and decision.body:
if decision.body.decision == "approve":
# Execute the agent's action
pass
else:
# Block the agent's action
reason = decision.body.reason # Optional reason from the approver Acknowledge decision delivery.
After the approver’s decision is delivered, call
POST /v1/acks
(via submit_ack / submitAck / etc.) so the gateway records receipt.
Pass the decision envelope’s msgId and the exchange
request_id. This is fire-and-forget: log failures but do not block your main flow.
# After receiving a decision: confirm delivery to the gateway (fire-and-forget)
await client.submit_ack(msg_id, request_id) Withdraw a pending request:
client.withdraw_exchange(request_id) 7. Unpairing & sign-out
Revoke pairing when the user unpairs. For OAuth sign-out, revoke the refresh token at the Keycloak endpoint.
client.revoke_pairing(routing_token) Architecture constraints
Third-party enforcers must communicate only with the
Integrations Gateway
(igw.airlocks.io).
┌─────────────────┐ HTTPS ┌──────────────────────┐
│ Your Enforcer │ ──────────────→ │ Integrations Gateway │
│ (3rd-party app) │ │ (igw.airlocks.io) │
└─────────────────┘ └──────────┬───────────┘
│
Internal routing
│
┌──────────▼───────────┐
│ Platform Backend │
│ (not accessible │
│ to enforcers) │
└──────────────────────┘ Test enforcer reference implementations
The SDK repo includes interactive test enforcer CLIs that implement the full lifecycle.
These are useful end-to-end references for pairing, presence, artifact submission,
decision polling, delivery acknowledgment (POST /v1/acks),
withdrawal, unpairing, and sign-out.
Run the test enforcers
.NET
cd src/dotnet/Airlock.Gateway.Sdk.TestEnforcer
dotnet run TypeScript
cd src/typescript/test-enforcer
npm install
npx ts-node index.ts Go
cd src/go
go run ./cmd/test-enforcer Python
cd src/python
pip install -e .
python test_enforcer.py Rust
cd src/rust
cargo run --bin test_enforcer Run these from the root of the gateway-clients repository clone.