@agentapprove/openclaw 0.1.2 → 0.1.3
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/dist/index.js +190 -42
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { fileURLToPath } from "url";
|
|
3
|
-
import { randomBytes } from "crypto";
|
|
3
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
4
4
|
|
|
5
5
|
// src/config.ts
|
|
6
6
|
import { readFileSync, existsSync, statSync } from "fs";
|
|
@@ -81,16 +81,24 @@ function loadConfig(openclawConfig, logger) {
|
|
|
81
81
|
const debug = openclawConfig?.debug || process.env.AGENTAPPROVE_DEBUG === "true" || parseConfigValue(fileContent, "AGENTAPPROVE_DEBUG") === "true" || false;
|
|
82
82
|
const agentName = process.env.AGENTAPPROVE_AGENT_NAME || parseConfigValue(fileContent, "AGENTAPPROVE_OPENCLAW_NAME") || "OpenClaw";
|
|
83
83
|
const e2eEnabled = parseConfigValue(fileContent, "AGENTAPPROVE_E2E_ENABLED") === "true";
|
|
84
|
-
const
|
|
84
|
+
const e2eUserKeyPath = join(homedir(), ".agentapprove", "e2e-key");
|
|
85
|
+
const e2eServerKeyPath = join(homedir(), ".agentapprove", "e2e-server-key");
|
|
85
86
|
let e2eUserKey;
|
|
86
87
|
let e2eServerKey;
|
|
87
|
-
if (e2eEnabled
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
if (e2eEnabled) {
|
|
89
|
+
if (existsSync(e2eUserKeyPath)) {
|
|
90
|
+
try {
|
|
91
|
+
e2eUserKey = readFileSync(e2eUserKeyPath, "utf-8").trim();
|
|
92
|
+
} catch {
|
|
93
|
+
logger?.warn("Failed to load E2E user key");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (existsSync(e2eServerKeyPath)) {
|
|
97
|
+
try {
|
|
98
|
+
e2eServerKey = readFileSync(e2eServerKeyPath, "utf-8").trim();
|
|
99
|
+
} catch {
|
|
100
|
+
logger?.warn("Failed to load E2E server key");
|
|
101
|
+
}
|
|
94
102
|
}
|
|
95
103
|
}
|
|
96
104
|
return {
|
|
@@ -166,6 +174,94 @@ function applyPrivacyFilter(request, privacyTier) {
|
|
|
166
174
|
return filtered;
|
|
167
175
|
}
|
|
168
176
|
|
|
177
|
+
// src/e2e-crypto.ts
|
|
178
|
+
import { createHash, createHmac, createCipheriv, randomBytes } from "crypto";
|
|
179
|
+
function keyId(keyHex) {
|
|
180
|
+
const keyBytes = Buffer.from(keyHex, "hex");
|
|
181
|
+
const hash = createHash("sha256").update(keyBytes).digest();
|
|
182
|
+
return hash.subarray(0, 4).toString("hex");
|
|
183
|
+
}
|
|
184
|
+
function deriveEncKey(keyHex) {
|
|
185
|
+
const prefix = Buffer.from("agentapprove-e2e-enc:");
|
|
186
|
+
const keyBytes = Buffer.from(keyHex, "hex");
|
|
187
|
+
return createHash("sha256").update(Buffer.concat([prefix, keyBytes])).digest();
|
|
188
|
+
}
|
|
189
|
+
function deriveMacKey(keyHex) {
|
|
190
|
+
const prefix = Buffer.from("agentapprove-e2e-mac:");
|
|
191
|
+
const keyBytes = Buffer.from(keyHex, "hex");
|
|
192
|
+
return createHash("sha256").update(Buffer.concat([prefix, keyBytes])).digest();
|
|
193
|
+
}
|
|
194
|
+
function e2eEncrypt(keyHex, plaintext) {
|
|
195
|
+
const kid = keyId(keyHex);
|
|
196
|
+
const encKey = deriveEncKey(keyHex);
|
|
197
|
+
const macKey = deriveMacKey(keyHex);
|
|
198
|
+
const iv = randomBytes(16);
|
|
199
|
+
const cipher = createCipheriv("aes-256-ctr", encKey, iv);
|
|
200
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
201
|
+
const ivHex = iv.toString("hex");
|
|
202
|
+
const ciphertextBase64 = ciphertext.toString("base64");
|
|
203
|
+
const hmac = createHmac("sha256", macKey).update(Buffer.concat([iv, ciphertext])).digest("hex");
|
|
204
|
+
return `E2E:v1:${kid}:${ivHex}:${ciphertextBase64}:${hmac}`;
|
|
205
|
+
}
|
|
206
|
+
function applyApprovalE2E(payload, userKey, serverKey) {
|
|
207
|
+
const sensitiveFields = {};
|
|
208
|
+
for (const field of ["command", "toolInput", "cwd"]) {
|
|
209
|
+
if (payload[field] != null) {
|
|
210
|
+
sensitiveFields[field] = payload[field];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (Object.keys(sensitiveFields).length === 0) {
|
|
214
|
+
return payload;
|
|
215
|
+
}
|
|
216
|
+
const sensitiveJson = JSON.stringify(sensitiveFields);
|
|
217
|
+
const result = { ...payload };
|
|
218
|
+
delete result.command;
|
|
219
|
+
delete result.toolInput;
|
|
220
|
+
delete result.cwd;
|
|
221
|
+
const e2e = {
|
|
222
|
+
user: e2eEncrypt(userKey, sensitiveJson)
|
|
223
|
+
};
|
|
224
|
+
if (serverKey) {
|
|
225
|
+
e2e.server = e2eEncrypt(serverKey, sensitiveJson);
|
|
226
|
+
}
|
|
227
|
+
result.e2e = e2e;
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
function applyEventE2E(payload, userKey) {
|
|
231
|
+
const contentFields = {};
|
|
232
|
+
for (const field of [
|
|
233
|
+
"command",
|
|
234
|
+
"toolInput",
|
|
235
|
+
"response",
|
|
236
|
+
"responsePreview",
|
|
237
|
+
"text",
|
|
238
|
+
"textPreview",
|
|
239
|
+
"prompt",
|
|
240
|
+
"output",
|
|
241
|
+
"cwd"
|
|
242
|
+
]) {
|
|
243
|
+
if (payload[field] != null) {
|
|
244
|
+
contentFields[field] = payload[field];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (Object.keys(contentFields).length === 0) {
|
|
248
|
+
return payload;
|
|
249
|
+
}
|
|
250
|
+
const e2ePayload = e2eEncrypt(userKey, JSON.stringify(contentFields));
|
|
251
|
+
const result = { ...payload };
|
|
252
|
+
delete result.command;
|
|
253
|
+
delete result.toolInput;
|
|
254
|
+
delete result.response;
|
|
255
|
+
delete result.responsePreview;
|
|
256
|
+
delete result.text;
|
|
257
|
+
delete result.textPreview;
|
|
258
|
+
delete result.prompt;
|
|
259
|
+
delete result.output;
|
|
260
|
+
delete result.cwd;
|
|
261
|
+
result.e2ePayload = e2ePayload;
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
|
|
169
265
|
// src/debug.ts
|
|
170
266
|
import { appendFileSync, existsSync as existsSync2, mkdirSync, statSync as statSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
171
267
|
import { join as join2, dirname } from "path";
|
|
@@ -256,8 +352,14 @@ async function sendApprovalRequest(request, config, pluginPath) {
|
|
|
256
352
|
if (!config.token) {
|
|
257
353
|
throw new Error("No Agent Approve token configured");
|
|
258
354
|
}
|
|
259
|
-
|
|
260
|
-
|
|
355
|
+
let payload;
|
|
356
|
+
if (config.e2eEnabled && config.e2eUserKey) {
|
|
357
|
+
payload = applyApprovalE2E(request, config.e2eUserKey, config.e2eServerKey);
|
|
358
|
+
debugLog("E2E encryption applied to approval request");
|
|
359
|
+
} else {
|
|
360
|
+
payload = applyPrivacyFilter(request, config.privacyTier);
|
|
361
|
+
}
|
|
362
|
+
const bodyStr = JSON.stringify(payload);
|
|
261
363
|
const pluginHash = getPluginHash(pluginPath);
|
|
262
364
|
const hmacHeaders = buildHMACHeaders(bodyStr, config.token, config.hookVersion, pluginHash);
|
|
263
365
|
const headers = {
|
|
@@ -284,7 +386,11 @@ async function sendApprovalRequest(request, config, pluginPath) {
|
|
|
284
386
|
async function sendEvent(event, config, pluginPath) {
|
|
285
387
|
if (!config.token) return;
|
|
286
388
|
try {
|
|
287
|
-
|
|
389
|
+
let payload = { ...event };
|
|
390
|
+
if (config.e2eEnabled && config.e2eUserKey) {
|
|
391
|
+
payload = applyEventE2E(payload, config.e2eUserKey);
|
|
392
|
+
}
|
|
393
|
+
const bodyStr = JSON.stringify(payload);
|
|
288
394
|
const pluginHash = getPluginHash(pluginPath);
|
|
289
395
|
const hmacHeaders = buildHMACHeaders(bodyStr, config.token, config.hookVersion, pluginHash);
|
|
290
396
|
const headers = {
|
|
@@ -307,19 +413,8 @@ try {
|
|
|
307
413
|
} catch {
|
|
308
414
|
pluginFilePath = __filename;
|
|
309
415
|
}
|
|
310
|
-
var gatewaySessionId =
|
|
311
|
-
function
|
|
312
|
-
if (!params) return void 0;
|
|
313
|
-
const raw = params.sessionId || params.session_id || params.conversationId || params.conversation_id;
|
|
314
|
-
if (typeof raw === "string" && raw.trim().length > 0) {
|
|
315
|
-
return raw.trim();
|
|
316
|
-
}
|
|
317
|
-
return void 0;
|
|
318
|
-
}
|
|
319
|
-
function resolveSessionId(ctx, params) {
|
|
320
|
-
return extractSessionIdFromParams(params) || ctx?.sessionKey || gatewaySessionId;
|
|
321
|
-
}
|
|
322
|
-
function classifyTool(toolName) {
|
|
416
|
+
var gatewaySessionId = randomBytes2(12).toString("hex");
|
|
417
|
+
function classifyTool(toolName, params) {
|
|
323
418
|
const lower = toolName.toLowerCase();
|
|
324
419
|
if (lower === "exec" || lower === "process") {
|
|
325
420
|
return { toolType: "shell_command", displayName: toolName };
|
|
@@ -334,6 +429,9 @@ function classifyTool(toolName) {
|
|
|
334
429
|
return { toolType: "browser", displayName: toolName };
|
|
335
430
|
}
|
|
336
431
|
if (lower === "message" || lower === "agent_send") {
|
|
432
|
+
const action = params?.action;
|
|
433
|
+
if (action === "send") return { toolType: "message_send", displayName: toolName };
|
|
434
|
+
if (action === "read") return { toolType: "message_read", displayName: toolName };
|
|
337
435
|
return { toolType: "message", displayName: toolName };
|
|
338
436
|
}
|
|
339
437
|
if (lower === "sessions_spawn" || lower === "sessions_send") {
|
|
@@ -358,8 +456,42 @@ function extractCommand(toolName, params) {
|
|
|
358
456
|
if (lower === "apply_patch") {
|
|
359
457
|
return "apply_patch";
|
|
360
458
|
}
|
|
459
|
+
if (lower === "message" || lower === "agent_send") {
|
|
460
|
+
const action = params.action;
|
|
461
|
+
const target = params.target || params.to || params.channelId || params.channel_id;
|
|
462
|
+
const provider = params.channel;
|
|
463
|
+
const channelLabel = target ? provider ? `${provider}:${target}` : target : provider || "channel";
|
|
464
|
+
if (action === "send") {
|
|
465
|
+
const msg = params.message;
|
|
466
|
+
return msg ? `[${channelLabel}] ${msg}` : `Send to ${channelLabel}`;
|
|
467
|
+
}
|
|
468
|
+
if (action === "read") {
|
|
469
|
+
return `Read from ${channelLabel}`;
|
|
470
|
+
}
|
|
471
|
+
return void 0;
|
|
472
|
+
}
|
|
361
473
|
return void 0;
|
|
362
474
|
}
|
|
475
|
+
function extractResultPreview(toolName, params, result, maxLen = 300) {
|
|
476
|
+
if (!result) return void 0;
|
|
477
|
+
const lower = toolName.toLowerCase();
|
|
478
|
+
if (lower === "message" && params.action === "read") {
|
|
479
|
+
const res = result;
|
|
480
|
+
const messages = res.messages;
|
|
481
|
+
if (Array.isArray(messages) && messages.length > 0) {
|
|
482
|
+
const previews = messages.slice(0, 5).map((m) => {
|
|
483
|
+
const author = m.user || "?";
|
|
484
|
+
const text = m.text || "";
|
|
485
|
+
return `${author}: ${text}`;
|
|
486
|
+
});
|
|
487
|
+
const summary = previews.join("\n");
|
|
488
|
+
return summary.length > maxLen ? summary.slice(0, maxLen) + "..." : summary;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const str = typeof result === "string" ? result : JSON.stringify(result);
|
|
492
|
+
if (str.length <= maxLen) return str;
|
|
493
|
+
return str.slice(0, maxLen) + "...";
|
|
494
|
+
}
|
|
363
495
|
function handleFailBehavior(config, error, toolName, logger) {
|
|
364
496
|
logger.warn(`Agent Approve API error for tool "${toolName}": ${error.message}`);
|
|
365
497
|
debugLog(`API error: ${error.message}, failBehavior: ${config.failBehavior}`);
|
|
@@ -385,7 +517,7 @@ function register(api) {
|
|
|
385
517
|
api.logger.info(`Agent Approve: Plugin loaded (privacy: ${config.privacyTier}, fail: ${config.failBehavior})`);
|
|
386
518
|
debugLog(`Plugin loaded, API: ${config.apiUrl}, agent: ${config.agentName}`);
|
|
387
519
|
api.on("before_tool_call", async (event, ctx) => {
|
|
388
|
-
const { toolType, displayName } = classifyTool(event.toolName);
|
|
520
|
+
const { toolType, displayName } = classifyTool(event.toolName, event.params);
|
|
389
521
|
const command = extractCommand(event.toolName, event.params);
|
|
390
522
|
const request = {
|
|
391
523
|
toolName: displayName,
|
|
@@ -394,7 +526,7 @@ function register(api) {
|
|
|
394
526
|
toolInput: event.params,
|
|
395
527
|
agent: config.agentName,
|
|
396
528
|
hookType: "before_tool_call",
|
|
397
|
-
sessionId:
|
|
529
|
+
sessionId: gatewaySessionId,
|
|
398
530
|
cwd: event.params.workdir || void 0,
|
|
399
531
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
400
532
|
};
|
|
@@ -419,15 +551,18 @@ function register(api) {
|
|
|
419
551
|
}
|
|
420
552
|
});
|
|
421
553
|
api.on("after_tool_call", async (event, ctx) => {
|
|
422
|
-
const { toolType } = classifyTool(event.toolName);
|
|
554
|
+
const { toolType } = classifyTool(event.toolName, event.params);
|
|
555
|
+
const resultPreview = extractResultPreview(event.toolName, event.params, event.result);
|
|
423
556
|
void sendEvent({
|
|
424
557
|
toolName: event.toolName,
|
|
425
558
|
toolType,
|
|
559
|
+
eventType: "tool_complete",
|
|
426
560
|
agent: config.agentName,
|
|
427
561
|
hookType: "after_tool_call",
|
|
428
|
-
sessionId:
|
|
562
|
+
sessionId: gatewaySessionId,
|
|
563
|
+
command: extractCommand(event.toolName, event.params),
|
|
429
564
|
status: event.error ? "error" : "success",
|
|
430
|
-
|
|
565
|
+
response: event.error || resultPreview || void 0,
|
|
431
566
|
durationMs: event.durationMs,
|
|
432
567
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
433
568
|
}, config, pluginFilePath);
|
|
@@ -435,14 +570,19 @@ function register(api) {
|
|
|
435
570
|
api.registerHook(
|
|
436
571
|
["command:new", "command:stop", "command:reset"],
|
|
437
572
|
async (event) => {
|
|
573
|
+
const eventTypeMap = {
|
|
574
|
+
new: "session_start",
|
|
575
|
+
stop: "session_end",
|
|
576
|
+
reset: "session_start"
|
|
577
|
+
};
|
|
438
578
|
void sendEvent({
|
|
439
579
|
toolName: `command:${event.action}`,
|
|
440
580
|
toolType: "command",
|
|
581
|
+
eventType: eventTypeMap[event.action] || "command_event",
|
|
441
582
|
agent: config.agentName,
|
|
442
583
|
hookType: "command_event",
|
|
443
|
-
sessionId:
|
|
444
|
-
timestamp: event.timestamp.toISOString()
|
|
445
|
-
metadata: { sessionKey: event.sessionKey, action: event.action }
|
|
584
|
+
sessionId: gatewaySessionId,
|
|
585
|
+
timestamp: event.timestamp.toISOString()
|
|
446
586
|
}, config, pluginFilePath);
|
|
447
587
|
},
|
|
448
588
|
{ name: "agentapprove-command-monitor", description: "Log command events to Agent Approve" }
|
|
@@ -450,22 +590,30 @@ function register(api) {
|
|
|
450
590
|
api.registerHook(
|
|
451
591
|
["message:received", "message:sent"],
|
|
452
592
|
async (event) => {
|
|
453
|
-
const
|
|
593
|
+
const isInbound = event.action === "received";
|
|
594
|
+
const provider = event.context.channelId;
|
|
595
|
+
const peer = isInbound ? event.context.from : event.context.to;
|
|
596
|
+
const channelLabel = [provider, peer].filter(Boolean).join(":") || void 0;
|
|
454
597
|
const payload = {
|
|
455
598
|
toolName: `message:${event.action}`,
|
|
456
599
|
toolType: "message_event",
|
|
600
|
+
eventType: isInbound ? "prompt_submitted" : "response",
|
|
457
601
|
agent: config.agentName,
|
|
458
602
|
hookType: "session_event",
|
|
459
|
-
sessionId:
|
|
603
|
+
sessionId: gatewaySessionId,
|
|
460
604
|
timestamp: event.timestamp.toISOString(),
|
|
461
|
-
|
|
462
|
-
direction,
|
|
463
|
-
channelId: event.context.channelId,
|
|
464
|
-
sessionKey: event.sessionKey
|
|
465
|
-
}
|
|
605
|
+
cwd: channelLabel
|
|
466
606
|
};
|
|
467
|
-
|
|
468
|
-
|
|
607
|
+
const content = event.context.content;
|
|
608
|
+
if (content) {
|
|
609
|
+
if (config.privacyTier === "full") {
|
|
610
|
+
payload.textPreview = content.slice(0, 200);
|
|
611
|
+
} else if (config.privacyTier === "summary") {
|
|
612
|
+
payload.textPreview = content.slice(0, 50);
|
|
613
|
+
}
|
|
614
|
+
if (isInbound && peer) {
|
|
615
|
+
payload.prompt = `${peer}: ${content}`.slice(0, 200);
|
|
616
|
+
}
|
|
469
617
|
}
|
|
470
618
|
void sendEvent(payload, config, pluginFilePath);
|
|
471
619
|
},
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "openclaw",
|
|
3
3
|
"name": "Agent Approve",
|
|
4
4
|
"description": "Mobile approval for AI agent tool execution. Approve or deny tool calls from your iPhone and Apple Watch.",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.3",
|
|
6
6
|
"homepage": "https://agentapprove.com",
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED