clawmatrix 0.1.12 → 0.1.14
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/package.json +4 -4
- package/src/auth.ts +11 -7
- package/src/cluster-service.ts +8 -0
- package/src/config.ts +6 -0
- package/src/peer-manager.ts +13 -3
- package/src/tool-proxy.ts +6 -0
- package/src/web-ui.ts +1270 -0
- package/src/web.ts +230 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawmatrix",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Decentralized mesh cluster plugin for OpenClaw — inter-gateway communication, model proxy, task handoff, and tool proxy.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,12 +33,12 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"ws": "^8.19.0",
|
|
36
|
-
"zod": "^4.3.6"
|
|
36
|
+
"zod": "^4.3.6",
|
|
37
|
+
"@mariozechner/pi-coding-agent": ">=0.55.0"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@types/bun": "latest",
|
|
40
|
-
"@types/ws": "^8.18.1"
|
|
41
|
-
"openclaw": "^2026.3.2"
|
|
41
|
+
"@types/ws": "^8.18.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"typescript": "^5"
|
package/src/auth.ts
CHANGED
|
@@ -22,17 +22,21 @@ export async function computeHmac(
|
|
|
22
22
|
return Buffer.from(sig).toString("hex");
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/** Constant-time string comparison. */
|
|
26
|
+
export function timingSafeEqual(a: string, b: string): boolean {
|
|
27
|
+
if (a.length !== b.length) return false;
|
|
28
|
+
let diff = 0;
|
|
29
|
+
for (let i = 0; i < a.length; i++) {
|
|
30
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
31
|
+
}
|
|
32
|
+
return diff === 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
export async function verifyHmac(
|
|
26
36
|
nonce: string,
|
|
27
37
|
secret: string,
|
|
28
38
|
sig: string,
|
|
29
39
|
): Promise<boolean> {
|
|
30
40
|
const expected = await computeHmac(nonce, secret);
|
|
31
|
-
|
|
32
|
-
// Constant-time comparison
|
|
33
|
-
let diff = 0;
|
|
34
|
-
for (let i = 0; i < expected.length; i++) {
|
|
35
|
-
diff |= expected.charCodeAt(i) ^ sig.charCodeAt(i);
|
|
36
|
-
}
|
|
37
|
-
return diff === 0;
|
|
41
|
+
return timingSafeEqual(expected, sig);
|
|
38
42
|
}
|
package/src/cluster-service.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { PeerManager } from "./peer-manager.ts";
|
|
|
11
11
|
import { HandoffManager } from "./handoff.ts";
|
|
12
12
|
import { ModelProxy } from "./model-proxy.ts";
|
|
13
13
|
import { ToolProxy, type GatewayInfo } from "./tool-proxy.ts";
|
|
14
|
+
import { WebHandler } from "./web.ts";
|
|
14
15
|
import type {
|
|
15
16
|
AnyClusterFrame,
|
|
16
17
|
HandoffRequest,
|
|
@@ -69,6 +70,13 @@ export class ClusterRuntime {
|
|
|
69
70
|
this.logger.info(`[clawmatrix] Peer disconnected: ${nodeId}`);
|
|
70
71
|
});
|
|
71
72
|
|
|
73
|
+
// Web dashboard (must be set before peerManager.start() creates the HTTP server)
|
|
74
|
+
if (this.config.web?.enabled) {
|
|
75
|
+
const webHandler = new WebHandler(this.config, this.peerManager);
|
|
76
|
+
this.peerManager.setHttpHandler((req, res) => webHandler.handle(req, res));
|
|
77
|
+
this.logger.info(`[clawmatrix] Web dashboard enabled on listen port`);
|
|
78
|
+
}
|
|
79
|
+
|
|
72
80
|
// Start subsystems
|
|
73
81
|
this.peerManager.start();
|
|
74
82
|
this.modelProxy.start();
|
package/src/config.ts
CHANGED
|
@@ -68,6 +68,11 @@ const ProxyModelSchema = z.object({
|
|
|
68
68
|
...ModelParamsSchema,
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
const WebConfigSchema = z.object({
|
|
72
|
+
enabled: z.boolean().default(false),
|
|
73
|
+
token: z.string().min(8, "web token must be at least 8 characters"),
|
|
74
|
+
}).optional();
|
|
75
|
+
|
|
71
76
|
export const ClawMatrixConfigSchema = z.object({
|
|
72
77
|
nodeId: z.string(),
|
|
73
78
|
secret: z.string().min(16, "secret must be at least 16 characters"),
|
|
@@ -82,6 +87,7 @@ export const ClawMatrixConfigSchema = z.object({
|
|
|
82
87
|
proxyPort: z.number().default(19001),
|
|
83
88
|
toolProxy: ToolProxyConfigSchema.optional(),
|
|
84
89
|
handoffTimeout: z.number().default(600_000),
|
|
90
|
+
web: WebConfigSchema,
|
|
85
91
|
});
|
|
86
92
|
|
|
87
93
|
export type ClawMatrixConfig = z.infer<typeof ClawMatrixConfigSchema>;
|
package/src/peer-manager.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import { createServer, type IncomingMessage, type Server } from "node:http";
|
|
2
|
+
import { createServer, type IncomingMessage, type ServerResponse, type Server } from "node:http";
|
|
3
3
|
import { WebSocketServer, WebSocket as WsWebSocket } from "ws";
|
|
4
4
|
import type { ClawMatrixConfig, PeerConfig } from "./config.ts";
|
|
5
5
|
import { Connection } from "./connection.ts";
|
|
@@ -95,12 +95,20 @@ export class PeerManager extends EventEmitter<PeerManagerEvents> {
|
|
|
95
95
|
this.router.destroy();
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/** Set an HTTP request handler for non-WebSocket requests (e.g. web dashboard). */
|
|
99
|
+
private httpRequestHandler: ((req: IncomingMessage, res: ServerResponse) => boolean) | null = null;
|
|
100
|
+
|
|
101
|
+
setHttpHandler(handler: (req: IncomingMessage, res: ServerResponse) => boolean) {
|
|
102
|
+
this.httpRequestHandler = handler;
|
|
103
|
+
}
|
|
104
|
+
|
|
98
105
|
// ── Inbound WS server (node:http + ws) ──────────────────────────
|
|
99
106
|
private startListening() {
|
|
100
107
|
const port = this.config.listenPort;
|
|
101
108
|
const hostname = this.config.listenHost;
|
|
102
109
|
|
|
103
|
-
this.httpServer = createServer((
|
|
110
|
+
this.httpServer = createServer((req, res) => {
|
|
111
|
+
if (this.httpRequestHandler && this.httpRequestHandler(req, res)) return;
|
|
104
112
|
res.writeHead(426, { "Content-Type": "text/plain" });
|
|
105
113
|
res.end("WebSocket upgrade required");
|
|
106
114
|
});
|
|
@@ -270,7 +278,9 @@ export class PeerManager extends EventEmitter<PeerManagerEvents> {
|
|
|
270
278
|
// Validate from field: must be the direct peer or a known node (relayed)
|
|
271
279
|
if (frame.from && frame.from !== from.remoteNodeId && !this.router.getRoute(frame.from)) return;
|
|
272
280
|
|
|
273
|
-
|
|
281
|
+
// Skip dedup for streaming frame types — they share one id across many chunks
|
|
282
|
+
const isStreamFrame = frame.type === "model_stream" || frame.type === "handoff_stream";
|
|
283
|
+
if (frame.id && !isStreamFrame && this.router.isDuplicate(frame.id)) return;
|
|
274
284
|
|
|
275
285
|
if (frame.type === "peer_sync") {
|
|
276
286
|
this.handlePeerSync(frame as PeerSync, from);
|
package/src/tool-proxy.ts
CHANGED
|
@@ -114,11 +114,17 @@ export class ToolProxy {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
try {
|
|
117
|
+
console.log(`Received tool request for: ${payload.tool}`);
|
|
118
|
+
console.log(`Is local tool? ${isLocalTool(payload.tool)}`);
|
|
119
|
+
|
|
117
120
|
const result = isLocalTool(payload.tool)
|
|
118
121
|
? await executeLocally(payload.tool, payload.params)
|
|
119
122
|
: await this.executeViaGateway(payload.tool, payload.params);
|
|
123
|
+
|
|
124
|
+
console.log(`Tool execution result: ${JSON.stringify(result)}`);
|
|
120
125
|
this.sendResponse(id, from, { success: true, result });
|
|
121
126
|
} catch (err) {
|
|
127
|
+
console.error(`Tool execution error: ${err}`);
|
|
122
128
|
this.sendResponse(id, from, {
|
|
123
129
|
success: false,
|
|
124
130
|
error: err instanceof Error ? err.message : String(err),
|