clawmatrix 0.4.2 → 0.5.1
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 +17 -21
- package/cli/bin/clawmatrix.mjs +300 -1
- package/package.json +8 -1
- package/src/acp-proxy.ts +122 -50
- package/src/{web.ts → api.ts} +646 -25
- package/src/audit.ts +37 -2
- package/src/auth.ts +5 -10
- package/src/automation.ts +625 -0
- package/src/cluster-service.ts +172 -16
- package/src/compat.ts +103 -0
- package/src/config.ts +75 -27
- package/src/connection.ts +225 -37
- package/src/crypto.ts +72 -5
- package/src/device-info.ts +21 -2
- package/src/file-transfer.ts +3 -2
- package/src/handoff.ts +90 -32
- package/src/health-tracker.ts +91 -356
- package/src/index.ts +421 -13
- package/src/kanban.ts +507 -0
- package/src/knowledge-sync.ts +158 -7
- package/src/local-tools.ts +65 -2
- package/src/log-replication.ts +198 -0
- package/src/model-proxy.ts +152 -60
- package/src/peer-approval.ts +3 -2
- package/src/peer-manager.ts +237 -47
- package/src/retry.ts +81 -0
- package/src/router.ts +152 -104
- package/src/sentinel.ts +86 -52
- package/src/store.ts +578 -0
- package/src/terminal.ts +17 -8
- package/src/tool-proxy.ts +6 -5
- package/src/tools/cluster-events.ts +6 -6
- package/src/tools/cluster-kanban.ts +345 -0
- package/src/tools/cluster-peers.ts +1 -1
- package/src/tools/cluster-query.ts +145 -0
- package/src/types.ts +95 -9
package/src/cluster-service.ts
CHANGED
|
@@ -15,10 +15,15 @@ import { ToolProxy, type GatewayInfo } from "./tool-proxy.ts";
|
|
|
15
15
|
import { AcpProxy, readAllSessionStoresFromDisk } from "./acp-proxy.ts";
|
|
16
16
|
import { TerminalManager } from "./terminal.ts";
|
|
17
17
|
import { FileTransferManager } from "./file-transfer.ts";
|
|
18
|
-
import {
|
|
18
|
+
import { ApiHandler } from "./api.ts";
|
|
19
19
|
import { KnowledgeSync } from "./knowledge-sync.ts";
|
|
20
20
|
import { HealthTracker } from "./health-tracker.ts";
|
|
21
|
+
import { KanbanManager } from "./kanban.ts";
|
|
21
22
|
import { SentinelManager } from "./sentinel-manager.ts";
|
|
23
|
+
import { AutomationManager } from "./automation.ts";
|
|
24
|
+
import { Store } from "./store.ts";
|
|
25
|
+
import { LogReplicator } from "./log-replication.ts";
|
|
26
|
+
import { setAuditStore } from "./audit.ts";
|
|
22
27
|
import type {
|
|
23
28
|
AnyClusterFrame,
|
|
24
29
|
HandoffRequest,
|
|
@@ -30,7 +35,6 @@ import type {
|
|
|
30
35
|
HandoffInputRequired,
|
|
31
36
|
HandoffInput,
|
|
32
37
|
KnowledgeSyncFrame,
|
|
33
|
-
HealthSyncFrame,
|
|
34
38
|
AvailabilityRequest,
|
|
35
39
|
AvailabilityResponse,
|
|
36
40
|
ModelRequest,
|
|
@@ -71,6 +75,9 @@ import type {
|
|
|
71
75
|
FileTransferChunk,
|
|
72
76
|
FileTransferChunkAck,
|
|
73
77
|
FileTransferComplete,
|
|
78
|
+
KanbanSyncFrame,
|
|
79
|
+
KanbanNotifyFrame,
|
|
80
|
+
LogSyncFrame,
|
|
74
81
|
} from "./types.ts";
|
|
75
82
|
|
|
76
83
|
function resolveGatewayInfo(openclawConfig: OpenClawConfig): GatewayInfo {
|
|
@@ -96,8 +103,15 @@ export class ClusterRuntime {
|
|
|
96
103
|
readonly terminalManager: TerminalManager;
|
|
97
104
|
readonly fileTransferManager: FileTransferManager | null;
|
|
98
105
|
knowledgeSync: KnowledgeSync | null = null;
|
|
106
|
+
/** Resolved workspace path (null if not configured). */
|
|
107
|
+
workspacePath: string | null = null;
|
|
99
108
|
healthTracker: HealthTracker;
|
|
100
|
-
|
|
109
|
+
kanbanManager: KanbanManager | null = null;
|
|
110
|
+
apiHandler: ApiHandler | null = null;
|
|
111
|
+
automationManager: AutomationManager | null = null;
|
|
112
|
+
store: Store | null = null;
|
|
113
|
+
logReplicator: LogReplicator | null = null;
|
|
114
|
+
private storeCleanupTimer: ReturnType<typeof setInterval> | null = null;
|
|
101
115
|
private sentinelManager: SentinelManager | null = null;
|
|
102
116
|
private logger: PluginLogger;
|
|
103
117
|
private openclawConfig: OpenClawConfig;
|
|
@@ -113,7 +127,7 @@ export class ClusterRuntime {
|
|
|
113
127
|
this.logger = logger;
|
|
114
128
|
this.openclawConfig = openclawConfig;
|
|
115
129
|
const gatewayInfo = resolveGatewayInfo(openclawConfig);
|
|
116
|
-
this.peerManager = new PeerManager(config, openclawVersion);
|
|
130
|
+
this.peerManager = new PeerManager(config, openclawVersion, openclawConfig as Record<string, unknown>);
|
|
117
131
|
this.handoffManager = new HandoffManager(config, this.peerManager, gatewayInfo);
|
|
118
132
|
this.modelProxy = new ModelProxy(config, this.peerManager, gatewayInfo, openclawConfig);
|
|
119
133
|
this.toolProxy = new ToolProxy(config, this.peerManager, gatewayInfo, logger);
|
|
@@ -126,7 +140,6 @@ export class ClusterRuntime {
|
|
|
126
140
|
: null;
|
|
127
141
|
this.healthTracker = new HealthTracker({
|
|
128
142
|
nodeId: config.nodeId,
|
|
129
|
-
peerManager: this.peerManager,
|
|
130
143
|
});
|
|
131
144
|
// Build agent indexes
|
|
132
145
|
for (const a of config.agents) {
|
|
@@ -169,19 +182,48 @@ export class ClusterRuntime {
|
|
|
169
182
|
this.trackRelayPeerHealth();
|
|
170
183
|
});
|
|
171
184
|
|
|
172
|
-
//
|
|
173
|
-
if (this.config.
|
|
174
|
-
this.
|
|
175
|
-
this.peerManager.setHttpHandler((req, res) => this.
|
|
176
|
-
// Enable satellite tool routing through
|
|
177
|
-
this.toolProxy.setSatelliteHandler(this.
|
|
178
|
-
this.
|
|
179
|
-
this.logger.info(`[clawmatrix]
|
|
185
|
+
// HTTP API (must be set before peerManager.start() creates the HTTP server)
|
|
186
|
+
if (this.config.listen) {
|
|
187
|
+
this.apiHandler = new ApiHandler(this.config, this.peerManager, this.handoffManager);
|
|
188
|
+
this.peerManager.setHttpHandler((req, res) => this.apiHandler!.handle(req, res));
|
|
189
|
+
// Enable satellite tool routing through ApiHandler
|
|
190
|
+
this.toolProxy.setSatelliteHandler(this.apiHandler);
|
|
191
|
+
this.apiHandler.setHealthTracker(this.healthTracker);
|
|
192
|
+
this.logger.info(`[clawmatrix] HTTP API enabled on listen port`);
|
|
180
193
|
}
|
|
181
194
|
|
|
195
|
+
// Automation manager (always enabled — no rules = no overhead)
|
|
196
|
+
{
|
|
197
|
+
const workspacePath = this.resolveWorkspacePath();
|
|
198
|
+
const stateDir = workspacePath
|
|
199
|
+
? path.join(workspacePath, ".clawmatrix")
|
|
200
|
+
: null;
|
|
201
|
+
if (stateDir) {
|
|
202
|
+
const automationsPath = path.join(stateDir, "automations.json");
|
|
203
|
+
this.automationManager = new AutomationManager(
|
|
204
|
+
automationsPath, this.handoffManager, this.peerManager, this.config.nodeId,
|
|
205
|
+
);
|
|
206
|
+
if (this.apiHandler) {
|
|
207
|
+
this.apiHandler.setAutomationManager(this.automationManager);
|
|
208
|
+
}
|
|
209
|
+
this.automationManager.setToolInvoker(this.toolProxy);
|
|
210
|
+
this.automationManager.start().catch((err) => {
|
|
211
|
+
this.logger.error(`[clawmatrix] Automation manager failed to start: ${err}`);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Resolve workspace path (used by knowledge sync)
|
|
217
|
+
this.workspacePath = this.resolveWorkspacePath();
|
|
218
|
+
|
|
182
219
|
// Knowledge sync
|
|
183
220
|
if (this.config.knowledge?.enabled) {
|
|
184
|
-
|
|
221
|
+
// Advertise "knowledge" capability so peers can detect it
|
|
222
|
+
if (!this.config.tags.includes("knowledge")) {
|
|
223
|
+
this.config.tags.push("knowledge");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const workspacePath = this.workspacePath;
|
|
185
227
|
if (workspacePath) {
|
|
186
228
|
const stateDir = path.join(workspacePath, ".clawmatrix");
|
|
187
229
|
this.knowledgeSync = new KnowledgeSync({
|
|
@@ -198,6 +240,9 @@ export class ClusterRuntime {
|
|
|
198
240
|
}).catch((err) => {
|
|
199
241
|
this.logger.error(`[clawmatrix] Knowledge sync failed to start: ${err}`);
|
|
200
242
|
});
|
|
243
|
+
if (this.apiHandler) {
|
|
244
|
+
this.apiHandler.setKnowledgeSync(this.knowledgeSync);
|
|
245
|
+
}
|
|
201
246
|
|
|
202
247
|
// Sync with peers on connect/disconnect
|
|
203
248
|
this.peerManager.on("peerConnected", (nodeId) => {
|
|
@@ -206,6 +251,20 @@ export class ClusterRuntime {
|
|
|
206
251
|
this.peerManager.on("peerDisconnected", (nodeId) => {
|
|
207
252
|
this.knowledgeSync?.removePeerSync(nodeId);
|
|
208
253
|
});
|
|
254
|
+
|
|
255
|
+
// Wire ACP tool write attribution to knowledge-sync
|
|
256
|
+
if (this.acpProxy) {
|
|
257
|
+
const ks = this.knowledgeSync;
|
|
258
|
+
const nodeId = this.config.nodeId;
|
|
259
|
+
const agentTypeMap: Record<string, string> = { claude: "Claude Code", codex: "Codex", gemini: "Gemini CLI" };
|
|
260
|
+
this.acpProxy.onToolWrite = (filePath, agentId) => {
|
|
261
|
+
ks.setPendingAttribution(filePath, {
|
|
262
|
+
nodeId,
|
|
263
|
+
agentId,
|
|
264
|
+
agentType: agentTypeMap[agentId] ?? agentId,
|
|
265
|
+
});
|
|
266
|
+
};
|
|
267
|
+
}
|
|
209
268
|
}
|
|
210
269
|
}
|
|
211
270
|
|
|
@@ -222,11 +281,87 @@ export class ClusterRuntime {
|
|
|
222
281
|
});
|
|
223
282
|
}
|
|
224
283
|
|
|
284
|
+
// SQLite store + log replication
|
|
285
|
+
{
|
|
286
|
+
const workspacePath = this.resolveWorkspacePath();
|
|
287
|
+
const stateDir = workspacePath
|
|
288
|
+
? path.join(workspacePath, ".clawmatrix")
|
|
289
|
+
: path.join(process.env.HOME ?? process.env.USERPROFILE ?? "/tmp", ".openclaw", "clawmatrix");
|
|
290
|
+
try {
|
|
291
|
+
this.store = new Store(path.join(stateDir, "clawmatrix.db"));
|
|
292
|
+
this.logReplicator = new LogReplicator({
|
|
293
|
+
store: this.store,
|
|
294
|
+
nodeId: this.config.nodeId,
|
|
295
|
+
sendTo: (peerId, frame) => this.peerManager.sendTo(peerId, frame),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
this.peerManager.on("peerConnected", (nodeId) => {
|
|
299
|
+
this.logReplicator?.initPeerSync(nodeId);
|
|
300
|
+
});
|
|
301
|
+
this.peerManager.on("peerDisconnected", (nodeId) => {
|
|
302
|
+
this.logReplicator?.removePeerSync(nodeId);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Periodic TTL cleanup (every 24h)
|
|
306
|
+
const retentionMs = (this.config.store?.retentionDays ?? 90) * 86400_000;
|
|
307
|
+
this.storeCleanupTimer = setInterval(() => {
|
|
308
|
+
const cutoff = Date.now() - retentionMs;
|
|
309
|
+
const result = this.store?.cleanup(cutoff);
|
|
310
|
+
if (result) {
|
|
311
|
+
const total = result.audit + result.health + result.handoff + result.satellite + result.ingested + result.automation;
|
|
312
|
+
if (total > 0) this.logger.info(`[clawmatrix] Store cleanup: ${total} old rows removed`);
|
|
313
|
+
}
|
|
314
|
+
}, 86400_000);
|
|
315
|
+
|
|
316
|
+
// Wire subsystems to store for persistence
|
|
317
|
+
setAuditStore(this.store, this.config.nodeId, this.logReplicator!);
|
|
318
|
+
this.handoffManager.store = this.store;
|
|
319
|
+
this.handoffManager.logReplicator = this.logReplicator;
|
|
320
|
+
this.healthTracker.setStore(this.store, this.logReplicator!);
|
|
321
|
+
if (this.acpProxy) this.acpProxy.store = this.store;
|
|
322
|
+
this.apiHandler?.setStore(this.store);
|
|
323
|
+
this.automationManager?.setStore(this.store);
|
|
324
|
+
|
|
325
|
+
this.logger.info(`[clawmatrix] Store initialized at ${stateDir}/clawmatrix.db`);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
this.logger.warn(`[clawmatrix] Store initialization failed (non-fatal): ${err}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
225
331
|
// Health tracker (always enabled, no config gate)
|
|
226
332
|
this.healthTracker.start().catch((err) => {
|
|
227
333
|
this.logger.error(`[clawmatrix] Health tracker failed to start: ${err}`);
|
|
228
334
|
});
|
|
229
335
|
|
|
336
|
+
// Kanban board
|
|
337
|
+
if (this.config.kanban?.enabled) {
|
|
338
|
+
const workspacePath = this.resolveWorkspacePath();
|
|
339
|
+
const stateDir = workspacePath
|
|
340
|
+
? path.join(workspacePath, ".clawmatrix")
|
|
341
|
+
: undefined;
|
|
342
|
+
this.kanbanManager = new KanbanManager({
|
|
343
|
+
nodeId: this.config.nodeId,
|
|
344
|
+
peerManager: this.peerManager,
|
|
345
|
+
prefix: this.config.kanban.prefix,
|
|
346
|
+
autoAssign: this.config.kanban.autoAssign,
|
|
347
|
+
archiveAfterMs: this.config.kanban.archiveAfterMs,
|
|
348
|
+
stateDir,
|
|
349
|
+
});
|
|
350
|
+
this.kanbanManager.start().catch((err) => {
|
|
351
|
+
this.logger.error(`[clawmatrix] Kanban manager failed to start: ${err}`);
|
|
352
|
+
});
|
|
353
|
+
if (this.apiHandler) {
|
|
354
|
+
this.apiHandler.setKanbanManager(this.kanbanManager);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
this.peerManager.on("peerConnected", (nodeId) => {
|
|
358
|
+
this.kanbanManager?.initPeerSync(nodeId);
|
|
359
|
+
});
|
|
360
|
+
this.peerManager.on("peerDisconnected", (nodeId) => {
|
|
361
|
+
this.kanbanManager?.removePeerSync(nodeId);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
230
365
|
// Sentinel: detached subprocess for diagnostics when gateway dies.
|
|
231
366
|
// Default on; starts when not explicitly disabled AND (has outbound peers OR gateway is a listener for port takeover)
|
|
232
367
|
const sentinelEnabled = (this.config.sentinel?.enabled ?? true) && (this.config.peers.length > 0 || this.config.listen);
|
|
@@ -299,15 +434,21 @@ export class ClusterRuntime {
|
|
|
299
434
|
}
|
|
300
435
|
|
|
301
436
|
private async stopInternal() {
|
|
437
|
+
this.automationManager?.stop();
|
|
302
438
|
await this.healthTracker.stop();
|
|
439
|
+
await this.kanbanManager?.stop();
|
|
303
440
|
await this.knowledgeSync?.stop();
|
|
304
441
|
this.syncCleanup();
|
|
305
442
|
await this.peerManager.stop();
|
|
443
|
+
// Close store after all subsystems are done
|
|
444
|
+
if (this.storeCleanupTimer) { clearInterval(this.storeCleanupTimer); this.storeCleanupTimer = null; }
|
|
445
|
+
this.logReplicator?.destroy();
|
|
446
|
+
this.store?.close();
|
|
306
447
|
}
|
|
307
448
|
|
|
308
449
|
/** Synchronous cleanup that never blocks. */
|
|
309
450
|
private syncCleanup() {
|
|
310
|
-
this.
|
|
451
|
+
this.apiHandler?.destroy();
|
|
311
452
|
this.handoffManager.destroy();
|
|
312
453
|
this.acpProxy?.destroy();
|
|
313
454
|
this.terminalManager.destroy();
|
|
@@ -496,7 +637,8 @@ export class ClusterRuntime {
|
|
|
496
637
|
});
|
|
497
638
|
break;
|
|
498
639
|
case "health_sync":
|
|
499
|
-
|
|
640
|
+
// Legacy: Automerge health sync frames are ignored.
|
|
641
|
+
// Health events now sync via log_sync (LogReplicator).
|
|
500
642
|
break;
|
|
501
643
|
case "availability_req": {
|
|
502
644
|
const af = frame as AvailabilityRequest;
|
|
@@ -720,6 +862,20 @@ export class ClusterRuntime {
|
|
|
720
862
|
case "file_transfer_complete":
|
|
721
863
|
this.fileTransferManager?.handleComplete(frame as FileTransferComplete);
|
|
722
864
|
break;
|
|
865
|
+
case "log_sync":
|
|
866
|
+
this.logReplicator?.handleSyncMessage(frame as LogSyncFrame);
|
|
867
|
+
break;
|
|
868
|
+
case "kanban_sync":
|
|
869
|
+
this.kanbanManager?.handleSyncMessage(frame as KanbanSyncFrame);
|
|
870
|
+
break;
|
|
871
|
+
case "kanban_notify":
|
|
872
|
+
// Kanban notifications are consumed by mobile/web clients.
|
|
873
|
+
// Push to satellite events if web handler is active.
|
|
874
|
+
if (this.apiHandler) {
|
|
875
|
+
const kf = frame as KanbanNotifyFrame;
|
|
876
|
+
this.apiHandler.pushKanbanEvent(kf.payload);
|
|
877
|
+
}
|
|
878
|
+
break;
|
|
723
879
|
}
|
|
724
880
|
}
|
|
725
881
|
|
package/src/compat.ts
CHANGED
|
@@ -70,6 +70,109 @@ export function spawnProcess(
|
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// ── SQLite support ────────────────────────────────────────────────
|
|
74
|
+
//
|
|
75
|
+
// Normalized interface over bun:sqlite (Bun runtime) and better-sqlite3 (Node.js).
|
|
76
|
+
// Tests run with `bun test` → bun:sqlite; production runs inside OpenClaw Node.js → better-sqlite3.
|
|
77
|
+
|
|
78
|
+
export interface SqliteDatabase {
|
|
79
|
+
exec(sql: string): void;
|
|
80
|
+
prepare(sql: string): SqliteStatement;
|
|
81
|
+
close(): void;
|
|
82
|
+
transaction<T extends (...args: unknown[]) => unknown>(fn: T): T;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface SqliteStatement {
|
|
86
|
+
run(...params: unknown[]): SqliteRunResult;
|
|
87
|
+
all(...params: unknown[]): unknown[];
|
|
88
|
+
get(...params: unknown[]): unknown;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface SqliteRunResult {
|
|
92
|
+
changes: number;
|
|
93
|
+
lastInsertRowid: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function openDatabase(dbPath: string): SqliteDatabase {
|
|
97
|
+
// Bun runtime: use bun:sqlite (built-in, no native addon needed)
|
|
98
|
+
if (typeof globalThis.Bun !== "undefined") {
|
|
99
|
+
return openBunSqlite(dbPath);
|
|
100
|
+
}
|
|
101
|
+
// Node.js runtime: use better-sqlite3
|
|
102
|
+
return openBetterSqlite3(dbPath);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function openBunSqlite(dbPath: string): SqliteDatabase {
|
|
106
|
+
// Dynamic require to avoid bundler issues in Node.js
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
108
|
+
const { Database } = require("bun:sqlite");
|
|
109
|
+
const db = new Database(dbPath);
|
|
110
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
111
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
112
|
+
|
|
113
|
+
const changesStmt = db.prepare("SELECT changes() AS v");
|
|
114
|
+
const rowidStmt = db.prepare("SELECT last_insert_rowid() AS v");
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
exec: (sql: string) => db.exec(sql),
|
|
118
|
+
prepare: (sql: string) => {
|
|
119
|
+
const stmt = db.prepare(sql);
|
|
120
|
+
return {
|
|
121
|
+
run(...params: unknown[]): SqliteRunResult {
|
|
122
|
+
stmt.run(...params);
|
|
123
|
+
const changes: number = Number((changesStmt.get() as Record<string, unknown>).v);
|
|
124
|
+
const lastInsertRowid: number = Number((rowidStmt.get() as Record<string, unknown>).v);
|
|
125
|
+
return { changes, lastInsertRowid };
|
|
126
|
+
},
|
|
127
|
+
all(...params: unknown[]): unknown[] {
|
|
128
|
+
return stmt.all(...params);
|
|
129
|
+
},
|
|
130
|
+
get(...params: unknown[]): unknown {
|
|
131
|
+
return stmt.get(...params);
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
close: () => db.close(),
|
|
136
|
+
transaction: <T extends (...args: unknown[]) => unknown>(fn: T): T => db.transaction(fn),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function openBetterSqlite3(dbPath: string): SqliteDatabase {
|
|
141
|
+
const req = createRequire(import.meta.url);
|
|
142
|
+
let BetterSqlite3: unknown;
|
|
143
|
+
try {
|
|
144
|
+
BetterSqlite3 = req("better-sqlite3");
|
|
145
|
+
} catch {
|
|
146
|
+
throw new Error("better-sqlite3 is not available — install it with: npm install better-sqlite3");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const db = new (BetterSqlite3 as new (path: string) => Record<string, unknown>)(dbPath) as Record<string, (...args: unknown[]) => unknown>;
|
|
150
|
+
(db.pragma as (s: string) => void)("journal_mode = WAL");
|
|
151
|
+
(db.pragma as (s: string) => void)("synchronous = NORMAL");
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
exec: (sql: string) => (db.exec as (s: string) => void)(sql),
|
|
155
|
+
prepare: (sql: string) => {
|
|
156
|
+
const stmt = (db.prepare as (s: string) => Record<string, (...args: unknown[]) => unknown>)(sql);
|
|
157
|
+
return {
|
|
158
|
+
run(...params: unknown[]): SqliteRunResult {
|
|
159
|
+
const result = stmt.run(...params) as { changes: number; lastInsertRowid: number | bigint };
|
|
160
|
+
return { changes: result.changes, lastInsertRowid: Number(result.lastInsertRowid) };
|
|
161
|
+
},
|
|
162
|
+
all(...params: unknown[]): unknown[] {
|
|
163
|
+
return stmt.all(...params) as unknown[];
|
|
164
|
+
},
|
|
165
|
+
get(...params: unknown[]): unknown {
|
|
166
|
+
return stmt.get(...params);
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
close: () => (db.close as () => void)(),
|
|
171
|
+
transaction: <T extends (...args: unknown[]) => unknown>(fn: T): T =>
|
|
172
|
+
(db.transaction as (fn: T) => T)(fn),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
73
176
|
// ── PTY support (optional, requires node-pty) ────────────────────
|
|
74
177
|
|
|
75
178
|
export interface PtyHandle {
|
package/src/config.ts
CHANGED
|
@@ -49,12 +49,15 @@ const ModelInfoSchema = z.object({
|
|
|
49
49
|
...ModelParamsSchema,
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
const
|
|
52
|
+
const PeerConfigObjectSchema = z.object({
|
|
53
53
|
nodeId: z.string(),
|
|
54
54
|
/** Single URL or array of URLs for multi-channel connections. */
|
|
55
55
|
url: z.union([z.string(), z.array(z.string()).min(1)]),
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
/** Peer config: full object or URL string shorthand (nodeId derived from handshake). */
|
|
59
|
+
const PeerConfigSchema = z.union([z.string(), PeerConfigObjectSchema]);
|
|
60
|
+
|
|
58
61
|
const ToolProxyConfigSchema = z.object({
|
|
59
62
|
enabled: z.boolean().default(false),
|
|
60
63
|
allow: z.array(z.string()).default([]),
|
|
@@ -92,11 +95,6 @@ const SentinelConfigSchema = z.object({
|
|
|
92
95
|
listenHost: z.string().optional(),
|
|
93
96
|
}).optional();
|
|
94
97
|
|
|
95
|
-
const WebConfigSchema = z.object({
|
|
96
|
-
enabled: z.boolean().default(false),
|
|
97
|
-
token: z.string().min(8, "web token must be at least 8 characters"),
|
|
98
|
-
}).optional();
|
|
99
|
-
|
|
100
98
|
const AcpAgentInfoSchema = z.object({
|
|
101
99
|
id: z.string(),
|
|
102
100
|
description: z.string().default(""),
|
|
@@ -143,6 +141,16 @@ const FileTransferConfigSchema = z.object({
|
|
|
143
141
|
allowedPaths: z.array(z.string()).default([]), // empty = no restriction
|
|
144
142
|
}).optional();
|
|
145
143
|
|
|
144
|
+
const KanbanConfigSchema = z.object({
|
|
145
|
+
enabled: z.boolean().default(false),
|
|
146
|
+
/** Card ID prefix (e.g. "CM" → CM-1, CM-2). */
|
|
147
|
+
prefix: z.string().default("CM"),
|
|
148
|
+
/** Allow agents to auto-claim matching cards. */
|
|
149
|
+
autoAssign: z.boolean().default(true),
|
|
150
|
+
/** Archive cards older than this (ms). Default: 30 days. */
|
|
151
|
+
archiveAfterMs: z.number().default(30 * 24 * 60 * 60 * 1000),
|
|
152
|
+
}).optional();
|
|
153
|
+
|
|
146
154
|
const AcpConfigSchema = z.object({
|
|
147
155
|
enabled: z.boolean().default(false),
|
|
148
156
|
/** ACP agents available on this node. Advertised to peers via capabilities. */
|
|
@@ -164,30 +172,38 @@ const RawClawMatrixConfigSchema = z.object({
|
|
|
164
172
|
listenHost: z.string().default("0.0.0.0"),
|
|
165
173
|
listenPort: z.number().int().min(0).max(65535).default(0),
|
|
166
174
|
peers: z.array(PeerConfigSchema).default([]),
|
|
175
|
+
/** Agents provided by this node. Auto-discovered from OpenClaw if omitted. */
|
|
167
176
|
agents: z.array(AgentInfoSchema).default([]),
|
|
168
177
|
models: z.array(ModelInfoSchema).default([]),
|
|
169
178
|
proxyModels: z.array(ProxyModelGroupSchema).default([]),
|
|
170
179
|
tags: z.array(z.string()).default([]),
|
|
171
180
|
proxyPort: z.number().int().min(0).max(65535).default(0),
|
|
172
|
-
|
|
181
|
+
/** Tool proxy: boolean shorthand (true = enable all, false = disable) or full object. */
|
|
182
|
+
toolProxy: z.union([z.boolean(), ToolProxyConfigSchema]).optional(),
|
|
173
183
|
handoffTimeout: z.number().positive().default(600_000),
|
|
174
184
|
modelTimeout: z.number().positive().default(120_000),
|
|
185
|
+
modelConcurrency: z.number().positive().default(5),
|
|
175
186
|
toolTimeout: z.number().positive().default(30_000),
|
|
176
187
|
/** Grace period (ms) before broadcasting peer_leave after disconnect.
|
|
177
188
|
* Allows brief reconnections (WiFi/cellular handoff) to be invisible to the mesh. */
|
|
178
189
|
disconnectGrace: z.number().nonnegative().default(30_000),
|
|
179
190
|
sentinel: SentinelConfigSchema,
|
|
180
|
-
web: WebConfigSchema,
|
|
181
191
|
knowledge: KnowledgeConfigSchema,
|
|
182
192
|
terminal: TerminalConfigSchema,
|
|
193
|
+
kanban: KanbanConfigSchema,
|
|
183
194
|
acp: AcpConfigSchema,
|
|
184
195
|
fileTransfer: FileTransferConfigSchema,
|
|
196
|
+
store: z.object({
|
|
197
|
+
/** Retention period in days for replicated and local event data (default: 90). */
|
|
198
|
+
retentionDays: z.number().default(90),
|
|
199
|
+
}).optional(),
|
|
185
200
|
peerApproval: z.union([
|
|
186
201
|
z.boolean(), // true = required mode, false = disabled
|
|
202
|
+
z.enum(["required", "notify", "none"]), // string shorthand
|
|
187
203
|
PeerApprovalConfigSchema,
|
|
188
204
|
]).default(true),
|
|
189
205
|
e2ee: z.boolean().default(true),
|
|
190
|
-
compression: z.boolean().default(
|
|
206
|
+
compression: z.boolean().default(true),
|
|
191
207
|
});
|
|
192
208
|
|
|
193
209
|
/** Flat proxy model after group expansion (used internally). */
|
|
@@ -214,15 +230,29 @@ export interface PeerApprovalConfig {
|
|
|
214
230
|
notifyTargets: Array<{ channel: string; to: string; accountId?: string; threadId?: string | number }>;
|
|
215
231
|
}
|
|
216
232
|
|
|
217
|
-
export type ClawMatrixConfig = Omit<z.infer<typeof RawClawMatrixConfigSchema>, "proxyModels" | "peerApproval"> & {
|
|
233
|
+
export type ClawMatrixConfig = Omit<z.infer<typeof RawClawMatrixConfigSchema>, "proxyModels" | "peerApproval" | "toolProxy" | "peers"> & {
|
|
234
|
+
peers: PeerConfig[];
|
|
218
235
|
proxyModels: ProxyModel[];
|
|
219
236
|
peerApproval: PeerApprovalConfig;
|
|
237
|
+
toolProxy?: ToolProxyConfig;
|
|
220
238
|
};
|
|
221
239
|
|
|
222
240
|
export { RawClawMatrixConfigSchema as ClawMatrixConfigSchema };
|
|
223
|
-
|
|
241
|
+
/** Normalized peer config (always object form). */
|
|
242
|
+
export type PeerConfig = z.infer<typeof PeerConfigObjectSchema>;
|
|
224
243
|
export type ToolProxyConfig = z.infer<typeof ToolProxyConfigSchema>;
|
|
225
244
|
|
|
245
|
+
/** Auto-generate a temporary nodeId from a URL for pre-handshake use.
|
|
246
|
+
* After handshake the real nodeId replaces this. */
|
|
247
|
+
function nodeIdFromUrl(url: string): string {
|
|
248
|
+
try {
|
|
249
|
+
const u = new URL(url);
|
|
250
|
+
return `_${u.hostname}:${u.port || (u.protocol === "wss:" ? "443" : "80")}`;
|
|
251
|
+
} catch {
|
|
252
|
+
return `_${url}`;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
226
256
|
/** Parse and flatten grouped proxyModels into flat array.
|
|
227
257
|
* Uses passthrough() to ignore unknown fields and safeParse to avoid crashing. */
|
|
228
258
|
export function parseConfig(raw: unknown): ClawMatrixConfig {
|
|
@@ -233,6 +263,14 @@ export function parseConfig(raw: unknown): ClawMatrixConfig {
|
|
|
233
263
|
}
|
|
234
264
|
const parsed = result.data;
|
|
235
265
|
|
|
266
|
+
// Normalize peers: string → { nodeId: derived, url: string }
|
|
267
|
+
const peers: PeerConfig[] = parsed.peers.map((p) => {
|
|
268
|
+
if (typeof p === "string") {
|
|
269
|
+
return { nodeId: nodeIdFromUrl(p), url: p };
|
|
270
|
+
}
|
|
271
|
+
return p;
|
|
272
|
+
});
|
|
273
|
+
|
|
236
274
|
// Flatten proxy model groups
|
|
237
275
|
const proxyModels: ProxyModel[] = [];
|
|
238
276
|
for (const group of parsed.proxyModels) {
|
|
@@ -253,17 +291,34 @@ export function parseConfig(raw: unknown): ClawMatrixConfig {
|
|
|
253
291
|
}
|
|
254
292
|
}
|
|
255
293
|
|
|
256
|
-
// Normalize
|
|
294
|
+
// Normalize toolProxy: boolean → object
|
|
295
|
+
let toolProxy: ToolProxyConfig | undefined;
|
|
296
|
+
const rawToolProxy = parsed.toolProxy;
|
|
297
|
+
if (typeof rawToolProxy === "boolean") {
|
|
298
|
+
toolProxy = rawToolProxy
|
|
299
|
+
? { enabled: true, allow: ["*"], deny: [], maxOutputBytes: 1_048_576 }
|
|
300
|
+
: { enabled: false, allow: [], deny: [], maxOutputBytes: 1_048_576 };
|
|
301
|
+
} else {
|
|
302
|
+
toolProxy = rawToolProxy;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Normalize peerApproval: boolean | string → object
|
|
257
306
|
let peerApproval: PeerApprovalConfig;
|
|
258
307
|
const rawApproval = parsed.peerApproval;
|
|
308
|
+
const defaultApproval: Omit<PeerApprovalConfig, "enabled" | "mode"> = {
|
|
309
|
+
timeout: 1_200_000,
|
|
310
|
+
allowList: [],
|
|
311
|
+
persistPath: "approved-peers.json",
|
|
312
|
+
notifyTargets: [],
|
|
313
|
+
};
|
|
259
314
|
if (typeof rawApproval === "boolean") {
|
|
315
|
+
peerApproval = { ...defaultApproval, enabled: rawApproval, mode: "required" };
|
|
316
|
+
} else if (typeof rawApproval === "string") {
|
|
317
|
+
// "required" | "notify" | "none"
|
|
260
318
|
peerApproval = {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
allowList: [],
|
|
265
|
-
persistPath: "approved-peers.json",
|
|
266
|
-
notifyTargets: [],
|
|
319
|
+
...defaultApproval,
|
|
320
|
+
enabled: rawApproval !== "none",
|
|
321
|
+
mode: rawApproval === "none" ? "notify" : rawApproval,
|
|
267
322
|
};
|
|
268
323
|
} else if (rawApproval) {
|
|
269
324
|
peerApproval = {
|
|
@@ -275,15 +330,8 @@ export function parseConfig(raw: unknown): ClawMatrixConfig {
|
|
|
275
330
|
notifyTargets: rawApproval.notifyTargets,
|
|
276
331
|
};
|
|
277
332
|
} else {
|
|
278
|
-
peerApproval = {
|
|
279
|
-
enabled: false,
|
|
280
|
-
mode: "notify",
|
|
281
|
-
timeout: 1_200_000,
|
|
282
|
-
allowList: [],
|
|
283
|
-
persistPath: "approved-peers.json",
|
|
284
|
-
notifyTargets: [],
|
|
285
|
-
};
|
|
333
|
+
peerApproval = { ...defaultApproval, enabled: false, mode: "notify" };
|
|
286
334
|
}
|
|
287
335
|
|
|
288
|
-
return { ...parsed, proxyModels, peerApproval };
|
|
336
|
+
return { ...parsed, peers, proxyModels, toolProxy, peerApproval };
|
|
289
337
|
}
|