@agentapprove/openclaw 0.1.9 → 0.2.0

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.
Files changed (2) hide show
  1. package/dist/index.js +162 -15
  2. package/package.json +11 -3
package/dist/index.js CHANGED
@@ -415,7 +415,7 @@ function loadConfig(openclawConfig, logger) {
415
415
  failBehavior,
416
416
  privacyTier,
417
417
  debug,
418
- hookVersion: "1.1.5",
418
+ hookVersion: "1.2.0",
419
419
  agentType: "openclaw",
420
420
  agentName,
421
421
  e2eEnabled,
@@ -851,6 +851,153 @@ async function sendEvent(event, config, pluginPath, hookName = "openclaw-plugin"
851
851
  }
852
852
  }
853
853
 
854
+ // src/gateway-client.ts
855
+ import { randomUUID } from "crypto";
856
+ var DEFAULT_GATEWAY_PORT = 18789;
857
+ var CONNECT_TIMEOUT_MS = 1e4;
858
+ var HOOK_NAME = "openclaw-gateway";
859
+ function resolveGatewayConfig(pluginConfig, debug) {
860
+ const gw = pluginConfig?.gateway;
861
+ const envPortRaw = process.env.OPENCLAW_GATEWAY_PORT;
862
+ const envPort = parseInt(envPortRaw || "", 10);
863
+ if (envPortRaw && isNaN(envPort)) {
864
+ debugLog(`Invalid OPENCLAW_GATEWAY_PORT "${envPortRaw}", using default ${DEFAULT_GATEWAY_PORT}`, HOOK_NAME);
865
+ }
866
+ const port = (typeof gw?.port === "number" && gw.port > 0 ? gw.port : void 0) || (!isNaN(envPort) ? envPort : void 0) || DEFAULT_GATEWAY_PORT;
867
+ const auth = gw?.auth;
868
+ const configToken = typeof auth?.token === "string" ? auth.token : "";
869
+ const token = configToken || process.env.OPENCLAW_GATEWAY_TOKEN || "";
870
+ if (!token) {
871
+ return null;
872
+ }
873
+ return { port, token, debug };
874
+ }
875
+ async function sendChatMessage(gwConfig, sessionKey, message, idempotencyKey = randomUUID()) {
876
+ let WebSocket;
877
+ try {
878
+ ({ default: WebSocket } = await import("ws"));
879
+ } catch {
880
+ throw new Error(
881
+ 'The "ws" package is required for Gateway WebSocket support. Install it with: npm install ws'
882
+ );
883
+ }
884
+ return new Promise((resolve, reject) => {
885
+ const url = `ws://127.0.0.1:${gwConfig.port}`;
886
+ let settled = false;
887
+ let connectReqId;
888
+ let chatReqId;
889
+ if (gwConfig.debug) {
890
+ debugLog(`Connecting to gateway at ${url}`, HOOK_NAME);
891
+ }
892
+ const ws = new WebSocket(url);
893
+ const sendOrFail = (payload) => {
894
+ ws.send(payload, (sendErr) => {
895
+ if (sendErr && !settled) {
896
+ settled = true;
897
+ cleanup();
898
+ reject(sendErr);
899
+ }
900
+ });
901
+ };
902
+ const timeout = setTimeout(() => {
903
+ if (!settled) {
904
+ settled = true;
905
+ try {
906
+ ws.terminate();
907
+ } catch {
908
+ }
909
+ reject(new Error(`Gateway connection timed out after ${CONNECT_TIMEOUT_MS}ms`));
910
+ }
911
+ }, CONNECT_TIMEOUT_MS);
912
+ const cleanup = () => {
913
+ clearTimeout(timeout);
914
+ try {
915
+ ws.terminate();
916
+ } catch {
917
+ }
918
+ };
919
+ ws.on("error", (err) => {
920
+ if (!settled) {
921
+ settled = true;
922
+ cleanup();
923
+ reject(err);
924
+ }
925
+ });
926
+ ws.on("message", (data) => {
927
+ let msg;
928
+ try {
929
+ msg = JSON.parse(data.toString());
930
+ } catch {
931
+ return;
932
+ }
933
+ if (msg.type === "event" && msg.event === "connect.challenge" && !connectReqId) {
934
+ connectReqId = randomUUID();
935
+ sendOrFail(JSON.stringify({
936
+ type: "req",
937
+ id: connectReqId,
938
+ method: "connect",
939
+ params: {
940
+ minProtocol: 3,
941
+ maxProtocol: 3,
942
+ client: { id: "agentapprove-plugin", version: "1.0", platform: "node", mode: "operator" },
943
+ role: "operator",
944
+ scopes: ["operator.admin"],
945
+ auth: { token: gwConfig.token }
946
+ }
947
+ }));
948
+ if (gwConfig.debug) {
949
+ debugLog("Sent connect request", HOOK_NAME);
950
+ }
951
+ return;
952
+ }
953
+ if (msg.type === "res" && connectReqId && msg.id === connectReqId && !chatReqId) {
954
+ if (!msg.ok) {
955
+ settled = true;
956
+ cleanup();
957
+ reject(new Error(`Gateway auth failed: ${msg.error?.message || "unknown"}`));
958
+ return;
959
+ }
960
+ chatReqId = randomUUID();
961
+ sendOrFail(JSON.stringify({
962
+ type: "req",
963
+ id: chatReqId,
964
+ method: "chat.send",
965
+ params: {
966
+ sessionKey,
967
+ message,
968
+ idempotencyKey
969
+ }
970
+ }));
971
+ if (gwConfig.debug) {
972
+ debugLog(`Sent chat.send for session ${sessionKey}`, HOOK_NAME);
973
+ }
974
+ return;
975
+ }
976
+ if (msg.type === "res" && chatReqId && msg.id === chatReqId) {
977
+ settled = true;
978
+ cleanup();
979
+ if (msg.ok) {
980
+ const runId = msg.payload?.runId;
981
+ if (gwConfig.debug) {
982
+ debugLog(`chat.send succeeded, runId=${runId || "unknown"}`, HOOK_NAME);
983
+ }
984
+ resolve({ runId });
985
+ } else {
986
+ reject(new Error(`chat.send failed: ${msg.error?.message || "unknown"}`));
987
+ }
988
+ return;
989
+ }
990
+ });
991
+ ws.on("close", () => {
992
+ if (!settled) {
993
+ settled = true;
994
+ cleanup();
995
+ reject(new Error("Gateway connection closed unexpectedly"));
996
+ }
997
+ });
998
+ });
999
+ }
1000
+
854
1001
  // src/index.ts
855
1002
  var pluginFilePath;
856
1003
  try {
@@ -1209,29 +1356,29 @@ function register(api) {
1209
1356
  }
1210
1357
  followupText = decrypted;
1211
1358
  }
1212
- if (!api.runtime?.system?.enqueueSystemEvent) {
1359
+ const gwConfig = resolveGatewayConfig(api.pluginConfig, config.debug);
1360
+ if (!gwConfig) {
1213
1361
  api.logger.warn(
1214
- "Agent Approve: follow-up received but runtime.system API unavailable (OpenClaw version may be too old)"
1362
+ "Agent Approve: follow-up received but no gateway token configured \u2014 set OPENCLAW_GATEWAY_TOKEN or gateway.auth.token in plugin config"
1215
1363
  );
1216
1364
  return;
1217
1365
  }
1218
- try {
1219
- const sessionKey = ctx.sessionKey || conversationId;
1220
- api.runtime.system.enqueueSystemEvent(
1221
- `Agent Approve follow-up from user: ${followupText}`,
1222
- { sessionKey }
1366
+ if (!ctx.sessionKey) {
1367
+ api.logger.warn(
1368
+ "Agent Approve: follow-up received but session key unavailable \u2014 cannot send to OpenClaw"
1223
1369
  );
1224
- api.runtime.system.requestHeartbeatNow({
1225
- sessionKey,
1226
- reason: "agentapprove-followup"
1227
- });
1370
+ return;
1371
+ }
1372
+ try {
1373
+ const idempotencyKey = createHash2("sha256").update(`${ctx.sessionKey}:${stopRequest.timestamp}:${followupText}`).digest("hex").slice(0, 32);
1374
+ const result = await sendChatMessage(gwConfig, ctx.sessionKey, followupText, idempotencyKey);
1228
1375
  if (config.debug) {
1229
- debugLog(`Follow-up injected via system event, heartbeat requested`, HOOK_AGENT_END);
1376
+ debugLog(`Follow-up sent via gateway chat.send, runId=${result.runId || "unknown"}`, HOOK_AGENT_END);
1230
1377
  }
1231
- api.logger.info(`Agent Approve: follow-up injected, heartbeat requested`);
1378
+ api.logger.info(`Agent Approve: follow-up sent as user prompt via gateway`);
1232
1379
  } catch (injectionError) {
1233
1380
  const injMsg = injectionError instanceof Error ? injectionError.message : String(injectionError);
1234
- api.logger.warn(`Agent Approve: follow-up injection failed \u2014 ${injMsg}`);
1381
+ api.logger.warn(`Agent Approve: follow-up injection via gateway failed \u2014 ${injMsg}`);
1235
1382
  }
1236
1383
  }
1237
1384
  } catch (error) {
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@agentapprove/openclaw",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "description": "Agent Approve plugin for OpenClaw - approve or deny AI agent tool calls from your iPhone and Apple Watch",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
7
- "build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --format=esm --external:crypto --external:fs --external:path --external:os --external:https --external:http --external:url --external:child_process",
7
+ "build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --format=esm --external:crypto --external:fs --external:path --external:os --external:https --external:http --external:url --external:child_process --external:ws",
8
8
  "hash": "shasum -a 256 dist/index.js | cut -d' ' -f1",
9
- "dev": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --format=esm --external:crypto --external:fs --external:path --external:os --external:https --external:http --external:url --external:child_process --watch"
9
+ "dev": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --format=esm --external:crypto --external:fs --external:path --external:os --external:https --external:http --external:url --external:child_process --external:ws --watch"
10
10
  },
11
11
  "openclaw": {
12
12
  "extensions": ["./dist/index.js"]
@@ -36,6 +36,14 @@
36
36
  "engines": {
37
37
  "node": ">=18"
38
38
  },
39
+ "peerDependencies": {
40
+ "ws": ">=7.0.0"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "ws": {
44
+ "optional": true
45
+ }
46
+ },
39
47
  "devDependencies": {
40
48
  "esbuild": "^0.24.0",
41
49
  "typescript": "^5.0.0"