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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmatrix",
3
- "version": "0.1.12",
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
- if (expected.length !== sig.length) return false;
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
  }
@@ -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>;
@@ -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((_req, res) => {
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
- if (frame.id && this.router.isDuplicate(frame.id)) return;
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),