@agentapprove/openclaw 0.1.8 → 0.1.9

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.
Files changed (2) hide show
  1. package/dist/index.js +133 -10
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { homedir as homedir3 } from "os";
9
9
  import { execSync } from "child_process";
10
10
 
11
11
  // src/e2e-crypto.ts
12
- import { createHash, createHmac, createCipheriv, randomBytes } from "crypto";
12
+ import { createHash, createHmac, createCipheriv, createDecipheriv, randomBytes, timingSafeEqual } from "crypto";
13
13
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, copyFileSync } from "fs";
14
14
  import { join as join2 } from "path";
15
15
  import { homedir as homedir2 } from "os";
@@ -135,6 +135,35 @@ function e2eEncrypt(keyHex, plaintext) {
135
135
  const hmac = createHmac("sha256", macKey).update(Buffer.concat([iv, ciphertext])).digest("hex");
136
136
  return `E2E:v1:${kid}:${ivHex}:${ciphertextBase64}:${hmac}`;
137
137
  }
138
+ function isE2EEncrypted(value) {
139
+ return value.startsWith("E2E:v1:");
140
+ }
141
+ function e2eDecrypt(keyHex, encrypted) {
142
+ try {
143
+ if (!isE2EEncrypted(encrypted)) return null;
144
+ const parts = encrypted.split(":");
145
+ if (parts.length !== 6) return null;
146
+ const kid = parts[2];
147
+ if (kid !== keyId(keyHex)) return null;
148
+ const ivHex = parts[3];
149
+ const ciphertextBase64 = parts[4];
150
+ const hmacHex = parts[5];
151
+ const encKey = deriveEncKey(keyHex);
152
+ const macKey = deriveMacKey(keyHex);
153
+ const iv = Buffer.from(ivHex, "hex");
154
+ const ciphertext = Buffer.from(ciphertextBase64, "base64");
155
+ const expectedHmac = createHmac("sha256", macKey).update(Buffer.concat([iv, ciphertext])).digest();
156
+ const actualHmac = Buffer.from(hmacHex, "hex");
157
+ if (expectedHmac.length !== actualHmac.length || !timingSafeEqual(expectedHmac, actualHmac)) {
158
+ return null;
159
+ }
160
+ const decipher = createDecipheriv("aes-256-ctr", encKey, iv);
161
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
162
+ return plaintext.toString("utf-8");
163
+ } catch {
164
+ return null;
165
+ }
166
+ }
138
167
  function applyApprovalE2E(payload, userKey, serverKey) {
139
168
  const sensitiveFields = {};
140
169
  for (const field of ["command", "toolInput", "cwd"]) {
@@ -386,7 +415,7 @@ function loadConfig(openclawConfig, logger) {
386
415
  failBehavior,
387
416
  privacyTier,
388
417
  debug,
389
- hookVersion: "1.1.4",
418
+ hookVersion: "1.1.5",
390
419
  agentType: "openclaw",
391
420
  agentName,
392
421
  e2eEnabled,
@@ -731,6 +760,51 @@ async function sendApprovalRequest(request, config, pluginPath, hookName = "open
731
760
  processConfigSync(parsed, config);
732
761
  return parsed;
733
762
  }
763
+ async function sendStopRequest(request, config, pluginPath, hookName = "openclaw-plugin") {
764
+ if (!config.token) {
765
+ throw new Error("Missing token. Download the iOS app, run npx agentapprove, and pair your device. Subscription required.");
766
+ }
767
+ let payload = applyEventPrivacyFilter(
768
+ { ...request },
769
+ config.privacyTier
770
+ );
771
+ if (config.e2eEnabled && config.e2eUserKey) {
772
+ payload = applyEventE2E(payload, config.e2eUserKey);
773
+ if (config.debug) {
774
+ debugLog("E2E encryption applied to stop request", hookName);
775
+ }
776
+ }
777
+ const bodyStr = JSON.stringify(payload);
778
+ const pluginHash = getPluginHash(pluginPath, config.debug, hookName);
779
+ const hmacHeaders = buildHMACHeaders(bodyStr, config.token, config.hookVersion, pluginHash);
780
+ const headers = {
781
+ "Authorization": `Bearer ${config.token}`,
782
+ ...hmacHeaders
783
+ };
784
+ const url = `${config.apiUrl}/${config.apiVersion}/stop`;
785
+ const stopTimeoutMs = 3e5;
786
+ if (config.debug) {
787
+ debugLog(`Sending stop request to ${url}`, hookName);
788
+ debugLog(`=== SENT TO ${url} ===`, hookName);
789
+ debugLogRawInline(bodyStr);
790
+ debugLog("=== END SENT ===", hookName);
791
+ }
792
+ const response = await httpPost(url, bodyStr, headers, stopTimeoutMs);
793
+ if (config.debug) {
794
+ debugLog(`Stop response: ${response.body || "<empty>"}`, hookName);
795
+ }
796
+ if (response.status !== 200) {
797
+ throw new Error(`Stop API returned status ${response.status}: ${response.body.slice(0, 200)}`);
798
+ }
799
+ let parsed;
800
+ try {
801
+ parsed = JSON.parse(response.body);
802
+ } catch {
803
+ parsed = {};
804
+ }
805
+ processConfigSync(parsed, config);
806
+ return parsed;
807
+ }
734
808
  async function sendEvent(event, config, pluginPath, hookName = "openclaw-plugin") {
735
809
  if (!config.token) return;
736
810
  const eventType = event.eventType;
@@ -1106,18 +1180,67 @@ function register(api) {
1106
1180
  debugLog(`Agent ended: success=${event.success}${event.durationMs ? `, duration=${event.durationMs}ms` : ""}${event.error ? `, error=${event.error}` : ""}`, HOOK_AGENT_END);
1107
1181
  }
1108
1182
  const conversationId = resolveConversationId();
1109
- void sendEvent({
1110
- eventType: "stop",
1183
+ const stopRequest = {
1111
1184
  agent: config.agentType,
1112
1185
  agentName: config.agentName,
1113
- hookType: "agent_end",
1186
+ eventType: "stop",
1187
+ status: event.success ? "completed" : "error",
1188
+ loopCount: Array.isArray(event.messages) ? event.messages.length : 0,
1114
1189
  sessionId: conversationId,
1115
1190
  conversationId,
1116
- status: event.success ? "completed" : "error",
1117
- durationMs: event.durationMs,
1118
- response: event.error || void 0,
1119
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1120
- }, config, pluginFilePath, HOOK_AGENT_END);
1191
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1192
+ ...config.e2eEnabled && config.e2eUserKey ? { e2eKeyId: keyId(config.e2eUserKey) } : {}
1193
+ };
1194
+ try {
1195
+ const response = await sendStopRequest(stopRequest, config, pluginFilePath, HOOK_AGENT_END);
1196
+ if (config.debug) {
1197
+ debugLog(`Stop response: followup=${response.followup_message ? "yes" : "no"}`, HOOK_AGENT_END);
1198
+ }
1199
+ if (response.followup_message) {
1200
+ let followupText = response.followup_message;
1201
+ if (config.e2eEnabled && config.e2eUserKey && isE2EEncrypted(followupText)) {
1202
+ const decrypted = e2eDecrypt(config.e2eUserKey, followupText);
1203
+ if (!decrypted) {
1204
+ api.logger.warn("Agent Approve: failed to decrypt follow-up message");
1205
+ return;
1206
+ }
1207
+ if (config.debug) {
1208
+ debugLog(`Decrypted follow-up: ${decrypted.slice(0, 50)}...`, HOOK_AGENT_END);
1209
+ }
1210
+ followupText = decrypted;
1211
+ }
1212
+ if (!api.runtime?.system?.enqueueSystemEvent) {
1213
+ api.logger.warn(
1214
+ "Agent Approve: follow-up received but runtime.system API unavailable (OpenClaw version may be too old)"
1215
+ );
1216
+ return;
1217
+ }
1218
+ try {
1219
+ const sessionKey = ctx.sessionKey || conversationId;
1220
+ api.runtime.system.enqueueSystemEvent(
1221
+ `Agent Approve follow-up from user: ${followupText}`,
1222
+ { sessionKey }
1223
+ );
1224
+ api.runtime.system.requestHeartbeatNow({
1225
+ sessionKey,
1226
+ reason: "agentapprove-followup"
1227
+ });
1228
+ if (config.debug) {
1229
+ debugLog(`Follow-up injected via system event, heartbeat requested`, HOOK_AGENT_END);
1230
+ }
1231
+ api.logger.info(`Agent Approve: follow-up injected, heartbeat requested`);
1232
+ } catch (injectionError) {
1233
+ const injMsg = injectionError instanceof Error ? injectionError.message : String(injectionError);
1234
+ api.logger.warn(`Agent Approve: follow-up injection failed \u2014 ${injMsg}`);
1235
+ }
1236
+ }
1237
+ } catch (error) {
1238
+ const msg = error instanceof Error ? error.message : String(error);
1239
+ if (config.debug) {
1240
+ debugLog(`Stop request error: ${msg}`, HOOK_AGENT_END);
1241
+ }
1242
+ api.logger.warn(`Agent Approve: stop request failed \u2014 ${msg}`);
1243
+ }
1121
1244
  });
1122
1245
  api.on("before_compaction", async (event, ctx) => {
1123
1246
  if (config.debug) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentapprove/openclaw",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
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": {