clawmatrix 0.1.15 → 0.1.17

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/BOOTSTRAP.md CHANGED
@@ -175,12 +175,23 @@ ClawMatrix 的 Agent 工具注册为可选工具(optional),需要在 OpenC
175
175
  ```json
176
176
  {
177
177
  "tools": {
178
- "allow": ["clawmatrix"]
178
+ "profile": "full",
179
+ "sessions": {
180
+ "visibility": "all"
181
+ },
182
+ "allow": [
183
+ "clawmatrix"
184
+ ]
179
185
  }
180
186
  }
181
187
  ```
182
188
 
183
- 或按 Agent 粒度启用:
189
+ 配置说明:
190
+ - `profile: "full"` — 启用完整工具集(包括可选工具)
191
+ - `sessions.visibility: "all"` — 允许跨会话访问工具(集群工具需要此设置才能在所有会话中可用)
192
+ - `allow: ["clawmatrix"]` — 显式允许 ClawMatrix 插件注册的所有集群工具
193
+
194
+ 也可以按 Agent 粒度启用:
184
195
 
185
196
  ```json
186
197
  {
@@ -189,6 +200,10 @@ ClawMatrix 的 Agent 工具注册为可选工具(optional),需要在 OpenC
189
200
  {
190
201
  "id": "main",
191
202
  "tools": {
203
+ "profile": "full",
204
+ "sessions": {
205
+ "visibility": "all"
206
+ },
192
207
  "allow": ["clawmatrix"]
193
208
  }
194
209
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmatrix",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
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",
package/src/auth.ts CHANGED
@@ -3,17 +3,40 @@ export function generateNonce(): string {
3
3
  return Buffer.from(bytes).toString("hex");
4
4
  }
5
5
 
6
- export async function computeHmac(
7
- nonce: string,
8
- secret: string,
9
- ): Promise<string> {
10
- const key = await crypto.subtle.importKey(
6
+ /** Cache imported CryptoKey keyed by a hash of the secret (avoid storing plaintext). Max 8 entries. */
7
+ const KEY_CACHE_MAX = 8;
8
+ const keyCache = new Map<string, CryptoKey>();
9
+
10
+ function cacheKeyFor(secret: string): string {
11
+ const { createHash } = require("node:crypto");
12
+ return createHash("sha256").update(secret).digest("hex");
13
+ }
14
+
15
+ async function getHmacKey(secret: string): Promise<CryptoKey> {
16
+ const cacheKey = cacheKeyFor(secret);
17
+ let key = keyCache.get(cacheKey);
18
+ if (key) return key;
19
+ key = await crypto.subtle.importKey(
11
20
  "raw",
12
21
  new TextEncoder().encode(secret),
13
22
  { name: "HMAC", hash: "SHA-256" },
14
23
  false,
15
24
  ["sign"],
16
25
  );
26
+ if (keyCache.size >= KEY_CACHE_MAX) {
27
+ // Evict oldest entry
28
+ const oldest = keyCache.keys().next().value!;
29
+ keyCache.delete(oldest);
30
+ }
31
+ keyCache.set(cacheKey, key);
32
+ return key;
33
+ }
34
+
35
+ export async function computeHmac(
36
+ nonce: string,
37
+ secret: string,
38
+ ): Promise<string> {
39
+ const key = await getHmacKey(secret);
17
40
  const sig = await crypto.subtle.sign(
18
41
  "HMAC",
19
42
  key,
@@ -22,14 +45,21 @@ export async function computeHmac(
22
45
  return Buffer.from(sig).toString("hex");
23
46
  }
24
47
 
25
- /** Constant-time string comparison. */
48
+ /** Constant-time string comparison. Uses native crypto.timingSafeEqual with
49
+ * SHA-256 pre-hash to normalize lengths (avoids length-leak from early return). */
26
50
  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);
51
+ const nodeTimingSafeEqual = require("node:crypto").timingSafeEqual;
52
+ const encoder = new TextEncoder();
53
+ const bufA = encoder.encode(a);
54
+ const bufB = encoder.encode(b);
55
+ if (bufA.byteLength !== bufB.byteLength) {
56
+ // Hash both to fixed length so we still compare in constant time
57
+ const { createHash } = require("node:crypto");
58
+ const hashA = createHash("sha256").update(bufA).digest();
59
+ const hashB = createHash("sha256").update(bufB).digest();
60
+ return nodeTimingSafeEqual(hashA, hashB);
31
61
  }
32
- return diff === 0;
62
+ return nodeTimingSafeEqual(Buffer.from(bufA), Buffer.from(bufB));
33
63
  }
34
64
 
35
65
  export async function verifyHmac(
@@ -38,5 +68,5 @@ export async function verifyHmac(
38
68
  sig: string,
39
69
  ): Promise<boolean> {
40
70
  const expected = await computeHmac(nonce, secret);
41
- return timingSafeEqual(expected, sig);
71
+ return timingSafeEqual(expected, sig); // timingSafeEqual is now sync
42
72
  }
@@ -17,6 +17,11 @@ import type {
17
17
  HandoffRequest,
18
18
  HandoffResponse,
19
19
  HandoffStreamChunk,
20
+ HandoffCancel,
21
+ HandoffStatusQuery,
22
+ HandoffStatusResponse,
23
+ HandoffInputRequired,
24
+ HandoffInput,
20
25
  ModelRequest,
21
26
  ModelResponse,
22
27
  ModelStreamChunk,
@@ -44,6 +49,7 @@ export class ClusterRuntime {
44
49
  readonly handoffManager: HandoffManager;
45
50
  readonly modelProxy: ModelProxy;
46
51
  readonly toolProxy: ToolProxy;
52
+ webHandler: WebHandler | null = null;
47
53
  private logger: PluginLogger;
48
54
 
49
55
  constructor(config: ClawMatrixConfig, logger: PluginLogger, openclawConfig: OpenClawConfig, openclawVersion?: string) {
@@ -58,7 +64,7 @@ export class ClusterRuntime {
58
64
 
59
65
  start() {
60
66
  // Wire up frame dispatch
61
- this.peerManager.on("frame", (frame, conn) => {
67
+ this.peerManager.on("frame", (frame) => {
62
68
  this.dispatchFrame(frame);
63
69
  });
64
70
 
@@ -72,8 +78,10 @@ export class ClusterRuntime {
72
78
 
73
79
  // Web dashboard (must be set before peerManager.start() creates the HTTP server)
74
80
  if (this.config.web?.enabled) {
75
- const webHandler = new WebHandler(this.config, this.peerManager, this.handoffManager);
76
- this.peerManager.setHttpHandler((req, res) => webHandler.handle(req, res));
81
+ this.webHandler = new WebHandler(this.config, this.peerManager, this.handoffManager);
82
+ this.peerManager.setHttpHandler((req, res) => this.webHandler!.handle(req, res));
83
+ // Enable satellite tool routing through WebHandler
84
+ this.toolProxy.setSatelliteHandler(this.webHandler);
77
85
  this.logger.info(`[clawmatrix] Web dashboard enabled on listen port`);
78
86
  }
79
87
 
@@ -89,6 +97,7 @@ export class ClusterRuntime {
89
97
  }
90
98
 
91
99
  async stop() {
100
+ this.webHandler?.destroy();
92
101
  this.handoffManager.destroy();
93
102
  this.modelProxy.stop();
94
103
  this.toolProxy.destroy();
@@ -98,7 +107,7 @@ export class ClusterRuntime {
98
107
 
99
108
  private dispatchFrame(frame: AnyClusterFrame) {
100
109
  if (frame.type.startsWith("model_")) {
101
- debug("dispatch", `${frame.type} id=${frame.id} from=${(frame as ClusterFrame).from}`);
110
+ debug("dispatch", `${frame.type} id=${frame.id} from=${frame.from}`);
102
111
  }
103
112
  switch (frame.type) {
104
113
  case "handoff_req":
@@ -112,6 +121,24 @@ export class ClusterRuntime {
112
121
  case "handoff_res":
113
122
  this.handoffManager.handleResponse(frame as HandoffResponse);
114
123
  break;
124
+ case "handoff_cancel":
125
+ this.handoffManager.handleCancel(frame as HandoffCancel);
126
+ break;
127
+ case "handoff_status":
128
+ this.handoffManager.handleStatusQuery(frame as HandoffStatusQuery);
129
+ break;
130
+ case "handoff_input_required":
131
+ this.handoffManager.handleInputRequired(frame as HandoffInputRequired);
132
+ break;
133
+ case "handoff_input":
134
+ this.handoffManager.handleInput(frame as HandoffInput).catch((err) => {
135
+ this.logger.error(`[clawmatrix] Handoff input error: ${err}`);
136
+ });
137
+ break;
138
+ case "handoff_status_res":
139
+ // Status responses are handled by the requester via pending callbacks
140
+ // (currently informational — logged but not routed to pending map)
141
+ break;
115
142
  case "model_req":
116
143
  this.modelProxy.handleModelRequest(frame as ModelRequest).catch((err) => {
117
144
  this.logger.error(`[clawmatrix] Model request error: ${err}`);
package/src/compat.ts CHANGED
@@ -40,6 +40,9 @@ export function spawnProcess(
40
40
  stream.on("end", () => controller.close());
41
41
  stream.on("error", (err) => controller.error(err));
42
42
  },
43
+ cancel() {
44
+ stream.destroy();
45
+ },
43
46
  });
44
47
  }
45
48
 
package/src/connection.ts CHANGED
@@ -9,6 +9,7 @@ import type {
9
9
  DeviceInfo,
10
10
  ModelInfo,
11
11
  NodeCapabilities,
12
+ ToolProxyInfo,
12
13
  } from "./types.ts";
13
14
  import { generateNonce, computeHmac, verifyHmac } from "./auth.ts";
14
15
 
@@ -68,13 +69,15 @@ export class Connection extends EventEmitter<ConnectionEvents> {
68
69
  this.secret = secret;
69
70
  this.localCapabilities = localCapabilities;
70
71
 
72
+ // Both inbound and outbound get an auth timeout to prevent hanging connections
73
+ this.authTimer = setTimeout(() => {
74
+ if (!this.authenticated) {
75
+ this.close(4003, "auth timeout");
76
+ }
77
+ }, AUTH_TIMEOUT);
78
+
71
79
  if (role === "inbound") {
72
80
  this.sendAuthChallenge();
73
- this.authTimer = setTimeout(() => {
74
- if (!this.authenticated) {
75
- this.close(4003, "auth timeout");
76
- }
77
- }, AUTH_TIMEOUT);
78
81
  }
79
82
  }
80
83
 
@@ -162,13 +165,14 @@ export class Connection extends EventEmitter<ConnectionEvents> {
162
165
  private async handleAuthMessage(frame: AnyClusterFrame) {
163
166
  if (this.role === "inbound") {
164
167
  if (frame.type !== "auth") return;
165
- const { nodeId, sig, agents, models, tags, deviceInfo } = frame.payload as {
168
+ const { nodeId, sig, agents, models, tags, deviceInfo, toolProxy } = frame.payload as {
166
169
  nodeId: string;
167
170
  sig: string;
168
171
  agents?: AgentInfo[];
169
172
  models?: ModelInfo[];
170
173
  tags?: string[];
171
174
  deviceInfo?: DeviceInfo;
175
+ toolProxy?: ToolProxyInfo;
172
176
  };
173
177
  if (!this.pendingNonce) return;
174
178
 
@@ -193,6 +197,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
193
197
  models: models ?? [],
194
198
  tags: tags ?? [],
195
199
  deviceInfo,
200
+ toolProxy,
196
201
  };
197
202
  this.authenticated = true;
198
203
  this.clearAuthTimer();
@@ -207,6 +212,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
207
212
  models: this.localCapabilities.models,
208
213
  tags: this.localCapabilities.tags,
209
214
  deviceInfo: this.localCapabilities.deviceInfo,
215
+ toolProxy: this.localCapabilities.toolProxy,
210
216
  },
211
217
  } as AuthOk);
212
218
 
@@ -227,6 +233,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
227
233
  models: this.localCapabilities.models,
228
234
  tags: this.localCapabilities.tags,
229
235
  deviceInfo: this.localCapabilities.deviceInfo,
236
+ toolProxy: this.localCapabilities.toolProxy,
230
237
  },
231
238
  });
232
239
  return;
@@ -241,8 +248,10 @@ export class Connection extends EventEmitter<ConnectionEvents> {
241
248
  models: ok.payload.models,
242
249
  tags: ok.payload.tags,
243
250
  deviceInfo: ok.payload.deviceInfo,
251
+ toolProxy: ok.payload.toolProxy,
244
252
  };
245
253
  this.authenticated = true;
254
+ this.clearAuthTimer();
246
255
  this.startHeartbeat();
247
256
  this.emit("authenticated", this.remoteCapabilities);
248
257
  return;