@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 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 e2eKeyPath = join(homedir(), ".agentapprove", "e2e-key");
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 && existsSync(e2eKeyPath)) {
88
- try {
89
- const keyData = JSON.parse(readFileSync(e2eKeyPath, "utf-8"));
90
- e2eUserKey = keyData.userKey;
91
- e2eServerKey = keyData.serverKey;
92
- } catch {
93
- logger?.warn("Failed to load E2E encryption keys");
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
- const filtered = applyPrivacyFilter(request, config.privacyTier);
260
- const bodyStr = JSON.stringify(filtered);
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
- const bodyStr = JSON.stringify(event);
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 = randomBytes(12).toString("hex");
311
- function extractSessionIdFromParams(params) {
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: resolveSessionId(ctx, event.params),
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: resolveSessionId(ctx, event.params),
562
+ sessionId: gatewaySessionId,
563
+ command: extractCommand(event.toolName, event.params),
429
564
  status: event.error ? "error" : "success",
430
- error: event.error,
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: event.sessionKey || gatewaySessionId,
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 direction = event.action === "received" ? "inbound" : "outbound";
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: event.sessionKey || gatewaySessionId,
603
+ sessionId: gatewaySessionId,
460
604
  timestamp: event.timestamp.toISOString(),
461
- metadata: {
462
- direction,
463
- channelId: event.context.channelId,
464
- sessionKey: event.sessionKey
465
- }
605
+ cwd: channelLabel
466
606
  };
467
- if (config.privacyTier === "full" && event.context.content) {
468
- payload.metadata.contentPreview = event.context.content.slice(0, 100);
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
  },
@@ -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.2",
5
+ "version": "0.1.3",
6
6
  "homepage": "https://agentapprove.com",
7
7
  "configSchema": {
8
8
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentapprove/openclaw",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Agent Approve plugin for OpenClaw - approve or deny AI agent tool calls from your iPhone and Apple Watch",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {