@alfe.ai/openclaw 0.4.9 → 0.4.11
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 +10 -0
- package/dist/index.cjs +129 -50
- package/dist/index.d.cts +26 -22
- package/dist/index.d.ts +26 -22
- package/dist/index.js +128 -49
- package/dist/plugin2.cjs +622 -252
- package/dist/plugin2.js +623 -253
- package/openclaw.plugin.json +2 -0
- package/package.json +3 -3
package/dist/plugin2.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { extname, join } from "node:path";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
3
|
import { homedir } from "node:os";
|
|
5
4
|
import { resolveConfig } from "@alfe.ai/config";
|
|
6
5
|
import { AgentApiClient, installToolErrorCapture } from "@alfe.ai/agent-api-client";
|
|
7
|
-
import { createConnection } from "node:net";
|
|
8
6
|
import { randomUUID } from "node:crypto";
|
|
9
7
|
import { EventEmitter } from "node:events";
|
|
8
|
+
import { createConnection } from "node:net";
|
|
9
|
+
import { StringDecoder } from "node:string_decoder";
|
|
10
10
|
import { createLogger } from "@auriclabs/logger";
|
|
11
|
+
import { constants } from "node:fs";
|
|
12
|
+
import { lstat, open, realpath } from "node:fs/promises";
|
|
11
13
|
//#region \0rolldown/runtime.js
|
|
12
14
|
var __create = Object.create;
|
|
13
15
|
var __defProp = Object.defineProperty;
|
|
@@ -2787,197 +2789,183 @@ const Type = /* @__PURE__ */ __exportAll({
|
|
|
2787
2789
|
//#endregion
|
|
2788
2790
|
//#region src/types.ts
|
|
2789
2791
|
function isIPCRequest(msg) {
|
|
2790
|
-
|
|
2792
|
+
if (!isRecord$1(msg)) return false;
|
|
2793
|
+
return msg.type === "req" && isBoundedString(msg.id, 128) && isBoundedString(msg.method, 256) && isRecord$1(msg.params);
|
|
2791
2794
|
}
|
|
2792
2795
|
function isIPCResponse(msg) {
|
|
2793
|
-
|
|
2796
|
+
if (!isRecord$1(msg)) return false;
|
|
2797
|
+
return isBoundedString(msg.id, 128) && typeof msg.ok === "boolean" && !("type" in msg) && (msg.error === void 0 || isIPCError(msg.error));
|
|
2794
2798
|
}
|
|
2795
2799
|
function isIPCEvent(msg) {
|
|
2796
|
-
|
|
2800
|
+
if (!isRecord$1(msg)) return false;
|
|
2801
|
+
return msg.type === "event" && isBoundedString(msg.event, 256);
|
|
2802
|
+
}
|
|
2803
|
+
function isIPCError(value) {
|
|
2804
|
+
return isRecord$1(value) && isBoundedString(value.code, 128) && isBoundedString(value.message, 4096);
|
|
2805
|
+
}
|
|
2806
|
+
function isBoundedString(value, maxLength) {
|
|
2807
|
+
return typeof value === "string" && value.length > 0 && value.length <= maxLength;
|
|
2808
|
+
}
|
|
2809
|
+
function isRecord$1(value) {
|
|
2810
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2797
2811
|
}
|
|
2798
2812
|
/** IPC protocol version — must match daemon's PROTOCOL_VERSION */
|
|
2799
2813
|
const PROTOCOL_VERSION = 1;
|
|
2800
2814
|
//#endregion
|
|
2801
2815
|
//#region src/ipc-client.ts
|
|
2802
2816
|
/**
|
|
2803
|
-
*
|
|
2804
|
-
*
|
|
2805
|
-
*
|
|
2806
|
-
* Request: { type: 'req', id, method, params }
|
|
2807
|
-
* Response: { id, ok, payload?, error? }
|
|
2808
|
-
* Event: { type: 'event', event, payload }
|
|
2809
|
-
*
|
|
2810
|
-
* Features:
|
|
2811
|
-
* - Automatic reconnection with exponential backoff (1s → 30s)
|
|
2812
|
-
* - Request/response correlation via message ID
|
|
2813
|
-
* - Event emission for incoming daemon events + requests
|
|
2814
|
-
* - Graceful handling if daemon not running (warns, retries, doesn't crash)
|
|
2817
|
+
* Reconnecting newline-delimited JSON client for the local Alfe gateway.
|
|
2818
|
+
* The socket is local, but it is still a bidirectional runtime boundary: both
|
|
2819
|
+
* peers cap frames, validate envelopes, and bind callbacks to one connection.
|
|
2815
2820
|
*/
|
|
2816
2821
|
const MIN_BACKOFF_MS = 1e3;
|
|
2817
2822
|
const MAX_BACKOFF_MS = 3e4;
|
|
2818
2823
|
const DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
2824
|
+
const MAX_REQUEST_TIMEOUT_MS = 12e4;
|
|
2825
|
+
const MAX_PENDING_REQUESTS = 128;
|
|
2826
|
+
/** Must stay aligned with `packages/gateway/src/protocol.ts`. */
|
|
2827
|
+
const MAX_IPC_MESSAGE_BYTES = 1024 * 1024;
|
|
2819
2828
|
var IPCClient = class extends EventEmitter {
|
|
2820
2829
|
socket = null;
|
|
2821
|
-
socketPath;
|
|
2822
2830
|
buffer = "";
|
|
2831
|
+
decoder = new StringDecoder("utf8");
|
|
2823
2832
|
backoffMs = MIN_BACKOFF_MS;
|
|
2824
2833
|
closed = false;
|
|
2825
|
-
|
|
2834
|
+
isSocketConnected = false;
|
|
2826
2835
|
reconnectTimer = null;
|
|
2827
2836
|
pending = /* @__PURE__ */ new Map();
|
|
2828
|
-
log = createLogger("IpcClient")
|
|
2829
|
-
constructor(socketPath) {
|
|
2837
|
+
constructor(socketPath, log = createLogger("IpcClient")) {
|
|
2830
2838
|
super();
|
|
2831
2839
|
this.socketPath = socketPath;
|
|
2840
|
+
this.log = log;
|
|
2841
|
+
if (socketPath.length < 1 || socketPath.length > 4096 || socketPath.includes("\0")) throw new TypeError("IPC socket path must contain 1 to 4096 characters without NUL bytes");
|
|
2832
2842
|
}
|
|
2833
2843
|
get connected() {
|
|
2834
|
-
return this.
|
|
2844
|
+
return this.isSocketConnected;
|
|
2835
2845
|
}
|
|
2836
|
-
/**
|
|
2837
|
-
* Start the IPC connection with auto-reconnect.
|
|
2838
|
-
*/
|
|
2839
2846
|
start() {
|
|
2847
|
+
if (this.socket || this.reconnectTimer) return;
|
|
2840
2848
|
this.closed = false;
|
|
2841
|
-
this.
|
|
2849
|
+
this.openSocket();
|
|
2842
2850
|
}
|
|
2843
|
-
/**
|
|
2844
|
-
* Stop the IPC connection and all timers.
|
|
2845
|
-
*/
|
|
2846
2851
|
stop() {
|
|
2847
2852
|
this.closed = true;
|
|
2848
2853
|
this.clearReconnectTimer();
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
}
|
|
2858
|
-
});
|
|
2859
|
-
}
|
|
2860
|
-
this.pending.clear();
|
|
2861
|
-
if (this.socket) {
|
|
2862
|
-
try {
|
|
2863
|
-
this.socket.end();
|
|
2864
|
-
} catch {}
|
|
2865
|
-
this.socket = null;
|
|
2866
|
-
}
|
|
2867
|
-
this._connected = false;
|
|
2868
|
-
this.removeAllListeners();
|
|
2854
|
+
this.resolveAllPending("CLIENT_STOPPED", "IPC client stopped");
|
|
2855
|
+
const socket = this.socket;
|
|
2856
|
+
this.socket = null;
|
|
2857
|
+
this.isSocketConnected = false;
|
|
2858
|
+
this.resetDecoder();
|
|
2859
|
+
if (socket) try {
|
|
2860
|
+
socket.destroy();
|
|
2861
|
+
} catch {}
|
|
2869
2862
|
}
|
|
2870
|
-
/**
|
|
2871
|
-
* Send a request to the daemon and wait for a response.
|
|
2872
|
-
*/
|
|
2873
2863
|
async request(method, params = {}, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
2874
|
-
if (!this.
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
message: "Not connected to daemon"
|
|
2880
|
-
}
|
|
2881
|
-
};
|
|
2864
|
+
if (!this.isSocketConnected || !this.socket) return this.errorResponse("", "NOT_CONNECTED", "Not connected to daemon");
|
|
2865
|
+
if (method.length < 1 || method.length > 256) return this.errorResponse("", "INVALID_REQUEST", "IPC method must contain 1 to 256 characters");
|
|
2866
|
+
const runtimeParams = params;
|
|
2867
|
+
if (typeof runtimeParams !== "object" || runtimeParams === null || Array.isArray(runtimeParams)) return this.errorResponse("", "INVALID_REQUEST", "IPC params must be an object");
|
|
2868
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_REQUEST_TIMEOUT_MS) return this.errorResponse("", "INVALID_REQUEST", `IPC timeout must be an integer from 1 to ${String(MAX_REQUEST_TIMEOUT_MS)}ms`);
|
|
2882
2869
|
const id = randomUUID();
|
|
2870
|
+
if (this.pending.size >= MAX_PENDING_REQUESTS) return this.errorResponse(id, "TOO_MANY_REQUESTS", "Too many IPC requests are in flight");
|
|
2883
2871
|
const request = {
|
|
2884
2872
|
type: "req",
|
|
2885
2873
|
id,
|
|
2886
2874
|
method,
|
|
2887
2875
|
params
|
|
2888
2876
|
};
|
|
2877
|
+
const frame = this.serializeFrame(request, id);
|
|
2878
|
+
if (typeof frame !== "string") return frame;
|
|
2879
|
+
const socket = this.socket;
|
|
2889
2880
|
return new Promise((resolve) => {
|
|
2890
2881
|
const timer = setTimeout(() => {
|
|
2891
2882
|
this.pending.delete(id);
|
|
2892
|
-
resolve({
|
|
2893
|
-
id,
|
|
2894
|
-
ok: false,
|
|
2895
|
-
error: {
|
|
2896
|
-
code: "TIMEOUT",
|
|
2897
|
-
message: `Request ${method} timed out after ${String(timeoutMs)}ms`
|
|
2898
|
-
}
|
|
2899
|
-
});
|
|
2883
|
+
resolve(this.errorResponse(id, "TIMEOUT", `Request ${method} timed out after ${String(timeoutMs)}ms`));
|
|
2900
2884
|
}, timeoutMs);
|
|
2885
|
+
timer.unref();
|
|
2901
2886
|
this.pending.set(id, {
|
|
2902
2887
|
resolve,
|
|
2903
2888
|
timer
|
|
2904
2889
|
});
|
|
2905
2890
|
try {
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
} catch (err) {
|
|
2891
|
+
socket.write(frame);
|
|
2892
|
+
} catch (error) {
|
|
2909
2893
|
clearTimeout(timer);
|
|
2910
2894
|
this.pending.delete(id);
|
|
2911
|
-
resolve({
|
|
2912
|
-
id,
|
|
2913
|
-
ok: false,
|
|
2914
|
-
error: {
|
|
2915
|
-
code: "SEND_FAILED",
|
|
2916
|
-
message: `Failed to send: ${err instanceof Error ? err.message : String(err)}`
|
|
2917
|
-
}
|
|
2918
|
-
});
|
|
2895
|
+
resolve(this.errorResponse(id, "SEND_FAILED", `Failed to send: ${error instanceof Error ? error.message : String(error)}`));
|
|
2919
2896
|
}
|
|
2920
2897
|
});
|
|
2921
2898
|
}
|
|
2922
|
-
/**
|
|
2923
|
-
* Send a response to a daemon request.
|
|
2924
|
-
* Used internally by the request event handler.
|
|
2925
|
-
*/
|
|
2926
2899
|
sendResponse(response) {
|
|
2927
|
-
|
|
2900
|
+
const socket = this.socket;
|
|
2901
|
+
if (!this.isSocketConnected || !socket) return;
|
|
2902
|
+
if (!isIPCResponse(response)) {
|
|
2903
|
+
this.log.warn("Refusing malformed outgoing IPC response");
|
|
2904
|
+
return;
|
|
2905
|
+
}
|
|
2906
|
+
const frame = this.serializeFrame(response, response.id);
|
|
2907
|
+
if (typeof frame !== "string") {
|
|
2908
|
+
this.log.warn(`Refusing oversized or unserializable IPC response for ${response.id}`);
|
|
2909
|
+
return;
|
|
2910
|
+
}
|
|
2928
2911
|
try {
|
|
2929
|
-
|
|
2912
|
+
socket.write(frame);
|
|
2930
2913
|
} catch {}
|
|
2931
2914
|
}
|
|
2932
|
-
|
|
2933
|
-
if (this.closed) return;
|
|
2915
|
+
openSocket() {
|
|
2916
|
+
if (this.closed || this.socket) return;
|
|
2917
|
+
this.resetDecoder();
|
|
2934
2918
|
this.log.debug(`Connecting to daemon at ${this.socketPath}...`);
|
|
2935
|
-
|
|
2919
|
+
const socket = createConnection(this.socketPath, () => {
|
|
2920
|
+
if (this.closed || this.socket !== socket) {
|
|
2921
|
+
socket.destroy();
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2936
2924
|
this.log.info(`Connected to daemon (${this.socketPath})`);
|
|
2937
|
-
this.
|
|
2925
|
+
this.isSocketConnected = true;
|
|
2938
2926
|
this.backoffMs = MIN_BACKOFF_MS;
|
|
2939
|
-
this.
|
|
2940
|
-
this.emit("connected");
|
|
2927
|
+
this.safeEmit("connected");
|
|
2941
2928
|
});
|
|
2942
|
-
this.socket
|
|
2943
|
-
|
|
2944
|
-
this.
|
|
2929
|
+
this.socket = socket;
|
|
2930
|
+
socket.on("data", (data) => {
|
|
2931
|
+
if (this.socket !== socket || this.closed) return;
|
|
2932
|
+
this.buffer += this.decoder.write(data);
|
|
2933
|
+
this.processBuffer(socket);
|
|
2934
|
+
if (Buffer.byteLength(this.buffer, "utf8") > 1048576) this.closeOversized(socket, "unterminated frame");
|
|
2945
2935
|
});
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
ok: false,
|
|
2954
|
-
error: {
|
|
2955
|
-
code: "DISCONNECTED",
|
|
2956
|
-
message: "Connection closed"
|
|
2957
|
-
}
|
|
2958
|
-
});
|
|
2959
|
-
}
|
|
2960
|
-
this.pending.clear();
|
|
2936
|
+
socket.on("close", () => {
|
|
2937
|
+
if (this.socket !== socket) return;
|
|
2938
|
+
const wasConnected = this.isSocketConnected;
|
|
2939
|
+
this.socket = null;
|
|
2940
|
+
this.isSocketConnected = false;
|
|
2941
|
+
this.resetDecoder();
|
|
2942
|
+
this.resolveAllPending("DISCONNECTED", "Connection closed");
|
|
2961
2943
|
if (wasConnected) {
|
|
2962
2944
|
this.log.warn("Disconnected from daemon");
|
|
2963
|
-
this.
|
|
2945
|
+
this.safeEmit("disconnected", "connection closed");
|
|
2964
2946
|
}
|
|
2965
|
-
this.scheduleReconnect();
|
|
2947
|
+
if (!this.closed) this.scheduleReconnect();
|
|
2966
2948
|
});
|
|
2967
|
-
|
|
2968
|
-
if (
|
|
2969
|
-
|
|
2970
|
-
this.log.
|
|
2971
|
-
|
|
2949
|
+
socket.on("error", (error) => {
|
|
2950
|
+
if (this.socket !== socket || this.closed) return;
|
|
2951
|
+
if (error.code === "ECONNREFUSED" || error.code === "ENOENT" || error.code === "ECONNRESET") {
|
|
2952
|
+
this.log.debug(`Daemon not available: ${error.code}`);
|
|
2953
|
+
return;
|
|
2972
2954
|
}
|
|
2955
|
+
this.log.error(`IPC socket error: ${error.message}`);
|
|
2956
|
+
if (this.listenerCount("error") > 0) this.safeEmit("error", error);
|
|
2973
2957
|
});
|
|
2974
2958
|
}
|
|
2975
|
-
processBuffer() {
|
|
2976
|
-
let
|
|
2977
|
-
while ((
|
|
2978
|
-
const line = this.buffer.slice(0,
|
|
2979
|
-
this.buffer = this.buffer.slice(
|
|
2959
|
+
processBuffer(socket) {
|
|
2960
|
+
let newlineIndex;
|
|
2961
|
+
while ((newlineIndex = this.buffer.indexOf("\n")) !== -1) {
|
|
2962
|
+
const line = this.buffer.slice(0, newlineIndex).trim();
|
|
2963
|
+
this.buffer = this.buffer.slice(newlineIndex + 1);
|
|
2980
2964
|
if (!line) continue;
|
|
2965
|
+
if (Buffer.byteLength(line, "utf8") > 1048576) {
|
|
2966
|
+
this.closeOversized(socket, "frame");
|
|
2967
|
+
return;
|
|
2968
|
+
}
|
|
2981
2969
|
let parsed;
|
|
2982
2970
|
try {
|
|
2983
2971
|
parsed = JSON.parse(line);
|
|
@@ -2985,24 +2973,11 @@ var IPCClient = class extends EventEmitter {
|
|
|
2985
2973
|
this.log.warn("Received invalid JSON from daemon");
|
|
2986
2974
|
continue;
|
|
2987
2975
|
}
|
|
2988
|
-
if (isIPCResponse(parsed))
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
if (
|
|
2993
|
-
if (parsed.event === "plugin.replaced") {
|
|
2994
|
-
this.log.warn("Replaced by another process — stopping reconnection");
|
|
2995
|
-
this.closed = true;
|
|
2996
|
-
this.clearReconnectTimer();
|
|
2997
|
-
}
|
|
2998
|
-
this.emit("event", parsed.event, parsed.payload);
|
|
2999
|
-
continue;
|
|
3000
|
-
}
|
|
3001
|
-
if (isIPCRequest(parsed)) {
|
|
3002
|
-
this.handleIncomingRequest(parsed);
|
|
3003
|
-
continue;
|
|
3004
|
-
}
|
|
3005
|
-
this.log.debug({ msg: parsed }, "Unhandled message from daemon");
|
|
2976
|
+
if (isIPCResponse(parsed)) this.handleResponse(parsed);
|
|
2977
|
+
else if (isIPCEvent(parsed)) this.handleEvent(parsed.event, parsed.payload, socket);
|
|
2978
|
+
else if (isIPCRequest(parsed)) this.handleIncomingRequest(parsed);
|
|
2979
|
+
else this.log.warn("Ignoring malformed IPC envelope");
|
|
2980
|
+
if (this.socket !== socket) return;
|
|
3006
2981
|
}
|
|
3007
2982
|
}
|
|
3008
2983
|
handleResponse(response) {
|
|
@@ -3012,37 +2987,117 @@ var IPCClient = class extends EventEmitter {
|
|
|
3012
2987
|
this.pending.delete(response.id);
|
|
3013
2988
|
pending.resolve(response);
|
|
3014
2989
|
}
|
|
2990
|
+
handleEvent(event, payload, socket) {
|
|
2991
|
+
if (event === "plugin.replaced") {
|
|
2992
|
+
this.log.warn("Replaced by another process — closing this IPC client");
|
|
2993
|
+
this.closed = true;
|
|
2994
|
+
this.clearReconnectTimer();
|
|
2995
|
+
}
|
|
2996
|
+
this.safeEmit("event", event, payload);
|
|
2997
|
+
if (event === "plugin.replaced" && this.socket === socket) socket.destroy();
|
|
2998
|
+
}
|
|
3015
2999
|
handleIncomingRequest(request) {
|
|
3000
|
+
let responded = false;
|
|
3016
3001
|
const respond = (partial) => {
|
|
3017
|
-
|
|
3002
|
+
if (responded) return;
|
|
3003
|
+
responded = true;
|
|
3004
|
+
this.sendResponse({
|
|
3018
3005
|
id: request.id,
|
|
3019
3006
|
...partial
|
|
3020
|
-
};
|
|
3021
|
-
this.sendResponse(response);
|
|
3007
|
+
});
|
|
3022
3008
|
};
|
|
3023
|
-
this.
|
|
3009
|
+
if (this.listenerCount("request") === 0) {
|
|
3010
|
+
respond({
|
|
3011
|
+
ok: false,
|
|
3012
|
+
error: {
|
|
3013
|
+
code: "UNKNOWN_METHOD",
|
|
3014
|
+
message: `No handler for method: ${request.method}`
|
|
3015
|
+
}
|
|
3016
|
+
});
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
try {
|
|
3020
|
+
this.emit("request", request.method, request.params, respond);
|
|
3021
|
+
} catch (error) {
|
|
3022
|
+
this.log.error(`IPC request handler failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3023
|
+
respond({
|
|
3024
|
+
ok: false,
|
|
3025
|
+
error: {
|
|
3026
|
+
code: "HANDLER_FAILED",
|
|
3027
|
+
message: "IPC request handler failed"
|
|
3028
|
+
}
|
|
3029
|
+
});
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
serializeFrame(value, id) {
|
|
3033
|
+
let frame;
|
|
3034
|
+
try {
|
|
3035
|
+
frame = `${JSON.stringify(value)}\n`;
|
|
3036
|
+
} catch (error) {
|
|
3037
|
+
return this.errorResponse(id, "SERIALIZE_FAILED", error instanceof Error ? error.message : String(error));
|
|
3038
|
+
}
|
|
3039
|
+
if (Buffer.byteLength(frame, "utf8") > 1048576) return this.errorResponse(id, "PAYLOAD_TOO_LARGE", `IPC frame exceeds ${String(MAX_IPC_MESSAGE_BYTES)} bytes`);
|
|
3040
|
+
return frame;
|
|
3041
|
+
}
|
|
3042
|
+
closeOversized(socket, kind) {
|
|
3043
|
+
this.log.warn(`Closing daemon connection after oversized IPC ${kind} (limit ${String(MAX_IPC_MESSAGE_BYTES)} bytes)`);
|
|
3044
|
+
this.resetDecoder();
|
|
3045
|
+
if (this.socket === socket) socket.destroy();
|
|
3046
|
+
}
|
|
3047
|
+
resolveAllPending(code, message) {
|
|
3048
|
+
for (const [id, pending] of this.pending) {
|
|
3049
|
+
clearTimeout(pending.timer);
|
|
3050
|
+
pending.resolve(this.errorResponse(id, code, message));
|
|
3051
|
+
}
|
|
3052
|
+
this.pending.clear();
|
|
3024
3053
|
}
|
|
3025
3054
|
scheduleReconnect() {
|
|
3026
|
-
if (this.closed) return;
|
|
3027
|
-
this.clearReconnectTimer();
|
|
3055
|
+
if (this.closed || this.reconnectTimer || this.socket) return;
|
|
3028
3056
|
const delay = this.backoffMs;
|
|
3029
3057
|
this.backoffMs = Math.min(this.backoffMs * 2, MAX_BACKOFF_MS);
|
|
3030
3058
|
this.log.debug(`Reconnecting in ${String(delay)}ms...`);
|
|
3031
3059
|
this.reconnectTimer = setTimeout(() => {
|
|
3032
|
-
this.
|
|
3060
|
+
this.reconnectTimer = null;
|
|
3061
|
+
this.openSocket();
|
|
3033
3062
|
}, delay);
|
|
3034
3063
|
this.reconnectTimer.unref();
|
|
3035
3064
|
}
|
|
3036
3065
|
clearReconnectTimer() {
|
|
3037
|
-
if (this.reconnectTimer)
|
|
3038
|
-
|
|
3039
|
-
|
|
3066
|
+
if (!this.reconnectTimer) return;
|
|
3067
|
+
clearTimeout(this.reconnectTimer);
|
|
3068
|
+
this.reconnectTimer = null;
|
|
3069
|
+
}
|
|
3070
|
+
resetDecoder() {
|
|
3071
|
+
this.buffer = "";
|
|
3072
|
+
this.decoder = new StringDecoder("utf8");
|
|
3073
|
+
}
|
|
3074
|
+
errorResponse(id, code, message) {
|
|
3075
|
+
return {
|
|
3076
|
+
id,
|
|
3077
|
+
ok: false,
|
|
3078
|
+
error: {
|
|
3079
|
+
code,
|
|
3080
|
+
message
|
|
3081
|
+
}
|
|
3082
|
+
};
|
|
3083
|
+
}
|
|
3084
|
+
safeEmit(event, ...args) {
|
|
3085
|
+
try {
|
|
3086
|
+
this.emit(event, ...args);
|
|
3087
|
+
} catch (error) {
|
|
3088
|
+
this.log.error(`IPC ${event} listener failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3040
3089
|
}
|
|
3041
3090
|
}
|
|
3042
3091
|
};
|
|
3043
3092
|
//#endregion
|
|
3044
3093
|
//#region src/capability-reporter.ts
|
|
3045
|
-
|
|
3094
|
+
/**
|
|
3095
|
+
* Capability Reporter — registers OpenClaw's capabilities with the daemon.
|
|
3096
|
+
*
|
|
3097
|
+
* On connect (and reconnect), sends a `register` request to the daemon
|
|
3098
|
+
* with OpenClaw framework info, plugin version, and capabilities list.
|
|
3099
|
+
*/
|
|
3100
|
+
const pkg$1 = createRequire(import.meta.url)("../package.json");
|
|
3046
3101
|
const OPENCLAW_CAPABILITIES = [
|
|
3047
3102
|
"sessions",
|
|
3048
3103
|
"tools",
|
|
@@ -3058,7 +3113,7 @@ async function registerWithDaemon(client, log, pluginName = "@alfe.ai/openclaw")
|
|
|
3058
3113
|
const response = await client.request("register", {
|
|
3059
3114
|
framework: "openclaw",
|
|
3060
3115
|
name: pluginName,
|
|
3061
|
-
version:
|
|
3116
|
+
version: pkg$1.version,
|
|
3062
3117
|
protocolVersion: 1,
|
|
3063
3118
|
capabilities: [...OPENCLAW_CAPABILITIES],
|
|
3064
3119
|
pid: process.pid
|
|
@@ -3071,10 +3126,252 @@ async function registerWithDaemon(client, log, pluginName = "@alfe.ai/openclaw")
|
|
|
3071
3126
|
} else log.error(`Registration failed: ${err?.message ?? "unknown error"}`);
|
|
3072
3127
|
return null;
|
|
3073
3128
|
}
|
|
3129
|
+
if (!isRegisterResult(response.payload)) {
|
|
3130
|
+
log.error("Registration failed: daemon returned an invalid response");
|
|
3131
|
+
return null;
|
|
3132
|
+
}
|
|
3074
3133
|
const result = response.payload;
|
|
3075
3134
|
log.info(`Registered with daemon v${result.daemonVersion} (protocol v${String(result.protocolVersion)})`);
|
|
3076
3135
|
return result;
|
|
3077
3136
|
}
|
|
3137
|
+
function isRegisterResult(value) {
|
|
3138
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
3139
|
+
const result = value;
|
|
3140
|
+
return typeof result.status === "string" && result.status.length > 0 && result.status.length <= 128 && typeof result.daemonVersion === "string" && result.daemonVersion.length > 0 && result.daemonVersion.length <= 128 && Number.isSafeInteger(result.protocolVersion) && result.protocolVersion > 0;
|
|
3141
|
+
}
|
|
3142
|
+
//#endregion
|
|
3143
|
+
//#region src/avatar-source.ts
|
|
3144
|
+
/** Safe local/remote avatar loading for the `set_avatar` tool. */
|
|
3145
|
+
const MAX_AVATAR_BYTES = 5 * 1024 * 1024;
|
|
3146
|
+
const MAX_AVATAR_URL_LENGTH = 2048;
|
|
3147
|
+
const MAX_LOCAL_PATH_LENGTH = 4096;
|
|
3148
|
+
const AVATAR_DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
3149
|
+
const AVATAR_MIME_BY_EXT = {
|
|
3150
|
+
".png": "image/png",
|
|
3151
|
+
".jpg": "image/jpeg",
|
|
3152
|
+
".jpeg": "image/jpeg",
|
|
3153
|
+
".webp": "image/webp"
|
|
3154
|
+
};
|
|
3155
|
+
function deriveAgentAssetsOrigin(apiUrl) {
|
|
3156
|
+
const api = new URL(apiUrl);
|
|
3157
|
+
if (api.protocol !== "https:" || api.username || api.password || !api.hostname.startsWith("api.")) throw new Error("Cannot derive a trusted agent-assets origin from the configured API URL");
|
|
3158
|
+
api.hostname = api.hostname.replace(/^api\./u, "assets.");
|
|
3159
|
+
api.pathname = "/";
|
|
3160
|
+
api.search = "";
|
|
3161
|
+
api.hash = "";
|
|
3162
|
+
return api.origin;
|
|
3163
|
+
}
|
|
3164
|
+
async function loadAvatarSource(input, options) {
|
|
3165
|
+
if (input.url !== void 0 === (input.path !== void 0)) throw new Error("Provide exactly one of url or path");
|
|
3166
|
+
if (input.url !== void 0) return loadRemoteAvatar(input.url, options.agentAssetsOrigin, options.fetchImpl ?? fetch);
|
|
3167
|
+
return loadWorkspaceAvatar(input.path ?? "", options.workspacePath);
|
|
3168
|
+
}
|
|
3169
|
+
function assertPresignedS3UploadUrl(rawUrl) {
|
|
3170
|
+
if (rawUrl.length < 1 || rawUrl.length > MAX_AVATAR_URL_LENGTH) throw new Error("Avatar upload URL is invalid");
|
|
3171
|
+
const url = new URL(rawUrl);
|
|
3172
|
+
if (url.protocol !== "https:" || url.username || url.password || !(url.hostname === "amazonaws.com" || url.hostname.endsWith(".amazonaws.com"))) throw new Error("Avatar upload URL is not a trusted S3 endpoint");
|
|
3173
|
+
return url.href;
|
|
3174
|
+
}
|
|
3175
|
+
async function loadRemoteAvatar(rawUrl, agentAssetsOrigin, fetchImpl) {
|
|
3176
|
+
if (rawUrl.length < 1 || rawUrl.length > MAX_AVATAR_URL_LENGTH) throw new Error("Avatar URL must contain 1 to 2048 characters");
|
|
3177
|
+
const url = new URL(rawUrl);
|
|
3178
|
+
if (url.protocol !== "https:" || url.username || url.password || url.origin !== agentAssetsOrigin) throw new Error(`Avatar URL must use the trusted agent-assets origin ${agentAssetsOrigin}`);
|
|
3179
|
+
if (!url.pathname.startsWith("/generated/") && !url.pathname.startsWith("/avatars/")) throw new Error("Avatar URL must reference a generated or uploaded agent asset");
|
|
3180
|
+
const response = await fetchImpl(url, {
|
|
3181
|
+
redirect: "error",
|
|
3182
|
+
signal: AbortSignal.timeout(AVATAR_DOWNLOAD_TIMEOUT_MS)
|
|
3183
|
+
});
|
|
3184
|
+
if (!response.ok) throw new Error(`Failed to download the avatar image (${String(response.status)})`);
|
|
3185
|
+
const declaredLength = response.headers.get("content-length");
|
|
3186
|
+
if (declaredLength !== null) {
|
|
3187
|
+
const bytes = Number(declaredLength);
|
|
3188
|
+
if (!Number.isSafeInteger(bytes) || bytes < 1 || bytes > 5242880) throw new Error(`Avatar response exceeds the ${String(MAX_AVATAR_BYTES)} byte limit`);
|
|
3189
|
+
}
|
|
3190
|
+
const mimeType = avatarMime(url.pathname, response.headers.get("content-type"));
|
|
3191
|
+
const bytes = await readResponseBytes(response, MAX_AVATAR_BYTES);
|
|
3192
|
+
assertImageSignature(bytes, mimeType);
|
|
3193
|
+
return {
|
|
3194
|
+
bytes,
|
|
3195
|
+
mimeType
|
|
3196
|
+
};
|
|
3197
|
+
}
|
|
3198
|
+
async function loadWorkspaceAvatar(rawPath, workspacePath) {
|
|
3199
|
+
if (rawPath.length < 1 || rawPath.length > MAX_LOCAL_PATH_LENGTH || isAbsolute(rawPath)) throw new Error("Avatar path must be a workspace-relative path of at most 4096 characters");
|
|
3200
|
+
if (rawPath.includes("\0")) throw new Error("Avatar path contains an invalid character");
|
|
3201
|
+
let workspace;
|
|
3202
|
+
try {
|
|
3203
|
+
workspace = await realpath(workspacePath);
|
|
3204
|
+
} catch {
|
|
3205
|
+
throw new Error("Avatar workspace is unavailable");
|
|
3206
|
+
}
|
|
3207
|
+
const candidate = resolve(workspace, rawPath);
|
|
3208
|
+
if (!isWithin(workspace, candidate)) throw new Error("Avatar path resolves outside the workspace");
|
|
3209
|
+
let fileInfo;
|
|
3210
|
+
try {
|
|
3211
|
+
fileInfo = await lstat(candidate);
|
|
3212
|
+
} catch {
|
|
3213
|
+
throw new Error("Avatar file does not exist or cannot be accessed");
|
|
3214
|
+
}
|
|
3215
|
+
if (fileInfo.isSymbolicLink() || !fileInfo.isFile()) throw new Error("Avatar path must reference a regular non-symlink file");
|
|
3216
|
+
let canonical;
|
|
3217
|
+
try {
|
|
3218
|
+
canonical = await realpath(candidate);
|
|
3219
|
+
} catch {
|
|
3220
|
+
throw new Error("Avatar file does not exist or cannot be accessed");
|
|
3221
|
+
}
|
|
3222
|
+
if (!isWithin(workspace, canonical)) throw new Error("Avatar path resolves outside the workspace");
|
|
3223
|
+
const mimeType = avatarMime(canonical, null);
|
|
3224
|
+
const handle = await open(canonical, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
3225
|
+
try {
|
|
3226
|
+
const opened = await handle.stat();
|
|
3227
|
+
if (!opened.isFile() || opened.size < 1 || opened.size > 5242880) throw new Error(`Avatar file must contain 1 to ${String(MAX_AVATAR_BYTES)} bytes`);
|
|
3228
|
+
const target = Buffer.allocUnsafe(MAX_AVATAR_BYTES + 1);
|
|
3229
|
+
let offset = 0;
|
|
3230
|
+
while (offset <= MAX_AVATAR_BYTES) {
|
|
3231
|
+
const { bytesRead } = await handle.read(target, offset, target.length - offset, offset);
|
|
3232
|
+
if (bytesRead === 0) break;
|
|
3233
|
+
offset += bytesRead;
|
|
3234
|
+
}
|
|
3235
|
+
if (offset < 1 || offset > 5242880) throw new Error(`Avatar file must contain 1 to ${String(MAX_AVATAR_BYTES)} bytes`);
|
|
3236
|
+
const bytes = Buffer.from(target.subarray(0, offset));
|
|
3237
|
+
assertImageSignature(bytes, mimeType);
|
|
3238
|
+
return {
|
|
3239
|
+
bytes,
|
|
3240
|
+
mimeType
|
|
3241
|
+
};
|
|
3242
|
+
} finally {
|
|
3243
|
+
await handle.close();
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
async function readResponseBytes(response, maxBytes) {
|
|
3247
|
+
if (!response.body) throw new Error("Avatar response had no body");
|
|
3248
|
+
const reader = response.body.getReader();
|
|
3249
|
+
const chunks = [];
|
|
3250
|
+
let total = 0;
|
|
3251
|
+
try {
|
|
3252
|
+
for (;;) {
|
|
3253
|
+
const { done, value } = await reader.read();
|
|
3254
|
+
if (done) break;
|
|
3255
|
+
total += value.byteLength;
|
|
3256
|
+
if (total > maxBytes) {
|
|
3257
|
+
await reader.cancel("avatar exceeds size limit");
|
|
3258
|
+
throw new Error(`Avatar response exceeds the ${String(maxBytes)} byte limit`);
|
|
3259
|
+
}
|
|
3260
|
+
chunks.push(value);
|
|
3261
|
+
}
|
|
3262
|
+
} finally {
|
|
3263
|
+
reader.releaseLock();
|
|
3264
|
+
}
|
|
3265
|
+
if (total < 1) throw new Error("Avatar response was empty");
|
|
3266
|
+
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), total);
|
|
3267
|
+
}
|
|
3268
|
+
function avatarMime(pathname, contentType) {
|
|
3269
|
+
const normalizedType = (contentType ?? "").split(";", 1)[0]?.trim().toLowerCase();
|
|
3270
|
+
if (normalizedType === "image/png" || normalizedType === "image/jpeg" || normalizedType === "image/webp") return normalizedType;
|
|
3271
|
+
const fromExtension = AVATAR_MIME_BY_EXT[extname(pathname).toLowerCase()];
|
|
3272
|
+
if (fromExtension) return fromExtension;
|
|
3273
|
+
throw new Error("Avatar image must be PNG, JPEG, or WebP");
|
|
3274
|
+
}
|
|
3275
|
+
function assertImageSignature(bytes, mimeType) {
|
|
3276
|
+
if (!(mimeType === "image/png" && bytes.length >= 8 && bytes.subarray(0, 8).equals(Buffer.from([
|
|
3277
|
+
137,
|
|
3278
|
+
80,
|
|
3279
|
+
78,
|
|
3280
|
+
71,
|
|
3281
|
+
13,
|
|
3282
|
+
10,
|
|
3283
|
+
26,
|
|
3284
|
+
10
|
|
3285
|
+
])) || mimeType === "image/jpeg" && bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255 || mimeType === "image/webp" && bytes.length >= 12 && bytes.subarray(0, 4).toString("ascii") === "RIFF" && bytes.subarray(8, 12).toString("ascii") === "WEBP")) throw new Error(`Avatar bytes do not match the declared ${mimeType} image type`);
|
|
3286
|
+
}
|
|
3287
|
+
function isWithin(root, candidate) {
|
|
3288
|
+
const rel = relative(root, candidate);
|
|
3289
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
3290
|
+
}
|
|
3291
|
+
//#endregion
|
|
3292
|
+
//#region src/tool-input.ts
|
|
3293
|
+
/** Runtime validation for model-controlled OpenClaw tool parameters. */
|
|
3294
|
+
const MAX_JSON_DEPTH = 8;
|
|
3295
|
+
const MAX_JSON_NODES = 1e4;
|
|
3296
|
+
const MAX_RECORD_ENTRIES = 1e3;
|
|
3297
|
+
const MAX_RECORD_KEY_LENGTH = 256;
|
|
3298
|
+
const MAX_RECORD_STRING_LENGTH = 1e4;
|
|
3299
|
+
const MAX_JSON_BYTES = 1024 * 1024;
|
|
3300
|
+
const UNSAFE_KEYS = new Set([
|
|
3301
|
+
"__proto__",
|
|
3302
|
+
"constructor",
|
|
3303
|
+
"prototype"
|
|
3304
|
+
]);
|
|
3305
|
+
function requireString(params, field, maxLength, pattern) {
|
|
3306
|
+
const value = params[field];
|
|
3307
|
+
if (typeof value !== "string" || value.length < 1 || value.length > maxLength) throw new Error(`${field} must contain 1 to ${String(maxLength)} characters`);
|
|
3308
|
+
if (pattern && !pattern.test(value)) throw new Error(`${field} has an invalid format`);
|
|
3309
|
+
return value;
|
|
3310
|
+
}
|
|
3311
|
+
function optionalString(params, field, maxLength) {
|
|
3312
|
+
if (params[field] === void 0) return void 0;
|
|
3313
|
+
return requireString(params, field, maxLength);
|
|
3314
|
+
}
|
|
3315
|
+
function requireConfirmation(params, message) {
|
|
3316
|
+
if (params.confirm !== true) throw new Error(message);
|
|
3317
|
+
}
|
|
3318
|
+
function requireMatchingConfirmation(params, field, expected) {
|
|
3319
|
+
if (params[field] !== expected) throw new Error(`${field} must exactly match ${expected}`);
|
|
3320
|
+
}
|
|
3321
|
+
function optionalJsonRecord(params, field) {
|
|
3322
|
+
if (params[field] === void 0) return void 0;
|
|
3323
|
+
return requireJsonRecord(params, field);
|
|
3324
|
+
}
|
|
3325
|
+
function requireJsonRecord(params, field) {
|
|
3326
|
+
const state = {
|
|
3327
|
+
nodes: 0,
|
|
3328
|
+
ancestors: /* @__PURE__ */ new Set()
|
|
3329
|
+
};
|
|
3330
|
+
const cloned = cloneJson(params[field], field, 0, state);
|
|
3331
|
+
if (!isRecord(cloned)) throw new Error(`${field} must be an object`);
|
|
3332
|
+
const serialized = JSON.stringify(cloned);
|
|
3333
|
+
if (Buffer.byteLength(serialized, "utf8") > MAX_JSON_BYTES) throw new Error(`${field} exceeds the ${String(MAX_JSON_BYTES)} byte limit`);
|
|
3334
|
+
return cloned;
|
|
3335
|
+
}
|
|
3336
|
+
function cloneJson(value, label, depth, state) {
|
|
3337
|
+
state.nodes += 1;
|
|
3338
|
+
if (state.nodes > MAX_JSON_NODES) throw new Error(`${label} contains too many values`);
|
|
3339
|
+
if (depth > MAX_JSON_DEPTH) throw new Error(`${label} exceeds ${String(MAX_JSON_DEPTH)} nesting levels`);
|
|
3340
|
+
if (value === null || typeof value === "boolean") return value;
|
|
3341
|
+
if (typeof value === "number") {
|
|
3342
|
+
if (!Number.isFinite(value)) throw new Error(`${label} contains a non-finite number`);
|
|
3343
|
+
return value;
|
|
3344
|
+
}
|
|
3345
|
+
if (typeof value === "string") {
|
|
3346
|
+
if (value.length > MAX_RECORD_STRING_LENGTH) throw new Error(`${label} contains a string longer than ${String(MAX_RECORD_STRING_LENGTH)} characters`);
|
|
3347
|
+
return value;
|
|
3348
|
+
}
|
|
3349
|
+
if (typeof value !== "object") throw new Error(`${label} must contain only JSON values`);
|
|
3350
|
+
if (state.ancestors.has(value)) throw new Error(`${label} must not contain cycles`);
|
|
3351
|
+
state.ancestors.add(value);
|
|
3352
|
+
try {
|
|
3353
|
+
if (Array.isArray(value)) {
|
|
3354
|
+
if (value.length > MAX_RECORD_ENTRIES) throw new Error(`${label} contains too many entries`);
|
|
3355
|
+
return value.map((entry) => cloneJson(entry, label, depth + 1, state));
|
|
3356
|
+
}
|
|
3357
|
+
if (!isRecord(value)) throw new Error(`${label} must contain only plain objects`);
|
|
3358
|
+
const entries = Object.entries(value);
|
|
3359
|
+
if (entries.length > MAX_RECORD_ENTRIES) throw new Error(`${label} contains too many entries`);
|
|
3360
|
+
const output = Object.create(null);
|
|
3361
|
+
for (const [key, entry] of entries) {
|
|
3362
|
+
if (key.length < 1 || key.length > MAX_RECORD_KEY_LENGTH || UNSAFE_KEYS.has(key)) throw new Error(`${label} contains an invalid key`);
|
|
3363
|
+
output[key] = cloneJson(entry, label, depth + 1, state);
|
|
3364
|
+
}
|
|
3365
|
+
return output;
|
|
3366
|
+
} finally {
|
|
3367
|
+
state.ancestors.delete(value);
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
function isRecord(value) {
|
|
3371
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
3372
|
+
const prototype = Object.getPrototypeOf(value);
|
|
3373
|
+
return prototype === Object.prototype || prototype === null;
|
|
3374
|
+
}
|
|
3078
3375
|
//#endregion
|
|
3079
3376
|
//#region src/plugin.ts
|
|
3080
3377
|
/**
|
|
@@ -3086,44 +3383,11 @@ async function registerWithDaemon(client, log, pluginName = "@alfe.ai/openclaw")
|
|
|
3086
3383
|
* 3. Re-registers on reconnect (daemon may restart)
|
|
3087
3384
|
* 4. Gracefully handles daemon being unavailable
|
|
3088
3385
|
*
|
|
3089
|
-
* Follows the same plugin pattern as @alfe.ai/openclaw-voice.
|
|
3090
3386
|
*/
|
|
3091
3387
|
const pkg = createRequire(import.meta.url)("../package.json");
|
|
3092
3388
|
const DEFAULT_SOCKET_PATH = join(homedir(), ".alfe", "gateway.sock");
|
|
3093
3389
|
let ipcClient = null;
|
|
3094
|
-
|
|
3095
|
-
const MAX_AVATAR_BYTES = 5 * 1024 * 1024;
|
|
3096
|
-
/** Image types the avatar presign/finalize flow accepts, keyed by file extension. */
|
|
3097
|
-
const AVATAR_MIME_BY_EXT = {
|
|
3098
|
-
".png": "image/png",
|
|
3099
|
-
".jpg": "image/jpeg",
|
|
3100
|
-
".jpeg": "image/jpeg",
|
|
3101
|
-
".webp": "image/webp"
|
|
3102
|
-
};
|
|
3103
|
-
/** Infer an avatar mime type from a file path, rejecting unsupported types. */
|
|
3104
|
-
function avatarMimeFromPath(path) {
|
|
3105
|
-
const ext = extname(path).toLowerCase();
|
|
3106
|
-
const mimeType = AVATAR_MIME_BY_EXT[ext];
|
|
3107
|
-
if (!mimeType) throw new Error(`unsupported image type "${ext || path}" — set_avatar accepts .png, .jpg/.jpeg, or .webp`);
|
|
3108
|
-
return mimeType;
|
|
3109
|
-
}
|
|
3110
|
-
/**
|
|
3111
|
-
* Infer an avatar mime type for a remote image — prefer the response's
|
|
3112
|
-
* Content-Type, else the URL path extension. Lets `set_avatar` accept a URL
|
|
3113
|
-
* (e.g. the one `generate_image` returns), which is how an agent naturally
|
|
3114
|
-
* wants to set the picture it just generated.
|
|
3115
|
-
*/
|
|
3116
|
-
function avatarMimeFromUrl(url, contentType) {
|
|
3117
|
-
const ct = (contentType ?? "").split(";")[0].trim().toLowerCase();
|
|
3118
|
-
if (ct === "image/png" || ct === "image/jpeg" || ct === "image/webp") return ct;
|
|
3119
|
-
let pathname = url;
|
|
3120
|
-
try {
|
|
3121
|
-
pathname = new URL(url).pathname;
|
|
3122
|
-
} catch {}
|
|
3123
|
-
const byExt = AVATAR_MIME_BY_EXT[extname(pathname).toLowerCase()];
|
|
3124
|
-
if (byExt) return byExt;
|
|
3125
|
-
throw new Error(`could not determine the image type for "${url}" — set_avatar accepts png, jpeg, or webp`);
|
|
3126
|
-
}
|
|
3390
|
+
let ipcOwner = null;
|
|
3127
3391
|
function ok(result) {
|
|
3128
3392
|
return { content: [{
|
|
3129
3393
|
type: "text",
|
|
@@ -3153,7 +3417,7 @@ function defineTool(def) {
|
|
|
3153
3417
|
try {
|
|
3154
3418
|
return ok(await def.handler(params));
|
|
3155
3419
|
} catch (e) {
|
|
3156
|
-
return errResult(e.message);
|
|
3420
|
+
return errResult(e instanceof Error ? e.message : "Unknown error");
|
|
3157
3421
|
}
|
|
3158
3422
|
}
|
|
3159
3423
|
};
|
|
@@ -3163,15 +3427,19 @@ function buildIntegrationTools(client) {
|
|
|
3163
3427
|
defineTool({
|
|
3164
3428
|
name: "list_integrations",
|
|
3165
3429
|
description: "List all integrations installed for this agent, including their status and available config fields.",
|
|
3166
|
-
parameters: Type.Object({}),
|
|
3430
|
+
parameters: Type.Object({}, { additionalProperties: false }),
|
|
3167
3431
|
handler: async () => client.listIntegrations()
|
|
3168
3432
|
}),
|
|
3169
3433
|
defineTool({
|
|
3170
3434
|
name: "get_integration_config",
|
|
3171
3435
|
description: "Get the current configuration and editable fields for a specific integration.",
|
|
3172
|
-
parameters: Type.Object({ integrationId: Type.String({
|
|
3436
|
+
parameters: Type.Object({ integrationId: Type.String({
|
|
3437
|
+
description: "The integration to inspect (e.g. \"voice\", \"slack\")",
|
|
3438
|
+
minLength: 1,
|
|
3439
|
+
maxLength: 256
|
|
3440
|
+
}) }, { additionalProperties: false }),
|
|
3173
3441
|
handler: async (params) => {
|
|
3174
|
-
const integrationId = params
|
|
3442
|
+
const integrationId = requireString(params, "integrationId", 256);
|
|
3175
3443
|
return client.getIntegrationConfig(integrationId);
|
|
3176
3444
|
}
|
|
3177
3445
|
}),
|
|
@@ -3179,27 +3447,53 @@ function buildIntegrationTools(client) {
|
|
|
3179
3447
|
name: "update_integration_config",
|
|
3180
3448
|
description: "Update configuration for an installed integration. Only agent-editable fields can be changed.",
|
|
3181
3449
|
parameters: Type.Object({
|
|
3182
|
-
integrationId: Type.String({
|
|
3183
|
-
|
|
3184
|
-
|
|
3450
|
+
integrationId: Type.String({
|
|
3451
|
+
description: "The integration to update",
|
|
3452
|
+
minLength: 1,
|
|
3453
|
+
maxLength: 256
|
|
3454
|
+
}),
|
|
3455
|
+
fields: Type.Record(Type.String({
|
|
3456
|
+
minLength: 1,
|
|
3457
|
+
maxLength: 256
|
|
3458
|
+
}), Type.Unknown(), {
|
|
3459
|
+
description: "Key-value pairs of config fields to update",
|
|
3460
|
+
maxProperties: 1e3
|
|
3461
|
+
})
|
|
3462
|
+
}, { additionalProperties: false }),
|
|
3185
3463
|
handler: async (params) => {
|
|
3186
|
-
const integrationId = params
|
|
3187
|
-
const fields = params
|
|
3464
|
+
const integrationId = requireString(params, "integrationId", 256);
|
|
3465
|
+
const fields = requireJsonRecord(params, "fields");
|
|
3188
3466
|
return client.updateIntegrationConfig(integrationId, fields);
|
|
3189
3467
|
}
|
|
3190
3468
|
}),
|
|
3191
3469
|
defineTool({
|
|
3192
3470
|
name: "install_integration",
|
|
3193
|
-
description: "Install an integration from the registry.
|
|
3471
|
+
description: "Install an integration from the registry. This changes the agent runtime and may run integration lifecycle hooks. Set confirm=true only after the user approves the install.",
|
|
3194
3472
|
parameters: Type.Object({
|
|
3195
|
-
integrationId: Type.String({
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3473
|
+
integrationId: Type.String({
|
|
3474
|
+
description: "The integration to install (e.g. \"voice\", \"slack\", \"discord\")",
|
|
3475
|
+
minLength: 1,
|
|
3476
|
+
maxLength: 256
|
|
3477
|
+
}),
|
|
3478
|
+
version: Type.Optional(Type.String({
|
|
3479
|
+
description: "Specific version to install (defaults to latest)",
|
|
3480
|
+
minLength: 1,
|
|
3481
|
+
maxLength: 64
|
|
3482
|
+
})),
|
|
3483
|
+
config: Type.Optional(Type.Record(Type.String({
|
|
3484
|
+
minLength: 1,
|
|
3485
|
+
maxLength: 256
|
|
3486
|
+
}), Type.Unknown(), {
|
|
3487
|
+
description: "Initial configuration key-value pairs",
|
|
3488
|
+
maxProperties: 1e3
|
|
3489
|
+
})),
|
|
3490
|
+
confirm: Type.Literal(true, { description: "Must be true after the user explicitly approves the integration install" })
|
|
3491
|
+
}, { additionalProperties: false }),
|
|
3199
3492
|
handler: async (params) => {
|
|
3200
|
-
const integrationId = params
|
|
3201
|
-
const version = params
|
|
3202
|
-
const config = params
|
|
3493
|
+
const integrationId = requireString(params, "integrationId", 256);
|
|
3494
|
+
const version = optionalString(params, "version", 64);
|
|
3495
|
+
const config = optionalJsonRecord(params, "config");
|
|
3496
|
+
requireConfirmation(params, "confirm must be true after the user explicitly approves the integration install");
|
|
3203
3497
|
return client.installIntegration(integrationId, {
|
|
3204
3498
|
version,
|
|
3205
3499
|
config
|
|
@@ -3208,27 +3502,39 @@ function buildIntegrationTools(client) {
|
|
|
3208
3502
|
}),
|
|
3209
3503
|
defineTool({
|
|
3210
3504
|
name: "remove_integration",
|
|
3211
|
-
description: "Remove an installed integration. The daemon will
|
|
3212
|
-
parameters: Type.Object({
|
|
3505
|
+
description: "Remove an installed integration. The daemon will tear down its runtime resources during reconciliation. Copy the exact id into confirmIntegrationId after the user approves.",
|
|
3506
|
+
parameters: Type.Object({
|
|
3507
|
+
integrationId: Type.String({
|
|
3508
|
+
description: "The integration to remove",
|
|
3509
|
+
minLength: 1,
|
|
3510
|
+
maxLength: 256
|
|
3511
|
+
}),
|
|
3512
|
+
confirmIntegrationId: Type.String({
|
|
3513
|
+
description: "Exact integration id copied to confirm removal",
|
|
3514
|
+
minLength: 1,
|
|
3515
|
+
maxLength: 256
|
|
3516
|
+
})
|
|
3517
|
+
}, { additionalProperties: false }),
|
|
3213
3518
|
handler: async (params) => {
|
|
3214
|
-
const integrationId = params
|
|
3519
|
+
const integrationId = requireString(params, "integrationId", 256);
|
|
3520
|
+
requireMatchingConfirmation(params, "confirmIntegrationId", integrationId);
|
|
3215
3521
|
return client.removeIntegration(integrationId);
|
|
3216
3522
|
}
|
|
3217
3523
|
}),
|
|
3218
3524
|
defineTool({
|
|
3219
3525
|
name: "browse_integration_registry",
|
|
3220
3526
|
description: "Browse available integrations in the registry. Shows all integrations that can be installed.",
|
|
3221
|
-
parameters: Type.Object({}),
|
|
3527
|
+
parameters: Type.Object({}, { additionalProperties: false }),
|
|
3222
3528
|
handler: async () => client.getRegistry()
|
|
3223
3529
|
})
|
|
3224
3530
|
];
|
|
3225
3531
|
}
|
|
3226
|
-
function buildVoiceTools(client) {
|
|
3532
|
+
function buildVoiceTools(client, options) {
|
|
3227
3533
|
return [
|
|
3228
3534
|
defineTool({
|
|
3229
3535
|
name: "list_voices",
|
|
3230
3536
|
description: "List the available voices this agent can speak with (the ElevenLabs catalogue). Use this to pick or confirm a voice before calling set_voice with the chosen id.",
|
|
3231
|
-
parameters: Type.Object({}),
|
|
3537
|
+
parameters: Type.Object({}, { additionalProperties: false }),
|
|
3232
3538
|
handler: async () => {
|
|
3233
3539
|
const { voices } = await client.listVoices();
|
|
3234
3540
|
return { voices: voices.map((v) => ({
|
|
@@ -3244,9 +3550,14 @@ function buildVoiceTools(client) {
|
|
|
3244
3550
|
defineTool({
|
|
3245
3551
|
name: "set_voice",
|
|
3246
3552
|
description: "Change this agent's OWN voice. Pass a `voiceId` from list_voices. Returns the updated voice config. (Voice is always on — it cannot be turned off per-agent.)",
|
|
3247
|
-
parameters: Type.Object({ voiceId: Type.String({
|
|
3553
|
+
parameters: Type.Object({ voiceId: Type.String({
|
|
3554
|
+
description: "The ElevenLabs voice id to use, from list_voices (e.g. the `id` field).",
|
|
3555
|
+
minLength: 8,
|
|
3556
|
+
maxLength: 64,
|
|
3557
|
+
pattern: "^[A-Za-z0-9]{8,64}$"
|
|
3558
|
+
}) }, { additionalProperties: false }),
|
|
3248
3559
|
handler: async (params) => {
|
|
3249
|
-
const voiceId = params
|
|
3560
|
+
const voiceId = requireString(params, "voiceId", 64, /^[A-Za-z0-9]{8,64}$/u);
|
|
3250
3561
|
return {
|
|
3251
3562
|
updated: true,
|
|
3252
3563
|
voiceConfig: (await client.updateSelf({ voiceConfig: { voiceId } })).voiceConfig
|
|
@@ -3256,9 +3567,13 @@ function buildVoiceTools(client) {
|
|
|
3256
3567
|
defineTool({
|
|
3257
3568
|
name: "generate_avatar",
|
|
3258
3569
|
description: "Generate and set THIS agent's own profile picture / avatar from a text prompt (e.g. a character description). Updates the agent's real profile picture shown in the dashboard, chat, and apps. Use this — do NOT use `config set ui.assistant.avatar`, which has no effect on the real profile.",
|
|
3259
|
-
parameters: Type.Object({ prompt: Type.String({
|
|
3570
|
+
parameters: Type.Object({ prompt: Type.String({
|
|
3571
|
+
description: "Text description of the avatar to generate (e.g. a character or portrait description).",
|
|
3572
|
+
minLength: 1,
|
|
3573
|
+
maxLength: 2e3
|
|
3574
|
+
}) }, { additionalProperties: false }),
|
|
3260
3575
|
handler: async (params) => {
|
|
3261
|
-
const prompt = params
|
|
3576
|
+
const prompt = requireString(params, "prompt", 2e3);
|
|
3262
3577
|
return {
|
|
3263
3578
|
updated: true,
|
|
3264
3579
|
avatarUrl: (await client.generateAvatar({ prompt })).avatarUrl,
|
|
@@ -3268,34 +3583,40 @@ function buildVoiceTools(client) {
|
|
|
3268
3583
|
}),
|
|
3269
3584
|
defineTool({
|
|
3270
3585
|
name: "set_avatar",
|
|
3271
|
-
description: "Set THIS agent's OWN profile picture from an image — pass
|
|
3586
|
+
description: "Set THIS agent's OWN profile picture from an image — pass an Alfe agent-assets `url` (e.g. the URL returned by `generate_image`) OR a workspace-relative local `path` (png/jpeg/webp, <=5MB). Updates the real profile picture shown in the dashboard, chat, and apps. IMPORTANT: this is how you set YOUR OWN avatar — do NOT use `update_identity` (that edits a CONTACT/person you know, not your own profile) and do NOT use `config set ui.assistant.avatar` (no effect).",
|
|
3272
3587
|
parameters: Type.Object({
|
|
3273
|
-
url: Type.Optional(Type.String({
|
|
3274
|
-
|
|
3275
|
-
|
|
3588
|
+
url: Type.Optional(Type.String({
|
|
3589
|
+
description: "Trusted Alfe agent-assets URL — e.g. the `url` returned by `generate_image`. png/jpeg/webp, <=5MB.",
|
|
3590
|
+
minLength: 1,
|
|
3591
|
+
maxLength: 2048,
|
|
3592
|
+
pattern: "^https://"
|
|
3593
|
+
})),
|
|
3594
|
+
path: Type.Optional(Type.String({
|
|
3595
|
+
description: "Workspace-relative path to a local image file (.png, .jpg/.jpeg, or .webp, <=5MB).",
|
|
3596
|
+
minLength: 1,
|
|
3597
|
+
maxLength: 4096
|
|
3598
|
+
}))
|
|
3599
|
+
}, { additionalProperties: false }),
|
|
3276
3600
|
handler: async (params) => {
|
|
3277
|
-
const
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
bytes = Buffer.from(await dl.arrayBuffer());
|
|
3285
|
-
mimeType = avatarMimeFromUrl(url, dl.headers.get("content-type"));
|
|
3286
|
-
} else if (path) {
|
|
3287
|
-
mimeType = avatarMimeFromPath(path);
|
|
3288
|
-
bytes = await readFile(path);
|
|
3289
|
-
} else throw new Error("provide either `url` (an image URL, e.g. from generate_image) or `path` (a local image file)");
|
|
3290
|
-
if (bytes.length > MAX_AVATAR_BYTES) throw new Error(`image is ${String(bytes.length)} bytes — the avatar limit is ${String(MAX_AVATAR_BYTES)} bytes (5MB)`);
|
|
3601
|
+
const { bytes, mimeType } = await loadAvatarSource({
|
|
3602
|
+
url: optionalString(params, "url", 2048),
|
|
3603
|
+
path: optionalString(params, "path", 4096)
|
|
3604
|
+
}, {
|
|
3605
|
+
workspacePath: options.workspacePath,
|
|
3606
|
+
agentAssetsOrigin: options.agentAssetsOrigin
|
|
3607
|
+
});
|
|
3291
3608
|
const { uploadUrl, s3Key } = await client.presignAvatar({
|
|
3292
3609
|
mimeType,
|
|
3293
3610
|
size: bytes.length
|
|
3294
3611
|
});
|
|
3295
|
-
const
|
|
3612
|
+
const trustedUploadUrl = assertPresignedS3UploadUrl(uploadUrl);
|
|
3613
|
+
if (typeof s3Key !== "string" || s3Key.length < 1 || s3Key.length > 1024 || !s3Key.startsWith("avatars/")) throw new Error("Avatar presign returned an invalid object key");
|
|
3614
|
+
const putRes = await fetch(trustedUploadUrl, {
|
|
3296
3615
|
method: "PUT",
|
|
3297
3616
|
headers: { "Content-Type": mimeType },
|
|
3298
|
-
body: new Uint8Array(bytes)
|
|
3617
|
+
body: new Uint8Array(bytes),
|
|
3618
|
+
redirect: "error",
|
|
3619
|
+
signal: AbortSignal.timeout(3e4)
|
|
3299
3620
|
});
|
|
3300
3621
|
if (!putRes.ok) throw new Error(`failed to upload the avatar image (${String(putRes.status)})`);
|
|
3301
3622
|
return {
|
|
@@ -3307,19 +3628,35 @@ function buildVoiceTools(client) {
|
|
|
3307
3628
|
}),
|
|
3308
3629
|
defineTool({
|
|
3309
3630
|
name: "generate_image",
|
|
3310
|
-
description: "Generate an image from a text prompt. Returns a URL to the generated
|
|
3631
|
+
description: "Generate an image from a text prompt. Returns a URL to the generated image. To show the image to the user, embed it in your reply as markdown: . To set the generated image as YOUR OWN profile picture, pass its url to `set_avatar` (NOT update_identity, which edits other people you know). Or skip this and use `generate_avatar` to generate + set your avatar in one step. Optional `model` picks the image model (default gpt-image-1). Generation is queued server-side while this tool polls for completion and may take up to three minutes; if it times out, retry or use a standard size.",
|
|
3311
3632
|
parameters: Type.Object({
|
|
3312
|
-
prompt: Type.String({
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3633
|
+
prompt: Type.String({
|
|
3634
|
+
description: "Text description of the image to generate.",
|
|
3635
|
+
minLength: 1,
|
|
3636
|
+
maxLength: 4e3
|
|
3637
|
+
}),
|
|
3638
|
+
model: Type.Optional(Type.String({
|
|
3639
|
+
description: "Image model id (default \"gpt-image-1\"). Others may be available, e.g. dall-e-3.",
|
|
3640
|
+
minLength: 1,
|
|
3641
|
+
maxLength: 100
|
|
3642
|
+
})),
|
|
3643
|
+
size: Type.Optional(Type.String({
|
|
3644
|
+
description: "Image size. For the default gpt-image-1 use one of \"1024x1024\" (square), \"1536x1024\" (landscape), \"1024x1536\" (portrait), or \"auto\" — NOT \"512x512\". Other models accept their own sizes. Omit to use the provider default.",
|
|
3645
|
+
minLength: 1,
|
|
3646
|
+
maxLength: 32
|
|
3647
|
+
})),
|
|
3648
|
+
quality: Type.Optional(Type.String({
|
|
3649
|
+
description: "Image quality hint (provider-dependent).",
|
|
3650
|
+
minLength: 1,
|
|
3651
|
+
maxLength: 32
|
|
3652
|
+
}))
|
|
3653
|
+
}, { additionalProperties: false }),
|
|
3317
3654
|
handler: async (params) => {
|
|
3318
3655
|
const { imageUrl, model } = await client.generateImage({
|
|
3319
|
-
prompt: params
|
|
3320
|
-
model: params
|
|
3321
|
-
size: params
|
|
3322
|
-
quality: params
|
|
3656
|
+
prompt: requireString(params, "prompt", 4e3),
|
|
3657
|
+
model: optionalString(params, "model", 100),
|
|
3658
|
+
size: optionalString(params, "size", 32),
|
|
3659
|
+
quality: optionalString(params, "quality", 32)
|
|
3323
3660
|
});
|
|
3324
3661
|
return {
|
|
3325
3662
|
imageUrl,
|
|
@@ -3342,42 +3679,59 @@ const plugin = {
|
|
|
3342
3679
|
try {
|
|
3343
3680
|
cfg = resolveConfig();
|
|
3344
3681
|
} catch (err) {
|
|
3345
|
-
log.warn(`Integration tools not registered — config not available: ${err.message}`);
|
|
3682
|
+
log.warn(`Integration tools not registered — config not available: ${err instanceof Error ? err.message : "unknown error"}`);
|
|
3346
3683
|
}
|
|
3347
3684
|
if (cfg) {
|
|
3348
3685
|
const client = new AgentApiClient({
|
|
3349
3686
|
apiKey: cfg.apiKey,
|
|
3350
3687
|
apiUrl: cfg.apiUrl
|
|
3351
3688
|
});
|
|
3352
|
-
|
|
3689
|
+
let agentAssetsOrigin = "https://assets.invalid";
|
|
3690
|
+
try {
|
|
3691
|
+
agentAssetsOrigin = deriveAgentAssetsOrigin(cfg.apiUrl);
|
|
3692
|
+
} catch {
|
|
3693
|
+
log.warn("Remote set_avatar URLs are unavailable for this API configuration");
|
|
3694
|
+
}
|
|
3695
|
+
const tools = [...buildIntegrationTools(client), ...buildVoiceTools(client, {
|
|
3696
|
+
workspacePath: cfg.workspacePath,
|
|
3697
|
+
agentAssetsOrigin
|
|
3698
|
+
})];
|
|
3353
3699
|
for (const tool of tools) api.registerTool(tool);
|
|
3354
3700
|
log.info(`Registered ${String(tools.length)} agent tools: ${tools.map((t) => t.name).join(", ")}`);
|
|
3355
3701
|
}
|
|
3702
|
+
const serviceOwner = Symbol("alfe-daemon-ipc-owner");
|
|
3356
3703
|
const startDaemonIpc = () => {
|
|
3357
3704
|
if (globalThis.__alfeOpenclawPluginActivated === true) {
|
|
3358
3705
|
log.debug("Alfe OpenClaw Plugin already activated — skipping duplicate");
|
|
3359
3706
|
return;
|
|
3360
3707
|
}
|
|
3361
3708
|
globalThis.__alfeOpenclawPluginActivated = true;
|
|
3709
|
+
ipcOwner = serviceOwner;
|
|
3362
3710
|
if (ipcClient) {
|
|
3363
3711
|
log.warn("Stopping stale IPC client from prior activation");
|
|
3364
3712
|
ipcClient.stop();
|
|
3365
3713
|
ipcClient = null;
|
|
3366
3714
|
}
|
|
3367
3715
|
log.info("Alfe OpenClaw Plugin activating...");
|
|
3368
|
-
const
|
|
3369
|
-
|
|
3716
|
+
const configuredSocketPath = ((api.config ?? {}).plugins?.entries?.["@alfe.ai/openclaw"]?.config ?? {}).socketPath ?? process.env.ALFE_GATEWAY_SOCKET ?? DEFAULT_SOCKET_PATH;
|
|
3717
|
+
if (typeof configuredSocketPath !== "string" || configuredSocketPath.length < 1 || configuredSocketPath.length > 4096 || configuredSocketPath.includes("\0")) {
|
|
3718
|
+
ipcOwner = null;
|
|
3719
|
+
globalThis.__alfeOpenclawPluginActivated = false;
|
|
3720
|
+
throw new Error("Invalid Alfe gateway socket path");
|
|
3721
|
+
}
|
|
3722
|
+
const socketPath = configuredSocketPath;
|
|
3723
|
+
ipcClient = new IPCClient(socketPath, log);
|
|
3370
3724
|
ipcClient.on("event", (event, payload) => {
|
|
3371
3725
|
switch (event) {
|
|
3372
3726
|
case "cloud.status": {
|
|
3373
|
-
const
|
|
3374
|
-
log.info(`Alfe cloud: ${
|
|
3727
|
+
const connected = typeof payload === "object" && payload !== null && !Array.isArray(payload) && payload.connected === true;
|
|
3728
|
+
log.info(`Alfe cloud: ${connected ? "connected" : "disconnected"}`);
|
|
3375
3729
|
break;
|
|
3376
3730
|
}
|
|
3377
3731
|
case "daemon.shutdown":
|
|
3378
3732
|
log.info("Daemon shutting down — will reconnect when available");
|
|
3379
3733
|
break;
|
|
3380
|
-
default: log.debug(`Daemon event: ${event}
|
|
3734
|
+
default: log.debug(`Daemon event: ${event}`);
|
|
3381
3735
|
}
|
|
3382
3736
|
});
|
|
3383
3737
|
ipcClient.on("request", (method, _params, respond) => {
|
|
@@ -3394,7 +3748,9 @@ const plugin = {
|
|
|
3394
3748
|
ipcClient.on("connected", () => {
|
|
3395
3749
|
log.info("Connected to Alfe daemon — registering capabilities...");
|
|
3396
3750
|
registerWithDaemon(ipc, log, plugin.id).then((result) => {
|
|
3397
|
-
if (!result) log.error("Failed to register with daemon — commands may not be routed");
|
|
3751
|
+
if (ipcClient === ipc && ipcOwner === serviceOwner && !result) log.error("Failed to register with daemon — commands may not be routed");
|
|
3752
|
+
}).catch((error) => {
|
|
3753
|
+
if (ipcClient === ipc && ipcOwner === serviceOwner) log.error(`Capability registration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3398
3754
|
});
|
|
3399
3755
|
});
|
|
3400
3756
|
ipcClient.on("disconnected", (reason) => {
|
|
@@ -3403,15 +3759,28 @@ const plugin = {
|
|
|
3403
3759
|
ipcClient.on("error", (err) => {
|
|
3404
3760
|
log.error(`IPC error: ${err.message}`);
|
|
3405
3761
|
});
|
|
3406
|
-
|
|
3762
|
+
try {
|
|
3763
|
+
ipcClient.start();
|
|
3764
|
+
} catch (error) {
|
|
3765
|
+
ipcClient.stop();
|
|
3766
|
+
ipcClient = null;
|
|
3767
|
+
ipcOwner = null;
|
|
3768
|
+
globalThis.__alfeOpenclawPluginActivated = false;
|
|
3769
|
+
throw error;
|
|
3770
|
+
}
|
|
3407
3771
|
log.info(`Alfe OpenClaw Plugin activated (socket: ${socketPath})`);
|
|
3408
3772
|
};
|
|
3409
3773
|
const stopDaemonIpc = () => {
|
|
3774
|
+
if (ipcOwner !== serviceOwner) {
|
|
3775
|
+
log.debug("Ignoring stop from a non-owning Alfe IPC service registration");
|
|
3776
|
+
return;
|
|
3777
|
+
}
|
|
3410
3778
|
log.info("Alfe OpenClaw Plugin deactivating...");
|
|
3411
3779
|
if (ipcClient) {
|
|
3412
3780
|
ipcClient.stop();
|
|
3413
3781
|
ipcClient = null;
|
|
3414
3782
|
}
|
|
3783
|
+
ipcOwner = null;
|
|
3415
3784
|
globalThis.__alfeOpenclawPluginActivated = false;
|
|
3416
3785
|
log.info("Alfe OpenClaw Plugin deactivated");
|
|
3417
3786
|
};
|
|
@@ -3431,9 +3800,10 @@ const plugin = {
|
|
|
3431
3800
|
log.info("Alfe OpenClaw Plugin deactivating (legacy)...");
|
|
3432
3801
|
ipcClient.stop();
|
|
3433
3802
|
ipcClient = null;
|
|
3434
|
-
globalThis.__alfeOpenclawPluginActivated = false;
|
|
3435
3803
|
log.info("Alfe OpenClaw Plugin deactivated");
|
|
3436
3804
|
}
|
|
3805
|
+
ipcOwner = null;
|
|
3806
|
+
globalThis.__alfeOpenclawPluginActivated = false;
|
|
3437
3807
|
}
|
|
3438
3808
|
};
|
|
3439
3809
|
//#endregion
|