@agentapprove/openclaw 0.1.3 → 0.1.4

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,13 +1,228 @@
1
1
  // src/index.ts
2
2
  import { fileURLToPath } from "url";
3
- import { randomBytes as randomBytes2 } from "crypto";
3
+ import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
4
4
 
5
5
  // src/config.ts
6
- import { readFileSync, existsSync, statSync } from "fs";
7
- import { join } from "path";
8
- import { homedir } from "os";
6
+ import { readFileSync as readFileSync3, existsSync as existsSync3, statSync as statSync2 } from "fs";
7
+ import { join as join3 } from "path";
8
+ import { homedir as homedir3 } from "os";
9
9
  import { execSync } from "child_process";
10
- var CONFIG_PATH = join(homedir(), ".agentapprove", "env");
10
+
11
+ // src/e2e-crypto.ts
12
+ import { createHash, createHmac, createCipheriv, randomBytes } from "crypto";
13
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, copyFileSync } from "fs";
14
+ import { join as join2 } from "path";
15
+ import { homedir as homedir2 } from "os";
16
+
17
+ // src/debug.ts
18
+ import { appendFileSync, existsSync, mkdirSync, statSync, readFileSync, writeFileSync } from "fs";
19
+ import { join, dirname } from "path";
20
+ import { homedir } from "os";
21
+ var DEBUG_LOG_PATH = join(homedir(), ".agentapprove", "hook-debug.log");
22
+ var MAX_SIZE = 5 * 1024 * 1024;
23
+ var KEEP_SIZE = 2 * 1024 * 1024;
24
+ function ensureLogFile() {
25
+ const dir = dirname(DEBUG_LOG_PATH);
26
+ if (!existsSync(dir)) {
27
+ mkdirSync(dir, { recursive: true });
28
+ }
29
+ if (!existsSync(DEBUG_LOG_PATH)) {
30
+ writeFileSync(DEBUG_LOG_PATH, "", { mode: 384 });
31
+ return;
32
+ }
33
+ try {
34
+ const stat = statSync(DEBUG_LOG_PATH);
35
+ if (stat.size > MAX_SIZE) {
36
+ const content = readFileSync(DEBUG_LOG_PATH, "utf-8");
37
+ writeFileSync(DEBUG_LOG_PATH, content.slice(-KEEP_SIZE), { mode: 384 });
38
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
39
+ appendFileSync(DEBUG_LOG_PATH, `[${ts}] [openclaw-plugin] Log rotated (exceeded 5MB)
40
+ `);
41
+ }
42
+ } catch {
43
+ }
44
+ }
45
+ function debugLog(message, hookName = "openclaw-plugin") {
46
+ try {
47
+ ensureLogFile();
48
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
49
+ appendFileSync(DEBUG_LOG_PATH, `[${ts}] [${hookName}] ${message}
50
+ `);
51
+ } catch {
52
+ }
53
+ }
54
+
55
+ // src/e2e-crypto.ts
56
+ function keyId(keyHex) {
57
+ const keyBytes = Buffer.from(keyHex, "hex");
58
+ const hash = createHash("sha256").update(keyBytes).digest();
59
+ return hash.subarray(0, 4).toString("hex");
60
+ }
61
+ function deriveEncKey(keyHex) {
62
+ const prefix = Buffer.from("agentapprove-e2e-enc:");
63
+ const keyBytes = Buffer.from(keyHex, "hex");
64
+ return createHash("sha256").update(Buffer.concat([prefix, keyBytes])).digest();
65
+ }
66
+ function deriveMacKey(keyHex) {
67
+ const prefix = Buffer.from("agentapprove-e2e-mac:");
68
+ const keyBytes = Buffer.from(keyHex, "hex");
69
+ return createHash("sha256").update(Buffer.concat([prefix, keyBytes])).digest();
70
+ }
71
+ function e2eEncrypt(keyHex, plaintext) {
72
+ const kid = keyId(keyHex);
73
+ const encKey = deriveEncKey(keyHex);
74
+ const macKey = deriveMacKey(keyHex);
75
+ const iv = randomBytes(16);
76
+ const cipher = createCipheriv("aes-256-ctr", encKey, iv);
77
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
78
+ const ivHex = iv.toString("hex");
79
+ const ciphertextBase64 = ciphertext.toString("base64");
80
+ const hmac = createHmac("sha256", macKey).update(Buffer.concat([iv, ciphertext])).digest("hex");
81
+ return `E2E:v1:${kid}:${ivHex}:${ciphertextBase64}:${hmac}`;
82
+ }
83
+ function applyApprovalE2E(payload, userKey, serverKey) {
84
+ const sensitiveFields = {};
85
+ for (const field of ["command", "toolInput", "cwd"]) {
86
+ if (payload[field] != null) {
87
+ sensitiveFields[field] = payload[field];
88
+ }
89
+ }
90
+ if (Object.keys(sensitiveFields).length === 0) {
91
+ return payload;
92
+ }
93
+ const sensitiveJson = JSON.stringify(sensitiveFields);
94
+ const result = { ...payload };
95
+ delete result.command;
96
+ delete result.toolInput;
97
+ delete result.cwd;
98
+ const e2e = {
99
+ user: e2eEncrypt(userKey, sensitiveJson)
100
+ };
101
+ if (serverKey) {
102
+ e2e.server = e2eEncrypt(serverKey, sensitiveJson);
103
+ }
104
+ result.e2e = e2e;
105
+ return result;
106
+ }
107
+ function applyEventE2E(payload, userKey) {
108
+ const contentFields = {};
109
+ for (const field of [
110
+ "command",
111
+ "toolInput",
112
+ "response",
113
+ "responsePreview",
114
+ "text",
115
+ "textPreview",
116
+ "prompt",
117
+ "output",
118
+ "cwd"
119
+ ]) {
120
+ if (payload[field] != null) {
121
+ contentFields[field] = payload[field];
122
+ }
123
+ }
124
+ if (Object.keys(contentFields).length === 0) {
125
+ return payload;
126
+ }
127
+ const e2ePayload = e2eEncrypt(userKey, JSON.stringify(contentFields));
128
+ const result = { ...payload };
129
+ delete result.command;
130
+ delete result.toolInput;
131
+ delete result.response;
132
+ delete result.responsePreview;
133
+ delete result.text;
134
+ delete result.textPreview;
135
+ delete result.prompt;
136
+ delete result.output;
137
+ delete result.cwd;
138
+ result.e2ePayload = e2ePayload;
139
+ return result;
140
+ }
141
+ var AA_DIR = join2(homedir2(), ".agentapprove");
142
+ var E2E_KEY_FILE = join2(AA_DIR, "e2e-key");
143
+ var E2E_ROOT_KEY_FILE = join2(AA_DIR, "e2e-root-key");
144
+ var E2E_ROTATION_FILE = join2(AA_DIR, "e2e-rotation.json");
145
+ function rotateE2eKey(oldKeyHex) {
146
+ const prefix = Buffer.from("agentapprove-e2e-rotate:");
147
+ const keyBytes = Buffer.from(oldKeyHex, "hex");
148
+ return createHash("sha256").update(Buffer.concat([prefix, keyBytes])).digest("hex");
149
+ }
150
+ function deriveEpochKey(rootKeyHex, epoch) {
151
+ let current = rootKeyHex;
152
+ for (let i = 0; i < epoch; i++) {
153
+ current = rotateE2eKey(current);
154
+ }
155
+ return current;
156
+ }
157
+ function migrateRootKey() {
158
+ if (!existsSync2(E2E_KEY_FILE) || existsSync2(E2E_ROOT_KEY_FILE)) return;
159
+ try {
160
+ copyFileSync(E2E_KEY_FILE, E2E_ROOT_KEY_FILE);
161
+ try {
162
+ writeFileSync2(E2E_ROOT_KEY_FILE, readFileSync2(E2E_ROOT_KEY_FILE), { mode: 384 });
163
+ } catch {
164
+ }
165
+ if (!existsSync2(E2E_ROTATION_FILE)) {
166
+ const keyHex = readFileSync2(E2E_KEY_FILE, "utf-8").trim();
167
+ const kid = keyId(keyHex);
168
+ const config = {
169
+ rootKeyId: kid,
170
+ epoch: 0,
171
+ periodSeconds: 0,
172
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
173
+ };
174
+ writeFileSync2(E2E_ROTATION_FILE, JSON.stringify(config, null, 2), { mode: 384 });
175
+ }
176
+ debugLog("Migrated e2e-key to e2e-root-key");
177
+ } catch {
178
+ }
179
+ }
180
+ function checkAndRotateKeys(currentKeyHex) {
181
+ migrateRootKey();
182
+ if (!existsSync2(E2E_ROTATION_FILE) || !existsSync2(E2E_ROOT_KEY_FILE)) {
183
+ return currentKeyHex;
184
+ }
185
+ let rotCfg;
186
+ try {
187
+ rotCfg = JSON.parse(readFileSync2(E2E_ROTATION_FILE, "utf-8"));
188
+ } catch {
189
+ return currentKeyHex;
190
+ }
191
+ const periodSeconds = rotCfg.periodSeconds || 0;
192
+ if (periodSeconds <= 0 || !rotCfg.startedAt) {
193
+ return currentKeyHex;
194
+ }
195
+ const startedTs = Math.floor(new Date(rotCfg.startedAt).getTime() / 1e3);
196
+ if (isNaN(startedTs)) return currentKeyHex;
197
+ const nowTs = Math.floor(Date.now() / 1e3);
198
+ const elapsed = nowTs - startedTs;
199
+ if (elapsed < 0) return currentKeyHex;
200
+ const expectedEpoch = Math.floor(elapsed / periodSeconds);
201
+ const currentEpoch = rotCfg.epoch || 0;
202
+ if (expectedEpoch <= currentEpoch) {
203
+ return currentKeyHex;
204
+ }
205
+ let rootKeyHex;
206
+ try {
207
+ rootKeyHex = readFileSync2(E2E_ROOT_KEY_FILE, "utf-8").trim();
208
+ } catch {
209
+ return currentKeyHex;
210
+ }
211
+ if (rootKeyHex.length !== 64) return currentKeyHex;
212
+ const newKey = deriveEpochKey(rootKeyHex, expectedEpoch);
213
+ if (!newKey) return currentKeyHex;
214
+ try {
215
+ writeFileSync2(E2E_KEY_FILE, newKey, { mode: 384 });
216
+ rotCfg.epoch = expectedEpoch;
217
+ writeFileSync2(E2E_ROTATION_FILE, JSON.stringify(rotCfg, null, 2), { mode: 384 });
218
+ } catch {
219
+ }
220
+ debugLog(`E2E key rotated: epoch ${currentEpoch} -> ${expectedEpoch}`);
221
+ return newKey;
222
+ }
223
+
224
+ // src/config.ts
225
+ var CONFIG_PATH = join3(homedir3(), ".agentapprove", "env");
11
226
  var KEYCHAIN_SERVICE = "com.agentapprove";
