@agentapprove/openclaw 0.1.4 → 0.1.6

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
@@ -154,7 +154,7 @@ function deriveEpochKey(rootKeyHex, epoch) {
154
154
  }
155
155
  return current;
156
156
  }
157
- function migrateRootKey() {
157
+ function migrateRootKey(debug = false) {
158
158
  if (!existsSync2(E2E_KEY_FILE) || existsSync2(E2E_ROOT_KEY_FILE)) return;
159
159
  try {
160
160
  copyFileSync(E2E_KEY_FILE, E2E_ROOT_KEY_FILE);
@@ -173,12 +173,12 @@ function migrateRootKey() {
173
173
  };
174
174
  writeFileSync2(E2E_ROTATION_FILE, JSON.stringify(config, null, 2), { mode: 384 });
175
175
  }
176
- debugLog("Migrated e2e-key to e2e-root-key");
176
+ if (debug) debugLog("Migrated e2e-key to e2e-root-key");
177
177
  } catch {
178
178
  }
179
179
  }
180
- function checkAndRotateKeys(currentKeyHex) {
181
- migrateRootKey();
180
+ function checkAndRotateKeys(currentKeyHex, debug = false) {
181
+ migrateRootKey(debug);
182
182
  if (!existsSync2(E2E_ROTATION_FILE) || !existsSync2(E2E_ROOT_KEY_FILE)) {
183
183
  return currentKeyHex;
184
184
  }
@@ -217,7 +217,7 @@ function checkAndRotateKeys(currentKeyHex) {
217
217
  writeFileSync2(E2E_ROTATION_FILE, JSON.stringify(rotCfg, null, 2), { mode: 384 });
218
218
  } catch {
219
219
  }
220
- debugLog(`E2E key rotated: epoch ${currentEpoch} -> ${expectedEpoch}`);
220
+ if (debug) debugLog(`E2E key rotated: epoch ${currentEpoch} -> ${expectedEpoch}`);
221
221
  return newKey;
222
222
  }
223
223
 
@@ -293,7 +293,7 @@ function loadConfig(openclawConfig, logger) {
293
293
  const timeout = openclawConfig?.timeout || parseInt(process.env.AGENTAPPROVE_TIMEOUT || "", 10) || parseInt(parseConfigValue(fileContent, "AGENTAPPROVE_TIMEOUT") || "", 10) || 300;
294
294
  const failBehavior = openclawConfig?.failBehavior || process.env.AGENTAPPROVE_FAIL_BEHAVIOR || parseConfigValue(fileContent, "AGENTAPPROVE_FAIL_BEHAVIOR") || "ask";
295
295
  const privacyTier = openclawConfig?.privacyTier || process.env.AGENTAPPROVE_PRIVACY || parseConfigValue(fileContent, "AGENTAPPROVE_PRIVACY") || "full";
296
- const debug = openclawConfig?.debug || process.env.AGENTAPPROVE_DEBUG === "true" || parseConfigValue(fileContent, "AGENTAPPROVE_DEBUG") === "true" || false;
296
+ const debug = openclawConfig?.debug || process.env.AGENTAPPROVE_DEBUG === "true" || process.env.AGENTAPPROVE_DEBUG_LOG === "true" || parseConfigValue(fileContent, "AGENTAPPROVE_DEBUG") === "true" || parseConfigValue(fileContent, "AGENTAPPROVE_DEBUG_LOG") === "true" || false;
297
297
  const agentName = process.env.AGENTAPPROVE_AGENT_NAME || parseConfigValue(fileContent, "AGENTAPPROVE_OPENCLAW_NAME") || "OpenClaw";
298
298
  const e2eEnabled = process.env.AGENTAPPROVE_E2E_ENABLED === "true" || parseConfigValue(fileContent, "AGENTAPPROVE_E2E_ENABLED") === "true";
299
299
  const e2eUserKeyPath = join3(homedir3(), ".agentapprove", "e2e-key");
@@ -316,7 +316,7 @@ function loadConfig(openclawConfig, logger) {
316
316
  }
317
317
  }
318
318
  if (e2eUserKey) {
319
- e2eUserKey = checkAndRotateKeys(e2eUserKey) || e2eUserKey;
319
+ e2eUserKey = checkAndRotateKeys(e2eUserKey, debug) || e2eUserKey;
320
320
  }
321
321
  }
