@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.
- package/dist/index.js +133 -10
- 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.
|
|
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
|
-
|
|
1110
|
-
eventType: "stop",
|
|
1183
|
+
const stopRequest = {
|
|
1111
1184
|
agent: config.agentType,
|
|
1112
1185
|
agentName: config.agentName,
|
|
1113
|
-
|
|
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
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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