@agentapprove/openclaw 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2026 Agent Approve LLC. All rights reserved.
2
+
3
+ This software is proprietary and confidential. Unauthorized copying,
4
+ modification, distribution, or use of this software, via any medium,
5
+ is strictly prohibited without the express written permission of
6
+ Agent Approve LLC.
7
+
8
+ Use of this software is subject to the Agent Approve Terms of Service:
9
+ https://www.agentapprove.com/terms
10
+
11
+ Privacy Policy: https://www.agentapprove.com/privacy
12
+
13
+ For licensing inquiries, contact: hello@agentapprove.com
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # @agentapprove/openclaw
2
+
3
+ [Agent Approve](https://agentapprove.com) plugin for [OpenClaw](https://openclaw.ai). Approve or deny AI agent tool calls from your iPhone and Apple Watch.
4
+
5
+ ## Why Agent Approve?
6
+
7
+ - **Visibility** -- see what your OpenClaw agent is doing in real time from your mobile device
8
+ - **Centralized policy** -- define your default stance (allow, deny, or ask) and manage custom allowlists and denylists for tools, parameters, and commands
9
+ - **Mobile approval** -- when human approval is required, review and respond from your iPhone or Apple Watch with a tap
10
+
11
+ You decide the rules. Agent Approve enforces them.
12
+
13
+ ## How it works
14
+
15
+ This plugin hooks into OpenClaw's agent loop to enforce your approval policy:
16
+
17
+ - **before_tool_call** -- evaluates each tool call against your policy. Auto-allows or auto-denies based on your lists, or sends an approval request to your device when manual review is needed.
18
+ - **after_tool_call** -- logs tool completion to your activity feed
19
+ - **command/message events** -- monitors session activity
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ openclaw plugins install @agentapprove/openclaw
25
+ ```
26
+
27
+ ## Setup
28
+
29
+ 1. **Download Agent Approve** from the [App Store](https://agentapprove.com) and start your 7-day free trial
30
+ 2. **Follow the in-app onboarding** which will prompt you to run the installer on each machine where you have agents:
31
+
32
+ ```bash
33
+ npx agentapprove
34
+ ```
35
+
36
+ The installer handles pairing, token setup, and plugin activation. Select OpenClaw when prompted for which agents to configure.
37
+
38
+ 3. **Restart the OpenClaw gateway** to load the plugin
39
+
40
+ That's it. Tool calls will now be evaluated against your policy, and approval requests will appear on your paired device when needed.
41
+
42
+ ## Configuration
43
+
44
+ Plugin settings in `~/.openclaw/openclaw.json`:
45
+
46
+ ```json
47
+ {
48
+ "plugins": {
49
+ "entries": {
50
+ "agentapprove": {
51
+ "enabled": true,
52
+ "config": {
53
+ "timeout": 300,
54
+ "failBehavior": "ask",
55
+ "privacyTier": "full"
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ | Setting | Default | Description |
64
+ |---------|---------|-------------|
65
+ | `timeout` | `300` | Seconds to wait for an approval response |
66
+ | `failBehavior` | `ask` | Default policy when API is unreachable: `allow`, `deny`, or `ask` |
67
+ | `privacyTier` | `full` | What tool data is stored in event logs: `minimal`, `summary`, or `full` |
68
+ | `debug` | `false` | Write debug logs to `~/.agentapprove/hook-debug.log` |
69
+
70
+ ## Supported agents
71
+
72
+ Agent Approve works with OpenClaw, Claude Code, Cursor, Gemini CLI, VS Code Agent, Copilot CLI, OpenAI Codex (coming soon), and more. Run `npx agentapprove` to configure multiple agents at once.
73
+
74
+ ## Links
75
+
76
+ - [Agent Approve](https://agentapprove.com)
77
+ - [OpenClaw](https://openclaw.ai)
package/dist/index.js ADDED
@@ -0,0 +1,468 @@
1
+ // src/index.ts
2
+ import { fileURLToPath } from "url";
3
+
4
+ // src/config.ts
5
+ import { readFileSync, existsSync, statSync } from "fs";
6
+ import { join } from "path";
7
+ import { homedir } from "os";
8
+ import { execSync } from "child_process";
9
+ var CONFIG_PATH = join(homedir(), ".agentapprove", "env");
10
+ var KEYCHAIN_SERVICE = "com.agentapprove";
11
+ var KEYCHAIN_ACCOUNT = "api-token";
12
+ function parseConfigValue(content, key) {
13
+ const lines = content.split("\n");
14
+ let value;
15
+ for (const raw of lines) {
16
+ const line = raw.replace(/^export\s+/, "").trim();
17
+ if (!line.startsWith(`${key}=`)) continue;
18
+ let v = line.slice(key.length + 1);
19
+ if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
20
+ v = v.slice(1, -1);
21
+ }
22
+ if (/[`$(){};<>|&!\\]/.test(v)) continue;
23
+ value = v;
24
+ }
25
+ return value;
26
+ }
27
+ function normalizeApiUrl(url) {
28
+ let result = url.trim();
29
+ while (result.endsWith("/")) {
30
+ result = result.slice(0, -1);
31
+ }
32
+ return result;
33
+ }
34
+ function normalizeApiVersion(version) {
35
+ const cleaned = version.trim().replace(/^\/+/, "").replace(/\/+$/, "");
36
+ return cleaned || "v001";
37
+ }
38
+ function getKeychainToken() {
39
+ if (process.platform !== "darwin") return void 0;
40
+ try {
41
+ const result = execSync(
42
+ `security find-generic-password -s "${KEYCHAIN_SERVICE}" -a "${KEYCHAIN_ACCOUNT}" -w 2>/dev/null`,
43
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
44
+ ).trim();
45
+ return result || void 0;
46
+ } catch {
47
+ return void 0;
48
+ }
49
+ }
50
+ function checkPermissions(logger) {
51
+ if (!existsSync(CONFIG_PATH)) return;
52
+ try {
53
+ const stat = statSync(CONFIG_PATH);
54
+ const mode = stat.mode & 511;
55
+ if (mode & 54) {
56
+ logger?.warn(
57
+ `Config file ${CONFIG_PATH} is group or world readable/writable. Run: chmod 600 ${CONFIG_PATH}`
58
+ );
59
+ }
60
+ } catch {
61
+ }
62
+ }
63
+ function loadConfig(openclawConfig, logger) {
64
+ checkPermissions(logger);
65
+ let fileContent = "";
66
+ if (existsSync(CONFIG_PATH)) {
67
+ fileContent = readFileSync(CONFIG_PATH, "utf-8");
68
+ }
69
+ let token = process.env.AGENTAPPROVE_TOKEN || getKeychainToken() || parseConfigValue(fileContent, "AGENTAPPROVE_TOKEN") || "";
70
+ if (!token) {
71
+ logger?.warn("No Agent Approve token found. Run the Agent Approve installer to set up.");
72
+ }
73
+ const rawApiUrl = openclawConfig?.apiUrl || process.env.AGENTAPPROVE_API || parseConfigValue(fileContent, "AGENTAPPROVE_API") || "https://api.agentapprove.com";
74
+ const rawApiVersion = process.env.AGENTAPPROVE_API_VERSION || parseConfigValue(fileContent, "AGENTAPPROVE_API_VERSION") || "v001";
75
+ const apiUrl = normalizeApiUrl(rawApiUrl);
76
+ const apiVersion = normalizeApiVersion(rawApiVersion);
77
+ const timeout = openclawConfig?.timeout || parseInt(process.env.AGENTAPPROVE_TIMEOUT || "", 10) || parseInt(parseConfigValue(fileContent, "AGENTAPPROVE_TIMEOUT") || "", 10) || 300;
78
+ const failBehavior = openclawConfig?.failBehavior || process.env.AGENTAPPROVE_FAIL_BEHAVIOR || parseConfigValue(fileContent, "AGENTAPPROVE_FAIL_BEHAVIOR") || "ask";
79
+ const privacyTier = openclawConfig?.privacyTier || process.env.AGENTAPPROVE_PRIVACY || parseConfigValue(fileContent, "AGENTAPPROVE_PRIVACY") || "full";
80
+ const debug = openclawConfig?.debug || process.env.AGENTAPPROVE_DEBUG === "true" || parseConfigValue(fileContent, "AGENTAPPROVE_DEBUG") === "true" || false;
81
+ const agentName = process.env.AGENTAPPROVE_AGENT_NAME || parseConfigValue(fileContent, "AGENTAPPROVE_OPENCLAW_NAME") || "OpenClaw";
82
+ const e2eEnabled = parseConfigValue(fileContent, "AGENTAPPROVE_E2E_ENABLED") === "true";
83
+ const e2eKeyPath = join(homedir(), ".agentapprove", "e2e-key");
84
+ let e2eUserKey;
85
+ let e2eServerKey;
86
+ if (e2eEnabled && existsSync(e2eKeyPath)) {
87
+ try {
88
+ const keyData = JSON.parse(readFileSync(e2eKeyPath, "utf-8"));
89
+ e2eUserKey = keyData.userKey;
90
+ e2eServerKey = keyData.serverKey;
91
+ } catch {
92
+ logger?.warn("Failed to load E2E encryption keys");
93
+ }
94
+ }
95
+ return {
96
+ apiUrl,
97
+ apiVersion,
98
+ token,
99
+ timeout,
100
+ failBehavior,
101
+ privacyTier,
102
+ debug,
103
+ hookVersion: "1.1.0",
104
+ agentName,
105
+ e2eEnabled,
106
+ e2eUserKey,
107
+ e2eServerKey
108
+ };
109
+ }
110
+
111
+ // src/api-client.ts
112
+ import { request as httpsRequest } from "https";
113
+ import { request as httpRequest } from "http";
114
+ import { URL } from "url";
115
+
116
+ // src/hmac.ts
117
+ import crypto from "crypto";
118
+ import { readFileSync as readFileSync2 } from "fs";
119
+ function generateHMACSignature(body, token, hookVersion) {
120
+ const timestamp = Math.floor(Date.now() / 1e3);
121
+ const message = `${hookVersion}:${timestamp}:${body}`;
122
+ const signature = crypto.createHmac("sha256", token).update(message).digest("hex");
123
+ return { timestamp, signature };
124
+ }
125
+ function computePluginHash(pluginPath) {
126
+ try {
127
+ const content = readFileSync2(pluginPath, "utf-8");
128
+ return crypto.createHash("sha256").update(content).digest("hex");
129
+ } catch {
130
+ return "";
131
+ }
132
+ }
133
+ function buildHMACHeaders(body, token, hookVersion, pluginHash) {
134
+ const { timestamp, signature } = generateHMACSignature(body, token, hookVersion);
135
+ return {
136
+ "X-Hook-Version": hookVersion,
137
+ "X-Hook-Timestamp": String(timestamp),
138
+ "X-Hook-Signature": signature,
139
+ "X-Hook-Hash": pluginHash
140
+ };
141
+ }
142
+
143
+ // src/privacy.ts
144
+ var SUMMARY_MAX_LENGTH = 50;
145
+ function applyPrivacyFilter(request, privacyTier) {
146
+ if (privacyTier === "full") {
147
+ return request;
148
+ }
149
+ const filtered = { ...request };
150
+ if (privacyTier === "minimal") {
151
+ delete filtered.command;
152
+ filtered.toolInput = void 0;
153
+ delete filtered.cwd;
154
+ return filtered;
155
+ }
156
+ if (filtered.command && filtered.command.length > SUMMARY_MAX_LENGTH) {
157
+ filtered.command = filtered.command.slice(0, SUMMARY_MAX_LENGTH) + "...";
158
+ }
159
+ if (filtered.toolInput) {
160
+ const inputStr = JSON.stringify(filtered.toolInput);
161
+ if (inputStr.length > SUMMARY_MAX_LENGTH) {
162
+ filtered.toolInput = { _summary: inputStr.slice(0, SUMMARY_MAX_LENGTH) + "..." };
163
+ }
164
+ }
165
+ return filtered;
166
+ }
167
+
168
+ // src/debug.ts
169
+ import { appendFileSync, existsSync as existsSync2, mkdirSync, statSync as statSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
170
+ import { join as join2, dirname } from "path";
171
+ import { homedir as homedir2 } from "os";
172
+ var DEBUG_LOG_PATH = join2(homedir2(), ".agentapprove", "hook-debug.log");
173
+ var MAX_SIZE = 5 * 1024 * 1024;
174
+ var KEEP_SIZE = 2 * 1024 * 1024;
175
+ function ensureLogFile() {
176
+ const dir = dirname(DEBUG_LOG_PATH);
177
+ if (!existsSync2(dir)) {
178
+ mkdirSync(dir, { recursive: true });
179
+ }
180
+ if (!existsSync2(DEBUG_LOG_PATH)) {
181
+ writeFileSync(DEBUG_LOG_PATH, "", { mode: 384 });
182
+ return;
183
+ }
184
+ try {
185
+ const stat = statSync2(DEBUG_LOG_PATH);
186
+ if (stat.size > MAX_SIZE) {
187
+ const content = readFileSync3(DEBUG_LOG_PATH, "utf-8");
188
+ writeFileSync(DEBUG_LOG_PATH, content.slice(-KEEP_SIZE), { mode: 384 });
189
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
190
+ appendFileSync(DEBUG_LOG_PATH, `[${ts}] [openclaw-plugin] Log rotated (exceeded 5MB)
191
+ `);
192
+ }
193
+ } catch {
194
+ }
195
+ }
196
+ function debugLog(message, hookName = "openclaw-plugin") {
197
+ try {
198
+ ensureLogFile();
199
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
200
+ appendFileSync(DEBUG_LOG_PATH, `[${ts}] [${hookName}] ${message}
201
+ `);
202
+ } catch {
203
+ }
204
+ }
205
+
206
+ // src/api-client.ts
207
+ var cachedPluginHash;
208
+ function getPluginHash(pluginPath) {
209
+ if (!cachedPluginHash) {
210
+ cachedPluginHash = computePluginHash(pluginPath);
211
+ if (cachedPluginHash) {
212
+ debugLog(`Plugin hash computed: ${cachedPluginHash.slice(0, 16)}...`);
213
+ }
214
+ }
215
+ return cachedPluginHash || "";
216
+ }
217
+ function httpPost(url, body, headers, timeoutMs) {
218
+ return new Promise((resolve, reject) => {
219
+ const parsed = new URL(url);
220
+ const isHttps = parsed.protocol === "https:";
221
+ const reqFn = isHttps ? httpsRequest : httpRequest;
222
+ const req = reqFn(
223
+ {
224
+ hostname: parsed.hostname,
225
+ port: parsed.port || (isHttps ? 443 : 80),
226
+ path: parsed.pathname + parsed.search,
227
+ method: "POST",
228
+ headers: {
229
+ "Content-Type": "application/json",
230
+ "Content-Length": Buffer.byteLength(body),
231
+ ...headers
232
+ },
233
+ timeout: timeoutMs
234
+ },
235
+ (res) => {
236
+ let data = "";
237
+ res.on("data", (chunk) => {
238
+ data += chunk;
239
+ });
240
+ res.on("end", () => {
241
+ resolve({ status: res.statusCode || 0, body: data });
242
+ });
243
+ }
244
+ );
245
+ req.on("error", reject);
246
+ req.on("timeout", () => {
247
+ req.destroy();
248
+ reject(new Error(`Request timed out after ${timeoutMs}ms`));
249
+ });
250
+ req.write(body);
251
+ req.end();
252
+ });
253
+ }
254
+ async function sendApprovalRequest(request, config, pluginPath) {
255
+ if (!config.token) {
256
+ throw new Error("No Agent Approve token configured");
257
+ }
258
+ const filtered = applyPrivacyFilter(request, config.privacyTier);
259
+ const bodyStr = JSON.stringify(filtered);
260
+ const pluginHash = getPluginHash(pluginPath);
261
+ const hmacHeaders = buildHMACHeaders(bodyStr, config.token, config.hookVersion, pluginHash);
262
+ const headers = {
263
+ "Authorization": `Bearer ${config.token}`,
264
+ ...hmacHeaders
265
+ };
266
+ const url = `${config.apiUrl}/${config.apiVersion}/approve`;
267
+ if (config.debug) {
268
+ debugLog(`Sending approval request to ${url} for tool: ${request.toolName}`);
269
+ }
270
+ const response = await httpPost(url, bodyStr, headers, config.timeout * 1e3);
271
+ if (config.debug) {
272
+ debugLog(`Response status: ${response.status}, body: ${response.body.slice(0, 200)}`);
273
+ }
274
+ if (response.status !== 200) {
275
+ throw new Error(`API returned status ${response.status}: ${response.body.slice(0, 200)}`);
276
+ }
277
+ try {
278
+ return JSON.parse(response.body);
279
+ } catch {
280
+ throw new Error(`Failed to parse API response: ${response.body.slice(0, 200)}`);
281
+ }
282
+ }
283
+ async function sendEvent(event, config, pluginPath) {
284
+ if (!config.token) return;
285
+ try {
286
+ const bodyStr = JSON.stringify(event);
287
+ const pluginHash = getPluginHash(pluginPath);
288
+ const hmacHeaders = buildHMACHeaders(bodyStr, config.token, config.hookVersion, pluginHash);
289
+ const headers = {
290
+ "Authorization": `Bearer ${config.token}`,
291
+ ...hmacHeaders
292
+ };
293
+ const url = `${config.apiUrl}/${config.apiVersion}/events`;
294
+ await httpPost(url, bodyStr, headers, 5e3);
295
+ } catch {
296
+ if (config.debug) {
297
+ debugLog(`Failed to send event: ${JSON.stringify(event).slice(0, 100)}`);
298
+ }
299
+ }
300
+ }
301
+
302
+ // src/index.ts
303
+ var pluginFilePath;
304
+ try {
305
+ pluginFilePath = fileURLToPath(import.meta.url);
306
+ } catch {
307
+ pluginFilePath = __filename;
308
+ }
309
+ function classifyTool(toolName) {
310
+ const lower = toolName.toLowerCase();
311
+ if (lower === "exec" || lower === "process") {
312
+ return { toolType: "shell_command", displayName: toolName };
313
+ }
314
+ if (lower === "write" || lower === "edit" || lower === "apply_patch") {
315
+ return { toolType: "file_write", displayName: toolName };
316
+ }
317
+ if (lower === "read") {
318
+ return { toolType: "file_read", displayName: toolName };
319
+ }
320
+ if (lower === "browser" || lower.startsWith("browser_")) {
321
+ return { toolType: "browser", displayName: toolName };
322
+ }
323
+ if (lower === "message" || lower === "agent_send") {
324
+ return { toolType: "message", displayName: toolName };
325
+ }
326
+ if (lower === "sessions_spawn" || lower === "sessions_send") {
327
+ return { toolType: "session", displayName: toolName };
328
+ }
329
+ if (lower === "llm_task") {
330
+ return { toolType: "llm", displayName: toolName };
331
+ }
332
+ return { toolType: "tool_use", displayName: toolName };
333
+ }
334
+ function extractCommand(toolName, params) {
335
+ const lower = toolName.toLowerCase();
336
+ if (lower === "exec" || lower === "process") {
337
+ return params.command || void 0;
338
+ }
339
+ if (lower === "write" || lower === "edit") {
340
+ return params.file_path || params.path || void 0;
341
+ }
342
+ if (lower === "read") {
343
+ return params.file_path || params.path || void 0;
344
+ }
345
+ if (lower === "apply_patch") {
346
+ return "apply_patch";
347
+ }
348
+ return void 0;
349
+ }
350
+ function handleFailBehavior(config, error, toolName, logger) {
351
+ logger.warn(`Agent Approve API error for tool "${toolName}": ${error.message}`);
352
+ debugLog(`API error: ${error.message}, failBehavior: ${config.failBehavior}`);
353
+ switch (config.failBehavior) {
354
+ case "deny":
355
+ return { block: true, blockReason: "Agent Approve unavailable, denying by policy" };
356
+ case "allow":
357
+ return void 0;
358
+ // Don't block
359
+ case "ask":
360
+ default:
361
+ return void 0;
362
+ }
363
+ }
364
+ function register(api) {
365
+ const config = loadConfig(api.pluginConfig, api.logger);
366
+ if (!config.token) {
367
+ api.logger.warn(
368
+ "Agent Approve: No token found. Run the Agent Approve installer to pair with your account."
369
+ );
370
+ return;
371
+ }
372
+ api.logger.info(`Agent Approve: Plugin loaded (privacy: ${config.privacyTier}, fail: ${config.failBehavior})`);
373
+ debugLog(`Plugin loaded, API: ${config.apiUrl}, agent: ${config.agentName}`);
374
+ api.on("before_tool_call", async (event, ctx) => {
375
+ const { toolType, displayName } = classifyTool(event.toolName);
376
+ const command = extractCommand(event.toolName, event.params);
377
+ const request = {
378
+ toolName: displayName,
379
+ toolType,
380
+ command,
381
+ toolInput: event.params,
382
+ agent: config.agentName,
383
+ hookType: "before_tool_call",
384
+ cwd: event.params.workdir || void 0,
385
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
386
+ };
387
+ try {
388
+ const response = await sendApprovalRequest(request, config, pluginFilePath);
389
+ if (response.decision === "approve") {
390
+ debugLog(`Tool "${event.toolName}" approved${response.reason ? ": " + response.reason : ""}`);
391
+ return void 0;
392
+ }
393
+ debugLog(`Tool "${event.toolName}" denied${response.reason ? ": " + response.reason : ""}`);
394
+ return {
395
+ block: true,
396
+ blockReason: response.reason || "Denied by Agent Approve"
397
+ };
398
+ } catch (error) {
399
+ return handleFailBehavior(
400
+ config,
401
+ error instanceof Error ? error : new Error(String(error)),
402
+ event.toolName,
403
+ api.logger
404
+ );
405
+ }
406
+ });
407
+ api.on("after_tool_call", async (event, _ctx) => {
408
+ const { toolType } = classifyTool(event.toolName);
409
+ void sendEvent({
410
+ toolName: event.toolName,
411
+ toolType,
412
+ agent: config.agentName,
413
+ hookType: "after_tool_call",
414
+ status: event.error ? "error" : "success",
415
+ error: event.error,
416
+ durationMs: event.durationMs,
417
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
418
+ }, config, pluginFilePath);
419
+ });
420
+ api.registerHook(
421
+ ["command:new", "command:stop", "command:reset"],
422
+ async (event) => {
423
+ void sendEvent({
424
+ toolName: `command:${event.action}`,
425
+ toolType: "command",
426
+ agent: config.agentName,
427
+ hookType: "command_event",
428
+ timestamp: event.timestamp.toISOString(),
429
+ metadata: { sessionKey: event.sessionKey, action: event.action }
430
+ }, config, pluginFilePath);
431
+ },
432
+ { name: "agentapprove-command-monitor", description: "Log command events to Agent Approve" }
433
+ );
434
+ api.registerHook(
435
+ ["message:received", "message:sent"],
436
+ async (event) => {
437
+ const direction = event.action === "received" ? "inbound" : "outbound";
438
+ const payload = {
439
+ toolName: `message:${event.action}`,
440
+ toolType: "message_event",
441
+ agent: config.agentName,
442
+ hookType: "session_event",
443
+ timestamp: event.timestamp.toISOString(),
444
+ metadata: {
445
+ direction,
446
+ channelId: event.context.channelId,
447
+ sessionKey: event.sessionKey
448
+ }
449
+ };
450
+ if (config.privacyTier === "full" && event.context.content) {
451
+ payload.metadata.contentPreview = event.context.content.slice(0, 100);
452
+ }
453
+ void sendEvent(payload, config, pluginFilePath);
454
+ },
455
+ { name: "agentapprove-session-monitor", description: "Log message events to Agent Approve" }
456
+ );
457
+ api.logger.info("Agent Approve: Registered before_tool_call, after_tool_call, and event monitoring hooks");
458
+ }
459
+ var index_default = {
460
+ id: "agentapprove",
461
+ name: "Agent Approve",
462
+ description: "Mobile approval for AI agent tool execution",
463
+ register
464
+ };
465
+ export {
466
+ index_default as default,
467
+ register
468
+ };
@@ -0,0 +1,49 @@
1
+ {
2
+ "id": "agentapprove",
3
+ "name": "Agent Approve",
4
+ "description": "Mobile approval for AI agent tool execution. Approve or deny tool calls from your iPhone and Apple Watch.",
5
+ "version": "0.1.0",
6
+ "homepage": "https://agentapprove.com",
7
+ "configSchema": {
8
+ "type": "object",
9
+ "additionalProperties": false,
10
+ "properties": {
11
+ "apiUrl": {
12
+ "type": "string",
13
+ "description": "Agent Approve API URL",
14
+ "default": "https://api.agentapprove.com"
15
+ },
16
+ "timeout": {
17
+ "type": "number",
18
+ "description": "Max seconds to wait for approval response",
19
+ "default": 300,
20
+ "minimum": 10,
21
+ "maximum": 600
22
+ },
23
+ "failBehavior": {
24
+ "type": "string",
25
+ "description": "Action when API is unreachable",
26
+ "enum": ["allow", "deny", "ask"],
27
+ "default": "ask"
28
+ },
29
+ "privacyTier": {
30
+ "type": "string",
31
+ "description": "What tool data to send to the API",
32
+ "enum": ["minimal", "summary", "full"],
33
+ "default": "full"
34
+ },
35
+ "debug": {
36
+ "type": "boolean",
37
+ "description": "Enable debug logging to ~/.agentapprove/hook-debug.log",
38
+ "default": false
39
+ }
40
+ }
41
+ },
42
+ "uiHints": {
43
+ "apiUrl": { "label": "API URL", "placeholder": "https://api.agentapprove.com" },
44
+ "timeout": { "label": "Approval Timeout (seconds)" },
45
+ "failBehavior": { "label": "On API Error" },
46
+ "privacyTier": { "label": "Privacy Tier" },
47
+ "debug": { "label": "Debug Logging" }
48
+ }
49
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@agentapprove/openclaw",
3
+ "version": "0.1.0",
4
+ "description": "Agent Approve plugin for OpenClaw - approve or deny AI agent tool calls from your iPhone and Apple Watch",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --format=esm --external:crypto --external:fs --external:path --external:os --external:https --external:http --external:url --external:child_process",
8
+ "hash": "shasum -a 256 dist/index.js | cut -d' ' -f1",
9
+ "dev": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --format=esm --external:crypto --external:fs --external:path --external:os --external:https --external:http --external:url --external:child_process --watch"
10
+ },
11
+ "openclaw": {
12
+ "extensions": ["./dist/index.js"]
13
+ },
14
+ "files": [
15
+ "dist/",
16
+ "openclaw.plugin.json",
17
+ "README.md"
18
+ ],
19
+ "keywords": [
20
+ "openclaw",
21
+ "agentapprove",
22
+ "agent-approve",
23
+ "approval",
24
+ "governance",
25
+ "ai-safety",
26
+ "iphone",
27
+ "apple-watch",
28
+ "tool-approval"
29
+ ],
30
+ "author": "Agent Approve LLC <hello@agentapprove.com> (https://agentapprove.com)",
31
+ "license": "SEE LICENSE IN LICENSE",
32
+ "homepage": "https://www.agentapprove.com",
33
+ "bugs": {
34
+ "url": "https://github.com/agentapprove/support/issues"
35
+ },
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "devDependencies": {
40
+ "esbuild": "^0.24.0",
41
+ "typescript": "^5.0.0"
42
+ }
43
+ }