@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 +520 -165
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
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
|
-
|
|
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 (!
|
|
267
|
+
if (!existsSync3(CONFIG_PATH)) return;
|
|
53
268
|
try {
|
|
54
|
-
const stat =
|
|
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 (
|
|
68
|
-
fileContent =
|
|
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 =
|
|
85
|
-
const e2eServerKeyPath =
|
|
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 (
|
|
304
|
+
if (existsSync3(e2eUserKeyPath)) {
|
|
90
305
|
try {
|
|
91
|
-
e2eUserKey =
|
|
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 (
|
|
311
|
+
if (existsSync3(e2eServerKeyPath)) {
|
|
97
312
|
try {
|
|
98
|
-
e2eServerKey =
|
|
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
|
|
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 =
|
|
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
|
|
166
|
-
filtered.command = filtered.command
|
|
409
|
+
if (filtered.command) {
|
|
410
|
+
filtered.command = truncate(redactSensitive(filtered.command), SUMMARY_MAX_LENGTH);
|
|
167
411
|
}
|
|
168
412
|
if (filtered.toolInput) {
|
|
169
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
739
|
+
if (config.debug) {
|
|
740
|
+
debugLog(`Tool "${event.toolName}" approved${response.reason ? ": " + response.reason : ""}`);
|
|
741
|
+
}
|
|
537
742
|
return void 0;
|
|
538
743
|
}
|
|
539
|
-
|
|
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:
|
|
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:
|
|
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 ? "
|
|
955
|
+
eventType: isInbound ? "user_prompt" : "response",
|
|
601
956
|
agent: config.agentName,
|
|
602
957
|
hookType: "session_event",
|
|
603
|
-
sessionId:
|
|
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
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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}
|
|
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
|
-
|
|
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: "
|
|
982
|
+
description: "Agent Approve mobile approval for AI agent tool execution",
|
|
628
983
|
register
|
|
629
984
|
};
|
|
630
985
|
export {
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "0.1.4",
|
|
6
6
|
"homepage": "https://agentapprove.com",
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED