@agentapprove/openclaw 0.1.6 → 0.1.8
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/README.md +12 -0
- package/dist/index.js +200 -78
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
|
|
11
11
|
You decide the rules. Agent Approve enforces them.
|
|
12
12
|
|
|
13
|
+
## Important
|
|
14
|
+
|
|
15
|
+
- A valid Agent Approve subscription is required to use this plugin.
|
|
16
|
+
- You must download the iOS app, run `npx agentapprove`, and pair your device.
|
|
17
|
+
- This plugin is currently pre-launch software. Public App Store availability is not open yet.
|
|
18
|
+
- If a valid token is not configured, tool calls are blocked with setup guidance.
|
|
19
|
+
|
|
13
20
|
## How it works
|
|
14
21
|
|
|
15
22
|
This plugin hooks into OpenClaw's agent loop to enforce your approval policy:
|
|
@@ -67,6 +74,11 @@ Plugin settings in `~/.openclaw/openclaw.json`:
|
|
|
67
74
|
| `privacyTier` | `full` | What tool data is stored in event logs: `minimal`, `summary`, or `full` |
|
|
68
75
|
| `debug` | `false` | Write debug logs to `~/.agentapprove/hook-debug.log` |
|
|
69
76
|
|
|
77
|
+
## Requirements
|
|
78
|
+
|
|
79
|
+
- OpenClaw runtime
|
|
80
|
+
- Agent Approve iOS app and active subscription ($9.99/month or $99/year, 7-day free trial)
|
|
81
|
+
|
|
70
82
|
## Supported agents
|
|
71
83
|
|
|
72
84
|
Agent Approve works with OpenClaw, Claude Code, Cursor, Gemini CLI, VS Code Agent, Copilot CLI, OpenAI Codex (coming soon), and more. Run `npx agentapprove` to configure multiple agents at once.
|
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
|
-
|
|
52
|
+
appendFileSync(DEBUG_LOG_PATH, `[${localTimestamp()}] [${hookName}] ${message}
|
|
53
|
+
`);
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function debugLogRawInline(data) {
|
|
58
|
+
try {
|
|
59
|
+
ensureLogFile();
|
|
60
|
+
appendFileSync(DEBUG_LOG_PATH, `${data}
|
|
50
61
|
`);
|
|
51
62
|
} catch {
|
|
52
63
|
}
|
|
53
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) {
|
|
@@ -284,7 +339,7 @@ function loadConfig(openclawConfig, logger) {
|
|
|
284
339
|
}
|
|
285
340
|
let token = process.env.AGENTAPPROVE_TOKEN || getKeychainToken() || parseConfigValue(fileContent, "AGENTAPPROVE_TOKEN") || "";
|
|
286
341
|
if (!token) {
|
|
287
|
-
logger?.warn("
|
|
342
|
+
logger?.warn("Missing token. Download the iOS app, run npx agentapprove, and pair your device. Subscription required.");
|
|
288
343
|
}
|
|
289
344
|
const rawApiUrl = openclawConfig?.apiUrl || process.env.AGENTAPPROVE_API || parseConfigValue(fileContent, "AGENTAPPROVE_API") || "https://api.agentapprove.com";
|
|
290
345
|
const rawApiVersion = process.env.AGENTAPPROVE_API_VERSION || parseConfigValue(fileContent, "AGENTAPPROVE_API_VERSION") || "v001";
|
|
@@ -331,7 +386,8 @@ function loadConfig(openclawConfig, logger) {
|
|
|
331
386
|
failBehavior,
|
|
332
387
|
privacyTier,
|
|
333
388
|
debug,
|
|
334
|
-
hookVersion: "1.1.
|
|
389
|
+
hookVersion: "1.1.4",
|
|
390
|
+
agentType: "openclaw",
|
|
335
391
|
agentName,
|
|
336
392
|
e2eEnabled,
|
|
337
393
|
e2eUserKey,
|
|
@@ -418,6 +474,7 @@ var CONTENT_FIELDS = [
|
|
|
418
474
|
"command",
|
|
419
475
|
"toolInput",
|
|
420
476
|
"response",
|
|
477
|
+
"responsePreview",
|
|
421
478
|
"text",
|
|
422
479
|
"textPreview",
|
|
423
480
|
"prompt",
|
|
@@ -585,11 +642,11 @@ function getLocalConfigSetAt() {
|
|
|
585
642
|
|
|
586
643
|
// src/api-client.ts
|
|
587
644
|
var cachedPluginHash;
|
|
588
|
-
function getPluginHash(pluginPath, debug = false) {
|
|
645
|
+
function getPluginHash(pluginPath, debug = false, hookName = "openclaw-plugin") {
|
|
589
646
|
if (!cachedPluginHash) {
|
|
590
647
|
cachedPluginHash = computePluginHash(pluginPath);
|
|
591
648
|
if (cachedPluginHash && debug) {
|
|
592
|
-
debugLog(`Plugin hash computed: ${cachedPluginHash.slice(0, 16)}
|
|
649
|
+
debugLog(`Plugin hash computed: ${cachedPluginHash.slice(0, 16)}...`, hookName);
|
|
593
650
|
}
|
|
594
651
|
}
|
|
595
652
|
return cachedPluginHash || "";
|
|
@@ -631,21 +688,21 @@ function httpPost(url, body, headers, timeoutMs) {
|
|
|
631
688
|
req.end();
|
|
632
689
|
});
|
|
633
690
|
}
|
|
634
|
-
async function sendApprovalRequest(request, config, pluginPath) {
|
|
691
|
+
async function sendApprovalRequest(request, config, pluginPath, hookName = "openclaw-plugin") {
|
|
635
692
|
if (!config.token) {
|
|
636
|
-
throw new Error("
|
|
693
|
+
throw new Error("Missing token. Download the iOS app, run npx agentapprove, and pair your device. Subscription required.");
|
|
637
694
|
}
|
|
638
695
|
let payload;
|
|
639
696
|
if (config.e2eEnabled && config.e2eUserKey) {
|
|
640
697
|
payload = applyApprovalE2E(request, config.e2eUserKey, config.e2eServerKey);
|
|
641
698
|
if (config.debug) {
|
|
642
|
-
debugLog("E2E encryption applied to approval request");
|
|
699
|
+
debugLog("E2E encryption applied to approval request", hookName);
|
|
643
700
|
}
|
|
644
701
|
} else {
|
|
645
702
|
payload = applyPrivacyFilter(request, config.privacyTier);
|
|
646
703
|
}
|
|
647
704
|
const bodyStr = JSON.stringify(payload);
|
|
648
|
-
const pluginHash = getPluginHash(pluginPath, config.debug);
|
|
705
|
+
const pluginHash = getPluginHash(pluginPath, config.debug, hookName);
|
|
649
706
|
const hmacHeaders = buildHMACHeaders(bodyStr, config.token, config.hookVersion, pluginHash);
|
|
650
707
|
const headers = {
|
|
651
708
|
"Authorization": `Bearer ${config.token}`,
|
|
@@ -653,11 +710,14 @@ async function sendApprovalRequest(request, config, pluginPath) {
|
|
|
653
710
|
};
|
|
654
711
|
const url = `${config.apiUrl}/${config.apiVersion}/approve`;
|
|
655
712
|
if (config.debug) {
|
|
656
|
-
debugLog(`
|
|
713
|
+
debugLog(`Requesting approval from ${url}`, hookName);
|
|
714
|
+
debugLog(`=== SENT TO ${url} ===`, hookName);
|
|
715
|
+
debugLogRawInline(bodyStr);
|
|
716
|
+
debugLog("=== END SENT ===", hookName);
|
|
657
717
|
}
|
|
658
718
|
const response = await httpPost(url, bodyStr, headers, config.timeout * 1e3);
|
|
659
719
|
if (config.debug) {
|
|
660
|
-
debugLog(`Response
|
|
720
|
+
debugLog(`Response: ${response.body || "<empty>"}`, hookName);
|
|
661
721
|
}
|
|
662
722
|
if (response.status !== 200) {
|
|
663
723
|
throw new Error(`API returned status ${response.status}: ${response.body.slice(0, 200)}`);
|
|
@@ -671,23 +731,23 @@ async function sendApprovalRequest(request, config, pluginPath) {
|
|
|
671
731
|
processConfigSync(parsed, config);
|
|
672
732
|
return parsed;
|
|
673
733
|
}
|
|
674
|
-
async function sendEvent(event, config, pluginPath) {
|
|
734
|
+
async function sendEvent(event, config, pluginPath, hookName = "openclaw-plugin") {
|
|
675
735
|
if (!config.token) return;
|
|
676
736
|
const eventType = event.eventType;
|
|
677
737
|
const toolName = event.toolName;
|
|
678
738
|
if (config.debug) {
|
|
679
|
-
debugLog(`Sending ${eventType || "event"}${toolName ? ` (${toolName})` : ""} (privacy: ${config.privacyTier})
|
|
739
|
+
debugLog(`Sending ${eventType || "event"}${toolName ? ` (${toolName})` : ""} (privacy: ${config.privacyTier})`, hookName);
|
|
680
740
|
}
|
|
681
741
|
try {
|
|
682
742
|
let payload = applyEventPrivacyFilter(event, config.privacyTier);
|
|
683
743
|
if (config.e2eEnabled && config.e2eUserKey) {
|
|
684
744
|
payload = applyEventE2E(payload, config.e2eUserKey);
|
|
685
745
|
if (config.debug) {
|
|
686
|
-
debugLog(`E2E applied to event (type=${eventType})
|
|
746
|
+
debugLog(`E2E applied to event (type=${eventType})`, hookName);
|
|
687
747
|
}
|
|
688
748
|
}
|
|
689
749
|
const bodyStr = JSON.stringify(payload);
|
|
690
|
-
const pluginHash = getPluginHash(pluginPath, config.debug);
|
|
750
|
+
const pluginHash = getPluginHash(pluginPath, config.debug, hookName);
|
|
691
751
|
const hmacHeaders = buildHMACHeaders(bodyStr, config.token, config.hookVersion, pluginHash);
|
|
692
752
|
const headers = {
|
|
693
753
|
"Authorization": `Bearer ${config.token}`,
|
|
@@ -695,13 +755,13 @@ async function sendEvent(event, config, pluginPath) {
|
|
|
695
755
|
};
|
|
696
756
|
const url = `${config.apiUrl}/${config.apiVersion}/events`;
|
|
697
757
|
if (config.debug) {
|
|
698
|
-
debugLog(`=== SENT TO ${url}
|
|
699
|
-
|
|
700
|
-
debugLog("=== END SENT ===");
|
|
758
|
+
debugLog(`=== SENT TO ${url} ===`, hookName);
|
|
759
|
+
debugLogRawInline(bodyStr);
|
|
760
|
+
debugLog("=== END SENT ===", hookName);
|
|
701
761
|
}
|
|
702
762
|
const response = await httpPost(url, bodyStr, headers, 5e3);
|
|
703
763
|
if (config.debug) {
|
|
704
|
-
debugLog(`send_event response: ${response.body
|
|
764
|
+
debugLog(`send_event response: ${response.body || "<empty>"}`, hookName);
|
|
705
765
|
}
|
|
706
766
|
if (response.status === 200) {
|
|
707
767
|
try {
|
|
@@ -712,7 +772,7 @@ async function sendEvent(event, config, pluginPath) {
|
|
|
712
772
|
}
|
|
713
773
|
} catch (err) {
|
|
714
774
|
if (config.debug) {
|
|
715
|
-
debugLog(`Failed to send ${eventType || "event"}: ${err instanceof Error ? err.message : String(err)}
|
|
775
|
+
debugLog(`Failed to send ${eventType || "event"}: ${err instanceof Error ? err.message : String(err)}`, hookName);
|
|
716
776
|
}
|
|
717
777
|
}
|
|
718
778
|
}
|
|
@@ -728,6 +788,19 @@ var gatewaySessionId = randomBytes2(12).toString("hex");
|
|
|
728
788
|
var DEDUP_WINDOW_MS = 1200;
|
|
729
789
|
var DEDUP_MAX_SIZE = 300;
|
|
730
790
|
var recentCompletions = /* @__PURE__ */ new Map();
|
|
791
|
+
var HOOK_PLUGIN = "openclaw-plugin";
|
|
792
|
+
var HOOK_BEFORE_TOOL = "openclaw-before-tool";
|
|
793
|
+
var HOOK_AFTER_TOOL = "openclaw-after-tool";
|
|
794
|
+
var HOOK_SESSION_START = "openclaw-session-start";
|
|
795
|
+
var HOOK_SESSION_END = "openclaw-session-end";
|
|
796
|
+
var HOOK_LLM_INPUT = "openclaw-llm-input";
|
|
797
|
+
var HOOK_LLM_OUTPUT = "openclaw-llm-output";
|
|
798
|
+
var HOOK_AGENT_END = "openclaw-agent-end";
|
|
799
|
+
var HOOK_BEFORE_COMPACTION = "openclaw-before-compaction";
|
|
800
|
+
var HOOK_SUBAGENT_SPAWNED = "openclaw-subagent-spawned";
|
|
801
|
+
var HOOK_SUBAGENT_ENDED = "openclaw-subagent-ended";
|
|
802
|
+
var HOOK_COMMAND = "openclaw-command";
|
|
803
|
+
var HOOK_MESSAGE = "openclaw-message";
|
|
731
804
|
function resolveConversationId() {
|
|
732
805
|
return gatewaySessionId;
|
|
733
806
|
}
|
|
@@ -826,10 +899,10 @@ function extractResultPreview(toolName, params, result, maxLen = 300) {
|
|
|
826
899
|
if (str.length <= maxLen) return str;
|
|
827
900
|
return str.slice(0, maxLen) + "...";
|
|
828
901
|
}
|
|
829
|
-
function handleFailBehavior(config, error, toolName, logger) {
|
|
902
|
+
function handleFailBehavior(config, error, toolName, logger, hookName = HOOK_PLUGIN) {
|
|
830
903
|
logger.warn(`Agent Approve API error for tool "${toolName}": ${error.message}`);
|
|
831
904
|
if (config.debug) {
|
|
832
|
-
debugLog(`API error: ${error.message}, failBehavior: ${config.failBehavior}
|
|
905
|
+
debugLog(`API error: ${error.message}, failBehavior: ${config.failBehavior}`, hookName);
|
|
833
906
|
}
|
|
834
907
|
switch (config.failBehavior) {
|
|
835
908
|
case "deny":
|
|
@@ -844,46 +917,65 @@ function handleFailBehavior(config, error, toolName, logger) {
|
|
|
844
917
|
}
|
|
845
918
|
function register(api) {
|
|
846
919
|
const config = loadConfig(api.pluginConfig, api.logger);
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
);
|
|
851
|
-
|
|
920
|
+
const missingTokenMessage = "Missing token. Download the iOS app, run npx agentapprove, and pair your device. Subscription required.";
|
|
921
|
+
const noTokenConfigured = !config.token;
|
|
922
|
+
if (noTokenConfigured) {
|
|
923
|
+
api.logger.warn(`Agent Approve: ${missingTokenMessage}`);
|
|
924
|
+
} else {
|
|
925
|
+
api.logger.info(`Agent Approve: Plugin loaded (privacy: ${config.privacyTier}, fail: ${config.failBehavior})`);
|
|
852
926
|
}
|
|
853
|
-
api.logger.info(`Agent Approve: Plugin loaded (privacy: ${config.privacyTier}, fail: ${config.failBehavior})`);
|
|
854
927
|
if (config.debug) {
|
|
855
928
|
const e2eStatus = !config.e2eEnabled ? "disabled" : !config.e2eUserKey ? "enabled (key missing)" : "enabled";
|
|
856
929
|
debugLog(
|
|
857
|
-
`Plugin loaded: v${config.hookVersion}, api=${config.apiUrl}, privacy=${config.privacyTier}, e2e=${e2eStatus}, debug=${config.debug}
|
|
930
|
+
`Plugin loaded: v${config.hookVersion}, api=${config.apiUrl}, privacy=${config.privacyTier}, e2e=${e2eStatus}, debug=${config.debug}`,
|
|
931
|
+
HOOK_PLUGIN
|
|
858
932
|
);
|
|
859
|
-
debugLog(`Full config: agent=${config.agentName}, timeout=${config.timeout}s, fail=${config.failBehavior}
|
|
933
|
+
debugLog(`Full config: agent=${config.agentName}, timeout=${config.timeout}s, fail=${config.failBehavior}`, HOOK_PLUGIN);
|
|
860
934
|
}
|
|
861
935
|
api.on("before_tool_call", async (event, ctx) => {
|
|
936
|
+
if (config.debug) {
|
|
937
|
+
debugLogRaw({ event, ctx }, HOOK_BEFORE_TOOL);
|
|
938
|
+
debugLog("Started before_tool_call hook", HOOK_BEFORE_TOOL);
|
|
939
|
+
}
|
|
862
940
|
const conversationId = resolveConversationId();
|
|
863
941
|
const { toolType, displayName } = classifyTool(event.toolName, event.params);
|
|
864
942
|
const command = extractCommand(event.toolName, event.params);
|
|
943
|
+
if (config.debug) {
|
|
944
|
+
debugLog(`Tool: ${displayName} (${toolType}) [agent: ${config.agentName}]`, HOOK_BEFORE_TOOL);
|
|
945
|
+
}
|
|
946
|
+
if (noTokenConfigured) {
|
|
947
|
+
return {
|
|
948
|
+
block: true,
|
|
949
|
+
blockReason: missingTokenMessage
|
|
950
|
+
};
|
|
951
|
+
}
|
|
865
952
|
const request = {
|
|
866
953
|
toolName: displayName,
|
|
867
954
|
toolType,
|
|
868
955
|
command,
|
|
869
956
|
toolInput: event.params,
|
|
870
|
-
agent: config.
|
|
957
|
+
agent: config.agentType,
|
|
958
|
+
agentName: config.agentName,
|
|
871
959
|
hookType: "before_tool_call",
|
|
872
960
|
sessionId: conversationId,
|
|
873
961
|
conversationId,
|
|
874
962
|
cwd: event.params.workdir || void 0,
|
|
963
|
+
projectPath: process.cwd(),
|
|
875
964
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
876
965
|
};
|
|
877
966
|
try {
|
|
878
|
-
const response = await sendApprovalRequest(request, config, pluginFilePath);
|
|
967
|
+
const response = await sendApprovalRequest(request, config, pluginFilePath, HOOK_BEFORE_TOOL);
|
|
968
|
+
if (config.debug) {
|
|
969
|
+
debugLog(`Decision: ${response.decision}, Reason: ${response.reason || ""}`, HOOK_BEFORE_TOOL);
|
|
970
|
+
}
|
|
879
971
|
if (response.decision === "approve" || response.decision === "allow") {
|
|
880
972
|
if (config.debug) {
|
|
881
|
-
debugLog(
|
|
973
|
+
debugLog("Tool approved", HOOK_BEFORE_TOOL);
|
|
882
974
|
}
|
|
883
975
|
return void 0;
|
|
884
976
|
}
|
|
885
977
|
if (config.debug) {
|
|
886
|
-
debugLog(
|
|
978
|
+
debugLog("Tool denied", HOOK_BEFORE_TOOL);
|
|
887
979
|
}
|
|
888
980
|
return {
|
|
889
981
|
block: true,
|
|
@@ -894,73 +986,89 @@ function register(api) {
|
|
|
894
986
|
config,
|
|
895
987
|
error instanceof Error ? error : new Error(String(error)),
|
|
896
988
|
event.toolName,
|
|
897
|
-
api.logger
|
|
989
|
+
api.logger,
|
|
990
|
+
HOOK_BEFORE_TOOL
|
|
898
991
|
);
|
|
899
992
|
}
|
|
900
993
|
});
|
|
901
994
|
api.on("after_tool_call", async (event, ctx) => {
|
|
995
|
+
if (config.debug) {
|
|
996
|
+
debugLogRaw({ event, ctx }, HOOK_AFTER_TOOL);
|
|
997
|
+
}
|
|
902
998
|
const conversationId = resolveConversationId();
|
|
903
999
|
if (isDuplicateCompletion(event.toolName, event.params)) {
|
|
904
1000
|
if (config.debug) {
|
|
905
|
-
debugLog(`Skipping duplicate tool_complete for "${event.toolName}"
|
|
1001
|
+
debugLog(`Skipping duplicate tool_complete for "${event.toolName}"`, HOOK_AFTER_TOOL);
|
|
906
1002
|
}
|
|
907
1003
|
return;
|
|
908
1004
|
}
|
|
909
1005
|
const { toolType } = classifyTool(event.toolName, event.params);
|
|
910
1006
|
const resultPreview = extractResultPreview(event.toolName, event.params, event.result);
|
|
1007
|
+
const resultStr = event.result != null ? typeof event.result === "string" ? event.result : JSON.stringify(event.result) : void 0;
|
|
1008
|
+
const longPreview = resultStr && resultStr.length > 1e3 ? resultStr.slice(0, 1e3) + "..." : resultStr;
|
|
911
1009
|
void sendEvent({
|
|
912
1010
|
toolName: event.toolName,
|
|
913
1011
|
toolType,
|
|
914
1012
|
eventType: "tool_complete",
|
|
915
|
-
agent: config.
|
|
1013
|
+
agent: config.agentType,
|
|
1014
|
+
agentName: config.agentName,
|
|
916
1015
|
hookType: "after_tool_call",
|
|
917
1016
|
sessionId: conversationId,
|
|
918
1017
|
conversationId,
|
|
919
1018
|
command: extractCommand(event.toolName, event.params),
|
|
1019
|
+
toolInput: event.params,
|
|
920
1020
|
status: event.error ? "error" : "success",
|
|
921
1021
|
response: event.error || resultPreview || void 0,
|
|
1022
|
+
responsePreview: longPreview,
|
|
922
1023
|
durationMs: event.durationMs,
|
|
1024
|
+
cwd: event.params?.workdir || void 0,
|
|
923
1025
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
924
|
-
}, config, pluginFilePath);
|
|
1026
|
+
}, config, pluginFilePath, HOOK_AFTER_TOOL);
|
|
925
1027
|
});
|
|
926
1028
|
api.on("session_start", async (event, ctx) => {
|
|
927
|
-
const conversationId = resolveConversationId();
|
|
928
1029
|
if (config.debug) {
|
|
929
|
-
|
|
1030
|
+
debugLogRaw({ event, ctx }, HOOK_SESSION_START);
|
|
1031
|
+
debugLog(`Session started: ${event.sessionId}${event.resumedFrom ? ` (resumed from ${event.resumedFrom})` : ""}`, HOOK_SESSION_START);
|
|
930
1032
|
}
|
|
1033
|
+
const conversationId = resolveConversationId();
|
|
931
1034
|
void sendEvent({
|
|
932
1035
|
eventType: "session_start",
|
|
933
|
-
agent: config.
|
|
1036
|
+
agent: config.agentType,
|
|
1037
|
+
agentName: config.agentName,
|
|
934
1038
|
hookType: "session_start",
|
|
935
1039
|
sessionId: conversationId,
|
|
936
1040
|
conversationId,
|
|
937
1041
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
938
|
-
}, config, pluginFilePath);
|
|
1042
|
+
}, config, pluginFilePath, HOOK_SESSION_START);
|
|
939
1043
|
});
|
|
940
1044
|
api.on("session_end", async (event, ctx) => {
|
|
941
|
-
const conversationId = resolveConversationId();
|
|
942
1045
|
if (config.debug) {
|
|
943
|
-
|
|
1046
|
+
debugLogRaw({ event, ctx }, HOOK_SESSION_END);
|
|
1047
|
+
debugLog(`Session ended: ${event.sessionId} (${event.messageCount} messages, ${event.durationMs ?? "?"}ms)`, HOOK_SESSION_END);
|
|
944
1048
|
}
|
|
1049
|
+
const conversationId = resolveConversationId();
|
|
945
1050
|
void sendEvent({
|
|
946
1051
|
eventType: "session_end",
|
|
947
|
-
agent: config.
|
|
1052
|
+
agent: config.agentType,
|
|
1053
|
+
agentName: config.agentName,
|
|
948
1054
|
hookType: "session_end",
|
|
949
1055
|
sessionId: conversationId,
|
|
950
1056
|
conversationId,
|
|
951
1057
|
durationMs: event.durationMs,
|
|
952
1058
|
sessionStats: { messageCount: event.messageCount },
|
|
953
1059
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
954
|
-
}, config, pluginFilePath);
|
|
1060
|
+
}, config, pluginFilePath, HOOK_SESSION_END);
|
|
955
1061
|
});
|
|
956
1062
|
api.on("llm_input", async (event, ctx) => {
|
|
957
|
-
const conversationId = resolveConversationId();
|
|
958
1063
|
if (config.debug) {
|
|
959
|
-
|
|
1064
|
+
debugLogRaw({ event, ctx }, HOOK_LLM_INPUT);
|
|
1065
|
+
debugLog(`LLM input: model=${event.model}, prompt length=${event.prompt?.length ?? 0}`, HOOK_LLM_INPUT);
|
|
960
1066
|
}
|
|
1067
|
+
const conversationId = resolveConversationId();
|
|
961
1068
|
void sendEvent({
|
|
962
1069
|
eventType: "user_prompt",
|
|
963
|
-
agent: config.
|
|
1070
|
+
agent: config.agentType,
|
|
1071
|
+
agentName: config.agentName,
|
|
964
1072
|
hookType: "llm_input",
|
|
965
1073
|
sessionId: conversationId,
|
|
966
1074
|
conversationId,
|
|
@@ -968,18 +1076,20 @@ function register(api) {
|
|
|
968
1076
|
prompt: event.prompt,
|
|
969
1077
|
textLength: event.prompt?.length ?? 0,
|
|
970
1078
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
971
|
-
}, config, pluginFilePath);
|
|
1079
|
+
}, config, pluginFilePath, HOOK_LLM_INPUT);
|
|
972
1080
|
});
|
|
973
1081
|
api.on("llm_output", async (event, ctx) => {
|
|
974
1082
|
const conversationId = resolveConversationId();
|
|
975
1083
|
const responseText = event.assistantTexts?.join("\n") || "";
|
|
976
1084
|
const textLength = responseText.length;
|
|
977
1085
|
if (config.debug) {
|
|
978
|
-
|
|
1086
|
+
debugLogRaw({ event, ctx }, HOOK_LLM_OUTPUT);
|
|
1087
|
+
debugLog(`LLM output: model=${event.model}, length=${textLength}${event.usage?.total ? `, tokens=${event.usage.total}` : ""}`, HOOK_LLM_OUTPUT);
|
|
979
1088
|
}
|
|
980
1089
|
void sendEvent({
|
|
981
1090
|
eventType: "response",
|
|
982
|
-
agent: config.
|
|
1091
|
+
agent: config.agentType,
|
|
1092
|
+
agentName: config.agentName,
|
|
983
1093
|
hookType: "llm_output",
|
|
984
1094
|
sessionId: conversationId,
|
|
985
1095
|
conversationId,
|
|
@@ -988,16 +1098,18 @@ function register(api) {
|
|
|
988
1098
|
textPreview: responseText.slice(0, 200),
|
|
989
1099
|
textLength,
|
|
990
1100
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
991
|
-
}, config, pluginFilePath);
|
|
1101
|
+
}, config, pluginFilePath, HOOK_LLM_OUTPUT);
|
|
992
1102
|
});
|
|
993
1103
|
api.on("agent_end", async (event, ctx) => {
|
|
994
|
-
const conversationId = resolveConversationId();
|
|
995
1104
|
if (config.debug) {
|
|
996
|
-
|
|
1105
|
+
debugLogRaw({ event, ctx }, HOOK_AGENT_END);
|
|
1106
|
+
debugLog(`Agent ended: success=${event.success}${event.durationMs ? `, duration=${event.durationMs}ms` : ""}${event.error ? `, error=${event.error}` : ""}`, HOOK_AGENT_END);
|
|
997
1107
|
}
|
|
1108
|
+
const conversationId = resolveConversationId();
|
|
998
1109
|
void sendEvent({
|
|
999
1110
|
eventType: "stop",
|
|
1000
|
-
agent: config.
|
|
1111
|
+
agent: config.agentType,
|
|
1112
|
+
agentName: config.agentName,
|
|
1001
1113
|
hookType: "agent_end",
|
|
1002
1114
|
sessionId: conversationId,
|
|
1003
1115
|
conversationId,
|
|
@@ -1005,31 +1117,35 @@ function register(api) {
|
|
|
1005
1117
|
durationMs: event.durationMs,
|
|
1006
1118
|
response: event.error || void 0,
|
|
1007
1119
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1008
|
-
}, config, pluginFilePath);
|
|
1120
|
+
}, config, pluginFilePath, HOOK_AGENT_END);
|
|
1009
1121
|
});
|
|
1010
1122
|
api.on("before_compaction", async (event, ctx) => {
|
|
1011
|
-
const conversationId = resolveConversationId();
|
|
1012
1123
|
if (config.debug) {
|
|
1013
|
-
|
|
1124
|
+
debugLogRaw({ event, ctx }, HOOK_BEFORE_COMPACTION);
|
|
1125
|
+
debugLog(`Context compaction: ${event.messageCount} messages${event.tokenCount ? `, ${event.tokenCount} tokens` : ""}`, HOOK_BEFORE_COMPACTION);
|
|
1014
1126
|
}
|
|
1127
|
+
const conversationId = resolveConversationId();
|
|
1015
1128
|
void sendEvent({
|
|
1016
1129
|
eventType: "context_compact",
|
|
1017
|
-
agent: config.
|
|
1130
|
+
agent: config.agentType,
|
|
1131
|
+
agentName: config.agentName,
|
|
1018
1132
|
hookType: "before_compaction",
|
|
1019
1133
|
sessionId: conversationId,
|
|
1020
1134
|
conversationId,
|
|
1021
1135
|
trigger: `${event.messageCount} messages${event.tokenCount ? `, ${event.tokenCount} tokens` : ""}`,
|
|
1022
1136
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1023
|
-
}, config, pluginFilePath);
|
|
1137
|
+
}, config, pluginFilePath, HOOK_BEFORE_COMPACTION);
|
|
1024
1138
|
});
|
|
1025
1139
|
api.on("subagent_spawned", async (event, ctx) => {
|
|
1026
|
-
const conversationId = resolveConversationId();
|
|
1027
1140
|
if (config.debug) {
|
|
1028
|
-
|
|
1141
|
+
debugLogRaw({ event, ctx }, HOOK_SUBAGENT_SPAWNED);
|
|
1142
|
+
debugLog(`Subagent spawned: ${event.agentId} (${event.mode}${event.label ? `, label=${event.label}` : ""})`, HOOK_SUBAGENT_SPAWNED);
|
|
1029
1143
|
}
|
|
1144
|
+
const conversationId = resolveConversationId();
|
|
1030
1145
|
void sendEvent({
|
|
1031
1146
|
eventType: "subagent_start",
|
|
1032
|
-
agent: config.
|
|
1147
|
+
agent: config.agentType,
|
|
1148
|
+
agentName: config.agentName,
|
|
1033
1149
|
hookType: "subagent_spawned",
|
|
1034
1150
|
sessionId: conversationId,
|
|
1035
1151
|
conversationId,
|
|
@@ -1037,16 +1153,18 @@ function register(api) {
|
|
|
1037
1153
|
subagentMode: event.mode,
|
|
1038
1154
|
subagentLabel: event.label,
|
|
1039
1155
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1040
|
-
}, config, pluginFilePath);
|
|
1156
|
+
}, config, pluginFilePath, HOOK_SUBAGENT_SPAWNED);
|
|
1041
1157
|
});
|
|
1042
1158
|
api.on("subagent_ended", async (event, ctx) => {
|
|
1043
|
-
const conversationId = resolveConversationId();
|
|
1044
1159
|
if (config.debug) {
|
|
1045
|
-
|
|
1160
|
+
debugLogRaw({ event, ctx }, HOOK_SUBAGENT_ENDED);
|
|
1161
|
+
debugLog(`Subagent ended: ${event.targetKind} reason=${event.reason}${event.outcome ? `, outcome=${event.outcome}` : ""}`, HOOK_SUBAGENT_ENDED);
|
|
1046
1162
|
}
|
|
1163
|
+
const conversationId = resolveConversationId();
|
|
1047
1164
|
void sendEvent({
|
|
1048
1165
|
eventType: "subagent_stop",
|
|
1049
|
-
agent: config.
|
|
1166
|
+
agent: config.agentType,
|
|
1167
|
+
agentName: config.agentName,
|
|
1050
1168
|
hookType: "subagent_ended",
|
|
1051
1169
|
sessionId: conversationId,
|
|
1052
1170
|
conversationId,
|
|
@@ -1054,7 +1172,7 @@ function register(api) {
|
|
|
1054
1172
|
subagentType: event.targetKind,
|
|
1055
1173
|
response: event.error || void 0,
|
|
1056
1174
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1057
|
-
}, config, pluginFilePath);
|
|
1175
|
+
}, config, pluginFilePath, HOOK_SUBAGENT_ENDED);
|
|
1058
1176
|
});
|
|
1059
1177
|
api.registerHook(
|
|
1060
1178
|
["command:new", "command:stop", "command:reset"],
|
|
@@ -1065,18 +1183,20 @@ function register(api) {
|
|
|
1065
1183
|
reset: "session_start"
|
|
1066
1184
|
};
|
|
1067
1185
|
if (config.debug) {
|
|
1068
|
-
|
|
1186
|
+
debugLogRaw(event, HOOK_COMMAND);
|
|
1187
|
+
debugLog(`Command event: ${event.action}`, HOOK_COMMAND);
|
|
1069
1188
|
}
|
|
1070
1189
|
void sendEvent({
|
|
1071
1190
|
toolName: `command:${event.action}`,
|
|
1072
1191
|
toolType: "command",
|
|
1073
1192
|
eventType: eventTypeMap[event.action] || "command_event",
|
|
1074
|
-
agent: config.
|
|
1193
|
+
agent: config.agentType,
|
|
1194
|
+
agentName: config.agentName,
|
|
1075
1195
|
hookType: "command_event",
|
|
1076
1196
|
sessionId: resolveConversationId(),
|
|
1077
1197
|
conversationId: resolveConversationId(),
|
|
1078
1198
|
timestamp: event.timestamp.toISOString()
|
|
1079
|
-
}, config, pluginFilePath);
|
|
1199
|
+
}, config, pluginFilePath, HOOK_COMMAND);
|
|
1080
1200
|
},
|
|
1081
1201
|
{ name: "agentapprove-command-monitor", description: "Log command events to Agent Approve" }
|
|
1082
1202
|
);
|
|
@@ -1088,13 +1208,15 @@ function register(api) {
|
|
|
1088
1208
|
const peer = isInbound ? event.context.from : event.context.to;
|
|
1089
1209
|
const channelLabel = [provider, peer].filter(Boolean).join(":") || void 0;
|
|
1090
1210
|
if (config.debug) {
|
|
1091
|
-
|
|
1211
|
+
debugLogRaw(event, HOOK_MESSAGE);
|
|
1212
|
+
debugLog(`Message event: ${event.action} ${channelLabel || "(no channel)"}`, HOOK_MESSAGE);
|
|
1092
1213
|
}
|
|
1093
1214
|
const payload = {
|
|
1094
1215
|
toolName: `message:${event.action}`,
|
|
1095
1216
|
toolType: "message_event",
|
|
1096
1217
|
eventType: isInbound ? "user_prompt" : "response",
|
|
1097
|
-
agent: config.
|
|
1218
|
+
agent: config.agentType,
|
|
1219
|
+
agentName: config.agentName,
|
|
1098
1220
|
hookType: "session_event",
|
|
1099
1221
|
sessionId: resolveConversationId(),
|
|
1100
1222
|
conversationId: resolveConversationId(),
|
|
@@ -1110,7 +1232,7 @@ function register(api) {
|
|
|
1110
1232
|
payload.prompt = `${peer}: ${content}`;
|
|
1111
1233
|
}
|
|
1112
1234
|
}
|
|
1113
|
-
void sendEvent(payload, config, pluginFilePath);
|
|
1235
|
+
void sendEvent(payload, config, pluginFilePath, HOOK_MESSAGE);
|
|
1114
1236
|
},
|
|
1115
1237
|
{ name: "agentapprove-session-monitor", description: "Log message events to Agent Approve" }
|
|
1116
1238
|
);
|
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