322
322
  if (debug) {
@@ -331,7 +331,7 @@ function loadConfig(openclawConfig, logger) {
331
331
  failBehavior,
332
332
  privacyTier,
333
333
  debug,
334
- hookVersion: "1.1.1",
334
+ hookVersion: "1.1.3",
335
335
  agentName,
336
336
  e2eEnabled,
337
337
  e2eUserKey,
@@ -453,12 +453,142 @@ function applyEventPrivacyFilter(event, privacyTier) {
453
453
  return filtered;
454
454
  }
455
455
 
456
+ // src/config-sync.ts
457
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync4, renameSync } from "fs";
458
+ import { join as join4 } from "path";
459
+ import { homedir as homedir4 } from "os";
460
+ function getAADir() {
461
+ return process.env.__AGENTAPPROVE_TEST_DIR || join4(homedir4(), ".agentapprove");
462
+ }
463
+ function getEnvPath() {
464
+ return join4(getAADir(), "env");
465
+ }
466
+ function getRotationFile() {
467
+ return join4(getAADir(), "e2e-rotation.json");
468
+ }
469
+ var VALID_PRIVACY = /* @__PURE__ */ new Set(["minimal", "summary", "full"]);
470
+ var VALID_FAIL = /* @__PURE__ */ new Set(["allow", "deny", "ask"]);
471
+ function updateEnvValues(updates) {
472
+ const envPath = getEnvPath();
473
+ if (!existsSync4(envPath)) return;
474
+ const validUpdates = Object.fromEntries(
475
+ Object.entries(updates).filter(([, v]) => !/[`$(){};<>|&!\\]/.test(v))
476
+ );
477
+ const updateKeys = new Set(Object.keys(validUpdates));
478
+ if (updateKeys.size === 0) return;
479
+ try {
480
+ const content = readFileSync5(envPath, "utf-8");
481
+ const lines = content.split("\n");
482
+ const filtered = lines.filter((line) => {
483
+ const trimmed = line.replace(/^export\s+/, "").trim();
484
+ const eqIdx = trimmed.indexOf("=");
485
+ if (eqIdx <= 0) return true;
486
+ return !updateKeys.has(trimmed.slice(0, eqIdx));
487
+ });
488
+ for (const [key, value] of Object.entries(validUpdates)) {
489
+ filtered.push(`export ${key}=${value}`);
490
+ }
491
+ const tmpPath = `${envPath}.tmp.${process.pid}`;
492
+ writeFileSync3(tmpPath, filtered.join("\n"), { mode: 384 });
493
+ renameSync(tmpPath, envPath);
494
+ } catch {
495
+ }
496
+ }
497
+ function syncRotationConfig(serverPeriod, serverStartedAt, debug) {
498
+ const rotationFile = getRotationFile();
499
+ if (!existsSync4(rotationFile)) return;
500
+ try {
501
+ const raw = readFileSync5(rotationFile, "utf-8");
502
+ const config = JSON.parse(raw);
503
+ const currentPeriod = config.periodSeconds ?? 0;
504
+ const newPeriod = serverPeriod ?? 0;
505
+ let needsUpdate = false;
506
+ if (typeof newPeriod === "number" && newPeriod !== currentPeriod) {
507
+ config.periodSeconds = newPeriod;
508
+ needsUpdate = true;
509
+ }
510
+ if (serverStartedAt && (!config.startedAt || config.startedAt === "null")) {
511
+ config.startedAt = serverStartedAt;
512
+ needsUpdate = true;
513
+ }
514
+ if (needsUpdate) {
515
+ const tmpPath = `${rotationFile}.tmp.${process.pid}`;
516
+ writeFileSync3(tmpPath, JSON.stringify(config, null, 2), { mode: 384 });
517
+ renameSync(tmpPath, rotationFile);
518
+ if (debug) {
519
+ debugLog(`E2E rotation synced: periodSeconds=${newPeriod}`);
520
+ }
521
+ }
522
+ } catch {
523
+ }
524
+ }
525
+ function processConfigSync(parsed, config) {
526
+ const sync = parsed.configSync;
527
+ if (!sync || !sync.configSetAt) return;
528
+ const localSetAt = getLocalConfigSetAt();
529
+ if (sync.configSetAt <= localSetAt) return;
530
+ if (config.debug) {
531
+ debugLog(`Config sync: server=${sync.configSetAt} > local=${localSetAt}, updating...`);
532
+ }
533
+ const envUpdates = {};
534
+ if (sync.privacyTier && VALID_PRIVACY.has(sync.privacyTier)) {
535
+ envUpdates["AGENTAPPROVE_PRIVACY"] = sync.privacyTier;
536
+ config.privacyTier = sync.privacyTier;
537
+ if (config.debug) debugLog(`Config synced: privacy=${sync.privacyTier}`);
538
+ }
539
+ if (sync.timeoutSeconds != null) {
540
+ const t = sync.timeoutSeconds;
541
+ if (t >= 30 && t <= 600) {
542
+ envUpdates["AGENTAPPROVE_TIMEOUT"] = String(t);
543
+ config.timeout = t;
544
+ if (config.debug) debugLog(`Config synced: timeout=${t}`);
545
+ }
546
+ }
547
+ if (sync.failBehavior && VALID_FAIL.has(sync.failBehavior)) {
548
+ envUpdates["AGENTAPPROVE_FAIL_BEHAVIOR"] = sync.failBehavior;
549
+ config.failBehavior = sync.failBehavior;
550
+ if (config.debug) debugLog(`Config synced: failBehavior=${sync.failBehavior}`);
551
+ }
552
+ if (sync.e2eEnabled === true || sync.e2eEnabled === false) {
553
+ envUpdates["AGENTAPPROVE_E2E_ENABLED"] = String(sync.e2eEnabled);
554
+ config.e2eEnabled = sync.e2eEnabled;
555
+ if (config.debug) debugLog(`Config synced: e2eEnabled=${sync.e2eEnabled}`);
556
+ }
557
+ syncRotationConfig(sync.e2eRotationPeriod, sync.e2eRotationStartedAt, config.debug);
558
+ if (config.e2eEnabled && config.e2eUserKey) {
559
+ const rotatedKey = checkAndRotateKeys(config.e2eUserKey, config.debug);
560
+ if (rotatedKey && rotatedKey !== config.e2eUserKey) {
561
+ config.e2eUserKey = rotatedKey;
562
+ if (config.debug) debugLog(`Key rotated after config sync`);
563
+ }
564
+ }
565
+ envUpdates["AGENTAPPROVE_CONFIG_SET_AT"] = String(sync.configSetAt);
566
+ updateEnvValues(envUpdates);
567
+ }
568
+ function getLocalConfigSetAt() {
569
+ const envPath = getEnvPath();
570
+ if (!existsSync4(envPath)) return 0;
571
+ try {
572
+ const content = readFileSync5(envPath, "utf-8");
573
+ for (const raw of content.split("\n")) {
574
+ const line = raw.replace(/^export\s+/, "").trim();
575
+ if (line.startsWith("AGENTAPPROVE_CONFIG_SET_AT=")) {
576
+ const val = line.slice("AGENTAPPROVE_CONFIG_SET_AT=".length).replace(/^["']|["']$/g, "");
577
+ const num = parseInt(val, 10);
578
+ return isNaN(num) ? 0 : num;
579
+ }
580
+ }
581
+ } catch {
582
+ }
583
+ return 0;
584
+ }
585
+
456
586
  // src/api-client.ts
457
587
  var cachedPluginHash;
458
- function getPluginHash(pluginPath) {
588
+ function getPluginHash(pluginPath, debug = false) {
459
589
  if (!cachedPluginHash) {
460
590
  cachedPluginHash = computePluginHash(pluginPath);
461
- if (cachedPluginHash) {
591
+ if (cachedPluginHash && debug) {
462
592
  debugLog(`Plugin hash computed: ${cachedPluginHash.slice(0, 16)}...`);
463
593
  }
464
594
  }
@@ -515,7 +645,7 @@ async function sendApprovalRequest(request, config, pluginPath) {
515
645
  payload = applyPrivacyFilter(request, config.privacyTier);
516
646
  }
517
647
  const bodyStr = JSON.stringify(payload);
518
- const pluginHash = getPluginHash(pluginPath);
648
+ const pluginHash = getPluginHash(pluginPath, config.debug);
519
649
  const hmacHeaders = buildHMACHeaders(bodyStr, config.token, config.hookVersion, pluginHash);
520
650
  const headers = {
521
651
  "Authorization": `Bearer ${config.token}`,
@@ -532,11 +662,14 @@ async function sendApprovalRequest(request, config, pluginPath) {
532
662
  if (response.status !== 200) {
533
663
  throw new Error(`API returned status ${response.status}: ${response.body.slice(0, 200)}`);
534
664
  }
665
+ let parsed;
535
666
  try {
536
- return JSON.parse(response.body);
667
+ parsed = JSON.parse(response.body);
537
668
  } catch {
538
669
  throw new Error(`Failed to parse API response: ${response.body.slice(0, 200)}`);
539
670
  }
671
+ processConfigSync(parsed, config);
672
+ return parsed;
540
673
  }
541
674
  async function sendEvent(event, config, pluginPath) {
542
675
  if (!config.token) return;
@@ -554,7 +687,7 @@ async function sendEvent(event, config, pluginPath) {
554
687
  }
555
688
  }
556
689
  const bodyStr = JSON.stringify(payload);
557
- const pluginHash = getPluginHash(pluginPath);
690
+ const pluginHash = getPluginHash(pluginPath, config.debug);
558
691
  const hmacHeaders = buildHMACHeaders(bodyStr, config.token, config.hookVersion, pluginHash);
559
692
  const headers = {
560
693
  "Authorization": `Bearer ${config.token}`,
@@ -570,6 +703,13 @@ async function sendEvent(event, config, pluginPath) {
570
703
  if (config.debug) {
571
704
  debugLog(`send_event response: ${response.body.slice(0, 200)}`);
572
705
  }
706
+ if (response.status === 200) {
707
+ try {
708
+ const parsed = JSON.parse(response.body);
709
+ processConfigSync(parsed, config);
710
+ } catch {
711
+ }
712
+ }
573
713
  } catch (err) {
574
714
  if (config.debug) {
575
715
  debugLog(`Failed to send ${eventType || "event"}: ${err instanceof Error ? err.message : String(err)}`);
@@ -588,10 +728,7 @@ var gatewaySessionId = randomBytes2(12).toString("hex");
588
728
  var DEDUP_WINDOW_MS = 1200;
589
729
  var DEDUP_MAX_SIZE = 300;
590
730
  var recentCompletions = /* @__PURE__ */ new Map();
591
- function resolveConversationId(...candidates) {
592
- for (const id of candidates) {
593
- if (id && id.trim()) return id;
594
- }
731
+ function resolveConversationId() {
595
732
  return gatewaySessionId;
596
733
  }
597
734
  function isDuplicateCompletion(toolName, params) {
@@ -715,10 +852,14 @@ function register(api) {
715
852
  }
716
853
  api.logger.info(`Agent Approve: Plugin loaded (privacy: ${config.privacyTier}, fail: ${config.failBehavior})`);
717
854
  if (config.debug) {
718
- debugLog(`Plugin loaded, API: ${config.apiUrl}, agent: ${config.agentName}`);
855
+ const e2eStatus = !config.e2eEnabled ? "disabled" : !config.e2eUserKey ? "enabled (key missing)" : "enabled";
856
+ debugLog(
857
+ `Plugin loaded: v${config.hookVersion}, api=${config.apiUrl}, privacy=${config.privacyTier}, e2e=${e2eStatus}, debug=${config.debug}`
858
+ );
859
+ debugLog(`Full config: agent=${config.agentName}, timeout=${config.timeout}s, fail=${config.failBehavior}`);
719
860
  }
720
861
  api.on("before_tool_call", async (event, ctx) => {
721
- const conversationId = resolveConversationId(ctx.sessionKey);
862
+ const conversationId = resolveConversationId();
722
863
  const { toolType, displayName } = classifyTool(event.toolName, event.params);
723
864
  const command = extractCommand(event.toolName, event.params);
724
865
  const request = {
@@ -758,7 +899,7 @@ function register(api) {
758
899
  }
759
900
  });
760
901
  api.on("after_tool_call", async (event, ctx) => {
761
- const conversationId = resolveConversationId(ctx.sessionKey);
902
+ const conversationId = resolveConversationId();
762
903
  if (isDuplicateCompletion(event.toolName, event.params)) {
763
904
  if (config.debug) {
764
905
  debugLog(`Skipping duplicate tool_complete for "${event.toolName}"`);
@@ -783,7 +924,7 @@ function register(api) {
783
924
  }, config, pluginFilePath);
784
925
  });
785
926
  api.on("session_start", async (event, ctx) => {
786
- const conversationId = resolveConversationId(ctx.sessionId, event.sessionId);
927
+ const conversationId = resolveConversationId();
787
928
  if (config.debug) {
788
929
  debugLog(`Session started: ${event.sessionId}${event.resumedFrom ? ` (resumed from ${event.resumedFrom})` : ""}`);
789
930
  }
@@ -797,7 +938,7 @@ function register(api) {
797
938
  }, config, pluginFilePath);
798
939
  });
799
940
  api.on("session_end", async (event, ctx) => {
800
- const conversationId = resolveConversationId(ctx.sessionId, event.sessionId);
941
+ const conversationId = resolveConversationId();
801
942
  if (config.debug) {
802
943
  debugLog(`Session ended: ${event.sessionId} (${event.messageCount} messages, ${event.durationMs ?? "?"}ms)`);
803
944
  }
@@ -813,7 +954,7 @@ function register(api) {
813
954
  }, config, pluginFilePath);
814
955
  });
815
956
  api.on("llm_input", async (event, ctx) => {
816
- const conversationId = resolveConversationId(ctx.sessionId, event.sessionId, ctx.sessionKey);
957
+ const conversationId = resolveConversationId();
817
958
  if (config.debug) {
818
959
  debugLog(`LLM input: model=${event.model}, prompt length=${event.prompt?.length ?? 0}`);
819
960
  }
@@ -830,7 +971,7 @@ function register(api) {
830
971
  }, config, pluginFilePath);
831
972
  });
832
973
  api.on("llm_output", async (event, ctx) => {
833
- const conversationId = resolveConversationId(ctx.sessionId, event.sessionId, ctx.sessionKey);
974
+ const conversationId = resolveConversationId();
834
975
  const responseText = event.assistantTexts?.join("\n") || "";
835
976
  const textLength = responseText.length;
836
977
  if (config.debug) {
@@ -850,7 +991,7 @@ function register(api) {
850
991
  }, config, pluginFilePath);
851
992
  });
852
993
  api.on("agent_end", async (event, ctx) => {
853
- const conversationId = resolveConversationId(ctx.sessionId, ctx.sessionKey);
994
+ const conversationId = resolveConversationId();
854
995
  if (config.debug) {
855
996
  debugLog(`Agent ended: success=${event.success}${event.durationMs ? `, duration=${event.durationMs}ms` : ""}${event.error ? `, error=${event.error}` : ""}`);
856
997
  }
@@ -867,7 +1008,7 @@ function register(api) {
867
1008
  }, config, pluginFilePath);
868
1009
  });
869
1010
  api.on("before_compaction", async (event, ctx) => {
870
- const conversationId = resolveConversationId(ctx.sessionId, ctx.sessionKey);
1011
+ const conversationId = resolveConversationId();
871
1012
  if (config.debug) {
872
1013
  debugLog(`Context compaction: ${event.messageCount} messages${event.tokenCount ? `, ${event.tokenCount} tokens` : ""}`);
873
1014
  }
@@ -882,7 +1023,7 @@ function register(api) {
882
1023
  }, config, pluginFilePath);
883
1024
  });
884
1025
  api.on("subagent_spawned", async (event, ctx) => {
885
- const conversationId = resolveConversationId(ctx.requesterSessionKey, ctx.childSessionKey, event.childSessionKey);
1026
+ const conversationId = resolveConversationId();
886
1027
  if (config.debug) {
887
1028
  debugLog(`Subagent spawned: ${event.agentId} (${event.mode}${event.label ? `, label=${event.label}` : ""})`);
888
1029
  }
@@ -899,7 +1040,7 @@ function register(api) {
899
1040
  }, config, pluginFilePath);
900
1041
  });
901
1042
  api.on("subagent_ended", async (event, ctx) => {
902
- const conversationId = resolveConversationId(ctx.requesterSessionKey, ctx.childSessionKey, event.targetSessionKey);
1043
+ const conversationId = resolveConversationId();
903
1044
  if (config.debug) {
904
1045
  debugLog(`Subagent ended: ${event.targetKind} reason=${event.reason}${event.outcome ? `, outcome=${event.outcome}` : ""}`);
905
1046
  }
@@ -932,8 +1073,8 @@ function register(api) {
932
1073
  eventType: eventTypeMap[event.action] || "command_event",
933
1074
  agent: config.agentName,
934
1075
  hookType: "command_event",
935
- sessionId: resolveConversationId(event.sessionKey),
936
- conversationId: resolveConversationId(event.sessionKey),
1076
+ sessionId: resolveConversationId(),
1077
+ conversationId: resolveConversationId(),
937
1078
  timestamp: event.timestamp.toISOString()
938
1079
  }, config, pluginFilePath);
939
1080
  },
@@ -955,8 +1096,8 @@ function register(api) {
955
1096
  eventType: isInbound ? "user_prompt" : "response",
956
1097
  agent: config.agentName,
957
1098
  hookType: "session_event",
958
- sessionId: resolveConversationId(event.sessionKey),
959
- conversationId: resolveConversationId(event.sessionKey),
1099
+ sessionId: resolveConversationId(),
1100
+ conversationId: resolveConversationId(),
960
1101
  timestamp: event.timestamp.toISOString(),
961
1102
  cwd: channelLabel
962
1103
  };
@@ -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.4",
5
+ "version": "0.1.6",
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.4",
3
+ "version": "0.1.6",
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": {