12
227
  var KEYCHAIN_ACCOUNT = "api-token";
13
228
  function parseConfigValue(content, key) {
@@ -49,9 +264,9 @@ function getKeychainToken() {
49
264
  }
50
265
  }
51
266
  function checkPermissions(logger) {
52
- if (!existsSync(CONFIG_PATH)) return;
267
+ if (!existsSync3(CONFIG_PATH)) return;
53
268
  try {
54
- const stat = statSync(CONFIG_PATH);
269
+ const stat = statSync2(CONFIG_PATH);
55
270
  const mode = stat.mode & 511;
56
271
  if (mode & 54) {
57
272
  logger?.warn(
@@ -64,8 +279,8 @@ function checkPermissions(logger) {
64
279
  function loadConfig(openclawConfig, logger) {
65
280
  checkPermissions(logger);
66
281
  let fileContent = "";
67
- if (existsSync(CONFIG_PATH)) {
68
- fileContent = readFileSync(CONFIG_PATH, "utf-8");
282
+ if (existsSync3(CONFIG_PATH)) {
283
+ fileContent = readFileSync3(CONFIG_PATH, "utf-8");
69
284
  }
70
285
  let token = process.env.AGENTAPPROVE_TOKEN || getKeychainToken() || parseConfigValue(fileContent, "AGENTAPPROVE_TOKEN") || "";
71
286
  if (!token) {
@@ -80,26 +295,33 @@ function loadConfig(openclawConfig, logger) {
80
295
  const privacyTier = openclawConfig?.privacyTier || process.env.AGENTAPPROVE_PRIVACY || parseConfigValue(fileContent, "AGENTAPPROVE_PRIVACY") || "full";
81
296
  const debug = openclawConfig?.debug || process.env.AGENTAPPROVE_DEBUG === "true" || parseConfigValue(fileContent, "AGENTAPPROVE_DEBUG") === "true" || false;
82
297
  const agentName = process.env.AGENTAPPROVE_AGENT_NAME || parseConfigValue(fileContent, "AGENTAPPROVE_OPENCLAW_NAME") || "OpenClaw";
83
- const e2eEnabled = parseConfigValue(fileContent, "AGENTAPPROVE_E2E_ENABLED") === "true";
84
- const e2eUserKeyPath = join(homedir(), ".agentapprove", "e2e-key");
85
- const e2eServerKeyPath = join(homedir(), ".agentapprove", "e2e-server-key");
298
+ const e2eEnabled = process.env.AGENTAPPROVE_E2E_ENABLED === "true" || parseConfigValue(fileContent, "AGENTAPPROVE_E2E_ENABLED") === "true";
299
+ const e2eUserKeyPath = join3(homedir3(), ".agentapprove", "e2e-key");
300
+ const e2eServerKeyPath = join3(homedir3(), ".agentapprove", "e2e-server-key");
86
301
  let e2eUserKey;
87
302
  let e2eServerKey;
88
303
  if (e2eEnabled) {
89
- if (existsSync(e2eUserKeyPath)) {
304
+ if (existsSync3(e2eUserKeyPath)) {
90
305
  try {
91
- e2eUserKey = readFileSync(e2eUserKeyPath, "utf-8").trim();
306
+ e2eUserKey = readFileSync3(e2eUserKeyPath, "utf-8").trim();
92
307
  } catch {
93
308
  logger?.warn("Failed to load E2E user key");
94
309
  }
95
310
  }
96
- if (existsSync(e2eServerKeyPath)) {
311
+ if (existsSync3(e2eServerKeyPath)) {
97
312
  try {
98
- e2eServerKey = readFileSync(e2eServerKeyPath, "utf-8").trim();
313
+ e2eServerKey = readFileSync3(e2eServerKeyPath, "utf-8").trim();
99
314
  } catch {
100
315
  logger?.warn("Failed to load E2E server key");
101
316
  }
102
317
  }
318
+ if (e2eUserKey) {
319
+ e2eUserKey = checkAndRotateKeys(e2eUserKey) || e2eUserKey;
320
+ }
321
+ }
322
+ if (debug) {
323
+ const e2eStatus = !e2eEnabled ? "disabled" : !e2eUserKey ? "enabled but user key missing" : `enabled, keyId=${keyId(e2eUserKey)}`;
324
+ debugLog(`Config loaded: e2e=${e2eStatus}, serverKey=${e2eServerKey ? "present" : "missing"}, api=${apiUrl}`);
103
325
  }
104
326
  return {
105
327
  apiUrl,
@@ -124,7 +346,7 @@ import { URL } from "url";
124
346
 
125
347
  // src/hmac.ts
126
348
  import crypto from "crypto";
127
- import { readFileSync as readFileSync2 } from "fs";
349
+ import { readFileSync as readFileSync4 } from "fs";
128
350
  function generateHMACSignature(body, token, hookVersion) {
129
351
  const timestamp = Math.floor(Date.now() / 1e3);
130
352
  const message = `${hookVersion}:${timestamp}:${body}`;
@@ -133,7 +355,7 @@ function generateHMACSignature(body, token, hookVersion) {
133
355
  }
134
356
  function computePluginHash(pluginPath) {
135
357
  try {
136
- const content = readFileSync2(pluginPath, "utf-8");
358
+ const content = readFileSync4(pluginPath, "utf-8");
137
359
  return crypto.createHash("sha256").update(content).digest("hex");
138
360
  } catch {
139
361
  return "";
@@ -151,6 +373,28 @@ function buildHMACHeaders(body, token, hookVersion, pluginHash) {
151
373
 
152
374
  // src/privacy.ts
153
375
  var SUMMARY_MAX_LENGTH = 50;
376
+ var FILEPATH_MAX_LENGTH = 100;
377
+ var SENSITIVE_PATTERNS = [
378
+ /(?:password|secret|token|key|api_key|auth)=[^\s&]+/gi,
379
+ /Bearer [a-zA-Z0-9_-]+/gi
380
+ ];
381
+ function redactSensitive(value) {
382
+ let result = value;
383
+ for (const pattern of SENSITIVE_PATTERNS) {
384
+ result = result.replace(pattern, "***REDACTED***");
385
+ }
386
+ return result;
387
+ }
388
+ function truncate(value, maxLen) {
389
+ if (!value) return value;
390
+ if (value.length <= maxLen) return value;
391
+ return value.slice(0, maxLen) + "...";
392
+ }
393
+ function summarizeToolInput(value) {
394
+ const inputStr = redactSensitive(JSON.stringify(value));
395
+ const summary = inputStr.length > SUMMARY_MAX_LENGTH ? inputStr.slice(0, SUMMARY_MAX_LENGTH) + "..." : inputStr;
396
+ return { _summary: summary };
397
+ }
154
398
  function applyPrivacyFilter(request, privacyTier) {
155
399
  if (privacyTier === "full") {
156
400
  return request;
@@ -162,142 +406,51 @@ function applyPrivacyFilter(request, privacyTier) {
162
406
  delete filtered.cwd;
163
407
  return filtered;
164
408
  }
165
- if (filtered.command && filtered.command.length > SUMMARY_MAX_LENGTH) {
166
- filtered.command = filtered.command.slice(0, SUMMARY_MAX_LENGTH) + "...";
409
+ if (filtered.command) {
410
+ filtered.command = truncate(redactSensitive(filtered.command), SUMMARY_MAX_LENGTH);
167
411
  }
168
412
  if (filtered.toolInput) {
169
- const inputStr = JSON.stringify(filtered.toolInput);
170
- if (inputStr.length > SUMMARY_MAX_LENGTH) {
171
- filtered.toolInput = { _summary: inputStr.slice(0, SUMMARY_MAX_LENGTH) + "..." };
172
- }
413
+ filtered.toolInput = summarizeToolInput(filtered.toolInput);
173
414
  }
174
415
  return filtered;
175
416
  }
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);
417
+ var CONTENT_FIELDS = [
418
+ "command",
419
+ "toolInput",
420
+ "response",
421
+ "text",
422
+ "textPreview",
423
+ "prompt",
424
+ "output",
425
+ "cwd"
426
+ ];
427
+ function applyEventPrivacyFilter(event, privacyTier) {
428
+ if (privacyTier === "full") {
429
+ return event;
226
430
  }
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];
431
+ const filtered = { ...event };
432
+ if (privacyTier === "minimal") {
433
+ for (const field of CONTENT_FIELDS) {
434
+ delete filtered[field];
245
435
  }
436
+ if (typeof filtered.textLength === "undefined" && typeof event.text === "string") {
437
+ filtered.textLength = event.text.length;
438
+ }
439
+ return filtered;
246
440
  }
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
-
265
- // src/debug.ts
266
- import { appendFileSync, existsSync as existsSync2, mkdirSync, statSync as statSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
267
- import { join as join2, dirname } from "path";
268
- import { homedir as homedir2 } from "os";
269
- var DEBUG_LOG_PATH = join2(homedir2(), ".agentapprove", "hook-debug.log");
270
- var MAX_SIZE = 5 * 1024 * 1024;
271
- var KEEP_SIZE = 2 * 1024 * 1024;
272
- function ensureLogFile() {
273
- const dir = dirname(DEBUG_LOG_PATH);
274
- if (!existsSync2(dir)) {
275
- mkdirSync(dir, { recursive: true });
276
- }
277
- if (!existsSync2(DEBUG_LOG_PATH)) {
278
- writeFileSync(DEBUG_LOG_PATH, "", { mode: 384 });
279
- return;
280
- }
281
- try {
282
- const stat = statSync2(DEBUG_LOG_PATH);
283
- if (stat.size > MAX_SIZE) {
284
- const content = readFileSync3(DEBUG_LOG_PATH, "utf-8");
285
- writeFileSync(DEBUG_LOG_PATH, content.slice(-KEEP_SIZE), { mode: 384 });
286
- const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
287
- appendFileSync(DEBUG_LOG_PATH, `[${ts}] [openclaw-plugin] Log rotated (exceeded 5MB)
288
- `);
441
+ for (const field of CONTENT_FIELDS) {
442
+ const val = filtered[field];
443
+ if (val === void 0 || val === null) continue;
444
+ if (field === "toolInput") {
445
+ filtered[field] = summarizeToolInput(val);
446
+ } else if (typeof val === "string") {
447
+ filtered[field] = truncate(redactSensitive(val), SUMMARY_MAX_LENGTH);
289
448
  }
290
- } catch {
291
449
  }
292
- }
293
- function debugLog(message, hookName = "openclaw-plugin") {
294
- try {
295
- ensureLogFile();
296
- const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
297
- appendFileSync(DEBUG_LOG_PATH, `[${ts}] [${hookName}] ${message}
298
- `);
299
- } catch {
450
+ if (typeof filtered.filePath === "string") {
451
+ filtered.filePath = truncate(filtered.filePath, FILEPATH_MAX_LENGTH);
300
452
  }
453
+ return filtered;
301
454
  }
302
455
 
303
456
  // src/api-client.ts
@@ -355,7 +508,9 @@ async function sendApprovalRequest(request, config, pluginPath) {
355
508
  let payload;
356
509
  if (config.e2eEnabled && config.e2eUserKey) {
357
510
  payload = applyApprovalE2E(request, config.e2eUserKey, config.e2eServerKey);
358
- debugLog("E2E encryption applied to approval request");
511
+ if (config.debug) {
512
+ debugLog("E2E encryption applied to approval request");
513
+ }
359
514
  } else {
360
515
  payload = applyPrivacyFilter(request, config.privacyTier);
361
516
  }
@@ -385,10 +540,18 @@ async function sendApprovalRequest(request, config, pluginPath) {
385
540
  }
386
541
  async function sendEvent(event, config, pluginPath) {
387
542
  if (!config.token) return;
543
+ const eventType = event.eventType;
544
+ const toolName = event.toolName;
545
+ if (config.debug) {
546
+ debugLog(`Sending ${eventType || "event"}${toolName ? ` (${toolName})` : ""} (privacy: ${config.privacyTier})`);
547
+ }
388
548
  try {
389
- let payload = { ...event };
549
+ let payload = applyEventPrivacyFilter(event, config.privacyTier);
390
550
  if (config.e2eEnabled && config.e2eUserKey) {
391
551
  payload = applyEventE2E(payload, config.e2eUserKey);
552
+ if (config.debug) {
553
+ debugLog(`E2E applied to event (type=${eventType})`);
554
+ }
392
555
  }
393
556
  const bodyStr = JSON.stringify(payload);
394
557
  const pluginHash = getPluginHash(pluginPath);
@@ -398,10 +561,18 @@ async function sendEvent(event, config, pluginPath) {
398
561
  ...hmacHeaders
399
562
  };
400
563
  const url = `${config.apiUrl}/${config.apiVersion}/events`;
401
- await httpPost(url, bodyStr, headers, 5e3);
402
- } catch {
403
564
  if (config.debug) {
404
- debugLog(`Failed to send event: ${JSON.stringify(event).slice(0, 100)}`);
565
+ debugLog(`=== SENT TO ${url} ===`);
566
+ debugLog(bodyStr.slice(0, 500));
567
+ debugLog("=== END SENT ===");
568
+ }
569
+ const response = await httpPost(url, bodyStr, headers, 5e3);
570
+ if (config.debug) {
571
+ debugLog(`send_event response: ${response.body.slice(0, 200)}`);
572
+ }
573
+ } catch (err) {
574
+ if (config.debug) {
575
+ debugLog(`Failed to send ${eventType || "event"}: ${err instanceof Error ? err.message : String(err)}`);
405
576
  }
406
577
  }
407
578
  }
@@ -414,6 +585,32 @@ try {
414
585
  pluginFilePath = __filename;
415
586
  }
416
587
  var gatewaySessionId = randomBytes2(12).toString("hex");
588
+ var DEDUP_WINDOW_MS = 1200;
589
+ var DEDUP_MAX_SIZE = 300;
590
+ var recentCompletions = /* @__PURE__ */ new Map();
591
+ function resolveConversationId(...candidates) {
592
+ for (const id of candidates) {
593
+ if (id && id.trim()) return id;
594
+ }
595
+ return gatewaySessionId;
596
+ }
597
+ function isDuplicateCompletion(toolName, params) {
598
+ const paramsHash = createHash2("md5").update(JSON.stringify(params || {})).digest("hex").slice(0, 12);
599
+ const key = `${toolName}:${paramsHash}`;
600
+ const now = Date.now();
601
+ const last = recentCompletions.get(key);
602
+ if (last && now - last < DEDUP_WINDOW_MS) {
603
+ return true;
604
+ }
605
+ recentCompletions.set(key, now);
606
+ if (recentCompletions.size > DEDUP_MAX_SIZE) {
607
+ const cutoff = now - DEDUP_WINDOW_MS;
608
+ for (const [k, ts] of recentCompletions) {
609
+ if (ts < cutoff) recentCompletions.delete(k);
610
+ }
611
+ }
612
+ return false;
613
+ }
417
614
  function classifyTool(toolName, params) {
418
615
  const lower = toolName.toLowerCase();
419
616
  if (lower === "exec" || lower === "process") {
@@ -494,7 +691,9 @@ function extractResultPreview(toolName, params, result, maxLen = 300) {
494
691
  }
495
692
  function handleFailBehavior(config, error, toolName, logger) {
496
693
  logger.warn(`Agent Approve API error for tool "${toolName}": ${error.message}`);
497
- debugLog(`API error: ${error.message}, failBehavior: ${config.failBehavior}`);
694
+ if (config.debug) {
695
+ debugLog(`API error: ${error.message}, failBehavior: ${config.failBehavior}`);
696
+ }
498
697
  switch (config.failBehavior) {
499
698
  case "deny":
500
699
  return { block: true, blockReason: "Agent Approve unavailable, denying by policy" };
@@ -515,8 +714,11 @@ function register(api) {
515
714
  return;
516
715
  }
517
716
  api.logger.info(`Agent Approve: Plugin loaded (privacy: ${config.privacyTier}, fail: ${config.failBehavior})`);
518
- debugLog(`Plugin loaded, API: ${config.apiUrl}, agent: ${config.agentName}`);
717
+ if (config.debug) {
718
+ debugLog(`Plugin loaded, API: ${config.apiUrl}, agent: ${config.agentName}`);
719
+ }
519
720
  api.on("before_tool_call", async (event, ctx) => {
721
+ const conversationId = resolveConversationId(ctx.sessionKey);
520
722
  const { toolType, displayName } = classifyTool(event.toolName, event.params);
521
723
  const command = extractCommand(event.toolName, event.params);
522
724
  const request = {
@@ -526,17 +728,22 @@ function register(api) {
526
728
  toolInput: event.params,
527
729
  agent: config.agentName,
528
730
  hookType: "before_tool_call",
529
- sessionId: gatewaySessionId,
731
+ sessionId: conversationId,
732
+ conversationId,
530
733
  cwd: event.params.workdir || void 0,
531
734
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
532
735
  };
533
736
  try {
534
737
  const response = await sendApprovalRequest(request, config, pluginFilePath);
535
738
  if (response.decision === "approve" || response.decision === "allow") {
536
- debugLog(`Tool "${event.toolName}" approved${response.reason ? ": " + response.reason : ""}`);
739
+ if (config.debug) {
740
+ debugLog(`Tool "${event.toolName}" approved${response.reason ? ": " + response.reason : ""}`);
741
+ }
537
742
  return void 0;
538
743
  }
539
- debugLog(`Tool "${event.toolName}" denied${response.reason ? ": " + response.reason : ""}`);
744
+ if (config.debug) {
745
+ debugLog(`Tool "${event.toolName}" denied${response.reason ? ": " + response.reason : ""}`);
746
+ }
540
747
  return {
541
748
  block: true,
542
749
  blockReason: response.reason || "Denied by Agent Approve"
@@ -551,6 +758,13 @@ function register(api) {
551
758
  }
552
759
  });
553
760
  api.on("after_tool_call", async (event, ctx) => {
761
+ const conversationId = resolveConversationId(ctx.sessionKey);
762
+ if (isDuplicateCompletion(event.toolName, event.params)) {
763
+ if (config.debug) {
764
+ debugLog(`Skipping duplicate tool_complete for "${event.toolName}"`);
765
+ }
766
+ return;
767
+ }
554
768
  const { toolType } = classifyTool(event.toolName, event.params);
555
769
  const resultPreview = extractResultPreview(event.toolName, event.params, event.result);
556
770
  void sendEvent({
@@ -559,7 +773,8 @@ function register(api) {
559
773
  eventType: "tool_complete",
560
774
  agent: config.agentName,
561
775
  hookType: "after_tool_call",
562
- sessionId: gatewaySessionId,
776
+ sessionId: conversationId,
777
+ conversationId,
563
778
  command: extractCommand(event.toolName, event.params),
564
779
  status: event.error ? "error" : "success",
565
780
  response: event.error || resultPreview || void 0,
@@ -567,6 +782,139 @@ function register(api) {
567
782
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
568
783
  }, config, pluginFilePath);
569
784
  });
785
+ api.on("session_start", async (event, ctx) => {
786
+ const conversationId = resolveConversationId(ctx.sessionId, event.sessionId);
787
+ if (config.debug) {
788
+ debugLog(`Session started: ${event.sessionId}${event.resumedFrom ? ` (resumed from ${event.resumedFrom})` : ""}`);
789
+ }
790
+ void sendEvent({
791
+ eventType: "session_start",
792
+ agent: config.agentName,
793
+ hookType: "session_start",
794
+ sessionId: conversationId,
795
+ conversationId,
796
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
797
+ }, config, pluginFilePath);
798
+ });
799
+ api.on("session_end", async (event, ctx) => {
800
+ const conversationId = resolveConversationId(ctx.sessionId, event.sessionId);
801
+ if (config.debug) {
802
+ debugLog(`Session ended: ${event.sessionId} (${event.messageCount} messages, ${event.durationMs ?? "?"}ms)`);
803
+ }
804
+ void sendEvent({
805
+ eventType: "session_end",
806
+ agent: config.agentName,
807
+ hookType: "session_end",
808
+ sessionId: conversationId,
809
+ conversationId,
810
+ durationMs: event.durationMs,
811
+ sessionStats: { messageCount: event.messageCount },
812
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
813
+ }, config, pluginFilePath);
814
+ });
815
+ api.on("llm_input", async (event, ctx) => {
816
+ const conversationId = resolveConversationId(ctx.sessionId, event.sessionId, ctx.sessionKey);
817
+ if (config.debug) {
818
+ debugLog(`LLM input: model=${event.model}, prompt length=${event.prompt?.length ?? 0}`);
819
+ }
820
+ void sendEvent({
821
+ eventType: "user_prompt",
822
+ agent: config.agentName,
823
+ hookType: "llm_input",
824
+ sessionId: conversationId,
825
+ conversationId,
826
+ model: event.model,
827
+ prompt: event.prompt,
828
+ textLength: event.prompt?.length ?? 0,
829
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
830
+ }, config, pluginFilePath);
831
+ });
832
+ api.on("llm_output", async (event, ctx) => {
833
+ const conversationId = resolveConversationId(ctx.sessionId, event.sessionId, ctx.sessionKey);
834
+ const responseText = event.assistantTexts?.join("\n") || "";
835
+ const textLength = responseText.length;
836
+ if (config.debug) {
837
+ debugLog(`LLM output: model=${event.model}, length=${textLength}${event.usage?.total ? `, tokens=${event.usage.total}` : ""}`);
838
+ }
839
+ void sendEvent({
840
+ eventType: "response",
841
+ agent: config.agentName,
842
+ hookType: "llm_output",
843
+ sessionId: conversationId,
844
+ conversationId,
845
+ model: event.model,
846
+ text: responseText,
847
+ textPreview: responseText.slice(0, 200),
848
+ textLength,
849
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
850
+ }, config, pluginFilePath);
851
+ });
852
+ api.on("agent_end", async (event, ctx) => {
853
+ const conversationId = resolveConversationId(ctx.sessionId, ctx.sessionKey);
854
+ if (config.debug) {
855
+ debugLog(`Agent ended: success=${event.success}${event.durationMs ? `, duration=${event.durationMs}ms` : ""}${event.error ? `, error=${event.error}` : ""}`);
856
+ }
857
+ void sendEvent({
858
+ eventType: "stop",
859
+ agent: config.agentName,
860
+ hookType: "agent_end",
861
+ sessionId: conversationId,
862
+ conversationId,
863
+ status: event.success ? "completed" : "error",
864
+ durationMs: event.durationMs,
865
+ response: event.error || void 0,
866
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
867
+ }, config, pluginFilePath);
868
+ });
869
+ api.on("before_compaction", async (event, ctx) => {
870
+ const conversationId = resolveConversationId(ctx.sessionId, ctx.sessionKey);
871
+ if (config.debug) {
872
+ debugLog(`Context compaction: ${event.messageCount} messages${event.tokenCount ? `, ${event.tokenCount} tokens` : ""}`);
873
+ }
874
+ void sendEvent({
875
+ eventType: "context_compact",
876
+ agent: config.agentName,
877
+ hookType: "before_compaction",
878
+ sessionId: conversationId,
879
+ conversationId,
880
+ trigger: `${event.messageCount} messages${event.tokenCount ? `, ${event.tokenCount} tokens` : ""}`,
881
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
882
+ }, config, pluginFilePath);
883
+ });
884
+ api.on("subagent_spawned", async (event, ctx) => {
885
+ const conversationId = resolveConversationId(ctx.requesterSessionKey, ctx.childSessionKey, event.childSessionKey);
886
+ if (config.debug) {
887
+ debugLog(`Subagent spawned: ${event.agentId} (${event.mode}${event.label ? `, label=${event.label}` : ""})`);
888
+ }
889
+ void sendEvent({
890
+ eventType: "subagent_start",
891
+ agent: config.agentName,
892
+ hookType: "subagent_spawned",
893
+ sessionId: conversationId,
894
+ conversationId,
895
+ subagentType: event.agentId,
896
+ subagentMode: event.mode,
897
+ subagentLabel: event.label,
898
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
899
+ }, config, pluginFilePath);
900
+ });
901
+ api.on("subagent_ended", async (event, ctx) => {
902
+ const conversationId = resolveConversationId(ctx.requesterSessionKey, ctx.childSessionKey, event.targetSessionKey);
903
+ if (config.debug) {
904
+ debugLog(`Subagent ended: ${event.targetKind} reason=${event.reason}${event.outcome ? `, outcome=${event.outcome}` : ""}`);
905
+ }
906
+ void sendEvent({
907
+ eventType: "subagent_stop",
908
+ agent: config.agentName,
909
+ hookType: "subagent_ended",
910
+ sessionId: conversationId,
911
+ conversationId,
912
+ status: event.outcome || event.reason,
913
+ subagentType: event.targetKind,
914
+ response: event.error || void 0,
915
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
916
+ }, config, pluginFilePath);
917
+ });
570
918
  api.registerHook(
571
919
  ["command:new", "command:stop", "command:reset"],
572
920
  async (event) => {
@@ -575,13 +923,17 @@ function register(api) {
575
923
  stop: "session_end",
576
924
  reset: "session_start"
577
925
  };
926
+ if (config.debug) {
927
+ debugLog(`Command event: ${event.action}`);
928
+ }
578
929
  void sendEvent({
579
930
  toolName: `command:${event.action}`,
580
931
  toolType: "command",
581
932
  eventType: eventTypeMap[event.action] || "command_event",
582
933
  agent: config.agentName,
583
934
  hookType: "command_event",
584
- sessionId: gatewaySessionId,
935
+ sessionId: resolveConversationId(event.sessionKey),
936
+ conversationId: resolveConversationId(event.sessionKey),
585
937
  timestamp: event.timestamp.toISOString()
586
938
  }, config, pluginFilePath);
587
939
  },
@@ -594,37 +946,40 @@ function register(api) {
594
946
  const provider = event.context.channelId;
595
947
  const peer = isInbound ? event.context.from : event.context.to;
596
948
  const channelLabel = [provider, peer].filter(Boolean).join(":") || void 0;
949
+ if (config.debug) {
950
+ debugLog(`Message event: ${event.action} ${channelLabel || "(no channel)"}`);
951
+ }
597
952
  const payload = {
598
953
  toolName: `message:${event.action}`,
599
954
  toolType: "message_event",
600
- eventType: isInbound ? "prompt_submitted" : "response",
955
+ eventType: isInbound ? "user_prompt" : "response",
601
956
  agent: config.agentName,
602
957
  hookType: "session_event",
603
- sessionId: gatewaySessionId,
958
+ sessionId: resolveConversationId(event.sessionKey),
959
+ conversationId: resolveConversationId(event.sessionKey),
604
960
  timestamp: event.timestamp.toISOString(),
605
961
  cwd: channelLabel
606
962
  };
607
963
  const content = event.context.content;
608
964
  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
- }
965
+ payload.text = content;
966
+ payload.textPreview = content.slice(0, 200);
967
+ payload.textLength = content.length;
614
968
  if (isInbound && peer) {
615
- payload.prompt = `${peer}: ${content}`.slice(0, 200);
969
+ payload.prompt = `${peer}: ${content}`;
616
970
  }
617
971
  }
618
972
  void sendEvent(payload, config, pluginFilePath);
619
973
  },
620
974
  { name: "agentapprove-session-monitor", description: "Log message events to Agent Approve" }
621
975
  );
622
- api.logger.info("Agent Approve: Registered before_tool_call, after_tool_call, and event monitoring hooks");
976
+ const hookCount = 10;
977
+ api.logger.info(`Agent Approve: Registered ${hookCount} plugin hooks + event monitoring`);
623
978
  }
624
979
  var index_default = {
625
980
  id: "openclaw",
626
981
  name: "Agent Approve",
627
- description: "Mobile approval for AI agent tool execution",
982
+ description: "Agent Approve mobile approval for AI agent tool execution",
628
983
  register
629
984
  };
630
985
  export {
@@ -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.3",
5
+ "version": "0.1.4",
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.3",
3
+ "version": "0.1.4",
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": {