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
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: airlock-status — Diagnostic slash command.
|
|
3
|
+
*
|
|
4
|
+
* Shows the current Airlock configuration, gateway connectivity,
|
|
5
|
+
* pairing status, and enforcement settings.
|
|
6
|
+
*/
|
|
7
|
+
import { loadPairingState } from "../state.js";
|
|
8
|
+
/** Command definition for OpenClaw registration. */
|
|
9
|
+
export const airlockStatusCommandDef = {
|
|
10
|
+
name: "airlock-status",
|
|
11
|
+
description: "Show Airlock security gateway status and configuration",
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Register the /airlock-status command with the OpenClaw plugin API.
|
|
15
|
+
*/
|
|
16
|
+
export function registerAirlockStatusCommand(api, client, config) {
|
|
17
|
+
api.registerCommand(airlockStatusCommandDef, async () => {
|
|
18
|
+
const health = await client.checkHealth();
|
|
19
|
+
const persistedState = await loadPairingState();
|
|
20
|
+
const lines = [
|
|
21
|
+
"═══ Airlock Status ═══",
|
|
22
|
+
"",
|
|
23
|
+
`Gateway URL: ${config.gatewayUrl}`,
|
|
24
|
+
`Connectivity: ${health.connected ? "✓ Connected" : `✗ Unreachable (${health.error ?? "unknown"})`}`,
|
|
25
|
+
health.serverTime ? `Server Time: ${health.serverTime}` : "",
|
|
26
|
+
"",
|
|
27
|
+
`Enforcer ID: ${config.enforcerId}`,
|
|
28
|
+
`Workspace: ${config.workspaceName}`,
|
|
29
|
+
`Execution Mode: ${config.executionMode}`,
|
|
30
|
+
`Fail Mode: ${config.failMode}`,
|
|
31
|
+
`Timeout: ${config.timeoutMs / 1000}s`,
|
|
32
|
+
`Poll Interval: ${config.pollIntervalMs}ms`,
|
|
33
|
+
"",
|
|
34
|
+
`Auth (PAT): ${config.pat ? "✓ Configured" : "✗ Not set"}`,
|
|
35
|
+
`Auth (Client): ${config.clientId ? `✓ ${config.clientId}` : "✗ Not set"}`,
|
|
36
|
+
"",
|
|
37
|
+
`Pairing: ${config.routingToken ? "✓ Paired" : "✗ Not paired"}`,
|
|
38
|
+
`Encryption Key: ${config.encryptionKey ? "✓ Available" : "✗ Not available"}`,
|
|
39
|
+
`State File: ${persistedState ? `✓ Persisted (${persistedState.pairedAt})` : "✗ No state on disk"}`,
|
|
40
|
+
"",
|
|
41
|
+
`Protected Tools: ${config.protectedTools.length > 0
|
|
42
|
+
? config.protectedTools.join(", ")
|
|
43
|
+
: "(none — use requestApproval tool for explicit control)"}`,
|
|
44
|
+
];
|
|
45
|
+
return lines.filter((l) => l !== undefined).join("\n");
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=airlockStatus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"airlockStatus.js","sourceRoot":"","sources":["../../src/commands/airlockStatus.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,oDAAoD;AACpD,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,IAAI,EAAE,gBAAgB;IACtB,WAAW,EAAE,wDAAwD;CACtE,CAAC;AAEF;;GAEG;AACH,MAAM,UAAU,4BAA4B,CAC1C,GAAgF,EAChF,MAAqB,EACrB,MAAqB;IAErB,GAAG,CAAC,eAAe,CAAC,uBAAuB,EAAE,KAAK,IAAI,EAAE;QACtD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC;QAC1C,MAAM,cAAc,GAAG,MAAM,gBAAgB,EAAE,CAAC;QAEhD,MAAM,KAAK,GAAa;YACtB,wBAAwB;YACxB,EAAE;YACF,oBAAoB,MAAM,CAAC,UAAU,EAAE;YACvC,oBAAoB,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,kBAAkB,MAAM,CAAC,KAAK,IAAI,SAAS,GAAG,EAAE;YACvG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,oBAAoB,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE;YAChE,EAAE;YACF,oBAAoB,MAAM,CAAC,UAAU,EAAE;YACvC,oBAAoB,MAAM,CAAC,aAAa,EAAE;YAC1C,oBAAoB,MAAM,CAAC,aAAa,EAAE;YAC1C,oBAAoB,MAAM,CAAC,QAAQ,EAAE;YACrC,oBAAoB,MAAM,CAAC,SAAS,GAAG,IAAI,GAAG;YAC9C,oBAAoB,MAAM,CAAC,cAAc,IAAI;YAC7C,EAAE;YACF,oBAAoB,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,WAAW,EAAE;YAC/D,oBAAoB,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE;YAC5E,EAAE;YACF,oBAAoB,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,cAAc,EAAE;YACvE,oBAAoB,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,iBAAiB,EAAE;YAC9E,oBAAoB,cAAc,CAAC,CAAC,CAAC,gBAAgB,cAAc,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,oBAAoB,EAAE;YACxG,EAAE;YACF,oBAAoB,MAAM,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC;gBAClD,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC;gBAClC,CAAC,CAAC,wDAAwD,EAAE;SAC/D,CAAC;QAEF,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Airlock Plugin Config — parse, validate, and apply defaults.
|
|
3
|
+
*
|
|
4
|
+
* Config comes from OpenClaw's plugin config mechanism (openclaw.plugin.json schema).
|
|
5
|
+
* Auth: PAT + ClientId/ClientSecret. Pairing: pre-generated device code.
|
|
6
|
+
*/
|
|
7
|
+
/** Strongly-typed config for the Airlock OpenClaw plugin. */
|
|
8
|
+
export interface AirlockConfig {
|
|
9
|
+
/** Airlock Gateway base URL (e.g. "https://gw.airlocks.io"). */
|
|
10
|
+
gatewayUrl: string;
|
|
11
|
+
/** Unique identifier for this enforcer instance. */
|
|
12
|
+
enforcerId: string;
|
|
13
|
+
/** Personal Access Token for user authentication. */
|
|
14
|
+
pat?: string;
|
|
15
|
+
/** Enforcer App Client ID. */
|
|
16
|
+
clientId?: string;
|
|
17
|
+
/** Enforcer App Client Secret. */
|
|
18
|
+
clientSecret?: string;
|
|
19
|
+
/** Pre-generated device pairing code. */
|
|
20
|
+
pairingCode?: string;
|
|
21
|
+
/** Human-readable workspace name. */
|
|
22
|
+
workspaceName: string;
|
|
23
|
+
/** Approval timeout in milliseconds (default: 300000 = 5 min). */
|
|
24
|
+
timeoutMs: number;
|
|
25
|
+
/** Decision poll interval in milliseconds (default: 3000). */
|
|
26
|
+
pollIntervalMs: number;
|
|
27
|
+
/** Behavior on timeout/error: "open" = allow, "closed" = block. */
|
|
28
|
+
failMode: "open" | "closed";
|
|
29
|
+
/** Tool names requiring approval (empty = none protected, opt-in). */
|
|
30
|
+
protectedTools: string[];
|
|
31
|
+
/** How to wait for decisions: "poll" (default) or "webhook" (future). */
|
|
32
|
+
executionMode: "poll" | "webhook";
|
|
33
|
+
/** Routing token from completed pairing. */
|
|
34
|
+
routingToken?: string;
|
|
35
|
+
/** AES-256-GCM encryption key (base64url) derived via X25519 ECDH. */
|
|
36
|
+
encryptionKey?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Parse raw config from OpenClaw plugin context, validate required fields,
|
|
40
|
+
* and apply defaults.
|
|
41
|
+
*
|
|
42
|
+
* @throws Error if required fields are missing or values are invalid.
|
|
43
|
+
*/
|
|
44
|
+
export declare function loadAndValidateConfig(raw: Record<string, unknown>): AirlockConfig;
|
|
45
|
+
export declare class ConfigError extends Error {
|
|
46
|
+
constructor(message: string);
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,6DAA6D;AAC7D,MAAM,WAAW,aAAa;IAC5B,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yCAAyC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,aAAa,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,SAAS,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,cAAc,EAAE,MAAM,CAAC;IACvB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC5B,sEAAsE;IACtE,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,yEAAyE;IACzE,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAGlC,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,aAAa,CAwEjF;AAID,qBAAa,WAAY,SAAQ,KAAK;gBACxB,OAAO,EAAE,MAAM;CAI5B"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Airlock Plugin Config — parse, validate, and apply defaults.
|
|
3
|
+
*
|
|
4
|
+
* Config comes from OpenClaw's plugin config mechanism (openclaw.plugin.json schema).
|
|
5
|
+
* Auth: PAT + ClientId/ClientSecret. Pairing: pre-generated device code.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Parse raw config from OpenClaw plugin context, validate required fields,
|
|
9
|
+
* and apply defaults.
|
|
10
|
+
*
|
|
11
|
+
* @throws Error if required fields are missing or values are invalid.
|
|
12
|
+
*/
|
|
13
|
+
export function loadAndValidateConfig(raw) {
|
|
14
|
+
// ── Required fields ──
|
|
15
|
+
const gatewayUrl = requireString(raw, "gatewayUrl", "Gateway URL is required");
|
|
16
|
+
if (!/^https?:\/\//i.test(gatewayUrl)) {
|
|
17
|
+
throw new ConfigError("gatewayUrl must start with http:// or https://");
|
|
18
|
+
}
|
|
19
|
+
const enforcerId = requireString(raw, "enforcerId", "Enforcer ID is required");
|
|
20
|
+
// ── Auth: at least PAT or clientId+clientSecret ──
|
|
21
|
+
const pat = optionalString(raw, "pat");
|
|
22
|
+
const clientId = optionalString(raw, "clientId");
|
|
23
|
+
const clientSecret = optionalString(raw, "clientSecret");
|
|
24
|
+
if (!pat && !clientId) {
|
|
25
|
+
throw new ConfigError("Authentication required: provide either 'pat' (Personal Access Token) " +
|
|
26
|
+
"or 'clientId' + 'clientSecret' (Enforcer App credentials)");
|
|
27
|
+
}
|
|
28
|
+
if (clientId && !clientSecret) {
|
|
29
|
+
throw new ConfigError("'clientSecret' is required when 'clientId' is provided");
|
|
30
|
+
}
|
|
31
|
+
// ── Optional fields with defaults ──
|
|
32
|
+
const pairingCode = optionalString(raw, "pairingCode");
|
|
33
|
+
const workspaceName = optionalString(raw, "workspaceName") ?? "OpenClaw Workspace";
|
|
34
|
+
const timeoutMs = optionalNumber(raw, "timeoutMs") ?? 300_000;
|
|
35
|
+
const pollIntervalMs = Math.max(1000, optionalNumber(raw, "pollIntervalMs") ?? 3000);
|
|
36
|
+
const rawFailMode = optionalString(raw, "failMode") ?? "closed";
|
|
37
|
+
if (rawFailMode !== "open" && rawFailMode !== "closed") {
|
|
38
|
+
throw new ConfigError(`failMode must be "open" or "closed", got "${rawFailMode}"`);
|
|
39
|
+
}
|
|
40
|
+
const rawExecMode = optionalString(raw, "executionMode") ?? "poll";
|
|
41
|
+
if (rawExecMode !== "poll" && rawExecMode !== "webhook") {
|
|
42
|
+
throw new ConfigError(`executionMode must be "poll" or "webhook", got "${rawExecMode}"`);
|
|
43
|
+
}
|
|
44
|
+
if (rawExecMode === "webhook") {
|
|
45
|
+
throw new ConfigError("Webhook mode is not implemented yet (planned for Sprint 26+). Use 'poll'.");
|
|
46
|
+
}
|
|
47
|
+
const rawProtectedTools = raw["protectedTools"];
|
|
48
|
+
let protectedTools = [];
|
|
49
|
+
if (Array.isArray(rawProtectedTools)) {
|
|
50
|
+
protectedTools = rawProtectedTools
|
|
51
|
+
.map((t) => String(t).trim())
|
|
52
|
+
.filter((t) => t.length > 0);
|
|
53
|
+
}
|
|
54
|
+
// ── Pairing state (may come from config or be restored from disk at runtime) ──
|
|
55
|
+
const routingToken = optionalString(raw, "routingToken");
|
|
56
|
+
const encryptionKey = optionalString(raw, "encryptionKey");
|
|
57
|
+
return {
|
|
58
|
+
gatewayUrl: gatewayUrl.replace(/\/$/, ""),
|
|
59
|
+
enforcerId,
|
|
60
|
+
pat,
|
|
61
|
+
clientId,
|
|
62
|
+
clientSecret,
|
|
63
|
+
pairingCode,
|
|
64
|
+
workspaceName,
|
|
65
|
+
timeoutMs,
|
|
66
|
+
pollIntervalMs,
|
|
67
|
+
failMode: rawFailMode,
|
|
68
|
+
protectedTools,
|
|
69
|
+
executionMode: rawExecMode,
|
|
70
|
+
routingToken,
|
|
71
|
+
encryptionKey,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
75
|
+
export class ConfigError extends Error {
|
|
76
|
+
constructor(message) {
|
|
77
|
+
super(`[Airlock Config] ${message}`);
|
|
78
|
+
this.name = "ConfigError";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function requireString(raw, key, errorMsg) {
|
|
82
|
+
const value = raw[key];
|
|
83
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
84
|
+
throw new ConfigError(errorMsg);
|
|
85
|
+
}
|
|
86
|
+
return value.trim();
|
|
87
|
+
}
|
|
88
|
+
function optionalString(raw, key) {
|
|
89
|
+
const value = raw[key];
|
|
90
|
+
if (value === undefined || value === null)
|
|
91
|
+
return undefined;
|
|
92
|
+
if (typeof value !== "string")
|
|
93
|
+
return undefined;
|
|
94
|
+
const trimmed = value.trim();
|
|
95
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
96
|
+
}
|
|
97
|
+
function optionalNumber(raw, key) {
|
|
98
|
+
const value = raw[key];
|
|
99
|
+
if (value === undefined || value === null)
|
|
100
|
+
return undefined;
|
|
101
|
+
const num = Number(value);
|
|
102
|
+
return Number.isFinite(num) ? num : undefined;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cryptographic utilities for Airlock plugin.
|
|
3
|
+
*
|
|
4
|
+
* - X25519 key pair generation and ECDH shared key derivation
|
|
5
|
+
* - Ed25519 decision signature verification
|
|
6
|
+
*
|
|
7
|
+
* These mirror the Claude Code enforcer's crypto.js and pairing.js patterns.
|
|
8
|
+
*/
|
|
9
|
+
export interface X25519KeyPair {
|
|
10
|
+
/** Base64url-encoded SPKI DER public key. */
|
|
11
|
+
publicKey: string;
|
|
12
|
+
/** Base64url-encoded PKCS8 DER private key. */
|
|
13
|
+
privateKey: string;
|
|
14
|
+
}
|
|
15
|
+
/** Generate an X25519 key pair for ECDH key exchange. */
|
|
16
|
+
export declare function generateX25519KeyPair(): X25519KeyPair;
|
|
17
|
+
/**
|
|
18
|
+
* Derive a shared AES-256-GCM key from local X25519 private key and remote X25519 public key.
|
|
19
|
+
*
|
|
20
|
+
* Uses ECDH + HKDF-SHA256 with info "HARP-E2E-AES256GCM" (matching all other Airlock SDKs).
|
|
21
|
+
*
|
|
22
|
+
* @param localPrivateKeyBase64Url PKCS8 DER private key (base64url).
|
|
23
|
+
* @param remotePublicKeyBase64Url SPKI DER or raw 32-byte public key (base64url).
|
|
24
|
+
* @returns AES-256-GCM key as base64url string.
|
|
25
|
+
*/
|
|
26
|
+
export declare function deriveSharedKey(localPrivateKeyBase64Url: string, remotePublicKeyBase64Url: string): string;
|
|
27
|
+
export interface PairedKeyEntry {
|
|
28
|
+
/** Base64 public key (DER or raw Ed25519). */
|
|
29
|
+
publicKey: string;
|
|
30
|
+
/** Device identifier. */
|
|
31
|
+
deviceId: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Verify an Ed25519 decision signature.
|
|
35
|
+
*
|
|
36
|
+
* Canonical format: `${artifactHash}|${decision}|${nonce}`
|
|
37
|
+
*
|
|
38
|
+
* @returns True if the signature is valid, false otherwise.
|
|
39
|
+
*/
|
|
40
|
+
export declare function verifyDecisionSignature(pairedKeys: Record<string, PairedKeyEntry>, artifactHash: string, decision: string, nonce: string, signatureBase64Url: string, signerKeyId: string): boolean;
|
|
41
|
+
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAwBH,MAAM,WAAW,aAAa;IAC5B,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,yDAAyD;AACzD,wBAAgB,qBAAqB,IAAI,aAAa,CAUrD;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,wBAAwB,EAAE,MAAM,EAChC,wBAAwB,EAAE,MAAM,GAC/B,MAAM,CAiCR;AAID,MAAM,WAAW,cAAc;IAC7B,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,EAC1C,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,kBAAkB,EAAE,MAAM,EAC1B,WAAW,EAAE,MAAM,GAClB,OAAO,CAoDT"}
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cryptographic utilities for Airlock plugin.
|
|
3
|
+
*
|
|
4
|
+
* - X25519 key pair generation and ECDH shared key derivation
|
|
5
|
+
* - Ed25519 decision signature verification
|
|
6
|
+
*
|
|
7
|
+
* These mirror the Claude Code enforcer's crypto.js and pairing.js patterns.
|
|
8
|
+
*/
|
|
9
|
+
import { generateKeyPairSync, createPrivateKey, createPublicKey, diffieHellman, hkdfSync, verify, } from "node:crypto";
|
|
10
|
+
// ── Constants ───────────────────────────────────────────────────
|
|
11
|
+
const AES_KEY_BYTES = 32;
|
|
12
|
+
const HKDF_INFO = "HARP-E2E-AES256GCM";
|
|
13
|
+
/** ASN.1 DER prefix for X25519 SPKI encoding (raw 32-byte key → DER). */
|
|
14
|
+
const X25519_SPKI_HEADER = Buffer.from("302a300506032b656e032100", "hex");
|
|
15
|
+
/** ASN.1 DER prefix for Ed25519 SPKI encoding (raw 32-byte key → DER). */
|
|
16
|
+
const ED25519_SPKI_HEADER = Buffer.from("302a300506032b6570032100", "hex");
|
|
17
|
+
/** Generate an X25519 key pair for ECDH key exchange. */
|
|
18
|
+
export function generateX25519KeyPair() {
|
|
19
|
+
const { publicKey, privateKey } = generateKeyPairSync("x25519");
|
|
20
|
+
return {
|
|
21
|
+
publicKey: publicKey
|
|
22
|
+
.export({ type: "spki", format: "der" })
|
|
23
|
+
.toString("base64url"),
|
|
24
|
+
privateKey: privateKey
|
|
25
|
+
.export({ type: "pkcs8", format: "der" })
|
|
26
|
+
.toString("base64url"),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Derive a shared AES-256-GCM key from local X25519 private key and remote X25519 public key.
|
|
31
|
+
*
|
|
32
|
+
* Uses ECDH + HKDF-SHA256 with info "HARP-E2E-AES256GCM" (matching all other Airlock SDKs).
|
|
33
|
+
*
|
|
34
|
+
* @param localPrivateKeyBase64Url PKCS8 DER private key (base64url).
|
|
35
|
+
* @param remotePublicKeyBase64Url SPKI DER or raw 32-byte public key (base64url).
|
|
36
|
+
* @returns AES-256-GCM key as base64url string.
|
|
37
|
+
*/
|
|
38
|
+
export function deriveSharedKey(localPrivateKeyBase64Url, remotePublicKeyBase64Url) {
|
|
39
|
+
const privKey = createPrivateKey({
|
|
40
|
+
key: Buffer.from(localPrivateKeyBase64Url, "base64url"),
|
|
41
|
+
format: "der",
|
|
42
|
+
type: "pkcs8",
|
|
43
|
+
});
|
|
44
|
+
// Handle both raw 32-byte and full SPKI DER formats
|
|
45
|
+
let remotePubBuf = Buffer.from(remotePublicKeyBase64Url, "base64url");
|
|
46
|
+
if (remotePubBuf.length === 32) {
|
|
47
|
+
remotePubBuf = Buffer.concat([X25519_SPKI_HEADER, remotePubBuf]);
|
|
48
|
+
}
|
|
49
|
+
const pubKey = createPublicKey({
|
|
50
|
+
key: remotePubBuf,
|
|
51
|
+
format: "der",
|
|
52
|
+
type: "spki",
|
|
53
|
+
});
|
|
54
|
+
const sharedSecret = diffieHellman({
|
|
55
|
+
publicKey: pubKey,
|
|
56
|
+
privateKey: privKey,
|
|
57
|
+
});
|
|
58
|
+
const derivedKey = hkdfSync("sha256", sharedSecret, Buffer.alloc(0), // no salt
|
|
59
|
+
Buffer.from(HKDF_INFO, "utf-8"), // info
|
|
60
|
+
AES_KEY_BYTES);
|
|
61
|
+
return Buffer.from(derivedKey).toString("base64url");
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Verify an Ed25519 decision signature.
|
|
65
|
+
*
|
|
66
|
+
* Canonical format: `${artifactHash}|${decision}|${nonce}`
|
|
67
|
+
*
|
|
68
|
+
* @returns True if the signature is valid, false otherwise.
|
|
69
|
+
*/
|
|
70
|
+
export function verifyDecisionSignature(pairedKeys, artifactHash, decision, nonce, signatureBase64Url, signerKeyId) {
|
|
71
|
+
// Look up the signer's public key (try multiple key ID variants)
|
|
72
|
+
const keyEntry = pairedKeys[signerKeyId] ??
|
|
73
|
+
pairedKeys[signerKeyId.replace(/^key-/, "")] ??
|
|
74
|
+
pairedKeys[`key-${signerKeyId}`];
|
|
75
|
+
if (!keyEntry) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
// Decode the signature from base64url → standard base64
|
|
79
|
+
let sigB64 = signatureBase64Url.replace(/-/g, "+").replace(/_/g, "/");
|
|
80
|
+
while (sigB64.length % 4 !== 0)
|
|
81
|
+
sigB64 += "=";
|
|
82
|
+
const signatureBytes = Buffer.from(sigB64, "base64");
|
|
83
|
+
// Build canonical message
|
|
84
|
+
const canonical = `${artifactHash}|${decision}|${nonce}`;
|
|
85
|
+
const message = Buffer.from(canonical, "utf-8");
|
|
86
|
+
const publicKeyBytes = Buffer.from(keyEntry.publicKey, "base64");
|
|
87
|
+
// Try verification with full DER SPKI first
|
|
88
|
+
try {
|
|
89
|
+
if (verify(null, message, { key: publicKeyBytes, format: "der", type: "spki" }, signatureBytes)) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Might be raw key, try wrapping with Ed25519 header
|
|
95
|
+
}
|
|
96
|
+
// Try with raw 32-byte key wrapped in Ed25519 SPKI header
|
|
97
|
+
try {
|
|
98
|
+
const keyObj = createPublicKey({
|
|
99
|
+
key: Buffer.concat([ED25519_SPKI_HEADER, publicKeyBytes]),
|
|
100
|
+
format: "der",
|
|
101
|
+
type: "spki",
|
|
102
|
+
});
|
|
103
|
+
if (verify(null, message, keyObj, signatureBytes)) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Verification failed
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook: before_tool_call — automatic interception of protected tool executions.
|
|
3
|
+
*
|
|
4
|
+
* Uses OpenClaw's `before_tool_call` plugin hook event to intercept tools
|
|
5
|
+
* and require approval from the Airlock mobile approver.
|
|
6
|
+
*
|
|
7
|
+
* If `protectedTools` is empty, no tools are automatically protected
|
|
8
|
+
* (opt-in model). Use the requestApproval tool for explicit control.
|
|
9
|
+
*
|
|
10
|
+
* Checks DND (Do Not Disturb) policies before requesting approval —
|
|
11
|
+
* if a matching DND policy is active, the tool is auto-approved.
|
|
12
|
+
*
|
|
13
|
+
* Returns `{ block: true, blockReason }` to block, or `undefined` to allow.
|
|
14
|
+
*/
|
|
15
|
+
import type { AirlockClient } from "../client.js";
|
|
16
|
+
import type { AirlockConfig } from "../config.js";
|
|
17
|
+
/** Context passed to the before_tool_call hook by OpenClaw. */
|
|
18
|
+
export interface BeforeToolContext {
|
|
19
|
+
/** The name of the tool being executed. */
|
|
20
|
+
toolName: string;
|
|
21
|
+
/** The tool's input parameters. */
|
|
22
|
+
params?: Record<string, unknown>;
|
|
23
|
+
/** The tool's input arguments (legacy compat). */
|
|
24
|
+
toolInput?: unknown;
|
|
25
|
+
/** Optional metadata about the tool call. */
|
|
26
|
+
metadata?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
/** Return value for before_tool_call — OpenClaw checks these fields. */
|
|
29
|
+
export interface BeforeToolResult {
|
|
30
|
+
block?: boolean;
|
|
31
|
+
blockReason?: string;
|
|
32
|
+
params?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Register the before_tool_call hook with the OpenClaw plugin API.
|
|
36
|
+
*
|
|
37
|
+
* @param api OpenClaw plugin API (api.registerHook)
|
|
38
|
+
* @param client AirlockClient instance
|
|
39
|
+
* @param config Validated AirlockConfig
|
|
40
|
+
*/
|
|
41
|
+
export declare function registerBeforeToolHook(api: {
|
|
42
|
+
on: (hookName: string, handler: (event: unknown, ctx?: unknown) => unknown, opts?: {
|
|
43
|
+
priority?: number;
|
|
44
|
+
}) => void;
|
|
45
|
+
}, client: AirlockClient, config: AirlockConfig): void;
|
|
46
|
+
//# sourceMappingURL=beforeTool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"beforeTool.d.ts","sourceRoot":"","sources":["../../src/hooks/beforeTool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,+DAA+D;AAC/D,MAAM,WAAW,iBAAiB;IAChC,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,mCAAmC;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,kDAAkD;IAClD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE;IAAE,EAAE,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,OAAO,KAAK,OAAO,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CAAE,EAC1H,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,aAAa,GACpB,IAAI,CAsFN"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook: before_tool_call — automatic interception of protected tool executions.
|
|
3
|
+
*
|
|
4
|
+
* Uses OpenClaw's `before_tool_call` plugin hook event to intercept tools
|
|
5
|
+
* and require approval from the Airlock mobile approver.
|
|
6
|
+
*
|
|
7
|
+
* If `protectedTools` is empty, no tools are automatically protected
|
|
8
|
+
* (opt-in model). Use the requestApproval tool for explicit control.
|
|
9
|
+
*
|
|
10
|
+
* Checks DND (Do Not Disturb) policies before requesting approval —
|
|
11
|
+
* if a matching DND policy is active, the tool is auto-approved.
|
|
12
|
+
*
|
|
13
|
+
* Returns `{ block: true, blockReason }` to block, or `undefined` to allow.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Register the before_tool_call hook with the OpenClaw plugin API.
|
|
17
|
+
*
|
|
18
|
+
* @param api OpenClaw plugin API (api.registerHook)
|
|
19
|
+
* @param client AirlockClient instance
|
|
20
|
+
* @param config Validated AirlockConfig
|
|
21
|
+
*/
|
|
22
|
+
export function registerBeforeToolHook(api, client, config) {
|
|
23
|
+
// OpenClaw API: api.on(hookName, handler) → registerTypedHook → registry.typedHooks (callable)
|
|
24
|
+
// NOTE: api.registerHook() only adds to registry.hooks (metadata), never invoked by the hook runner.
|
|
25
|
+
api.on("before_tool_call", async (rawContext) => {
|
|
26
|
+
const context = rawContext;
|
|
27
|
+
const toolName = context.toolName;
|
|
28
|
+
console.log(`[Airlock] tool:before_call fired — tool=${toolName}`);
|
|
29
|
+
// ── Opt-in model: if protectedTools is empty, do nothing ──
|
|
30
|
+
if (config.protectedTools.length === 0) {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
// ── Check if this tool is in the protected list ──
|
|
34
|
+
const isProtected = config.protectedTools.some((pattern) => matchToolName(toolName, pattern));
|
|
35
|
+
if (!isProtected) {
|
|
36
|
+
console.log(`[Airlock] Tool ${toolName} not protected — allowing`);
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
console.info(`[Airlock] Tool ${toolName} is PROTECTED — requesting approval`);
|
|
40
|
+
// ── Check DND (Do Not Disturb) policies ──
|
|
41
|
+
const dndActive = await client.isDndActive();
|
|
42
|
+
if (dndActive) {
|
|
43
|
+
console.info(`[Airlock] DND active — auto-approving tool ${toolName}`);
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
// ── Build a readable command text from tool input ──
|
|
47
|
+
const toolInput = context.params ?? context.toolInput;
|
|
48
|
+
let commandText;
|
|
49
|
+
try {
|
|
50
|
+
commandText = typeof toolInput === "string"
|
|
51
|
+
? toolInput
|
|
52
|
+
: JSON.stringify(toolInput, null, 2);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
commandText = String(toolInput);
|
|
56
|
+
}
|
|
57
|
+
// Truncate very long inputs
|
|
58
|
+
const maxLen = 2000;
|
|
59
|
+
if (commandText.length > maxLen) {
|
|
60
|
+
commandText = commandText.slice(0, maxLen) + "\n... (truncated)";
|
|
61
|
+
}
|
|
62
|
+
// ── Request approval from Airlock Gateway ──
|
|
63
|
+
const decision = await client.requestApproval({
|
|
64
|
+
actionType: toolName,
|
|
65
|
+
commandText,
|
|
66
|
+
description: `Tool: ${toolName}`,
|
|
67
|
+
});
|
|
68
|
+
// ── Apply decision using OpenClaw's { block, blockReason } response ──
|
|
69
|
+
if (decision.decision === "approved") {
|
|
70
|
+
console.info(`[Airlock] Tool ${toolName} APPROVED`);
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
if (decision.decision === "rejected") {
|
|
74
|
+
const reason = decision.reason ?? "no reason provided";
|
|
75
|
+
console.warn(`[Airlock] Tool ${toolName} DENIED — ${reason}`);
|
|
76
|
+
return {
|
|
77
|
+
block: true,
|
|
78
|
+
blockReason: `Airlock: Action denied by approver — ${reason}. Do NOT retry this action automatically.`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// Timeout — apply failMode
|
|
82
|
+
if (config.failMode === "closed") {
|
|
83
|
+
console.warn(`[Airlock] Tool ${toolName} TIMEOUT — blocking (fail-closed)`);
|
|
84
|
+
return {
|
|
85
|
+
block: true,
|
|
86
|
+
blockReason: "Airlock: Approval timed out — blocking (fail-closed). Do NOT retry this action automatically.",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// failMode === "open" — allow silently
|
|
90
|
+
console.warn(`[Airlock] Tool ${toolName} TIMEOUT — allowing (fail-open)`);
|
|
91
|
+
return {};
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Match a tool name against a pattern.
|
|
96
|
+
* Supports exact match and glob-like wildcards (*).
|
|
97
|
+
*/
|
|
98
|
+
function matchToolName(toolName, pattern) {
|
|
99
|
+
// Exact match
|
|
100
|
+
if (toolName === pattern)
|
|
101
|
+
return true;
|
|
102
|
+
// Simple wildcard: "shell.*" matches "shell.exec", "shell.run", etc.
|
|
103
|
+
if (pattern.includes("*")) {
|
|
104
|
+
const regex = new RegExp("^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$");
|
|
105
|
+
return regex.test(toolName);
|
|
106
|
+
}
|
|
107
|
+
// Case-insensitive match
|
|
108
|
+
if (toolName.toLowerCase() === pattern.toLowerCase())
|
|
109
|
+
return true;
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=beforeTool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"beforeTool.js","sourceRoot":"","sources":["../../src/hooks/beforeTool.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Airlock Plugin for OpenClaw — Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Registers all Airlock capabilities with the OpenClaw plugin system:
|
|
5
|
+
* - Config validation
|
|
6
|
+
* - Pairing state restoration from disk
|
|
7
|
+
* - Presence heartbeat
|
|
8
|
+
* - requestApproval tool (AI-callable)
|
|
9
|
+
* - checkStatus tool (AI-callable)
|
|
10
|
+
* - beforeTool hook (automatic enforcement with DND check)
|
|
11
|
+
* - /airlock-status command (diagnostics)
|
|
12
|
+
* - airlock setup CLI command
|
|
13
|
+
* - airlock pair CLI command
|
|
14
|
+
*/
|
|
15
|
+
interface OpenClawPluginAPI {
|
|
16
|
+
getConfig(): Record<string, unknown>;
|
|
17
|
+
registerTool(def: unknown, handler: (input: unknown) => Promise<unknown>): void;
|
|
18
|
+
registerHook(event: string, handler: (context: unknown) => Promise<unknown>, options?: {
|
|
19
|
+
name?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
}): void;
|
|
22
|
+
registerCommand(def: unknown, handler: () => Promise<string>): void;
|
|
23
|
+
registerCli(registrar: (ctx: {
|
|
24
|
+
program: unknown;
|
|
25
|
+
config: unknown;
|
|
26
|
+
workspaceDir: string;
|
|
27
|
+
logger: unknown;
|
|
28
|
+
}) => void | Promise<void>, opts?: {
|
|
29
|
+
commands?: string[];
|
|
30
|
+
}): void;
|
|
31
|
+
on(hookName: string, handler: (event: unknown, ctx?: unknown) => unknown, opts?: {
|
|
32
|
+
priority?: number;
|
|
33
|
+
}): void;
|
|
34
|
+
}
|
|
35
|
+
interface OpenClawPluginAPICompat extends Partial<OpenClawPluginAPI> {
|
|
36
|
+
pluginConfig?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
interface PluginEntryDefinition {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
description: string;
|
|
42
|
+
register(api: OpenClawPluginAPICompat): void;
|
|
43
|
+
}
|
|
44
|
+
declare const _default: PluginEntryDefinition;
|
|
45
|
+
export default _default;
|
|
46
|
+
export type { AirlockConfig } from "./config.js";
|
|
47
|
+
export type { AirlockClient, Decision, ApprovalPayload, HealthResult, ConsentResult, ConsentStatus } from "./client.js";
|
|
48
|
+
export { loadAndValidateConfig, ConfigError } from "./config.js";
|
|
49
|
+
export { createAirlockClient } from "./client.js";
|
|
50
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAcH,UAAU,iBAAiB;IACzB,SAAS,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;IAChF,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACtI,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IACpE,WAAW,CACT,SAAS,EAAE,CAAC,GAAG,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,OAAO,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EACtH,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,GAC7B,IAAI,CAAC;IACR,EAAE,CACA,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,OAAO,KAAK,OAAO,EACnD,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAC3B,IAAI,CAAC;CACT;AAED,UAAU,uBAAwB,SAAQ,OAAO,CAAC,iBAAiB,CAAC;IAClE,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,UAAU,qBAAqB;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,GAAG,EAAE,uBAAuB,GAAG,IAAI,CAAC;CAC9C;;AAaD,wBAsEG;AAGH,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,eAAe,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACxH,OAAO,EAAE,qBAAqB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC"}
|