@agentapprove/openclaw 0.1.2 → 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 +601 -98
- 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 } 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,19 +295,34 @@ 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
|
|
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");
|
|
85
301
|
let e2eUserKey;
|
|
86
302
|
let e2eServerKey;
|
|
87
|
-
if (e2eEnabled
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
303
|
+
if (e2eEnabled) {
|
|
304
|
+
if (existsSync3(e2eUserKeyPath)) {
|
|
305
|
+
try {
|
|
306
|
+
e2eUserKey = readFileSync3(e2eUserKeyPath, "utf-8").trim();
|
|
307
|
+
} catch {
|
|
308
|
+
logger?.warn("Failed to load E2E user key");
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (existsSync3(e2eServerKeyPath)) {
|
|
312
|
+
try {
|
|
313
|
+
e2eServerKey = readFileSync3(e2eServerKeyPath, "utf-8").trim();
|
|
314
|
+
} catch {
|
|
315
|
+
logger?.warn("Failed to load E2E server key");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (e2eUserKey) {
|
|
319
|
+
e2eUserKey = checkAndRotateKeys(e2eUserKey) || e2eUserKey;
|
|
94
320
|
}
|
|
95
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}`);
|
|
325
|
+
}
|
|
96
326
|
return {
|
|
97
327
|
apiUrl,
|
|
98
328
|
apiVersion,
|
|
@@ -116,7 +346,7 @@ import { URL } from "url";
|
|
|
116
346
|
|
|
117
347
|
// src/hmac.ts
|
|
118
348
|
import crypto from "crypto";
|
|
119
|
-
import { readFileSync as
|
|
349
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
120
350
|
function generateHMACSignature(body, token, hookVersion) {
|
|
121
351
|
const timestamp = Math.floor(Date.now() / 1e3);
|
|
122
352
|
const message = `${hookVersion}:${timestamp}:${body}`;
|
|
@@ -125,7 +355,7 @@ function generateHMACSignature(body, token, hookVersion) {
|
|
|
125
355
|
}
|
|
126
356
|
function computePluginHash(pluginPath) {
|
|
127
357
|
try {
|
|
128
|
-
const content =
|
|
358
|
+
const content = readFileSync4(pluginPath, "utf-8");
|
|
129
359
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
130
360
|
} catch {
|
|
131
361
|
return "";
|
|
@@ -143,6 +373,28 @@ function buildHMACHeaders(body, token, hookVersion, pluginHash) {
|
|
|
143
373
|
|
|
144
374
|
// src/privacy.ts
|
|
145
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
|
+
}
|
|
146
398
|
function applyPrivacyFilter(request, privacyTier) {
|
|
147
399
|
if (privacyTier === "full") {
|
|
148
400
|
return request;
|
|
@@ -154,54 +406,51 @@ function applyPrivacyFilter(request, privacyTier) {
|
|
|
154
406
|
delete filtered.cwd;
|
|
155
407
|
return filtered;
|
|
156
408
|
}
|
|
157
|
-
if (filtered.command
|
|
158
|
-
filtered.command = filtered.command
|
|
409
|
+
if (filtered.command) {
|
|
410
|
+
filtered.command = truncate(redactSensitive(filtered.command), SUMMARY_MAX_LENGTH);
|
|
159
411
|
}
|
|
160
412
|
if (filtered.toolInput) {
|
|
161
|
-
|
|
162
|
-
if (inputStr.length > SUMMARY_MAX_LENGTH) {
|
|
163
|
-
filtered.toolInput = { _summary: inputStr.slice(0, SUMMARY_MAX_LENGTH) + "..." };
|
|
164
|
-
}
|
|
413
|
+
filtered.toolInput = summarizeToolInput(filtered.toolInput);
|
|
165
414
|
}
|
|
166
415
|
return filtered;
|
|
167
416
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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;
|
|
180
430
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
431
|
+
const filtered = { ...event };
|
|
432
|
+
if (privacyTier === "minimal") {
|
|
433
|
+
for (const field of CONTENT_FIELDS) {
|
|
434
|
+
delete filtered[field];
|
|
435
|
+
}
|
|
436
|
+
if (typeof filtered.textLength === "undefined" && typeof event.text === "string") {
|
|
437
|
+
filtered.textLength = event.text.length;
|
|
438
|
+
}
|
|
439
|
+
return filtered;
|
|
184
440
|
}
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
`);
|
|
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);
|
|
193
448
|
}
|
|
194
|
-
} catch {
|
|
195
449
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
try {
|
|
199
|
-
ensureLogFile();
|
|
200
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
|
|
201
|
-
appendFileSync(DEBUG_LOG_PATH, `[${ts}] [${hookName}] ${message}
|
|
202
|
-
`);
|
|
203
|
-
} catch {
|
|
450
|
+
if (typeof filtered.filePath === "string") {
|
|
451
|
+
filtered.filePath = truncate(filtered.filePath, FILEPATH_MAX_LENGTH);
|
|
204
452
|
}
|
|
453
|
+
return filtered;
|
|
205
454
|
}
|
|
206
455
|
|
|
207
456
|
// src/api-client.ts
|
|
@@ -256,8 +505,16 @@ async function sendApprovalRequest(request, config, pluginPath) {
|
|
|
256
505
|
if (!config.token) {
|
|
257
506
|
throw new Error("No Agent Approve token configured");
|
|
258
507
|
}
|
|
259
|
-
|
|
260
|
-
|
|
508
|
+
let payload;
|
|
509
|
+
if (config.e2eEnabled && config.e2eUserKey) {
|
|
510
|
+
payload = applyApprovalE2E(request, config.e2eUserKey, config.e2eServerKey);
|
|
511
|
+
if (config.debug) {
|
|
512
|
+
debugLog("E2E encryption applied to approval request");
|
|
513
|
+
}
|
|
514
|
+
} else {
|
|
515
|
+
payload = applyPrivacyFilter(request, config.privacyTier);
|
|
516
|
+
}
|
|
517
|
+
const bodyStr = JSON.stringify(payload);
|
|
261
518
|
const pluginHash = getPluginHash(pluginPath);
|
|
262
519
|
const hmacHeaders = buildHMACHeaders(bodyStr, config.token, config.hookVersion, pluginHash);
|
|
263
520
|
const headers = {
|
|
@@ -283,8 +540,20 @@ async function sendApprovalRequest(request, config, pluginPath) {
|
|
|
283
540
|
}
|
|
284
541
|
async function sendEvent(event, config, pluginPath) {
|
|
285
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
|
+
}
|
|
286
548
|
try {
|
|
287
|
-
|
|
549
|
+
let payload = applyEventPrivacyFilter(event, config.privacyTier);
|
|
550
|
+
if (config.e2eEnabled && config.e2eUserKey) {
|
|
551
|
+
payload = applyEventE2E(payload, config.e2eUserKey);
|
|
552
|
+
if (config.debug) {
|
|
553
|
+
debugLog(`E2E applied to event (type=${eventType})`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const bodyStr = JSON.stringify(payload);
|
|
288
557
|
const pluginHash = getPluginHash(pluginPath);
|
|
289
558
|
const hmacHeaders = buildHMACHeaders(bodyStr, config.token, config.hookVersion, pluginHash);
|
|
290
559
|
const headers = {
|
|
@@ -292,10 +561,18 @@ async function sendEvent(event, config, pluginPath) {
|
|
|
292
561
|
...hmacHeaders
|
|
293
562
|
};
|
|
294
563
|
const url = `${config.apiUrl}/${config.apiVersion}/events`;
|
|
295
|
-
await httpPost(url, bodyStr, headers, 5e3);
|
|
296
|
-
} catch {
|
|
297
564
|
if (config.debug) {
|
|
298
|
-
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)}`);
|
|
299
576
|
}
|
|
300
577
|
}
|
|
301
578
|
}
|
|
@@ -307,19 +584,34 @@ try {
|
|
|
307
584
|
} catch {
|
|
308
585
|
pluginFilePath = __filename;
|
|
309
586
|
}
|
|
310
|
-
var gatewaySessionId =
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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;
|
|
318
596
|
}
|
|
319
|
-
function
|
|
320
|
-
|
|
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;
|
|
321
613
|
}
|
|
322
|
-
function classifyTool(toolName) {
|
|
614
|
+
function classifyTool(toolName, params) {
|
|
323
615
|
const lower = toolName.toLowerCase();
|
|
324
616
|
if (lower === "exec" || lower === "process") {
|
|
325
617
|
return { toolType: "shell_command", displayName: toolName };
|
|
@@ -334,6 +626,9 @@ function classifyTool(toolName) {
|
|
|
334
626
|
return { toolType: "browser", displayName: toolName };
|
|
335
627
|
}
|
|
336
628
|
if (lower === "message" || lower === "agent_send") {
|
|
629
|
+
const action = params?.action;
|
|
630
|
+
if (action === "send") return { toolType: "message_send", displayName: toolName };
|
|
631
|
+
if (action === "read") return { toolType: "message_read", displayName: toolName };
|
|
337
632
|
return { toolType: "message", displayName: toolName };
|
|
338
633
|
}
|
|
339
634
|
if (lower === "sessions_spawn" || lower === "sessions_send") {
|
|
@@ -358,11 +653,47 @@ function extractCommand(toolName, params) {
|
|
|
358
653
|
if (lower === "apply_patch") {
|
|
359
654
|
return "apply_patch";
|
|
360
655
|
}
|
|
656
|
+
if (lower === "message" || lower === "agent_send") {
|
|
657
|
+
const action = params.action;
|
|
658
|
+
const target = params.target || params.to || params.channelId || params.channel_id;
|
|
659
|
+
const provider = params.channel;
|
|
660
|
+
const channelLabel = target ? provider ? `${provider}:${target}` : target : provider || "channel";
|
|
661
|
+
if (action === "send") {
|
|
662
|
+
const msg = params.message;
|
|
663
|
+
return msg ? `[${channelLabel}] ${msg}` : `Send to ${channelLabel}`;
|
|
664
|
+
}
|
|
665
|
+
if (action === "read") {
|
|
666
|
+
return `Read from ${channelLabel}`;
|
|
667
|
+
}
|
|
668
|
+
return void 0;
|
|
669
|
+
}
|
|
361
670
|
return void 0;
|
|
362
671
|
}
|
|
672
|
+
function extractResultPreview(toolName, params, result, maxLen = 300) {
|
|
673
|
+
if (!result) return void 0;
|
|
674
|
+
const lower = toolName.toLowerCase();
|
|
675
|
+
if (lower === "message" && params.action === "read") {
|
|
676
|
+
const res = result;
|
|
677
|
+
const messages = res.messages;
|
|
678
|
+
if (Array.isArray(messages) && messages.length > 0) {
|
|
679
|
+
const previews = messages.slice(0, 5).map((m) => {
|
|
680
|
+
const author = m.user || "?";
|
|
681
|
+
const text = m.text || "";
|
|
682
|
+
return `${author}: ${text}`;
|
|
683
|
+
});
|
|
684
|
+
const summary = previews.join("\n");
|
|
685
|
+
return summary.length > maxLen ? summary.slice(0, maxLen) + "..." : summary;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
const str = typeof result === "string" ? result : JSON.stringify(result);
|
|
689
|
+
if (str.length <= maxLen) return str;
|
|
690
|
+
return str.slice(0, maxLen) + "...";
|
|
691
|
+
}
|
|
363
692
|
function handleFailBehavior(config, error, toolName, logger) {
|
|
364
693
|
logger.warn(`Agent Approve API error for tool "${toolName}": ${error.message}`);
|
|
365
|
-
|
|
694
|
+
if (config.debug) {
|
|
695
|
+
debugLog(`API error: ${error.message}, failBehavior: ${config.failBehavior}`);
|
|
696
|
+
}
|
|
366
697
|
switch (config.failBehavior) {
|
|
367
698
|
case "deny":
|
|
368
699
|
return { block: true, blockReason: "Agent Approve unavailable, denying by policy" };
|
|
@@ -383,9 +714,12 @@ function register(api) {
|
|
|
383
714
|
return;
|
|
384
715
|
}
|
|
385
716
|
api.logger.info(`Agent Approve: Plugin loaded (privacy: ${config.privacyTier}, fail: ${config.failBehavior})`);
|
|
386
|
-
|
|
717
|
+
if (config.debug) {
|
|
718
|
+
debugLog(`Plugin loaded, API: ${config.apiUrl}, agent: ${config.agentName}`);
|
|
719
|
+
}
|
|
387
720
|
api.on("before_tool_call", async (event, ctx) => {
|
|
388
|
-
const
|
|
721
|
+
const conversationId = resolveConversationId(ctx.sessionKey);
|
|
722
|
+
const { toolType, displayName } = classifyTool(event.toolName, event.params);
|
|
389
723
|
const command = extractCommand(event.toolName, event.params);
|
|
390
724
|
const request = {
|
|
391
725
|
toolName: displayName,
|
|
@@ -394,17 +728,22 @@ function register(api) {
|
|
|
394
728
|
toolInput: event.params,
|
|
395
729
|
agent: config.agentName,
|
|
396
730
|
hookType: "before_tool_call",
|
|
397
|
-
sessionId:
|
|
731
|
+
sessionId: conversationId,
|
|
732
|
+
conversationId,
|
|
398
733
|
cwd: event.params.workdir || void 0,
|
|
399
734
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
400
735
|
};
|
|
401
736
|
try {
|
|
402
737
|
const response = await sendApprovalRequest(request, config, pluginFilePath);
|
|
403
738
|
if (response.decision === "approve" || response.decision === "allow") {
|
|
404
|
-
|
|
739
|
+
if (config.debug) {
|
|
740
|
+
debugLog(`Tool "${event.toolName}" approved${response.reason ? ": " + response.reason : ""}`);
|
|
741
|
+
}
|
|
405
742
|
return void 0;
|
|
406
743
|
}
|
|
407
|
-
|
|
744
|
+
if (config.debug) {
|
|
745
|
+
debugLog(`Tool "${event.toolName}" denied${response.reason ? ": " + response.reason : ""}`);
|
|
746
|
+
}
|
|
408
747
|
return {
|
|
409
748
|
block: true,
|
|
410
749
|
blockReason: response.reason || "Denied by Agent Approve"
|
|
@@ -419,30 +758,183 @@ function register(api) {
|
|
|
419
758
|
}
|
|
420
759
|
});
|
|
421
760
|
api.on("after_tool_call", async (event, ctx) => {
|
|
422
|
-
const
|
|
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
|
+
}
|
|
768
|
+
const { toolType } = classifyTool(event.toolName, event.params);
|
|
769
|
+
const resultPreview = extractResultPreview(event.toolName, event.params, event.result);
|
|
423
770
|
void sendEvent({
|
|
424
771
|
toolName: event.toolName,
|
|
425
772
|
toolType,
|
|
773
|
+
eventType: "tool_complete",
|
|
426
774
|
agent: config.agentName,
|
|
427
775
|
hookType: "after_tool_call",
|
|
428
|
-
sessionId:
|
|
776
|
+
sessionId: conversationId,
|
|
777
|
+
conversationId,
|
|
778
|
+
command: extractCommand(event.toolName, event.params),
|
|
429
779
|
status: event.error ? "error" : "success",
|
|
430
|
-
|
|
780
|
+
response: event.error || resultPreview || void 0,
|
|
431
781
|
durationMs: event.durationMs,
|
|
432
782
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
433
783
|
}, config, pluginFilePath);
|
|
434
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
|
+
});
|
|
435
918
|
api.registerHook(
|
|
436
919
|
["command:new", "command:stop", "command:reset"],
|
|
437
920
|
async (event) => {
|
|
921
|
+
const eventTypeMap = {
|
|
922
|
+
new: "session_start",
|
|
923
|
+
stop: "session_end",
|
|
924
|
+
reset: "session_start"
|
|
925
|
+
};
|
|
926
|
+
if (config.debug) {
|
|
927
|
+
debugLog(`Command event: ${event.action}`);
|
|
928
|
+
}
|
|
438
929
|
void sendEvent({
|
|
439
930
|
toolName: `command:${event.action}`,
|
|
440
931
|
toolType: "command",
|
|
932
|
+
eventType: eventTypeMap[event.action] || "command_event",
|
|
441
933
|
agent: config.agentName,
|
|
442
934
|
hookType: "command_event",
|
|
443
|
-
sessionId: event.sessionKey
|
|
444
|
-
|
|
445
|
-
|
|
935
|
+
sessionId: resolveConversationId(event.sessionKey),
|
|
936
|
+
conversationId: resolveConversationId(event.sessionKey),
|
|
937
|
+
timestamp: event.timestamp.toISOString()
|
|
446
938
|
}, config, pluginFilePath);
|
|
447
939
|
},
|
|
448
940
|
{ name: "agentapprove-command-monitor", description: "Log command events to Agent Approve" }
|
|
@@ -450,33 +942,44 @@ function register(api) {
|
|
|
450
942
|
api.registerHook(
|
|
451
943
|
["message:received", "message:sent"],
|
|
452
944
|
async (event) => {
|
|
453
|
-
const
|
|
945
|
+
const isInbound = event.action === "received";
|
|
946
|
+
const provider = event.context.channelId;
|
|
947
|
+
const peer = isInbound ? event.context.from : event.context.to;
|
|
948
|
+
const channelLabel = [provider, peer].filter(Boolean).join(":") || void 0;
|
|
949
|
+
if (config.debug) {
|
|
950
|
+
debugLog(`Message event: ${event.action} ${channelLabel || "(no channel)"}`);
|
|
951
|
+
}
|
|
454
952
|
const payload = {
|
|
455
953
|
toolName: `message:${event.action}`,
|
|
456
954
|
toolType: "message_event",
|
|
955
|
+
eventType: isInbound ? "user_prompt" : "response",
|
|
457
956
|
agent: config.agentName,
|
|
458
957
|
hookType: "session_event",
|
|
459
|
-
sessionId: event.sessionKey
|
|
958
|
+
sessionId: resolveConversationId(event.sessionKey),
|
|
959
|
+
conversationId: resolveConversationId(event.sessionKey),
|
|
460
960
|
timestamp: event.timestamp.toISOString(),
|
|
461
|
-
|
|
462
|
-
direction,
|
|
463
|
-
channelId: event.context.channelId,
|
|
464
|
-
sessionKey: event.sessionKey
|
|
465
|
-
}
|
|
961
|
+
cwd: channelLabel
|
|
466
962
|
};
|
|
467
|
-
|
|
468
|
-
|
|
963
|
+
const content = event.context.content;
|
|
964
|
+
if (content) {
|
|
965
|
+
payload.text = content;
|
|
966
|
+
payload.textPreview = content.slice(0, 200);
|
|
967
|
+
payload.textLength = content.length;
|
|
968
|
+
if (isInbound && peer) {
|
|
969
|
+
payload.prompt = `${peer}: ${content}`;
|
|
970
|
+
}
|
|
469
971
|
}
|
|
470
972
|
void sendEvent(payload, config, pluginFilePath);
|
|
471
973
|
},
|
|
472
974
|
{ name: "agentapprove-session-monitor", description: "Log message events to Agent Approve" }
|
|
473
975
|
);
|
|
474
|
-
|
|
976
|
+
const hookCount = 10;
|
|
977
|
+
api.logger.info(`Agent Approve: Registered ${hookCount} plugin hooks + event monitoring`);
|
|
475
978
|
}
|
|
476
979
|
var index_default = {
|
|
477
980
|
id: "openclaw",
|
|
478
981
|
name: "Agent Approve",
|
|
479
|
-
description: "
|
|
982
|
+
description: "Agent Approve mobile approval for AI agent tool execution",
|
|
480
983
|
register
|
|
481
984
|
};
|
|
482
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