Developer Guide
Build Host Enforcer applications that integrate with your organization's Airlock Integrations Gateway for human-in-the-loop approval.
Sample code language
Applies to all SDK examples below. HTTP and JSON blocks stay protocol-specific.
Overview
Airlock enables human-in-the-loop approval for AI agent operations. A Host Enforcer intercepts agent actions (commands, code changes, API calls) and routes them through your Gateway for human review before execution.
Enforcers communicate only with the Integrations Gateway
at https://<your-gateway-host>.
Identity is provided by your organization's OIDC realm at
https://<your-auth-host>/realms/<your-realm>.
To integrate a custom or sample enforcer in an enterprise deployment:
- Obtain Gateway URL, auth realm, and enforcer app credentials from your administrator
- Initialize a Gateway SDK client with those values
- Implement the enforcer lifecycle (PAT or OAuth, consent, pairing, presence, artifacts, decision ack)
- Point approvers at your organization-distributed Mobile Approver for pairing and decisions
Deployment placeholders
| Setting | Placeholder |
|---|---|
| Gateway base URL | https://<your-gateway-host> |
| Auth realm (OIDC) | https://<your-auth-host>/realms/<your-realm> |
| Enforcer Client ID | <your-client-id> |
| Enforcer Client Secret | <your-client-secret> (confidential clients) |
| User PAT | <your-pat> |
Step 1 — Obtain deployment credentials
Your organization deploys the Airlock Gateway and identity provider
inside its infrastructure. Before writing integration code, collect:
- Gateway base URL (
https://<your-gateway-host>) - OIDC realm URL (
https://<your-auth-host>/realms/<your-realm>) - Enforcer app registration (Client ID and, for confidential clients, Client Secret)
- Network access from enforcer hosts to the Gateway (TLS, firewall rules)
If you are evaluating Airlock,
contact us
for deployment architecture and credential provisioning.
Step 2 — Register an enforcer application
Each enforcer integration is registered as an Airlock App
in your enterprise deployment. Your platform administrator creates the
app and issues a Client ID and (for confidential clients) a Client Secret.
Developer profile metadata (name/org, contact email) may be shown to
approvers during the consent flow.
See
What are Airlock Apps?
for Client ID, Client Secret, and consent concepts.
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:
HTTP GET /v1/exchanges/req-123 HTTP/1.1
Host: <your-gateway-host>
X-Client-Id: <your-client-id>
Origin: https://myapp.example.com
Authorization: Bearer <user-jwt>
GET /v1/exchanges/req-123 HTTP/1.1
Host: <your-gateway-host>
X-Client-Id: <your-client-id>
Origin: https://myapp.example.com
Authorization: Bearer <user-jwt>
Confidential client example:
HTTP POST /v1/artifacts HTTP/1.1
Host: <your-gateway-host>
X-Client-Id: <your-client-id>
X-Client-Secret: <your-client-secret>
Authorization: Bearer <user-jwt>
POST /v1/artifacts HTTP/1.1
Host: <your-gateway-host>
X-Client-Id: <your-client-id>
X-Client-Secret: <your-client-secret>
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
# Recommended: set a Personal Access Token (X-PAT header)
# Issued by your org admin or from the Mobile Approver
client.set_pat("<your-pat>")
# Recommended: set a Personal Access Token (X-PAT header)
# Issued by your org admin or from the Mobile Approver
client.set_pat("<your-pat>")
Bearer token
from airlock_gateway import AirlockGatewayClient
async with AirlockGatewayClient(
"https://<your-gateway-host>",
token="<user-jwt>",
) as client:
echo = await client.echo()
from airlock_gateway import AirlockGatewayClient
async with AirlockGatewayClient(
"https://<your-gateway-host>",
token="<user-jwt>",
) as client:
echo = await client.echo()
Dual auth — Bearer after OAuth
# After user login (Device Auth Grant or Auth Code + PKCE)
client.set_bearer_token(access_token)
# After user login (Device Auth Grant or Auth Code + PKCE)
client.set_bearer_token(access_token)
Initialize the client
from airlock_gateway import AirlockGatewayClient
client = AirlockGatewayClient(
"https://<your-gateway-host>",
client_id="<your-client-id>",
client_secret="<your-client-secret>",
)
from airlock_gateway import AirlockGatewayClient
client = AirlockGatewayClient(
"https://<your-gateway-host>",
client_id="<your-client-id>",
client_secret="<your-client-secret>",
)
Step 4 — Implement the Enforcer Lifecycle
A complete enforcer typically follows this lifecycle:
Diagram Discovery → Set PAT (or Sign In) → Consent Check → Pair → Heartbeat ↺
↓
Submit Artifact → Wait for Decision → Ack delivery
↓
Unpair → Sign Out
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):
HTTP GET /v1/integrations/discovery
GET /v1/integrations/discovery
Response:
JSON {
"idp": {
"baseUrl": "https://<your-auth-host>/realms/<your-realm>",
"clientId": "<your-integrations-client-id>"
},
"auth": {
"patSupported": true
}
}
{
"idp": {
"baseUrl": "https://<your-auth-host>/realms/<your-realm>",
"clientId": "<your-integrations-client-id>"
},
"auth": {
"patSupported": true
}
}
2a. PAT authentication (recommended)
The simplest user identity for enforcers: obtain a Personal Access Token from your
platform administrator or create one in the
organization-distributed 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("<your-pat>")
# Set the PAT on the client (recommended for CLIs / scripts)
client.set_pat("<your-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 against your organization's identity provider. The integrations
client ID is returned by discovery (typically
<your-integrations-client-id>).
After obtaining an access token, set it on the SDK client:
# After obtaining the access token
client.set_bearer_token(access_token)
# After obtaining the access token
client.set_bearer_token(access_token)
3. Consent check
Check whether the user has consented to your app:
HTTP GET /v1/consents/check
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 approver
print(f"Pairing code: {response.pairing_code}")
print("Enter this code in the Mobile Approver.")
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 approver
print(f"Pairing code: {response.pairing_code}")
print("Enter this code in the Mobile Approver.")
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
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):
HTTP POST /v1/pairing/pre-generate
POST /v1/pairing/pre-generate
Enforcer claims the code:
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
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",
)
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",
},
)
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
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
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)
# 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)
client.withdraw_exchange(request_id)
7. Unpairing & sign-out
Revoke pairing when the user unpairs. For OAuth sign-out, revoke the refresh token at
your identity provider's token endpoint.
client.revoke_pairing(routing_token)
client.revoke_pairing(routing_token)
Architecture constraints
Host Enforcers must communicate only with the
Integrations Gateway
(<your-gateway-host>).
Do not call internal platform services directly — the Gateway is the sole
external integration boundary for enforcer applications in an enterprise deployment.
Diagram ┌─────────────────┐ HTTPS ┌──────────────────────┐
│ Your Enforcer │ ──────────────→ │ Integrations Gateway │
│ (custom / sample)│ │ (<your-gateway-host>) │
└─────────────────┘ └──────────┬───────────┘
│
Internal routing
│
┌──────────▼───────────┐
│ Enterprise platform │
│ services (internal; │
│ not exposed to │
│ enforcers directly) │
└──────────────────────┘
┌─────────────────┐ HTTPS ┌──────────────────────┐
│ Your Enforcer │ ──────────────→ │ Integrations Gateway │
│ (custom / sample)│ │ (<your-gateway-host>) │
└─────────────────┘ └──────────┬───────────┘
│
Internal routing
│
┌──────────▼───────────┐
│ Enterprise platform │
│ services (internal; │
│ not exposed to │
│ enforcers directly) │
└──────────────────────┘
Test enforcer reference implementations
The
gateway-clients
repository includes interactive test enforcer CLIs that implement the full lifecycle.
Configure them to point at https://<your-gateway-host>
using your organization's Client ID and Secret. They 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
cd src/python
pip install -e .
python test_enforcer.py
cd src/python
pip install -e .
python test_enforcer.py
Run these from the root of the
gateway-clients
repository clone.