@agentapprove/openclaw 0.1.5 → 0.1.7
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 +306 -66
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -35,22 +35,77 @@ function ensureLogFile() {
|
|
|
35
35
|
if (stat.size > MAX_SIZE) {
|
|
36
36
|
const content = readFileSync(DEBUG_LOG_PATH, "utf-8");
|
|
37
37
|
writeFileSync(DEBUG_LOG_PATH, content.slice(-KEEP_SIZE), { mode: 384 });
|
|
38
|
-
|
|
39
|
-
appendFileSync(DEBUG_LOG_PATH, `[${ts}] [openclaw-plugin] Log rotated (exceeded 5MB)
|
|
38
|
+
appendFileSync(DEBUG_LOG_PATH, `[${localTimestamp()}] [debug] Log rotated (exceeded 5MB, kept last 2MB)
|
|
40
39
|
`);
|
|
41
40
|
}
|
|
42
41
|
} catch {
|
|
43
42
|
}
|
|
44
43
|
}
|
|
44
|
+
function localTimestamp() {
|
|
45
|
+
const d = /* @__PURE__ */ new Date();
|
|
46
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
47
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
48
|
+
}
|
|
45
49
|
function debugLog(message, hookName = "openclaw-plugin") {
|
|
46
50
|
try {
|
|
47
51
|
ensureLogFile();
|
|
48
|
-
|
|
49
|
-
appendFileSync(DEBUG_LOG_PATH, `[${ts}] [${hookName}] ${message}
|
|
52
|
+
appendFileSync(DEBUG_LOG_PATH, `[${localTimestamp()}] [${hookName}] ${message}
|
|
50
53
|
`);
|
|
51
54
|
} catch {
|
|
52
55
|
}
|
|
53
56
|
}
|
|
57
|
+
function debugLogRawInline(data) {
|
|
58
|
+
try {
|
|
59
|
+
ensureLogFile();
|
|
60
|
+
appendFileSync(DEBUG_LOG_PATH, `${data}
|
|
61
|
+
`);
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function safeStringify(value) {
|
|
66
|
+
try {
|
|
67
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
68
|
+
return JSON.stringify(value, (_key, current) => {
|
|
69
|
+
if (typeof current === "bigint") return `BigInt(${current.toString()})`;
|
|
70
|
+
if (typeof current === "function") {
|
|
71
|
+
const fn = current;
|
|
72
|
+
return `[Function ${fn.name || "anonymous"}]`;
|
|
73
|
+
}
|
|
74
|
+
if (typeof current === "symbol") return current.toString();
|
|
75
|
+
if (current instanceof Error) {
|
|
76
|
+
return {
|
|
77
|
+
name: current.name,
|
|
78
|
+
message: current.message,
|
|
79
|
+
stack: current.stack
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (current && typeof current === "object") {
|
|
83
|
+
const obj = current;
|
|
84
|
+
if (seen.has(obj)) return "[Circular]";
|
|
85
|
+
seen.add(obj);
|
|
86
|
+
}
|
|
87
|
+
return current;
|
|
88
|
+
}) ?? "null";
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
91
|
+
return `"[Unserializable: ${msg}]"`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function debugLogRaw(data, hookName = "openclaw-plugin") {
|
|
95
|
+
try {
|
|
96
|
+
ensureLogFile();
|
|
97
|
+
const ts = localTimestamp();
|
|
98
|
+
const rawData = typeof data === "string" ? data : safeStringify(data);
|
|
99
|
+
appendFileSync(
|
|
100
|
+
DEBUG_LOG_PATH,
|
|
101
|
+
`[${ts}] [${hookName}] === RAW INPUT ===
|
|
102
|
+
${rawData}
|
|
103
|
+
[${ts}] [${hookName}] === END RAW ===
|
|
104
|
+
`
|
|
105
|
+
);
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
}
|
|
54
109
|
|
|
55
110
|
// src/e2e-crypto.ts
|
|
56
111
|
function keyId(keyHex) {
|
|
@@ -154,7 +209,7 @@ function deriveEpochKey(rootKeyHex, epoch) {
|
|
|
154
209
|
}
|
|
155
210
|
return current;
|
|
156
211
|
}
|
|
157
|
-
function migrateRootKey() {
|
|
212
|
+
function migrateRootKey(debug = false) {
|
|
158
213
|
if (!existsSync2(E2E_KEY_FILE) || existsSync2(E2E_ROOT_KEY_FILE)) return;
|
|
159
214
|
try {
|
|
160
215
|
copyFileSync(E2E_KEY_FILE, E2E_ROOT_KEY_FILE);
|
|
@@ -173,12 +228,12 @@ function migrateRootKey() {
|
|
|
173
228
|
};
|
|
174
229
|
writeFileSync2(E2E_ROTATION_FILE, JSON.stringify(config, null, 2), { mode: 384 });
|
|
175
230
|
}
|
|
176
|
-
debugLog("Migrated e2e-key to e2e-root-key");
|
|
231
|
+
if (debug) debugLog("Migrated e2e-key to e2e-root-key");
|
|
177
232
|
} catch {
|
|
178
233
|
}
|
|
179
234
|
}
|
|
180
|
-
function checkAndRotateKeys(currentKeyHex) {
|
|
181
|
-
migrateRootKey();
|
|
235
|
+
function checkAndRotateKeys(currentKeyHex, debug = false) {
|
|
236
|
+
migrateRootKey(debug);
|
|
182
237
|
if (!existsSync2(E2E_ROTATION_FILE) || !existsSync2(E2E_ROOT_KEY_FILE)) {
|
|
183
238
|
return currentKeyHex;
|
|
184
239
|
}
|
|
@@ -217,7 +272,7 @@ function checkAndRotateKeys(currentKeyHex) {
|
|
|
217
272
|
writeFileSync2(E2E_ROTATION_FILE, JSON.stringify(rotCfg, null, 2), { mode: 384 });
|
|
218
273
|
} catch {
|
|
219
274
|
}
|
|
220
|
-
debugLog(`E2E key rotated: epoch ${currentEpoch} -> ${expectedEpoch}`);
|
|
275
|
+
if (debug) debugLog(`E2E key rotated: epoch ${currentEpoch} -> ${expectedEpoch}`);
|
|
221
276
|
return newKey;
|
|
222
277
|
}
|
|
223
278
|
|
|
@@ -293,7 +348,7 @@ function loadConfig(openclawConfig, logger) {
|
|
|
293
348
|
const timeout = openclawConfig?.timeout || parseInt(process.env.AGENTAPPROVE_TIMEOUT || "", 10) || parseInt(parseConfigValue(fileContent, "AGENTAPPROVE_TIMEOUT") || "", 10) || 300;
|
|
294
349
|
const failBehavior = openclawConfig?.failBehavior || process.env.AGENTAPPROVE_FAIL_BEHAVIOR || parseConfigValue(fileContent, "AGENTAPPROVE_FAIL_BEHAVIOR") || "ask";
|
|
295
350
|
const privacyTier = openclawConfig?.privacyTier || process.env.AGENTAPPROVE_PRIVACY || parseConfigValue(fileContent, "AGENTAPPROVE_PRIVACY") || "full";
|
|
296
|
-
const debug = openclawConfig?.debug || process.env.AGENTAPPROVE_DEBUG === "true" || parseConfigValue(fileContent, "AGENTAPPROVE_DEBUG") === "true" || false;
|
|
351
|
+
const debug = openclawConfig?.debug || process.env.AGENTAPPROVE_DEBUG === "true" || process.env.AGENTAPPROVE_DEBUG_LOG === "true" || parseConfigValue(fileContent, "AGENTAPPROVE_DEBUG") === "true" || parseConfigValue(fileContent, "AGENTAPPROVE_DEBUG_LOG") === "true" || false;
|
|
297
352
|
const agentName = process.env.AGENTAPPROVE_AGENT_NAME || parseConfigValue(fileContent, "AGENTAPPROVE_OPENCLAW_NAME") || "OpenClaw";
|
|
298
353
|
const e2eEnabled = process.env.AGENTAPPROVE_E2E_ENABLED === "true" || parseConfigValue(fileContent, "AGENTAPPROVE_E2E_ENABLED") === "true";
|
|
299
354
|
const e2eUserKeyPath = join3(homedir3(), ".agentapprove", "e2e-key");
|
|
@@ -316,7 +371,7 @@ function loadConfig(openclawConfig, logger) {
|
|
|
316
371
|
}
|
|
317
372
|
}
|
|
318
373
|
if (e2eUserKey) {
|
|
319
|
-
e2eUserKey = checkAndRotateKeys(e2eUserKey) || e2eUserKey;
|
|
374
|
+
e2eUserKey = checkAndRotateKeys(e2eUserKey, debug) || e2eUserKey;
|
|
320
375
|
}
|
|
321
376
|
}
|
|
322
377
|
if (debug) {
|
|
@@ -331,7 +386,7 @@ function loadConfig(openclawConfig, logger) {
|
|
|
331
386
|
failBehavior,
|
|
332
387
|
privacyTier,
|
|
333
388
|
debug,
|
|
334
|
-
hookVersion: "1.1.
|
|
389
|
+
hookVersion: "1.1.4",
|
|
335
390
|
agentName,
|
|
336
391
|
e2eEnabled,
|
|
337
392
|
e2eUserKey,
|
|
@@ -453,13 +508,143 @@ function applyEventPrivacyFilter(event, privacyTier) {
|
|
|
453
508
|
return filtered;
|
|
454
509
|
}
|
|
455
510
|
|
|
511
|
+
// src/config-sync.ts
|
|
512
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync4, renameSync } from "fs";
|
|
513
|
+
import { join as join4 } from "path";
|
|
514
|
+
import { homedir as homedir4 } from "os";
|
|
515
|
+
function getAADir() {
|
|
516
|
+
return process.env.__AGENTAPPROVE_TEST_DIR || join4(homedir4(), ".agentapprove");
|
|
517
|
+
}
|
|
518
|
+
function getEnvPath() {
|
|
519
|
+
return join4(getAADir(), "env");
|
|
520
|
+
}
|
|
521
|
+
function getRotationFile() {
|
|
522
|
+
return join4(getAADir(), "e2e-rotation.json");
|
|
523
|
+
}
|
|
524
|
+
var VALID_PRIVACY = /* @__PURE__ */ new Set(["minimal", "summary", "full"]);
|
|
525
|
+
var VALID_FAIL = /* @__PURE__ */ new Set(["allow", "deny", "ask"]);
|
|
526
|
+
function updateEnvValues(updates) {
|
|
527
|
+
const envPath = getEnvPath();
|
|
528
|
+
if (!existsSync4(envPath)) return;
|
|
529
|
+
const validUpdates = Object.fromEntries(
|
|
530
|
+
Object.entries(updates).filter(([, v]) => !/[`$(){};<>|&!\\]/.test(v))
|
|
531
|
+
);
|
|
532
|
+
const updateKeys = new Set(Object.keys(validUpdates));
|
|
533
|
+
if (updateKeys.size === 0) return;
|
|
534
|
+
try {
|
|
535
|
+
const content = readFileSync5(envPath, "utf-8");
|
|
536
|
+
const lines = content.split("\n");
|
|
537
|
+
const filtered = lines.filter((line) => {
|
|
538
|
+
const trimmed = line.replace(/^export\s+/, "").trim();
|
|
539
|
+
const eqIdx = trimmed.indexOf("=");
|
|
540
|
+
if (eqIdx <= 0) return true;
|
|
541
|
+
return !updateKeys.has(trimmed.slice(0, eqIdx));
|
|
542
|
+
});
|
|
543
|
+
for (const [key, value] of Object.entries(validUpdates)) {
|
|
544
|
+
filtered.push(`export ${key}=${value}`);
|
|
545
|
+
}
|
|
546
|
+
const tmpPath = `${envPath}.tmp.${process.pid}`;
|
|
547
|
+
writeFileSync3(tmpPath, filtered.join("\n"), { mode: 384 });
|
|
548
|
+
renameSync(tmpPath, envPath);
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function syncRotationConfig(serverPeriod, serverStartedAt, debug) {
|
|
553
|
+
const rotationFile = getRotationFile();
|
|
554
|
+
if (!existsSync4(rotationFile)) return;
|
|
555
|
+
try {
|
|
556
|
+
const raw = readFileSync5(rotationFile, "utf-8");
|
|
557
|
+
const config = JSON.parse(raw);
|
|
558
|
+
const currentPeriod = config.periodSeconds ?? 0;
|
|
559
|
+
const newPeriod = serverPeriod ?? 0;
|
|
560
|
+
let needsUpdate = false;
|
|
561
|
+
if (typeof newPeriod === "number" && newPeriod !== currentPeriod) {
|
|
562
|
+
config.periodSeconds = newPeriod;
|
|
563
|
+
needsUpdate = true;
|
|
564
|
+
}
|
|
565
|
+
if (serverStartedAt && (!config.startedAt || config.startedAt === "null")) {
|
|
566
|
+
config.startedAt = serverStartedAt;
|
|
567
|
+
needsUpdate = true;
|
|
568
|
+
}
|
|
569
|
+
if (needsUpdate) {
|
|
570
|
+
const tmpPath = `${rotationFile}.tmp.${process.pid}`;
|
|
571
|
+
writeFileSync3(tmpPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
572
|
+
renameSync(tmpPath, rotationFile);
|
|
573
|
+
if (debug) {
|
|
574
|
+
debugLog(`E2E rotation synced: periodSeconds=${newPeriod}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
} catch {
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function processConfigSync(parsed, config) {
|
|
581
|
+
const sync = parsed.configSync;
|
|
582
|
+
if (!sync || !sync.configSetAt) return;
|
|
583
|
+
const localSetAt = getLocalConfigSetAt();
|
|
584
|
+
if (sync.configSetAt <= localSetAt) return;
|
|
585
|
+
if (config.debug) {
|
|
586
|
+
debugLog(`Config sync: server=${sync.configSetAt} > local=${localSetAt}, updating...`);
|
|
587
|
+
}
|
|
588
|
+
const envUpdates = {};
|
|
589
|
+
if (sync.privacyTier && VALID_PRIVACY.has(sync.privacyTier)) {
|
|
590
|
+
envUpdates["AGENTAPPROVE_PRIVACY"] = sync.privacyTier;
|
|
591
|
+
config.privacyTier = sync.privacyTier;
|
|
592
|
+
if (config.debug) debugLog(`Config synced: privacy=${sync.privacyTier}`);
|
|
593
|
+
}
|
|
594
|
+
if (sync.timeoutSeconds != null) {
|
|
595
|
+
const t = sync.timeoutSeconds;
|
|
596
|
+
if (t >= 30 && t <= 600) {
|
|
597
|
+
envUpdates["AGENTAPPROVE_TIMEOUT"] = String(t);
|
|
598
|
+
config.timeout = t;
|
|
599
|
+
if (config.debug) debugLog(`Config synced: timeout=${t}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (sync.failBehavior && VALID_FAIL.has(sync.failBehavior)) {
|
|
603
|
+
envUpdates["AGENTAPPROVE_FAIL_BEHAVIOR"] = sync.failBehavior;
|
|
604
|
+
config.failBehavior = sync.failBehavior;
|
|
605
|
+
if (config.debug) debugLog(`Config synced: failBehavior=${sync.failBehavior}`);
|
|
606
|
+
}
|
|
607
|
+
if (sync.e2eEnabled === true || sync.e2eEnabled === false) {
|
|
608
|
+
envUpdates["AGENTAPPROVE_E2E_ENABLED"] = String(sync.e2eEnabled);
|
|
609
|
+
config.e2eEnabled = sync.e2eEnabled;
|
|
610
|
+
if (config.debug) debugLog(`Config synced: e2eEnabled=${sync.e2eEnabled}`);
|
|
611
|
+
}
|
|
612
|
+
syncRotationConfig(sync.e2eRotationPeriod, sync.e2eRotationStartedAt, config.debug);
|
|
613
|
+
if (config.e2eEnabled && config.e2eUserKey) {
|
|
614
|
+
const rotatedKey = checkAndRotateKeys(config.e2eUserKey, config.debug);
|
|
615
|
+
if (rotatedKey && rotatedKey !== config.e2eUserKey) {
|
|
616
|
+
config.e2eUserKey = rotatedKey;
|
|
617
|
+
if (config.debug) debugLog(`Key rotated after config sync`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
envUpdates["AGENTAPPROVE_CONFIG_SET_AT"] = String(sync.configSetAt);
|
|
621
|
+
updateEnvValues(envUpdates);
|
|
622
|
+
}
|
|
623
|
+
function getLocalConfigSetAt() {
|
|
624
|
+
const envPath = getEnvPath();
|
|
625
|
+
if (!existsSync4(envPath)) return 0;
|
|
626
|
+
try {
|
|
627
|
+
const content = readFileSync5(envPath, "utf-8");
|
|
628
|
+
for (const raw of content.split("\n")) {
|
|
629
|
+
const line = raw.replace(/^export\s+/, "").trim();
|
|
630
|
+
if (line.startsWith("AGENTAPPROVE_CONFIG_SET_AT=")) {
|
|
631
|
+
const val = line.slice("AGENTAPPROVE_CONFIG_SET_AT=".length).replace(/^["']|["']$/g, "");
|
|
632
|
+
const num = parseInt(val, 10);
|
|
633
|
+
return isNaN(num) ? 0 : num;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
} catch {
|
|
637
|
+
}
|
|
638
|
+
return 0;
|
|
639
|
+
}
|
|
640
|
+
|
|
456
641
|
// src/api-client.ts
|
|
457
642
|
var cachedPluginHash;
|
|
458
|
-
function getPluginHash(pluginPath) {
|
|
643
|
+
function getPluginHash(pluginPath, debug = false, hookName = "openclaw-plugin") {
|
|
459
644
|
if (!cachedPluginHash) {
|
|
460
645
|
cachedPluginHash = computePluginHash(pluginPath);
|
|
461
|
-
if (cachedPluginHash) {
|
|
462
|
-
debugLog(`Plugin hash computed: ${cachedPluginHash.slice(0, 16)}
|
|
646
|
+
if (cachedPluginHash && debug) {
|
|
647
|
+
debugLog(`Plugin hash computed: ${cachedPluginHash.slice(0, 16)}...`, hookName);
|
|
463
648
|
}
|
|
464
649
|
}
|
|
465
650
|
return cachedPluginHash || "";
|
|
@@ -501,7 +686,7 @@ function httpPost(url, body, headers, timeoutMs) {
|
|
|
501
686
|
req.end();
|
|
502
687
|
});
|
|
503
688
|
}
|
|
504
|
-
async function sendApprovalRequest(request, config, pluginPath) {
|
|
689
|
+
async function sendApprovalRequest(request, config, pluginPath, hookName = "openclaw-plugin") {
|
|
505
690
|
if (!config.token) {
|
|
506
691
|
throw new Error("No Agent Approve token configured");
|
|
507
692
|
}
|
|
@@ -509,13 +694,13 @@ async function sendApprovalRequest(request, config, pluginPath) {
|
|
|
509
694
|
if (config.e2eEnabled && config.e2eUserKey) {
|
|
510
695
|
payload = applyApprovalE2E(request, config.e2eUserKey, config.e2eServerKey);
|
|
511
696
|
if (config.debug) {
|
|
512
|
-
debugLog("E2E encryption applied to approval request");
|
|
697
|
+
debugLog("E2E encryption applied to approval request", hookName);
|
|
513
698
|
}
|
|
514
699
|
} else {
|
|
515
700
|
payload = applyPrivacyFilter(request, config.privacyTier);
|
|
516
701
|
}
|
|
517
702
|
const bodyStr = JSON.stringify(payload);
|
|
518
|
-
const pluginHash = getPluginHash(pluginPath);
|
|
703
|
+
const pluginHash = getPluginHash(pluginPath, config.debug, hookName);
|
|
519
704
|
const hmacHeaders = buildHMACHeaders(bodyStr, config.token, config.hookVersion, pluginHash);
|
|
520
705
|
const headers = {
|
|
521
706
|
"Authorization": `Bearer ${config.token}`,
|
|
@@ -523,38 +708,44 @@ async function sendApprovalRequest(request, config, pluginPath) {
|
|
|
523
708
|
};
|
|
524
709
|
const url = `${config.apiUrl}/${config.apiVersion}/approve`;
|
|
525
710
|
if (config.debug) {
|
|
526
|
-
debugLog(`
|
|
711
|
+
debugLog(`Requesting approval from ${url}`, hookName);
|
|
712
|
+
debugLog(`=== SENT TO ${url} ===`, hookName);
|
|
713
|
+
debugLogRawInline(bodyStr);
|
|
714
|
+
debugLog("=== END SENT ===", hookName);
|
|
527
715
|
}
|
|
528
716
|
const response = await httpPost(url, bodyStr, headers, config.timeout * 1e3);
|
|
529
717
|
if (config.debug) {
|
|
530
|
-
debugLog(`Response
|
|
718
|
+
debugLog(`Response: ${response.body || "<empty>"}`, hookName);
|
|
531
719
|
}
|
|
532
720
|
if (response.status !== 200) {
|
|
533
721
|
throw new Error(`API returned status ${response.status}: ${response.body.slice(0, 200)}`);
|
|
534
722
|
}
|
|
723
|
+
let parsed;
|
|
535
724
|
try {
|
|
536
|
-
|
|
725
|
+
parsed = JSON.parse(response.body);
|
|
537
726
|
} catch {
|
|
538
727
|
throw new Error(`Failed to parse API response: ${response.body.slice(0, 200)}`);
|
|
539
728
|
}
|
|
729
|
+
processConfigSync(parsed, config);
|
|
730
|
+
return parsed;
|
|
540
731
|
}
|
|
541
|
-
async function sendEvent(event, config, pluginPath) {
|
|
732
|
+
async function sendEvent(event, config, pluginPath, hookName = "openclaw-plugin") {
|
|
542
733
|
if (!config.token) return;
|
|
543
734
|
const eventType = event.eventType;
|
|
544
735
|
const toolName = event.toolName;
|
|
545
736
|
if (config.debug) {
|
|
546
|
-
debugLog(`Sending ${eventType || "event"}${toolName ? ` (${toolName})` : ""} (privacy: ${config.privacyTier})
|
|
737
|
+
debugLog(`Sending ${eventType || "event"}${toolName ? ` (${toolName})` : ""} (privacy: ${config.privacyTier})`, hookName);
|
|
547
738
|
}
|
|
548
739
|
try {
|
|
549
740
|
let payload = applyEventPrivacyFilter(event, config.privacyTier);
|
|
550
741
|
if (config.e2eEnabled && config.e2eUserKey) {
|
|
551
742
|
payload = applyEventE2E(payload, config.e2eUserKey);
|
|
552
743
|
if (config.debug) {
|
|
553
|
-
debugLog(`E2E applied to event (type=${eventType})
|
|
744
|
+
debugLog(`E2E applied to event (type=${eventType})`, hookName);
|
|
554
745
|
}
|
|
555
746
|
}
|
|
556
747
|
const bodyStr = JSON.stringify(payload);
|
|
557
|
-
const pluginHash = getPluginHash(pluginPath);
|
|
748
|
+
const pluginHash = getPluginHash(pluginPath, config.debug, hookName);
|
|
558
749
|
const hmacHeaders = buildHMACHeaders(bodyStr, config.token, config.hookVersion, pluginHash);
|
|
559
750
|
const headers = {
|
|
560
751
|
"Authorization": `Bearer ${config.token}`,
|
|
@@ -562,17 +753,24 @@ async function sendEvent(event, config, pluginPath) {
|
|
|
562
753
|
};
|
|
563
754
|
const url = `${config.apiUrl}/${config.apiVersion}/events`;
|
|
564
755
|
if (config.debug) {
|
|
565
|
-
debugLog(`=== SENT TO ${url}
|
|
566
|
-
|
|
567
|
-
debugLog("=== END SENT ===");
|
|
756
|
+
debugLog(`=== SENT TO ${url} ===`, hookName);
|
|
757
|
+
debugLogRawInline(bodyStr);
|
|
758
|
+
debugLog("=== END SENT ===", hookName);
|
|
568
759
|
}
|
|
569
760
|
const response = await httpPost(url, bodyStr, headers, 5e3);
|
|
570
761
|
if (config.debug) {
|
|
571
|
-
debugLog(`send_event response: ${response.body
|
|
762
|
+
debugLog(`send_event response: ${response.body || "<empty>"}`, hookName);
|
|
763
|
+
}
|
|
764
|
+
if (response.status === 200) {
|
|
765
|
+
try {
|
|
766
|
+
const parsed = JSON.parse(response.body);
|
|
767
|
+
processConfigSync(parsed, config);
|
|
768
|
+
} catch {
|
|
769
|
+
}
|
|
572
770
|
}
|
|
573
771
|
} catch (err) {
|
|
574
772
|
if (config.debug) {
|
|
575
|
-
debugLog(`Failed to send ${eventType || "event"}: ${err instanceof Error ? err.message : String(err)}
|
|
773
|
+
debugLog(`Failed to send ${eventType || "event"}: ${err instanceof Error ? err.message : String(err)}`, hookName);
|
|
576
774
|
}
|
|
577
775
|
}
|
|
578
776
|
}
|
|
@@ -588,6 +786,19 @@ var gatewaySessionId = randomBytes2(12).toString("hex");
|
|
|
588
786
|
var DEDUP_WINDOW_MS = 1200;
|
|
589
787
|
var DEDUP_MAX_SIZE = 300;
|
|
590
788
|
var recentCompletions = /* @__PURE__ */ new Map();
|
|
789
|
+
var HOOK_PLUGIN = "openclaw-plugin";
|
|
790
|
+
var HOOK_BEFORE_TOOL = "openclaw-before-tool";
|
|
791
|
+
var HOOK_AFTER_TOOL = "openclaw-after-tool";
|
|
792
|
+
var HOOK_SESSION_START = "openclaw-session-start";
|
|
793
|
+
var HOOK_SESSION_END = "openclaw-session-end";
|
|
794
|
+
var HOOK_LLM_INPUT = "openclaw-llm-input";
|
|
795
|
+
var HOOK_LLM_OUTPUT = "openclaw-llm-output";
|
|
796
|
+
var HOOK_AGENT_END = "openclaw-agent-end";
|
|
797
|
+
var HOOK_BEFORE_COMPACTION = "openclaw-before-compaction";
|
|
798
|
+
var HOOK_SUBAGENT_SPAWNED = "openclaw-subagent-spawned";
|
|
799
|
+
var HOOK_SUBAGENT_ENDED = "openclaw-subagent-ended";
|
|
800
|
+
var HOOK_COMMAND = "openclaw-command";
|
|
801
|
+
var HOOK_MESSAGE = "openclaw-message";
|
|
591
802
|
function resolveConversationId() {
|
|
592
803
|
return gatewaySessionId;
|
|
593
804
|
}
|
|
@@ -686,10 +897,10 @@ function extractResultPreview(toolName, params, result, maxLen = 300) {
|
|
|
686
897
|
if (str.length <= maxLen) return str;
|
|
687
898
|
return str.slice(0, maxLen) + "...";
|
|
688
899
|
}
|
|
689
|
-
function handleFailBehavior(config, error, toolName, logger) {
|
|
900
|
+
function handleFailBehavior(config, error, toolName, logger, hookName = HOOK_PLUGIN) {
|
|
690
901
|
logger.warn(`Agent Approve API error for tool "${toolName}": ${error.message}`);
|
|
691
902
|
if (config.debug) {
|
|
692
|
-
debugLog(`API error: ${error.message}, failBehavior: ${config.failBehavior}
|
|
903
|
+
debugLog(`API error: ${error.message}, failBehavior: ${config.failBehavior}`, hookName);
|
|
693
904
|
}
|
|
694
905
|
switch (config.failBehavior) {
|
|
695
906
|
case "deny":
|
|
@@ -712,12 +923,24 @@ function register(api) {
|
|
|
712
923
|
}
|
|
713
924
|
api.logger.info(`Agent Approve: Plugin loaded (privacy: ${config.privacyTier}, fail: ${config.failBehavior})`);
|
|
714
925
|
if (config.debug) {
|
|
715
|
-
|
|
926
|
+
const e2eStatus = !config.e2eEnabled ? "disabled" : !config.e2eUserKey ? "enabled (key missing)" : "enabled";
|
|
927
|
+
debugLog(
|
|
928
|
+
`Plugin loaded: v${config.hookVersion}, api=${config.apiUrl}, privacy=${config.privacyTier}, e2e=${e2eStatus}, debug=${config.debug}`,
|
|
929
|
+
HOOK_PLUGIN
|
|
930
|
+
);
|
|
931
|
+
debugLog(`Full config: agent=${config.agentName}, timeout=${config.timeout}s, fail=${config.failBehavior}`, HOOK_PLUGIN);
|
|
716
932
|
}
|
|
717
933
|
api.on("before_tool_call", async (event, ctx) => {
|
|
934
|
+
if (config.debug) {
|
|
935
|
+
debugLogRaw({ event, ctx }, HOOK_BEFORE_TOOL);
|
|
936
|
+
debugLog("Started before_tool_call hook", HOOK_BEFORE_TOOL);
|
|
937
|
+
}
|
|
718
938
|
const conversationId = resolveConversationId();
|
|
719
939
|
const { toolType, displayName } = classifyTool(event.toolName, event.params);
|
|
720
940
|
const command = extractCommand(event.toolName, event.params);
|
|
941
|
+
if (config.debug) {
|
|
942
|
+
debugLog(`Tool: ${displayName} (${toolType}) [agent: ${config.agentName}]`, HOOK_BEFORE_TOOL);
|
|
943
|
+
}
|
|
721
944
|
const request = {
|
|
722
945
|
toolName: displayName,
|
|
723
946
|
toolType,
|
|
@@ -731,15 +954,18 @@ function register(api) {
|
|
|
731
954
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
732
955
|
};
|
|
733
956
|
try {
|
|
734
|
-
const response = await sendApprovalRequest(request, config, pluginFilePath);
|
|
957
|
+
const response = await sendApprovalRequest(request, config, pluginFilePath, HOOK_BEFORE_TOOL);
|
|
958
|
+
if (config.debug) {
|
|
959
|
+
debugLog(`Decision: ${response.decision}, Reason: ${response.reason || ""}`, HOOK_BEFORE_TOOL);
|
|
960
|
+
}
|
|
735
961
|
if (response.decision === "approve" || response.decision === "allow") {
|
|
736
962
|
if (config.debug) {
|
|
737
|
-
debugLog(
|
|
963
|
+
debugLog("Tool approved", HOOK_BEFORE_TOOL);
|
|
738
964
|
}
|
|
739
965
|
return void 0;
|
|
740
966
|
}
|
|
741
967
|
if (config.debug) {
|
|
742
|
-
debugLog(
|
|
968
|
+
debugLog("Tool denied", HOOK_BEFORE_TOOL);
|
|
743
969
|
}
|
|
744
970
|
return {
|
|
745
971
|
block: true,
|
|
@@ -750,15 +976,19 @@ function register(api) {
|
|
|
750
976
|
config,
|
|
751
977
|
error instanceof Error ? error : new Error(String(error)),
|
|
752
978
|
event.toolName,
|
|
753
|
-
api.logger
|
|
979
|
+
api.logger,
|
|
980
|
+
HOOK_BEFORE_TOOL
|
|
754
981
|
);
|
|
755
982
|
}
|
|
756
983
|
});
|
|
757
984
|
api.on("after_tool_call", async (event, ctx) => {
|
|
985
|
+
if (config.debug) {
|
|
986
|
+
debugLogRaw({ event, ctx }, HOOK_AFTER_TOOL);
|
|
987
|
+
}
|
|
758
988
|
const conversationId = resolveConversationId();
|
|
759
989
|
if (isDuplicateCompletion(event.toolName, event.params)) {
|
|
760
990
|
if (config.debug) {
|
|
761
|
-
debugLog(`Skipping duplicate tool_complete for "${event.toolName}"
|
|
991
|
+
debugLog(`Skipping duplicate tool_complete for "${event.toolName}"`, HOOK_AFTER_TOOL);
|
|
762
992
|
}
|
|
763
993
|
return;
|
|
764
994
|
}
|
|
@@ -777,13 +1007,14 @@ function register(api) {
|
|
|
777
1007
|
response: event.error || resultPreview || void 0,
|
|
778
1008
|
durationMs: event.durationMs,
|
|
779
1009
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
780
|
-
}, config, pluginFilePath);
|
|
1010
|
+
}, config, pluginFilePath, HOOK_AFTER_TOOL);
|
|
781
1011
|
});
|
|
782
1012
|
api.on("session_start", async (event, ctx) => {
|
|
783
|
-
const conversationId = resolveConversationId();
|
|
784
1013
|
if (config.debug) {
|
|
785
|
-
|
|
1014
|
+
debugLogRaw({ event, ctx }, HOOK_SESSION_START);
|
|
1015
|
+
debugLog(`Session started: ${event.sessionId}${event.resumedFrom ? ` (resumed from ${event.resumedFrom})` : ""}`, HOOK_SESSION_START);
|
|
786
1016
|
}
|
|
1017
|
+
const conversationId = resolveConversationId();
|
|
787
1018
|
void sendEvent({
|
|
788
1019
|
eventType: "session_start",
|
|
789
1020
|
agent: config.agentName,
|
|
@@ -791,13 +1022,14 @@ function register(api) {
|
|
|
791
1022
|
sessionId: conversationId,
|
|
792
1023
|
conversationId,
|
|
793
1024
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
794
|
-
}, config, pluginFilePath);
|
|
1025
|
+
}, config, pluginFilePath, HOOK_SESSION_START);
|
|
795
1026
|
});
|
|
796
1027
|
api.on("session_end", async (event, ctx) => {
|
|
797
|
-
const conversationId = resolveConversationId();
|
|
798
1028
|
if (config.debug) {
|
|
799
|
-
|
|
1029
|
+
debugLogRaw({ event, ctx }, HOOK_SESSION_END);
|
|
1030
|
+
debugLog(`Session ended: ${event.sessionId} (${event.messageCount} messages, ${event.durationMs ?? "?"}ms)`, HOOK_SESSION_END);
|
|
800
1031
|
}
|
|
1032
|
+
const conversationId = resolveConversationId();
|
|
801
1033
|
void sendEvent({
|
|
802
1034
|
eventType: "session_end",
|
|
803
1035
|
agent: config.agentName,
|
|
@@ -807,13 +1039,14 @@ function register(api) {
|
|
|
807
1039
|
durationMs: event.durationMs,
|
|
808
1040
|
sessionStats: { messageCount: event.messageCount },
|
|
809
1041
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
810
|
-
}, config, pluginFilePath);
|
|
1042
|
+
}, config, pluginFilePath, HOOK_SESSION_END);
|
|
811
1043
|
});
|
|
812
1044
|
api.on("llm_input", async (event, ctx) => {
|
|
813
|
-
const conversationId = resolveConversationId();
|
|
814
1045
|
if (config.debug) {
|
|
815
|
-
|
|
1046
|
+
debugLogRaw({ event, ctx }, HOOK_LLM_INPUT);
|
|
1047
|
+
debugLog(`LLM input: model=${event.model}, prompt length=${event.prompt?.length ?? 0}`, HOOK_LLM_INPUT);
|
|
816
1048
|
}
|
|
1049
|
+
const conversationId = resolveConversationId();
|
|
817
1050
|
void sendEvent({
|
|
818
1051
|
eventType: "user_prompt",
|
|
819
1052
|
agent: config.agentName,
|
|
@@ -824,14 +1057,15 @@ function register(api) {
|
|
|
824
1057
|
prompt: event.prompt,
|
|
825
1058
|
textLength: event.prompt?.length ?? 0,
|
|
826
1059
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
827
|
-
}, config, pluginFilePath);
|
|
1060
|
+
}, config, pluginFilePath, HOOK_LLM_INPUT);
|
|
828
1061
|
});
|
|
829
1062
|
api.on("llm_output", async (event, ctx) => {
|
|
830
1063
|
const conversationId = resolveConversationId();
|
|
831
1064
|
const responseText = event.assistantTexts?.join("\n") || "";
|
|
832
1065
|
const textLength = responseText.length;
|
|
833
1066
|
if (config.debug) {
|
|
834
|
-
|
|
1067
|
+
debugLogRaw({ event, ctx }, HOOK_LLM_OUTPUT);
|
|
1068
|
+
debugLog(`LLM output: model=${event.model}, length=${textLength}${event.usage?.total ? `, tokens=${event.usage.total}` : ""}`, HOOK_LLM_OUTPUT);
|
|
835
1069
|
}
|
|
836
1070
|
void sendEvent({
|
|
837
1071
|
eventType: "response",
|
|
@@ -844,13 +1078,14 @@ function register(api) {
|
|
|
844
1078
|
textPreview: responseText.slice(0, 200),
|
|
845
1079
|
textLength,
|
|
846
1080
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
847
|
-
}, config, pluginFilePath);
|
|
1081
|
+
}, config, pluginFilePath, HOOK_LLM_OUTPUT);
|
|
848
1082
|
});
|
|
849
1083
|
api.on("agent_end", async (event, ctx) => {
|
|
850
|
-
const conversationId = resolveConversationId();
|
|
851
1084
|
if (config.debug) {
|
|
852
|
-
|
|
1085
|
+
debugLogRaw({ event, ctx }, HOOK_AGENT_END);
|
|
1086
|
+
debugLog(`Agent ended: success=${event.success}${event.durationMs ? `, duration=${event.durationMs}ms` : ""}${event.error ? `, error=${event.error}` : ""}`, HOOK_AGENT_END);
|
|
853
1087
|
}
|
|
1088
|
+
const conversationId = resolveConversationId();
|
|
854
1089
|
void sendEvent({
|
|
855
1090
|
eventType: "stop",
|
|
856
1091
|
agent: config.agentName,
|
|
@@ -861,13 +1096,14 @@ function register(api) {
|
|
|
861
1096
|
durationMs: event.durationMs,
|
|
862
1097
|
response: event.error || void 0,
|
|
863
1098
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
864
|
-
}, config, pluginFilePath);
|
|
1099
|
+
}, config, pluginFilePath, HOOK_AGENT_END);
|
|
865
1100
|
});
|
|
866
1101
|
api.on("before_compaction", async (event, ctx) => {
|
|
867
|
-
const conversationId = resolveConversationId();
|
|
868
1102
|
if (config.debug) {
|
|
869
|
-
|
|
1103
|
+
debugLogRaw({ event, ctx }, HOOK_BEFORE_COMPACTION);
|
|
1104
|
+
debugLog(`Context compaction: ${event.messageCount} messages${event.tokenCount ? `, ${event.tokenCount} tokens` : ""}`, HOOK_BEFORE_COMPACTION);
|
|
870
1105
|
}
|
|
1106
|
+
const conversationId = resolveConversationId();
|
|
871
1107
|
void sendEvent({
|
|
872
1108
|
eventType: "context_compact",
|
|
873
1109
|
agent: config.agentName,
|
|
@@ -876,13 +1112,14 @@ function register(api) {
|
|
|
876
1112
|
conversationId,
|
|
877
1113
|
trigger: `${event.messageCount} messages${event.tokenCount ? `, ${event.tokenCount} tokens` : ""}`,
|
|
878
1114
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
879
|
-
}, config, pluginFilePath);
|
|
1115
|
+
}, config, pluginFilePath, HOOK_BEFORE_COMPACTION);
|
|
880
1116
|
});
|
|
881
1117
|
api.on("subagent_spawned", async (event, ctx) => {
|
|
882
|
-
const conversationId = resolveConversationId();
|
|
883
1118
|
if (config.debug) {
|
|
884
|
-
|
|
1119
|
+
debugLogRaw({ event, ctx }, HOOK_SUBAGENT_SPAWNED);
|
|
1120
|
+
debugLog(`Subagent spawned: ${event.agentId} (${event.mode}${event.label ? `, label=${event.label}` : ""})`, HOOK_SUBAGENT_SPAWNED);
|
|
885
1121
|
}
|
|
1122
|
+
const conversationId = resolveConversationId();
|
|
886
1123
|
void sendEvent({
|
|
887
1124
|
eventType: "subagent_start",
|
|
888
1125
|
agent: config.agentName,
|
|
@@ -893,13 +1130,14 @@ function register(api) {
|
|
|
893
1130
|
subagentMode: event.mode,
|
|
894
1131
|
subagentLabel: event.label,
|
|
895
1132
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
896
|
-
}, config, pluginFilePath);
|
|
1133
|
+
}, config, pluginFilePath, HOOK_SUBAGENT_SPAWNED);
|
|
897
1134
|
});
|
|
898
1135
|
api.on("subagent_ended", async (event, ctx) => {
|
|
899
|
-
const conversationId = resolveConversationId();
|
|
900
1136
|
if (config.debug) {
|
|
901
|
-
|
|
1137
|
+
debugLogRaw({ event, ctx }, HOOK_SUBAGENT_ENDED);
|
|
1138
|
+
debugLog(`Subagent ended: ${event.targetKind} reason=${event.reason}${event.outcome ? `, outcome=${event.outcome}` : ""}`, HOOK_SUBAGENT_ENDED);
|
|
902
1139
|
}
|
|
1140
|
+
const conversationId = resolveConversationId();
|
|
903
1141
|
void sendEvent({
|
|
904
1142
|
eventType: "subagent_stop",
|
|
905
1143
|
agent: config.agentName,
|
|
@@ -910,7 +1148,7 @@ function register(api) {
|
|
|
910
1148
|
subagentType: event.targetKind,
|
|
911
1149
|
response: event.error || void 0,
|
|
912
1150
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
913
|
-
}, config, pluginFilePath);
|
|
1151
|
+
}, config, pluginFilePath, HOOK_SUBAGENT_ENDED);
|
|
914
1152
|
});
|
|
915
1153
|
api.registerHook(
|
|
916
1154
|
["command:new", "command:stop", "command:reset"],
|
|
@@ -921,7 +1159,8 @@ function register(api) {
|
|
|
921
1159
|
reset: "session_start"
|
|
922
1160
|
};
|
|
923
1161
|
if (config.debug) {
|
|
924
|
-
|
|
1162
|
+
debugLogRaw(event, HOOK_COMMAND);
|
|
1163
|
+
debugLog(`Command event: ${event.action}`, HOOK_COMMAND);
|
|
925
1164
|
}
|
|
926
1165
|
void sendEvent({
|
|
927
1166
|
toolName: `command:${event.action}`,
|
|
@@ -932,7 +1171,7 @@ function register(api) {
|
|
|
932
1171
|
sessionId: resolveConversationId(),
|
|
933
1172
|
conversationId: resolveConversationId(),
|
|
934
1173
|
timestamp: event.timestamp.toISOString()
|
|
935
|
-
}, config, pluginFilePath);
|
|
1174
|
+
}, config, pluginFilePath, HOOK_COMMAND);
|
|
936
1175
|
},
|
|
937
1176
|
{ name: "agentapprove-command-monitor", description: "Log command events to Agent Approve" }
|
|
938
1177
|
);
|
|
@@ -944,7 +1183,8 @@ function register(api) {
|
|
|
944
1183
|
const peer = isInbound ? event.context.from : event.context.to;
|
|
945
1184
|
const channelLabel = [provider, peer].filter(Boolean).join(":") || void 0;
|
|
946
1185
|
if (config.debug) {
|
|
947
|
-
|
|
1186
|
+
debugLogRaw(event, HOOK_MESSAGE);
|
|
1187
|
+
debugLog(`Message event: ${event.action} ${channelLabel || "(no channel)"}`, HOOK_MESSAGE);
|
|
948
1188
|
}
|
|
949
1189
|
const payload = {
|
|
950
1190
|
toolName: `message:${event.action}`,
|
|
@@ -966,7 +1206,7 @@ function register(api) {
|
|
|
966
1206
|
payload.prompt = `${peer}: ${content}`;
|
|
967
1207
|
}
|
|
968
1208
|
}
|
|
969
|
-
void sendEvent(payload, config, pluginFilePath);
|
|
1209
|
+
void sendEvent(payload, config, pluginFilePath, HOOK_MESSAGE);
|
|
970
1210
|
},
|
|
971
1211
|
{ name: "agentapprove-session-monitor", description: "Log message events to Agent Approve" }
|
|
972
1212
|
);
|
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.7",
|
|
6
6
|
"homepage": "https://agentapprove.com",
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED