@agentapprove/openclaw 0.1.3 → 0.1.5
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 +518 -166
- 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,
|
|
@@ -109,7 +331,7 @@ function loadConfig(openclawConfig, logger) {
|
|
|
109
331
|
failBehavior,
|
|
110
332
|
privacyTier,
|
|
111
333
|
debug,
|
|
112
|
-
hookVersion: "1.1.
|
|
334
|
+
hookVersion: "1.1.2",
|
|
113
335
|
agentName,
|
|
114
336
|
e2eEnabled,
|
|
115
337
|
e2eUserKey,
|
|
@@ -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,29 @@ 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() {
|
|
592
|
+
return gatewaySessionId;
|
|
593
|
+
}
|
|
594
|
+
function isDuplicateCompletion(toolName, params) {
|
|
595
|
+
const paramsHash = createHash2("md5").update(JSON.stringify(params || {})).digest("hex").slice(0, 12);
|
|
596
|
+
const key = `${toolName}:${paramsHash}`;
|
|
597
|
+
const now = Date.now();
|
|
598
|
+
const last = recentCompletions.get(key);
|
|
599
|
+
if (last && now - last < DEDUP_WINDOW_MS) {
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
recentCompletions.set(key, now);
|
|
603
|
+
if (recentCompletions.size > DEDUP_MAX_SIZE) {
|
|
604
|
+
const cutoff = now - DEDUP_WINDOW_MS;
|
|
605
|
+
for (const [k, ts] of recentCompletions) {
|
|
606
|
+
if (ts < cutoff) recentCompletions.delete(k);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
417
611
|
function classifyTool(toolName, params) {
|
|
418
612
|
const lower = toolName.toLowerCase();
|
|
419
613
|
if (lower === "exec" || lower === "process") {
|
|
@@ -494,7 +688,9 @@ function extractResultPreview(toolName, params, result, maxLen = 300) {
|
|
|
494
688
|
}
|
|
495
689
|
function handleFailBehavior(config, error, toolName, logger) {
|
|
496
690
|
logger.warn(`Agent Approve API error for tool "${toolName}": ${error.message}`);
|
|
497
|
-
|
|
691
|
+
if (config.debug) {
|
|
692
|
+
debugLog(`API error: ${error.message}, failBehavior: ${config.failBehavior}`);
|
|
693
|
+
}
|
|
498
694
|
switch (config.failBehavior) {
|
|
499
695
|
case "deny":
|
|
500
696
|
return { block: true, blockReason: "Agent Approve unavailable, denying by policy" };
|
|
@@ -515,8 +711,11 @@ function register(api) {
|
|
|
515
711
|
return;
|
|
516
712
|
}
|
|
517
713
|
api.logger.info(`Agent Approve: Plugin loaded (privacy: ${config.privacyTier}, fail: ${config.failBehavior})`);
|
|
518
|
-
|
|
714
|
+
if (config.debug) {
|
|
715
|
+
debugLog(`Plugin loaded, API: ${config.apiUrl}, agent: ${config.agentName}`);
|
|
716
|
+
}
|
|
519
717
|
api.on("before_tool_call", async (event, ctx) => {
|
|
718
|
+
const conversationId = resolveConversationId();
|
|
520
719
|
const { toolType, displayName } = classifyTool(event.toolName, event.params);
|
|
521
720
|
const command = extractCommand(event.toolName, event.params);
|
|
522
721
|
const request = {
|
|
@@ -526,17 +725,22 @@ function register(api) {
|
|
|
526
725
|
toolInput: event.params,
|
|
527
726
|
agent: config.agentName,
|
|
528
727
|
hookType: "before_tool_call",
|
|
529
|
-
sessionId:
|
|
728
|
+
sessionId: conversationId,
|
|
729
|
+
conversationId,
|
|
530
730
|
cwd: event.params.workdir || void 0,
|
|
531
731
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
532
732
|
};
|
|
533
733
|
try {
|
|
534
734
|
const response = await sendApprovalRequest(request, config, pluginFilePath);
|
|
535
735
|
if (response.decision === "approve" || response.decision === "allow") {
|
|
536
|
-
|
|
736
|
+
if (config.debug) {
|
|
737
|
+
debugLog(`Tool "${event.toolName}" approved${response.reason ? ": " + response.reason : ""}`);
|
|
738
|
+
}
|
|
537
739
|
return void 0;
|
|
538
740
|
}
|
|
539
|
-
|
|
741
|
+
if (config.debug) {
|
|
742
|
+
debugLog(`Tool "${event.toolName}" denied${response.reason ? ": " + response.reason : ""}`);
|
|
743
|
+
}
|
|
540
744
|
return {
|
|
541
745
|
block: true,
|
|
542
746
|
blockReason: response.reason || "Denied by Agent Approve"
|
|
@@ -551,6 +755,13 @@ function register(api) {
|
|
|
551
755
|
}
|
|
552
756
|
});
|
|
553
757
|
api.on("after_tool_call", async (event, ctx) => {
|
|
758
|
+
const conversationId = resolveConversationId();
|
|
759
|
+
if (isDuplicateCompletion(event.toolName, event.params)) {
|
|
760
|
+
if (config.debug) {
|
|
761
|
+
debugLog(`Skipping duplicate tool_complete for "${event.toolName}"`);
|
|
762
|
+
}
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
554
765
|
const { toolType } = classifyTool(event.toolName, event.params);
|
|
555
766
|
const resultPreview = extractResultPreview(event.toolName, event.params, event.result);
|
|
556
767
|
void sendEvent({
|
|
@@ -559,7 +770,8 @@ function register(api) {
|
|
|
559
770
|
eventType: "tool_complete",
|
|
560
771
|
agent: config.agentName,
|
|
561
772
|
hookType: "after_tool_call",
|
|
562
|
-
sessionId:
|
|
773
|
+
sessionId: conversationId,
|
|
774
|
+
conversationId,
|
|
563
775
|
command: extractCommand(event.toolName, event.params),
|
|
564
776
|
status: event.error ? "error" : "success",
|
|
565
777
|
response: event.error || resultPreview || void 0,
|
|
@@ -567,6 +779,139 @@ function register(api) {
|
|
|
567
779
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
568
780
|
}, config, pluginFilePath);
|
|
569
781
|
});
|
|
782
|
+
api.on("session_start", async (event, ctx) => {
|
|
783
|
+
const conversationId = resolveConversationId();
|
|
784
|
+
if (config.debug) {
|
|
785
|
+
debugLog(`Session started: ${event.sessionId}${event.resumedFrom ? ` (resumed from ${event.resumedFrom})` : ""}`);
|
|
786
|
+
}
|
|
787
|
+
void sendEvent({
|
|
788
|
+
eventType: "session_start",
|
|
789
|
+
agent: config.agentName,
|
|
790
|
+
hookType: "session_start",
|
|
791
|
+
sessionId: conversationId,
|
|
792
|
+
conversationId,
|
|
793
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
794
|
+
}, config, pluginFilePath);
|
|
795
|
+
});
|
|
796
|
+
api.on("session_end", async (event, ctx) => {
|
|
797
|
+
const conversationId = resolveConversationId();
|
|
798
|
+
if (config.debug) {
|
|
799
|
+
debugLog(`Session ended: ${event.sessionId} (${event.messageCount} messages, ${event.durationMs ?? "?"}ms)`);
|
|
800
|
+
}
|
|
801
|
+
void sendEvent({
|
|
802
|
+
eventType: "session_end",
|
|
803
|
+
agent: config.agentName,
|
|
804
|
+
hookType: "session_end",
|
|
805
|
+
sessionId: conversationId,
|
|
806
|
+
conversationId,
|
|
807
|
+
durationMs: event.durationMs,
|
|
808
|
+
sessionStats: { messageCount: event.messageCount },
|
|
809
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
810
|
+
}, config, pluginFilePath);
|
|
811
|
+
});
|
|
812
|
+
api.on("llm_input", async (event, ctx) => {
|
|
813
|
+
const conversationId = resolveConversationId();
|
|
814
|
+
if (config.debug) {
|
|
815
|
+
debugLog(`LLM input: model=${event.model}, prompt length=${event.prompt?.length ?? 0}`);
|
|
816
|
+
}
|
|
817
|
+
void sendEvent({
|
|
818
|
+
eventType: "user_prompt",
|
|
819
|
+
agent: config.agentName,
|
|
820
|
+
hookType: "llm_input",
|
|
821
|
+
sessionId: conversationId,
|
|
822
|
+
conversationId,
|
|
823
|
+
model: event.model,
|
|
824
|
+
prompt: event.prompt,
|
|
825
|
+
textLength: event.prompt?.length ?? 0,
|
|
826
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
827
|
+
}, config, pluginFilePath);
|
|
828
|
+
});
|
|
829
|
+
api.on("llm_output", async (event, ctx) => {
|
|
830
|
+
const conversationId = resolveConversationId();
|
|
831
|
+
const responseText = event.assistantTexts?.join("\n") || "";
|
|
832
|
+
const textLength = responseText.length;
|
|
833
|
+
if (config.debug) {
|
|
834
|
+
debugLog(`LLM output: model=${event.model}, length=${textLength}${event.usage?.total ? `, tokens=${event.usage.total}` : ""}`);
|
|
835
|
+
}
|
|
836
|
+
void sendEvent({
|
|
837
|
+
eventType: "response",
|
|
838
|
+
agent: config.agentName,
|
|
839
|
+
hookType: "llm_output",
|
|
840
|
+
sessionId: conversationId,
|
|
841
|
+
conversationId,
|
|
842
|
+
model: event.model,
|
|
843
|
+
text: responseText,
|
|
844
|
+
textPreview: responseText.slice(0, 200),
|
|
845
|
+
textLength,
|
|
846
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
847
|
+
}, config, pluginFilePath);
|
|
848
|
+
});
|
|
849
|
+
api.on("agent_end", async (event, ctx) => {
|
|
850
|
+
const conversationId = resolveConversationId();
|
|
851
|
+
if (config.debug) {
|
|
852
|
+
debugLog(`Agent ended: success=${event.success}${event.durationMs ? `, duration=${event.durationMs}ms` : ""}${event.error ? `, error=${event.error}` : ""}`);
|
|
853
|
+
}
|
|
854
|
+
void sendEvent({
|
|
855
|
+
eventType: "stop",
|
|
856
|
+
agent: config.agentName,
|
|
857
|
+
hookType: "agent_end",
|
|
858
|
+
sessionId: conversationId,
|
|
859
|
+
conversationId,
|
|
860
|
+
status: event.success ? "completed" : "error",
|
|
861
|
+
durationMs: event.durationMs,
|
|
862
|
+
response: event.error || void 0,
|
|
863
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
864
|
+
}, config, pluginFilePath);
|
|
865
|
+
});
|
|
866
|
+
api.on("before_compaction", async (event, ctx) => {
|
|
867
|
+
const conversationId = resolveConversationId();
|
|
868
|
+
if (config.debug) {
|
|
869
|
+
debugLog(`Context compaction: ${event.messageCount} messages${event.tokenCount ? `, ${event.tokenCount} tokens` : ""}`);
|
|
870
|
+
}
|
|
871
|
+
void sendEvent({
|
|
872
|
+
eventType: "context_compact",
|
|
873
|
+
agent: config.agentName,
|
|
874
|
+
hookType: "before_compaction",
|
|
875
|
+
sessionId: conversationId,
|
|
876
|
+
conversationId,
|
|
877
|
+
trigger: `${event.messageCount} messages${event.tokenCount ? `, ${event.tokenCount} tokens` : ""}`,
|
|
878
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
879
|
+
}, config, pluginFilePath);
|
|
880
|
+
});
|
|
881
|
+
api.on("subagent_spawned", async (event, ctx) => {
|
|
882
|
+
const conversationId = resolveConversationId();
|
|
883
|
+
if (config.debug) {
|
|
884
|
+
debugLog(`Subagent spawned: ${event.agentId} (${event.mode}${event.label ? `, label=${event.label}` : ""})`);
|
|
885
|
+
}
|
|
886
|
+
void sendEvent({
|
|
887
|
+
eventType: "subagent_start",
|
|
888
|
+
agent: config.agentName,
|
|
889
|
+
hookType: "subagent_spawned",
|
|
890
|
+
sessionId: conversationId,
|
|
891
|
+
conversationId,
|
|
892
|
+
subagentType: event.agentId,
|
|
893
|
+
subagentMode: event.mode,
|
|
894
|
+
subagentLabel: event.label,
|
|
895
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
896
|
+
}, config, pluginFilePath);
|
|
897
|
+
});
|
|
898
|
+
api.on("subagent_ended", async (event, ctx) => {
|
|
899
|
+
const conversationId = resolveConversationId();
|
|
900
|
+
if (config.debug) {
|
|
901
|
+
debugLog(`Subagent ended: ${event.targetKind} reason=${event.reason}${event.outcome ? `, outcome=${event.outcome}` : ""}`);
|
|
902
|
+
}
|
|
903
|
+
void sendEvent({
|
|
904
|
+
eventType: "subagent_stop",
|
|
905
|
+
agent: config.agentName,
|
|
906
|
+
hookType: "subagent_ended",
|
|
907
|
+
sessionId: conversationId,
|
|
908
|
+
conversationId,
|
|
909
|
+
status: event.outcome || event.reason,
|
|
910
|
+
subagentType: event.targetKind,
|
|
911
|
+
response: event.error || void 0,
|
|
912
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
913
|
+
}, config, pluginFilePath);
|
|
914
|
+
});
|
|
570
915
|
api.registerHook(
|
|
571
916
|
["command:new", "command:stop", "command:reset"],
|
|
572
917
|
async (event) => {
|
|
@@ -575,13 +920,17 @@ function register(api) {
|
|
|
575
920
|
stop: "session_end",
|
|
576
921
|
reset: "session_start"
|
|
577
922
|
};
|
|
923
|
+
if (config.debug) {
|
|
924
|
+
debugLog(`Command event: ${event.action}`);
|
|
925
|
+
}
|
|
578
926
|
void sendEvent({
|
|
579
927
|
toolName: `command:${event.action}`,
|
|
580
928
|
toolType: "command",
|
|
581
929
|
eventType: eventTypeMap[event.action] || "command_event",
|
|
582
930
|
agent: config.agentName,
|
|
583
931
|
hookType: "command_event",
|
|
584
|
-
sessionId:
|
|
932
|
+
sessionId: resolveConversationId(),
|
|
933
|
+
conversationId: resolveConversationId(),
|
|
585
934
|
timestamp: event.timestamp.toISOString()
|
|
586
935
|
}, config, pluginFilePath);
|
|
587
936
|
},
|
|
@@ -594,37 +943,40 @@ function register(api) {
|
|
|
594
943
|
const provider = event.context.channelId;
|
|
595
944
|
const peer = isInbound ? event.context.from : event.context.to;
|
|
596
945
|
const channelLabel = [provider, peer].filter(Boolean).join(":") || void 0;
|
|
946
|
+
if (config.debug) {
|
|
947
|
+
debugLog(`Message event: ${event.action} ${channelLabel || "(no channel)"}`);
|
|
948
|
+
}
|
|
597
949
|
const payload = {
|
|
598
950
|
toolName: `message:${event.action}`,
|
|
599
951
|
toolType: "message_event",
|
|
600
|
-
eventType: isInbound ? "
|
|
952
|
+
eventType: isInbound ? "user_prompt" : "response",
|
|
601
953
|
agent: config.agentName,
|
|
602
954
|
hookType: "session_event",
|
|
603
|
-
sessionId:
|
|
955
|
+
sessionId: resolveConversationId(),
|
|
956
|
+
conversationId: resolveConversationId(),
|
|
604
957
|
timestamp: event.timestamp.toISOString(),
|
|
605
958
|
cwd: channelLabel
|
|
606
959
|
};
|
|
607
960
|
const content = event.context.content;
|
|
608
961
|
if (content) {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
payload.textPreview = content.slice(0, 50);
|
|
613
|
-
}
|
|
962
|
+
payload.text = content;
|
|
963
|
+
payload.textPreview = content.slice(0, 200);
|
|
964
|
+
payload.textLength = content.length;
|
|
614
965
|
if (isInbound && peer) {
|
|
615
|
-
payload.prompt = `${peer}: ${content}
|
|
966
|
+
payload.prompt = `${peer}: ${content}`;
|
|
616
967
|
}
|
|
617
968
|
}
|
|
618
969
|
void sendEvent(payload, config, pluginFilePath);
|
|
619
970
|
},
|
|
620
971
|
{ name: "agentapprove-session-monitor", description: "Log message events to Agent Approve" }
|
|
621
972
|
);
|
|
622
|
-
|
|
973
|
+
const hookCount = 10;
|
|
974
|
+
api.logger.info(`Agent Approve: Registered ${hookCount} plugin hooks + event monitoring`);
|
|
623
975
|
}
|
|
624
976
|
var index_default = {
|
|
625
977
|
id: "openclaw",
|
|
626
978
|
name: "Agent Approve",
|
|
627
|
-
description: "
|
|
979
|
+
description: "Agent Approve mobile approval for AI agent tool execution",
|
|
628
980
|
register
|
|
629
981
|
};
|
|
630
982
|
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.5",
|
|
6
6
|
"homepage": "https://agentapprove.com",
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED