openclaw-airlock 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +406 -0
- package/dist/cli/register.d.ts +22 -0
- package/dist/cli/register.d.ts.map +1 -0
- package/dist/cli/register.js +229 -0
- package/dist/cli/register.js.map +1 -0
- package/dist/client.d.ts +136 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +474 -0
- package/dist/client.js.map +1 -0
- package/dist/commands/airlockStatus.d.ts +20 -0
- package/dist/commands/airlockStatus.d.ts.map +1 -0
- package/dist/commands/airlockStatus.js +48 -0
- package/dist/commands/airlockStatus.js.map +1 -0
- package/dist/config.d.ts +48 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +104 -0
- package/dist/config.js.map +1 -0
- package/dist/crypto.d.ts +41 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +112 -0
- package/dist/crypto.js.map +1 -0
- package/dist/hooks/beforeTool.d.ts +46 -0
- package/dist/hooks/beforeTool.d.ts.map +1 -0
- package/dist/hooks/beforeTool.js +112 -0
- package/dist/hooks/beforeTool.js.map +1 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +88 -0
- package/dist/index.js.map +1 -0
- package/dist/state.d.ts +44 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +79 -0
- package/dist/state.js.map +1 -0
- package/dist/tools/checkStatus.d.ts +42 -0
- package/dist/tools/checkStatus.d.ts.map +1 -0
- package/dist/tools/checkStatus.js +67 -0
- package/dist/tools/checkStatus.js.map +1 -0
- package/dist/tools/requestApproval.d.ts +63 -0
- package/dist/tools/requestApproval.d.ts.map +1 -0
- package/dist/tools/requestApproval.js +85 -0
- package/dist/tools/requestApproval.js.map +1 -0
- package/openclaw.plugin.json +69 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
# Airlock Plugin for OpenClaw
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@airlockapp/openclaw-airlock)
|
|
4
|
+
|
|
5
|
+
Enforces human-in-the-loop approval for risky AI tool actions via the [Airlock Gateway](https://airlockapp.io).
|
|
6
|
+
|
|
7
|
+
**Airlock Approver** (mobile app): [App Store](https://apps.apple.com/us/app/airlock-approver/id6760250865) · [Google Play](https://play.google.com/store/apps/details?id=com.airlockapp.io)
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Tool-based approval** — AI calls `airlock_request_approval` for explicit approval
|
|
12
|
+
- **Hook-based enforcement** — Automatically intercepts protected tool executions
|
|
13
|
+
- **Polling-based decisions** — Long-polls the gateway for approver decisions
|
|
14
|
+
- **Pre-generated pairing** — X25519 ECDH key exchange with mobile approver app
|
|
15
|
+
- **Decision signature verification** — Ed25519 signatures prevent forgery
|
|
16
|
+
- **DND (Do Not Disturb)** — Auto-approves when DND policies are active
|
|
17
|
+
- **Presence heartbeat** — Mobile app shows enforcer online/offline status
|
|
18
|
+
- **State persistence** — Pairing state survives restarts (`.airlock/pairing-state.json`)
|
|
19
|
+
- **Fail modes** — Configurable fail-open or fail-closed on timeout/errors
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
### Prerequisites
|
|
26
|
+
|
|
27
|
+
| Requirement | Details |
|
|
28
|
+
|-------------|---------|
|
|
29
|
+
| **OpenClaw** | An OpenClaw-compatible AI agent runtime |
|
|
30
|
+
| **Node.js** | Version 18 or later (`node --version`) |
|
|
31
|
+
| **Airlock Gateway** | Access to an Airlock Integrations Gateway (default: `igw.airlocks.io`) |
|
|
32
|
+
| **Airlock Mobile App** | For approving/denying actions and generating pairing codes |
|
|
33
|
+
| **Airlock Developer Account** | Required to create an Enforcer App and get API credentials (see below) |
|
|
34
|
+
| **Personal Access Token (PAT)** | Created from the Platform or Mobile App (Settings → Access Tokens) |
|
|
35
|
+
|
|
36
|
+
### Step 1 — Join the Airlock Developer Programme
|
|
37
|
+
|
|
38
|
+
Before you can create an enforcer app, you must register as a developer.
|
|
39
|
+
|
|
40
|
+
1. **Sign in** to the [Airlock Platform](https://platform.airlocks.io) with your Airlock account.
|
|
41
|
+
2. Navigate to **Developer Programme** in the sidebar.
|
|
42
|
+
3. Fill in the application form.
|
|
43
|
+
4. **Submit** your application. An Airlock administrator will review it.
|
|
44
|
+
5. Once **Approved**, you can create enforcer apps.
|
|
45
|
+
|
|
46
|
+
> For the full walkthrough with field descriptions and application states, see the [Airlock Developer Guide](https://airlockapp.io/docs/developer-guide/).
|
|
47
|
+
|
|
48
|
+
### Step 2 — Create an Enforcer App
|
|
49
|
+
|
|
50
|
+
Once approved, create an app from **Developer Programme → My Apps** in the Platform web UI.
|
|
51
|
+
|
|
52
|
+
| Field | Value |
|
|
53
|
+
|-------|-------|
|
|
54
|
+
| **Name** | Your app name (e.g. "My OpenClaw Enforcer"). Must be unique. Max 128 characters. |
|
|
55
|
+
| **Kind** | Select **Agent** (confidential client — authenticates via Client ID + Client Secret) |
|
|
56
|
+
| **Is Open Source** | Whether your enforcer source code is public |
|
|
57
|
+
| **Description** | What your app does (max 2000 chars) |
|
|
58
|
+
|
|
59
|
+
On creation, you receive:
|
|
60
|
+
|
|
61
|
+
| Credential | Description |
|
|
62
|
+
|------------|-------------|
|
|
63
|
+
| **App ID** | Human-readable identifier (format: `ABC-1234567`) |
|
|
64
|
+
| **Client ID** | 20-character alphanumeric string (used in plugin config) |
|
|
65
|
+
| **Client Secret** | 40-character secret (**shown only once** — copy and save it!) |
|
|
66
|
+
|
|
67
|
+
> ⚠️ **Save the Client Secret immediately.** You cannot retrieve it later. You can rotate it from the Platform UI, but the old secret is invalidated.
|
|
68
|
+
|
|
69
|
+
### Step 3 — Create a Personal Access Token (PAT)
|
|
70
|
+
|
|
71
|
+
The plugin authenticates as a user via a **Personal Access Token**.
|
|
72
|
+
|
|
73
|
+
1. In the **Airlock Platform App** (Settings → Access Tokens) or the **Mobile Approver** app, create a new token.
|
|
74
|
+
2. Set an appropriate expiry (max 1 year).
|
|
75
|
+
3. Copy the token (prefixed with `airpat_`).
|
|
76
|
+
|
|
77
|
+
The token identifies which user the enforcer acts on behalf of.
|
|
78
|
+
|
|
79
|
+
### Step 4 — Generate a Pre-Generated Pairing Code
|
|
80
|
+
|
|
81
|
+
The OpenClaw plugin uses **pre-generated pairing codes** (approver-initiated) instead of interactive pairing. This means the approver creates the code first, and you paste it into the plugin config.
|
|
82
|
+
|
|
83
|
+
**From the Mobile Approver app:**
|
|
84
|
+
|
|
85
|
+
1. Open the **Airlock** mobile app on your phone.
|
|
86
|
+
2. Tap the **"+"** button or go to **Workspaces → Add Workspace**.
|
|
87
|
+
3. Select **"Generate Code"** (pre-generated pairing).
|
|
88
|
+
4. The app shows a **6-character pairing code** (e.g. `A3K9X2`).
|
|
89
|
+
5. Copy or note this code — you'll paste it into your plugin config.
|
|
90
|
+
|
|
91
|
+
> ⏱️ **Time limit**: Pre-generated codes expire after **30 minutes**. Complete plugin setup within this window.
|
|
92
|
+
|
|
93
|
+
The code is in **Pending** state until an enforcer claims it. You can see its status in the mobile app.
|
|
94
|
+
|
|
95
|
+
### Step 5 — Install the Plugin
|
|
96
|
+
|
|
97
|
+
**From npm** (recommended):
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm install @airlockapp/openclaw-airlock
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**From your OpenClaw workspace:**
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
openclaw plugins install @airlockapp/openclaw-airlock
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**From source** (for development):
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
cd gateway_sdk/src/openclaw-airlock
|
|
113
|
+
npm install
|
|
114
|
+
npm run build
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Step 6 — Configure the Plugin
|
|
118
|
+
|
|
119
|
+
Add to your OpenClaw plugin config:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"plugins": {
|
|
124
|
+
"entries": {
|
|
125
|
+
"airlock": {
|
|
126
|
+
"enabled": true,
|
|
127
|
+
"config": {
|
|
128
|
+
"gatewayUrl": "https://igw.airlocks.io",
|
|
129
|
+
"enforcerId": "my-enforcer-001",
|
|
130
|
+
"pat": "airpat_your_token_here",
|
|
131
|
+
"clientId": "ABCDEFGHJKLMNPRSTUVWXYZabc",
|
|
132
|
+
"clientSecret": "abcdEFGH1234567890abcdEFGH1234567890abcd",
|
|
133
|
+
"pairingCode": "A3K9X2",
|
|
134
|
+
"workspaceName": "My AI Workspace",
|
|
135
|
+
"failMode": "closed",
|
|
136
|
+
"protectedTools": ["exec", "shell.*", "deploy.run", "*"],
|
|
137
|
+
"timeoutMs": 300000,
|
|
138
|
+
"executionMode": "poll"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Config Reference
|
|
147
|
+
|
|
148
|
+
| Field | Required | Default | Description |
|
|
149
|
+
|-------|----------|---------|-------------|
|
|
150
|
+
| `gatewayUrl` | ✓ | — | Airlock Integrations Gateway URL (use `https://igw.airlocks.io` for production) |
|
|
151
|
+
| `enforcerId` | ✓ | — | Unique enforcer instance identifier (your choice, e.g. `oc-prod-001`) |
|
|
152
|
+
| `pat` | ✓* | — | Personal Access Token (`airpat_...`) |
|
|
153
|
+
| `clientId` | ✓* | — | Enforcer App Client ID (from Step 2) |
|
|
154
|
+
| `clientSecret` | ✓* | — | Enforcer App Client Secret (from Step 2) |
|
|
155
|
+
| `pairingCode` | — | — | Pre-generated pairing code from mobile app (from Step 4) |
|
|
156
|
+
| `workspaceName` | — | "OpenClaw Workspace" | Human-readable workspace name shown in mobile app |
|
|
157
|
+
| `timeoutMs` | — | 300000 | Approval timeout in ms (default: 5 min) |
|
|
158
|
+
| `pollIntervalMs` | — | 3000 | Decision poll interval in ms (min 1000) |
|
|
159
|
+
| `failMode` | — | "closed" | `"open"` = allow on timeout, `"closed"` = block |
|
|
160
|
+
| `protectedTools` | — | [] | Tool names requiring approval (empty = none) |
|
|
161
|
+
| `executionMode` | — | "poll" | `"poll"` (only mode available) |
|
|
162
|
+
|
|
163
|
+
> *`pat` is required for user identity. At least `clientId` **or** `pat` is required for authentication.
|
|
164
|
+
|
|
165
|
+
### Step 7 — Pair and Verify
|
|
166
|
+
|
|
167
|
+
Once configured, run the setup and pairing commands:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# Validate config and test gateway connectivity
|
|
171
|
+
openclaw airlock setup
|
|
172
|
+
|
|
173
|
+
# Claim the pre-generated pairing code (X25519 ECDH key exchange)
|
|
174
|
+
openclaw airlock pair
|
|
175
|
+
|
|
176
|
+
# Check everything is connected
|
|
177
|
+
/airlock-status
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The `airlock pair` command:
|
|
181
|
+
1. Generates an X25519 keypair for end-to-end encryption
|
|
182
|
+
2. Claims the pre-generated code with the gateway
|
|
183
|
+
3. Polls until the mobile app completes the pairing
|
|
184
|
+
4. Derives the shared AES-256-GCM encryption key via ECDH + HKDF-SHA256
|
|
185
|
+
5. Stores the approver's Ed25519 public key for decision verification
|
|
186
|
+
6. Persists all state to `.airlock/pairing-state.json`
|
|
187
|
+
|
|
188
|
+
> 🔒 The `.airlock/` directory is automatically gitignored. It contains secrets (encryption key, routing token) that must not be committed.
|
|
189
|
+
|
|
190
|
+
### Step 8 — You're Ready!
|
|
191
|
+
|
|
192
|
+
After pairing, the plugin is fully operational:
|
|
193
|
+
|
|
194
|
+
- **Protected tools** are automatically intercepted and require mobile approval
|
|
195
|
+
- **Presence heartbeat** keeps the mobile app showing your workspace as online
|
|
196
|
+
- **State persists** across restarts — no need to re-pair
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Usage
|
|
201
|
+
|
|
202
|
+
### Automatic Enforcement (Hook)
|
|
203
|
+
|
|
204
|
+
Configure `protectedTools` to automatically intercept tool executions:
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
{
|
|
208
|
+
"protectedTools": ["shell.exec", "deploy.run", "database.query"]
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Any tool matching these names will require mobile approval before executing. Supports glob patterns (e.g. `shell.*` matches `shell.exec`, `shell.run`).
|
|
213
|
+
|
|
214
|
+
When a DND (Do Not Disturb) policy is active, protected tools are auto-approved silently.
|
|
215
|
+
|
|
216
|
+
### Explicit Approval (Tool)
|
|
217
|
+
|
|
218
|
+
The AI agent can explicitly request approval using the `airlock_request_approval` tool:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
Action: deploy to production
|
|
222
|
+
Reason: User requested deployment of v2.1.0
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
This tool also respects DND policies.
|
|
226
|
+
|
|
227
|
+
### Status Check (Tool)
|
|
228
|
+
|
|
229
|
+
Check the status of a previous approval request using `airlock_check_status`:
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
Request ID: req-abc123
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Slash Command
|
|
236
|
+
|
|
237
|
+
Use `/airlock-status` in chat to see the current status including:
|
|
238
|
+
- Gateway connectivity
|
|
239
|
+
- Pairing status and state file info
|
|
240
|
+
- Encryption key availability
|
|
241
|
+
- Protected tools list
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Fail Modes
|
|
246
|
+
|
|
247
|
+
| Scenario | `failMode: "closed"` | `failMode: "open"` |
|
|
248
|
+
|----------|----------------------|---------------------|
|
|
249
|
+
| Approval timeout | ✗ Block | ✓ Allow |
|
|
250
|
+
| Gateway unreachable | ✗ Block | ✓ Allow |
|
|
251
|
+
| Network error | ✗ Block | ✓ Allow |
|
|
252
|
+
| Pairing revoked | ✗ Block (always) | ✗ Block (always) |
|
|
253
|
+
| No approver (stale) | ✗ Block (always) | ✗ Block (always) |
|
|
254
|
+
| Quota exceeded | ✗ Block (always) | ✗ Block (always) |
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Security
|
|
259
|
+
|
|
260
|
+
### End-to-End Encryption
|
|
261
|
+
|
|
262
|
+
All approval requests are encrypted using AES-256-GCM. The encryption key is derived via:
|
|
263
|
+
1. X25519 ECDH key exchange during pairing
|
|
264
|
+
2. HKDF-SHA256 key derivation with info `"HARP-E2E-AES256GCM"`
|
|
265
|
+
|
|
266
|
+
The gateway never sees the plaintext — it only routes encrypted artifacts.
|
|
267
|
+
|
|
268
|
+
### Decision Signature Verification
|
|
269
|
+
|
|
270
|
+
Approver decisions are signed with Ed25519. The plugin verifies signatures using the approver's public key stored during pairing. Canonical format: `${artifactHash}|${decision}|${nonce}`. Unsigned decisions are accepted; decisions with invalid signatures are rejected.
|
|
271
|
+
|
|
272
|
+
### State Storage
|
|
273
|
+
|
|
274
|
+
| Data | Location |
|
|
275
|
+
|------|----------|
|
|
276
|
+
| Routing token | `.airlock/pairing-state.json` |
|
|
277
|
+
| Encryption key (AES-256-GCM) | `.airlock/pairing-state.json` |
|
|
278
|
+
| Approver public keys | `.airlock/pairing-state.json` |
|
|
279
|
+
| Pairing timestamp | `.airlock/pairing-state.json` |
|
|
280
|
+
|
|
281
|
+
> ⚠️ **Security**: The `.airlock/` directory is gitignored by default. If you move or copy the project, ensure the state file is protected and not committed to version control.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Troubleshooting
|
|
286
|
+
|
|
287
|
+
### "Not paired — run 'airlock pair' first"
|
|
288
|
+
- Make sure `pairingCode` is set in config.
|
|
289
|
+
- Generate a new code if the previous one expired (30-min TTL).
|
|
290
|
+
- Run `openclaw airlock pair`.
|
|
291
|
+
|
|
292
|
+
### "No approver available — pairing may be stale"
|
|
293
|
+
- The pairing was revoked from the mobile app.
|
|
294
|
+
- Generate a new pre-generated code and run pair again.
|
|
295
|
+
|
|
296
|
+
### "Access denied: pairing_revoked"
|
|
297
|
+
- The mobile app user revoked the pairing.
|
|
298
|
+
- The plugin automatically clears stale pairing state.
|
|
299
|
+
- Generate a new code and run `openclaw airlock pair`.
|
|
300
|
+
|
|
301
|
+
### "Approval timed out"
|
|
302
|
+
- The approver didn't respond within the timeout period.
|
|
303
|
+
- Increase `timeoutMs` if needed, or check if the approver's mobile app is reachable.
|
|
304
|
+
|
|
305
|
+
### Plugin not intercepting tools
|
|
306
|
+
- Check that `protectedTools` includes the tool names you want to gate.
|
|
307
|
+
- An empty `protectedTools` array means no automatic enforcement (opt-in model).
|
|
308
|
+
- OpenClaw's built-in shell/bash execution tool is named **`exec`** internally — not `bash` or `shell.exec`. Add `"exec"` to `protectedTools`, or use `"*"` to intercept all tools regardless of name.
|
|
309
|
+
- Use the `airlock_request_approval` tool for explicit control.
|
|
310
|
+
|
|
311
|
+
### Gateway unreachable
|
|
312
|
+
- Verify the `gatewayUrl` is correct (`https://igw.airlocks.io` for production).
|
|
313
|
+
- Run `openclaw airlock setup` to test connectivity.
|
|
314
|
+
- Check network/firewall settings.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Development
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
npm install
|
|
322
|
+
npm run build # Compile TypeScript
|
|
323
|
+
npm run dev # Watch mode
|
|
324
|
+
npm run typecheck # Type-check without emitting
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Project Structure
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
gateway_sdk/src/openclaw-airlock/
|
|
331
|
+
openclaw.plugin.json # OpenClaw manifest + config schema
|
|
332
|
+
package.json # @airlockapp/openclaw-airlock npm package
|
|
333
|
+
tsconfig.json # TypeScript ESM config
|
|
334
|
+
.gitignore # Excludes .airlock/, dist/, node_modules/
|
|
335
|
+
|
|
336
|
+
src/
|
|
337
|
+
index.ts # Plugin entry — registers all capabilities
|
|
338
|
+
config.ts # Config parsing, validation, defaults
|
|
339
|
+
client.ts # Gateway SDK wrapper: approval, pairing, heartbeat, DND
|
|
340
|
+
crypto.ts # X25519 ECDH key exchange + Ed25519 signature verification
|
|
341
|
+
state.ts # Pairing state persistence (.airlock/pairing-state.json)
|
|
342
|
+
|
|
343
|
+
tools/
|
|
344
|
+
requestApproval.ts # AI-callable tool: explicit approval request
|
|
345
|
+
checkStatus.ts # AI-callable tool: check exchange status
|
|
346
|
+
|
|
347
|
+
hooks/
|
|
348
|
+
beforeTool.ts # Automatic tool interception hook (+ DND check)
|
|
349
|
+
|
|
350
|
+
commands/
|
|
351
|
+
airlockStatus.ts # /airlock-status diagnostic command
|
|
352
|
+
|
|
353
|
+
cli/
|
|
354
|
+
setup.ts # `airlock setup` — validate config + connectivity
|
|
355
|
+
pair.ts # `airlock pair` — claim pre-generated code
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Architecture
|
|
361
|
+
|
|
362
|
+
```
|
|
363
|
+
┌───────────────────┐ HTTPS ┌──────────────────────────┐
|
|
364
|
+
│ OpenClaw Agent │ ──────────→ │ Integrations Gateway │
|
|
365
|
+
│ + Airlock Plugin │ ←────────── │ (igw.airlocks.io) │
|
|
366
|
+
└───────────────────┘ └────────────┬─────────────┘
|
|
367
|
+
│
|
|
368
|
+
Internal routing
|
|
369
|
+
│
|
|
370
|
+
┌────────────▼─────────────┐
|
|
371
|
+
│ Airlock Platform Backend │
|
|
372
|
+
└────────────┬─────────────┘
|
|
373
|
+
│
|
|
374
|
+
Push notification
|
|
375
|
+
│
|
|
376
|
+
┌────────────▼─────────────┐
|
|
377
|
+
│ Mobile Approver App │
|
|
378
|
+
│ (approve / deny) │
|
|
379
|
+
└──────────────────────────┘
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Key components:
|
|
383
|
+
|
|
384
|
+
- **@airlockapp/gateway-sdk** — TypeScript SDK for the Airlock Integrations Gateway API
|
|
385
|
+
- **X25519 ECDH** — Key exchange for end-to-end encryption (AES-256-GCM)
|
|
386
|
+
- **Ed25519** — Decision signature verification
|
|
387
|
+
- **PAT + Client Credentials** — Dual authentication (user identity + app identity)
|
|
388
|
+
- **Pre-generated pairing codes** — Config-driven pairing (no interactive browser login)
|
|
389
|
+
- **Long-polling** — Server-side 25s timeout for efficient decision waiting
|
|
390
|
+
- **Presence heartbeat** — 45s interval for online/offline status in mobile app
|
|
391
|
+
- **DND policy check** — Auto-approval when Do Not Disturb is active
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Further Reading
|
|
396
|
+
|
|
397
|
+
- [Airlock Developer Guide](https://airlockapp.io/docs/developer-guide/) — Developer programme, enforcer lifecycle, API reference
|
|
398
|
+
- [Gateway Client SDKs](https://airlockapp.io/docs/sdk/) — Published SDK packages and documentation
|
|
399
|
+
- [Gateway SDK (TypeScript)](../typescript/README.md) — TypeScript SDK documentation
|
|
400
|
+
- [HARP Specification](../../../harp-spec/) — Human-Approvable Request Protocol specification
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## License
|
|
405
|
+
|
|
406
|
+
MIT
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI registration — registers all Airlock CLI subcommands under `openclaw airlock`.
|
|
3
|
+
*
|
|
4
|
+
* All subcommands are registered in a single `registerCli` call because
|
|
5
|
+
* OpenClaw deduplicates by top-level command name: if "airlock" is already
|
|
6
|
+
* claimed, subsequent registrars for the same command are skipped.
|
|
7
|
+
*
|
|
8
|
+
* Subcommands:
|
|
9
|
+
* openclaw airlock setup — Validate config and test gateway connectivity
|
|
10
|
+
* openclaw airlock consent — Trigger and wait for user consent on the mobile app
|
|
11
|
+
* openclaw airlock pair — Claim a pre-generated pairing code
|
|
12
|
+
*/
|
|
13
|
+
import type { AirlockClient } from "../client.js";
|
|
14
|
+
import type { AirlockConfig } from "../config.js";
|
|
15
|
+
export declare function registerCliCommands(api: {
|
|
16
|
+
registerCli?(registrar: (ctx: {
|
|
17
|
+
program: unknown;
|
|
18
|
+
}) => void | Promise<void>, opts?: {
|
|
19
|
+
commands?: string[];
|
|
20
|
+
}): void;
|
|
21
|
+
}, client: AirlockClient, config: AirlockConfig): void;
|
|
22
|
+
//# sourceMappingURL=register.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.d.ts","sourceRoot":"","sources":["../../src/cli/register.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,wBAAgB,mBAAmB,CACjC,GAAG,EAAE;IACH,WAAW,CAAC,CACV,SAAS,EAAE,CAAC,GAAG,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EAC9D,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,GAC7B,IAAI,CAAC;CACT,EACD,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,aAAa,GACpB,IAAI,CAyQN"}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI registration — registers all Airlock CLI subcommands under `openclaw airlock`.
|
|
3
|
+
*
|
|
4
|
+
* All subcommands are registered in a single `registerCli` call because
|
|
5
|
+
* OpenClaw deduplicates by top-level command name: if "airlock" is already
|
|
6
|
+
* claimed, subsequent registrars for the same command are skipped.
|
|
7
|
+
*
|
|
8
|
+
* Subcommands:
|
|
9
|
+
* openclaw airlock setup — Validate config and test gateway connectivity
|
|
10
|
+
* openclaw airlock consent — Trigger and wait for user consent on the mobile app
|
|
11
|
+
* openclaw airlock pair — Claim a pre-generated pairing code
|
|
12
|
+
*/
|
|
13
|
+
export function registerCliCommands(api, client, config) {
|
|
14
|
+
if (!api.registerCli)
|
|
15
|
+
return;
|
|
16
|
+
api.registerCli(({ program }) => {
|
|
17
|
+
const airlockCmd = program
|
|
18
|
+
.command("airlock")
|
|
19
|
+
.description("Airlock security gateway");
|
|
20
|
+
// ── airlock setup ───────────────────────────────────────
|
|
21
|
+
airlockCmd
|
|
22
|
+
.command("setup")
|
|
23
|
+
.description("Validate Airlock configuration and test gateway connectivity")
|
|
24
|
+
.action(async () => {
|
|
25
|
+
// Ensure pairing state is loaded from disk before checking config
|
|
26
|
+
await client.ensureInitialized();
|
|
27
|
+
console.log("Airlock Setup");
|
|
28
|
+
console.log("═".repeat(40));
|
|
29
|
+
// 1. Config summary
|
|
30
|
+
console.log(`\nGateway URL: ${config.gatewayUrl}`);
|
|
31
|
+
console.log(`Enforcer ID: ${config.enforcerId}`);
|
|
32
|
+
console.log(`Workspace: ${config.workspaceName}`);
|
|
33
|
+
console.log(`Fail Mode: ${config.failMode}`);
|
|
34
|
+
console.log(`Auth (PAT): ${config.pat ? "✓" : "✗"}`);
|
|
35
|
+
console.log(`Auth (Client): ${config.clientId ? "✓" : "✗"}`);
|
|
36
|
+
// 2. Gateway connectivity
|
|
37
|
+
console.log("\nTesting gateway connectivity...");
|
|
38
|
+
const health = await client.checkHealth();
|
|
39
|
+
if (health.connected) {
|
|
40
|
+
console.log(`✓ Connected to ${health.gatewayUrl}`);
|
|
41
|
+
if (health.serverTime) {
|
|
42
|
+
console.log(` Server time: ${health.serverTime}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.error(`✗ Cannot reach gateway: ${health.error}`);
|
|
47
|
+
console.error(" Check the gatewayUrl in your plugin config.");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// 3. Consent status
|
|
51
|
+
try {
|
|
52
|
+
const consent = await client.checkConsent();
|
|
53
|
+
if (consent.status === "approved") {
|
|
54
|
+
console.log("\n✓ App consent: approved");
|
|
55
|
+
}
|
|
56
|
+
else if (consent.status === "required") {
|
|
57
|
+
console.log("\n⚠ App consent: required — run 'openclaw airlock consent' to trigger approval");
|
|
58
|
+
if (consent.message)
|
|
59
|
+
console.log(` ${consent.message}`);
|
|
60
|
+
}
|
|
61
|
+
else if (consent.status === "pending") {
|
|
62
|
+
console.log("\n⏳ App consent: pending — check your Airlock mobile app");
|
|
63
|
+
}
|
|
64
|
+
else if (consent.status === "denied") {
|
|
65
|
+
console.error("\n✗ App consent: denied by user");
|
|
66
|
+
if (consent.message)
|
|
67
|
+
console.error(` ${consent.message}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
console.log("\n⚠ App consent: could not check (non-fatal)");
|
|
72
|
+
}
|
|
73
|
+
// 4. Pairing status
|
|
74
|
+
if (config.routingToken && config.encryptionKey) {
|
|
75
|
+
console.log("\n✓ Paired — ready to enforce");
|
|
76
|
+
}
|
|
77
|
+
else if (config.pairingCode) {
|
|
78
|
+
console.log("\n⚠ Not paired — run 'openclaw airlock pair' to claim your pairing code");
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
console.log("\n⚠ Not paired — configure a pairingCode and run 'openclaw airlock pair'");
|
|
82
|
+
}
|
|
83
|
+
// 5. Protected tools
|
|
84
|
+
if (config.protectedTools.length > 0) {
|
|
85
|
+
console.log(`\nProtected tools (${config.protectedTools.length}):`);
|
|
86
|
+
for (const tool of config.protectedTools) {
|
|
87
|
+
console.log(` • ${tool}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.log("\nNo tools protected via hook (opt-in model).");
|
|
92
|
+
console.log("Use the airlock_request_approval tool for explicit approval.");
|
|
93
|
+
}
|
|
94
|
+
console.log("\n✓ Setup complete");
|
|
95
|
+
});
|
|
96
|
+
// ── airlock consent ─────────────────────────────────────
|
|
97
|
+
airlockCmd
|
|
98
|
+
.command("consent")
|
|
99
|
+
.description("Trigger and wait for user consent on the Airlock mobile app")
|
|
100
|
+
.action(async () => {
|
|
101
|
+
await client.ensureInitialized();
|
|
102
|
+
console.log("Airlock Consent");
|
|
103
|
+
console.log("═".repeat(40));
|
|
104
|
+
console.log(`\nEnforcer ID: ${config.enforcerId}`);
|
|
105
|
+
console.log(`Gateway: ${config.gatewayUrl}`);
|
|
106
|
+
// Initial check — this triggers the consent push if first time
|
|
107
|
+
console.log("\nChecking consent status...");
|
|
108
|
+
let consent = await client.checkConsent();
|
|
109
|
+
if (consent.status === "approved") {
|
|
110
|
+
console.log("✓ Consent already granted — you're good to go.");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (consent.status === "denied") {
|
|
114
|
+
console.error("✗ Consent was denied by the user.");
|
|
115
|
+
console.error(" Ask the user to re-approve in the Airlock mobile app.");
|
|
116
|
+
if (consent.consentUrl) {
|
|
117
|
+
console.error(` Consent URL: ${consent.consentUrl}`);
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// Status is "required" or "pending" — show instructions and poll
|
|
122
|
+
console.log("\n┌─ Consent Required ──────────────────────────────┐");
|
|
123
|
+
if (consent.message) {
|
|
124
|
+
console.log(`│ ${consent.message}`);
|
|
125
|
+
}
|
|
126
|
+
console.log("│ A consent request has been sent to the user's");
|
|
127
|
+
console.log("│ Airlock mobile app. Please approve it there.");
|
|
128
|
+
if (consent.consentUrl) {
|
|
129
|
+
console.log(`│ Or open: ${consent.consentUrl}`);
|
|
130
|
+
}
|
|
131
|
+
console.log("└─────────────────────────────────────────────────┘");
|
|
132
|
+
console.log("\nWaiting for consent approval...");
|
|
133
|
+
// Poll every 5 seconds for up to 5 minutes
|
|
134
|
+
const deadline = Date.now() + 5 * 60 * 1000;
|
|
135
|
+
let pollCount = 0;
|
|
136
|
+
while (Date.now() < deadline) {
|
|
137
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
138
|
+
pollCount++;
|
|
139
|
+
try {
|
|
140
|
+
consent = await client.checkConsent();
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
console.log(` Poll ${pollCount}: error (retrying...)`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (consent.status === "approved") {
|
|
147
|
+
console.log(`\n✓ Consent granted! (after ${pollCount * 5}s)`);
|
|
148
|
+
console.log(" You can now proceed with 'openclaw airlock pair'.");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (consent.status === "denied") {
|
|
152
|
+
console.error(`\n✗ Consent was denied. (after ${pollCount * 5}s)`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const elapsed = pollCount * 5;
|
|
156
|
+
console.log(` Waiting... (${elapsed}s elapsed, status: ${consent.status})`);
|
|
157
|
+
}
|
|
158
|
+
console.error("\n✗ Timed out waiting for consent (5 minutes).");
|
|
159
|
+
console.error(" Try again or check the Airlock mobile app.");
|
|
160
|
+
});
|
|
161
|
+
// ── airlock pair ────────────────────────────────────────
|
|
162
|
+
airlockCmd
|
|
163
|
+
.command("pair")
|
|
164
|
+
.description("Claim a pre-generated pairing code and establish encrypted communication")
|
|
165
|
+
.action(async () => {
|
|
166
|
+
// Ensure pairing state is loaded from disk before checking config
|
|
167
|
+
await client.ensureInitialized();
|
|
168
|
+
console.log("Airlock Pairing");
|
|
169
|
+
console.log("═".repeat(40));
|
|
170
|
+
// Check if already paired
|
|
171
|
+
if (config.routingToken && config.encryptionKey) {
|
|
172
|
+
console.log("✓ Already paired.");
|
|
173
|
+
console.log(" To re-pair, clear the existing pairing first.");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// Check for pairing code
|
|
177
|
+
if (!config.pairingCode) {
|
|
178
|
+
console.error("✗ No pairing code configured.");
|
|
179
|
+
console.error(" Set 'pairingCode' in your Airlock plugin config.");
|
|
180
|
+
console.error(" Generate a code in the Airlock admin dashboard or mobile app.");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// Test gateway connectivity first
|
|
184
|
+
const health = await client.checkHealth();
|
|
185
|
+
if (!health.connected) {
|
|
186
|
+
console.error(`✗ Cannot reach gateway: ${health.error}`);
|
|
187
|
+
console.error(" Fix connectivity before pairing.");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
// Check consent before attempting to pair
|
|
191
|
+
try {
|
|
192
|
+
const consent = await client.checkConsent();
|
|
193
|
+
if (consent.status !== "approved") {
|
|
194
|
+
console.error("⚠ App consent not yet granted.");
|
|
195
|
+
console.error(" Run 'openclaw airlock consent' first to get user approval.");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// Consent check failed — continue with pairing anyway
|
|
201
|
+
console.log("⚠ Could not verify consent status (continuing...)");
|
|
202
|
+
}
|
|
203
|
+
console.log(`Gateway: ${config.gatewayUrl}`);
|
|
204
|
+
console.log(`Enforcer ID: ${config.enforcerId}`);
|
|
205
|
+
console.log(`Workspace: ${config.workspaceName}`);
|
|
206
|
+
console.log(`Pairing Code: ${config.pairingCode}`);
|
|
207
|
+
console.log("\nClaiming pairing code (X25519 ECDH key exchange)...");
|
|
208
|
+
try {
|
|
209
|
+
const result = await client.claimPairing(config.pairingCode);
|
|
210
|
+
console.log("\n✓ Pairing successful!");
|
|
211
|
+
console.log(` Routing Token: ${result.routingToken.slice(0, 12)}...`);
|
|
212
|
+
console.log(" Encryption: ✓ Key derived (X25519 ECDH + HKDF-SHA256)");
|
|
213
|
+
console.log(" State File: ✓ Persisted to ~/.openclaw/.airlock/pairing-state.json");
|
|
214
|
+
console.log("\n Airlock is now ready to enforce approvals.");
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
218
|
+
console.error(`\n✗ Pairing failed: ${msg}`);
|
|
219
|
+
if (msg.includes("expired")) {
|
|
220
|
+
console.error(" Generate a new pairing code and update your config.");
|
|
221
|
+
}
|
|
222
|
+
if (msg.includes("x25519PublicKey")) {
|
|
223
|
+
console.error(" The pairing response may be missing key exchange data.");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}, { commands: ["airlock"] });
|
|
228
|
+
}
|
|
229
|
+
//# sourceMappingURL=register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.js","sourceRoot":"","sources":["../../src/cli/register.ts"],"names":[],"mappings":"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"}
|