clawmatrix 0.1.0

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 ADDED
@@ -0,0 +1,15 @@
1
+ # clawmatrix
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install
7
+ ```
8
+
9
+ To run:
10
+
11
+ ```bash
12
+ bun run index.ts
13
+ ```
14
+
15
+ This project was created using `bun init` in bun v1.3.6. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
package/llms.txt ADDED
@@ -0,0 +1,191 @@
1
+ # ClawMatrix — OpenClaw Mesh Cluster Plugin
2
+
3
+ > Decentralized mesh network plugin for OpenClaw. Multiple Gateways install the same plugin, form a peer-to-peer mesh via WebSocket, and share agents, models, and tools across nodes.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ openclaw plugins install clawmatrix
9
+ ```
10
+
11
+ Then restart the Gateway:
12
+
13
+ ```bash
14
+ openclaw gateway restart
15
+ ```
16
+
17
+ ## What it does
18
+
19
+ - **Model Proxy** — Use LLMs hosted on remote nodes as if they were local. A local HTTP proxy bridges cluster WebSocket to OpenAI-compatible API.
20
+ - **Task Handoff** — Delegate complex tasks to agents running on other nodes (e.g. hand off to an internal "coder" agent that has repo access).
21
+ - **Tool Proxy** — Execute single tool calls (shell commands, file read/write, directory listing) on remote nodes without delegating the entire task.
22
+ - **Gossip Discovery** — Nodes discover each other via gossip protocol; no central registry needed.
23
+ - **Auto Failover** — If a node goes down, requests route to backup nodes automatically.
24
+
25
+ ## Configuration
26
+
27
+ Add to `plugins.entries.clawmatrix.config` in your `openclaw.json`:
28
+
29
+ ### Public node (relay + own agents)
30
+
31
+ ```json
32
+ {
33
+ "nodeId": "cloud-01",
34
+ "secret": "your-shared-secret",
35
+ "listen": true,
36
+ "listenPort": 19000,
37
+ "peers": [],
38
+ "agents": [
39
+ { "id": "reviewer", "description": "Reviews code and PRs", "tags": ["review"] }
40
+ ],
41
+ "models": [],
42
+ "tags": ["cloud"]
43
+ }
44
+ ```
45
+
46
+ ### Internal node (has models + code repos)
47
+
48
+ ```json
49
+ {
50
+ "nodeId": "office-01",
51
+ "secret": "your-shared-secret",
52
+ "listen": false,
53
+ "peers": [
54
+ { "nodeId": "cloud-01", "url": "wss://cloud-01.example.com:19000" }
55
+ ],
56
+ "agents": [
57
+ { "id": "coder", "description": "Writes and debugs code, has repo access", "tags": ["coding"] }
58
+ ],
59
+ "models": [
60
+ { "id": "claude-sonnet", "provider": "anthropic" },
61
+ { "id": "deepseek-coder", "provider": "ollama" }
62
+ ],
63
+ "tags": ["office", "gpu"],
64
+ "toolProxy": {
65
+ "enabled": true,
66
+ "allow": ["exec", "read", "ls"],
67
+ "deny": ["write"],
68
+ "allowPaths": ["/repo", "/data"],
69
+ "denyPaths": ["/etc", "/root"]
70
+ }
71
+ }
72
+ ```
73
+
74
+ ### Home node (lightweight, borrows cluster resources)
75
+
76
+ ```json
77
+ {
78
+ "nodeId": "home-01",
79
+ "secret": "your-shared-secret",
80
+ "listen": false,
81
+ "peers": [
82
+ { "nodeId": "cloud-01", "url": "wss://cloud-01.example.com:19000" }
83
+ ],
84
+ "agents": [
85
+ { "id": "assistant", "description": "Personal assistant", "tags": ["general"] }
86
+ ],
87
+ "models": [],
88
+ "tags": ["home"]
89
+ }
90
+ ```
91
+
92
+ To use cluster models, set your agent's model to a cluster-proxied model:
93
+
94
+ ```json
95
+ {
96
+ "agents": {
97
+ "defaults": {
98
+ "model": "clawmatrix/claude-sonnet"
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ ## Config reference
105
+
106
+ | Field | Type | Default | Description |
107
+ |-------|------|---------|-------------|
108
+ | `nodeId` | string | *required* | Unique identifier for this node |
109
+ | `secret` | string | *required* | Shared HMAC secret for cluster authentication |
110
+ | `listen` | boolean | `false` | Accept inbound WebSocket connections |
111
+ | `listenHost` | string | `"0.0.0.0"` | Bind address for WebSocket listener |
112
+ | `listenPort` | number | `19000` | Port for inbound WebSocket connections |
113
+ | `peers` | array | `[]` | List of `{ nodeId, url }` peers to connect to |
114
+ | `agents` | array | `[]` | Agents this node provides: `{ id, description, tags }` |
115
+ | `models` | array | `[]` | Models this node shares: `{ id, provider, description? }` |
116
+ | `tags` | array | `[]` | Free-form tags for capability routing |
117
+ | `proxyPort` | number | `19001` | Local HTTP proxy port for model requests |
118
+ | `toolProxy` | object | — | Tool proxy settings (see below) |
119
+
120
+ ### toolProxy
121
+
122
+ | Field | Type | Default | Description |
123
+ |-------|------|---------|-------------|
124
+ | `enabled` | boolean | `false` | Allow remote tool execution on this node |
125
+ | `allow` | array | `[]` | Allowed tools: `"exec"`, `"read"`, `"write"`, `"ls"` |
126
+ | `deny` | array | `[]` | Denied tools (takes precedence over allow) |
127
+ | `allowPaths` | array | `[]` | Allowed path prefixes for file operations |
128
+ | `denyPaths` | array | `[]` | Denied path prefixes |
129
+ | `execAllowlist` | array | `[]` | If non-empty, only these commands are allowed |
130
+ | `maxOutputBytes` | number | `1048576` | Max output size per tool response (1 MB) |
131
+
132
+ ## Agent tools
133
+
134
+ ClawMatrix registers 7 tools available to agents:
135
+
136
+ ### cluster_peers
137
+ List all reachable peers, their agents, and available models.
138
+
139
+ ### cluster_handoff
140
+ Hand off a task to another agent in the cluster and wait for the result.
141
+ - `target`: Agent ID or `"tags:<tag>"` expression
142
+ - `task`: Task description
143
+ - `context`: Optional additional context
144
+
145
+ ### cluster_send
146
+ Send a one-way message to another agent (injected into their session).
147
+ - `target`: Agent ID or `"tags:<tag>"` expression
148
+ - `message`: Message content
149
+
150
+ ### cluster_exec
151
+ Execute a shell command on a remote node.
152
+ - `node`: Target nodeId or `"tags:<tag>"`
153
+ - `command`: Shell command
154
+ - `cwd`: Optional working directory
155
+ - `timeout`: Optional timeout in ms (default 30000)
156
+
157
+ ### cluster_read
158
+ Read a file from a remote node.
159
+ - `node`: Target nodeId or `"tags:<tag>"`
160
+ - `path`: Absolute file path
161
+
162
+ ### cluster_write
163
+ Write content to a file on a remote node.
164
+ - `node`: Target nodeId or `"tags:<tag>"`
165
+ - `path`: Absolute file path
166
+ - `content`: File content
167
+
168
+ ### cluster_ls
169
+ List directory contents on a remote node.
170
+ - `node`: Target nodeId or `"tags:<tag>"`
171
+ - `path`: Directory path
172
+
173
+ ## Verify cluster status
174
+
175
+ ```bash
176
+ openclaw clawmatrix status
177
+ ```
178
+
179
+ ## Architecture
180
+
181
+ - Nodes form a decentralized mesh over WebSocket (no leader, no consensus protocol)
182
+ - Authentication via HMAC-SHA256 challenge-response (secret never sent in plaintext)
183
+ - Messages relay through intermediate nodes with TTL-based loop prevention
184
+ - Heartbeat every 15s; 3 missed pings = disconnect + peer_leave broadcast
185
+ - Reconnection with exponential backoff (1s to 60s max)
186
+ - Production deployments should use `wss://` (TLS)
187
+
188
+ ## Source
189
+
190
+ - GitHub: https://github.com/nicepkg/clawmatrix
191
+ - npm: https://www.npmjs.com/package/clawmatrix
@@ -0,0 +1,76 @@
1
+ {
2
+ "id": "clawmatrix",
3
+ "name": "ClawMatrix",
4
+ "description": "Decentralized mesh cluster for inter-gateway communication, model proxy, task handoff, and tool proxy.",
5
+ "providers": ["clawmatrix"],
6
+ "configSchema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "nodeId": { "type": "string" },
10
+ "secret": { "type": "string" },
11
+ "listen": { "type": "boolean", "default": false },
12
+ "listenHost": { "type": "string", "default": "0.0.0.0" },
13
+ "listenPort": { "type": "number", "default": 19000 },
14
+ "peers": {
15
+ "type": "array",
16
+ "items": {
17
+ "type": "object",
18
+ "properties": {
19
+ "nodeId": { "type": "string" },
20
+ "url": { "type": "string" }
21
+ },
22
+ "required": ["nodeId", "url"]
23
+ },
24
+ "default": []
25
+ },
26
+ "agents": {
27
+ "type": "array",
28
+ "items": {
29
+ "type": "object",
30
+ "properties": {
31
+ "id": { "type": "string" },
32
+ "description": { "type": "string" },
33
+ "tags": { "type": "array", "items": { "type": "string" }, "default": [] }
34
+ },
35
+ "required": ["id", "description"]
36
+ },
37
+ "default": []
38
+ },
39
+ "models": {
40
+ "type": "array",
41
+ "items": {
42
+ "type": "object",
43
+ "properties": {
44
+ "id": { "type": "string" },
45
+ "provider": { "type": "string" },
46
+ "description": { "type": "string" }
47
+ },
48
+ "required": ["id", "provider"]
49
+ },
50
+ "default": []
51
+ },
52
+ "tags": { "type": "array", "items": { "type": "string" }, "default": [] },
53
+ "proxyPort": { "type": "number", "default": 19001 },
54
+ "toolProxy": {
55
+ "type": "object",
56
+ "properties": {
57
+ "enabled": { "type": "boolean", "default": false },
58
+ "allow": { "type": "array", "items": { "type": "string" }, "default": [] },
59
+ "deny": { "type": "array", "items": { "type": "string" }, "default": [] },
60
+ "allowPaths": { "type": "array", "items": { "type": "string" }, "default": [] },
61
+ "denyPaths": { "type": "array", "items": { "type": "string" }, "default": [] },
62
+ "execAllowlist": { "type": "array", "items": { "type": "string" }, "default": [] },
63
+ "maxOutputBytes": { "type": "number", "default": 1048576 }
64
+ }
65
+ }
66
+ },
67
+ "required": ["nodeId", "secret"]
68
+ },
69
+ "uiHints": {
70
+ "secret": { "sensitive": true, "label": "Cluster Secret", "help": "Shared secret for HMAC authentication between nodes" },
71
+ "nodeId": { "label": "Node ID", "help": "Unique identifier for this node in the cluster" },
72
+ "listenHost": { "label": "Listen Host", "help": "Bind address (use 127.0.0.1 with Cloudflare Tunnel)", "advanced": true },
73
+ "listenPort": { "label": "Listen Port", "help": "Port for inbound WebSocket connections", "advanced": true },
74
+ "proxyPort": { "label": "Proxy Port", "help": "Port for local model proxy HTTP server", "advanced": true }
75
+ }
76
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "clawmatrix",
3
+ "version": "0.1.0",
4
+ "description": "Decentralized mesh cluster plugin for OpenClaw — inter-gateway communication, model proxy, task handoff, and tool proxy.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "openclaw",
9
+ "openclaw-plugin",
10
+ "mesh",
11
+ "cluster",
12
+ "gateway",
13
+ "decentralized",
14
+ "model-proxy",
15
+ "handoff"
16
+ ],
17
+ "files": [
18
+ "src/",
19
+ "!src/**/*.test.ts",
20
+ "openclaw.plugin.json",
21
+ "llms.txt",
22
+ "README.md"
23
+ ],
24
+ "openclaw": {
25
+ "extensions": [
26
+ "./src/index.ts"
27
+ ]
28
+ },
29
+ "scripts": {
30
+ "test": "bun test",
31
+ "prepublishOnly": "bun test"
32
+ },
33
+ "dependencies": {
34
+ "zod": "^4.3.6"
35
+ },
36
+ "devDependencies": {
37
+ "@types/bun": "latest",
38
+ "openclaw": "^2026.3.2"
39
+ },
40
+ "peerDependencies": {
41
+ "typescript": "^5"
42
+ }
43
+ }
package/src/auth.ts ADDED
@@ -0,0 +1,38 @@
1
+ export function generateNonce(): string {
2
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
3
+ return Buffer.from(bytes).toString("hex");
4
+ }
5
+
6
+ export async function computeHmac(
7
+ nonce: string,
8
+ secret: string,
9
+ ): Promise<string> {
10
+ const key = await crypto.subtle.importKey(
11
+ "raw",
12
+ new TextEncoder().encode(secret),
13
+ { name: "HMAC", hash: "SHA-256" },
14
+ false,
15
+ ["sign"],
16
+ );
17
+ const sig = await crypto.subtle.sign(
18
+ "HMAC",
19
+ key,
20
+ new TextEncoder().encode(nonce),
21
+ );
22
+ return Buffer.from(sig).toString("hex");
23
+ }
24
+
25
+ export async function verifyHmac(
26
+ nonce: string,
27
+ secret: string,
28
+ sig: string,
29
+ ): Promise<boolean> {
30
+ 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;
38
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,84 @@
1
+ import type { Command } from "commander";
2
+ import { getClusterRuntime } from "./cluster-service.ts";
3
+
4
+ export const registerClusterCli = ({ program }: { program: Command }) => {
5
+ const cmd = program.command("clawmatrix").description("ClawMatrix cluster management");
6
+
7
+ cmd
8
+ .command("status")
9
+ .description("Show cluster topology and peer status")
10
+ .action(() => {
11
+ let runtime: ReturnType<typeof getClusterRuntime>;
12
+ try {
13
+ runtime = getClusterRuntime();
14
+ } catch {
15
+ console.log("ClawMatrix service not running.");
16
+ return;
17
+ }
18
+
19
+ const config = runtime.config;
20
+ const peers = runtime.peerManager.router.getAllPeers();
21
+
22
+ // Header
23
+ console.log(`\nNode: ${config.nodeId}`);
24
+ console.log(
25
+ `Listen: ${config.listen ? `port ${config.listenPort}` : "no"}`,
26
+ );
27
+ console.log(`Model proxy: port ${config.proxyPort}`);
28
+ console.log(`Agents: [${config.agents.map((a) => a.id).join(", ")}]`);
29
+ console.log(
30
+ `Models: [${config.models.map((m) => m.id).join(", ")}]`,
31
+ );
32
+ console.log("");
33
+
34
+ if (peers.length === 0) {
35
+ console.log("No peers connected.");
36
+ return;
37
+ }
38
+
39
+ // Peer table
40
+ const header = padRow("NODE", "AGENTS", "MODELS", "STATUS", "LATENCY");
41
+ console.log(header);
42
+ console.log("-".repeat(header.length));
43
+
44
+ for (const peer of peers) {
45
+ const agents = peer.agents.map((a) => a.id).join(", ") || "-";
46
+ const models = peer.models.map((m) => m.id).join(", ") || "-";
47
+ const status = peer.connection?.isOpen ? "connected" : "unreachable";
48
+ const latency = peer.latencyMs > 0 ? `${peer.latencyMs}ms` : "-";
49
+ console.log(
50
+ padRow(peer.nodeId, `[${agents}]`, `[${models}]`, status, latency),
51
+ );
52
+ }
53
+ console.log("");
54
+ });
55
+
56
+ cmd
57
+ .command("peers")
58
+ .description("List known peers (JSON)")
59
+ .action(() => {
60
+ let runtime: ReturnType<typeof getClusterRuntime>;
61
+ try {
62
+ runtime = getClusterRuntime();
63
+ } catch {
64
+ console.log("[]");
65
+ return;
66
+ }
67
+
68
+ const peers = runtime.peerManager.router.getAllPeers().map((p) => ({
69
+ nodeId: p.nodeId,
70
+ agents: p.agents,
71
+ models: p.models,
72
+ tags: p.tags,
73
+ connected: !!p.connection?.isOpen,
74
+ reachableVia: p.reachableVia,
75
+ latencyMs: p.latencyMs,
76
+ }));
77
+ console.log(JSON.stringify(peers, null, 2));
78
+ });
79
+ };
80
+
81
+ function padRow(...cols: string[]): string {
82
+ const widths = [16, 24, 30, 14, 8];
83
+ return cols.map((c, i) => c.padEnd(widths[i] ?? 12)).join(" ");
84
+ }
@@ -0,0 +1,160 @@
1
+ import type {
2
+ OpenClawPluginService,
3
+ OpenClawPluginServiceContext,
4
+ PluginLogger,
5
+ } from "openclaw/plugin-sdk";
6
+ import type { ClawMatrixConfig } from "./config.ts";
7
+ import { PeerManager } from "./peer-manager.ts";
8
+ import { HandoffManager } from "./handoff.ts";
9
+ import { ModelProxy } from "./model-proxy.ts";
10
+ import { ToolProxy } from "./tool-proxy.ts";
11
+ import type {
12
+ AnyClusterFrame,
13
+ HandoffRequest,
14
+ HandoffResponse,
15
+ ModelRequest,
16
+ ModelResponse,
17
+ ModelStreamChunk,
18
+ SendMessage,
19
+ ToolProxyRequest,
20
+ ToolProxyResponse,
21
+ } from "./types.ts";
22
+
23
+ /** Singleton cluster state shared across plugin components. */
24
+ export class ClusterRuntime {
25
+ readonly config: ClawMatrixConfig;
26
+ readonly peerManager: PeerManager;
27
+ readonly handoffManager: HandoffManager;
28
+ readonly modelProxy: ModelProxy;
29
+ readonly toolProxy: ToolProxy;
30
+ private logger: PluginLogger;
31
+
32
+ constructor(config: ClawMatrixConfig, logger: PluginLogger) {
33
+ this.config = config;
34
+ this.logger = logger;
35
+ this.peerManager = new PeerManager(config);
36
+ this.handoffManager = new HandoffManager(config, this.peerManager);
37
+ this.modelProxy = new ModelProxy(config, this.peerManager);
38
+ this.toolProxy = new ToolProxy(config, this.peerManager);
39
+ }
40
+
41
+ start() {
42
+ // Wire up frame dispatch
43
+ this.peerManager.on("frame", (frame, conn) => {
44
+ this.dispatchFrame(frame);
45
+ });
46
+
47
+ this.peerManager.on("peerConnected", (nodeId) => {
48
+ this.logger.info(`[clawmatrix] Peer connected: ${nodeId}`);
49
+ });
50
+
51
+ this.peerManager.on("peerDisconnected", (nodeId) => {
52
+ this.logger.info(`[clawmatrix] Peer disconnected: ${nodeId}`);
53
+ });
54
+
55
+ // Start subsystems
56
+ this.peerManager.start();
57
+ this.modelProxy.start();
58
+
59
+ this.logger.info(
60
+ `[clawmatrix] Node "${this.config.nodeId}" started` +
61
+ (this.config.listen ? ` (listening on port ${this.config.listenPort})` : "") +
62
+ ` (model proxy on port ${this.config.proxyPort})`,
63
+ );
64
+ }
65
+
66
+ async stop() {
67
+ this.handoffManager.destroy();
68
+ this.modelProxy.stop();
69
+ this.toolProxy.destroy();
70
+ await this.peerManager.stop();
71
+ this.logger.info(`[clawmatrix] Node "${this.config.nodeId}" stopped`);
72
+ }
73
+
74
+ private dispatchFrame(frame: AnyClusterFrame) {
75
+ switch (frame.type) {
76
+ case "handoff_req":
77
+ this.handoffManager.handleRequest(frame as HandoffRequest).catch((err) => {
78
+ this.logger.error(`[clawmatrix] Handoff request error: ${err}`);
79
+ });
80
+ break;
81
+ case "handoff_res":
82
+ this.handoffManager.handleResponse(frame as HandoffResponse);
83
+ break;
84
+ case "model_req":
85
+ this.modelProxy.handleModelRequest(frame as ModelRequest).catch((err) => {
86
+ this.logger.error(`[clawmatrix] Model request error: ${err}`);
87
+ });
88
+ break;
89
+ case "model_res":
90
+ this.modelProxy.handleModelResponse(frame as ModelResponse);
91
+ break;
92
+ case "model_stream":
93
+ this.modelProxy.handleModelStream(frame as ModelStreamChunk);
94
+ break;
95
+ case "tool_req":
96
+ this.toolProxy.handleRequest(frame as ToolProxyRequest).catch((err) => {
97
+ this.logger.error(`[clawmatrix] Tool request error: ${err}`);
98
+ });
99
+ break;
100
+ case "tool_res":
101
+ this.toolProxy.handleResponse(frame as ToolProxyResponse);
102
+ break;
103
+ case "send":
104
+ this.handleSendMessage(frame as SendMessage);
105
+ break;
106
+ }
107
+ }
108
+
109
+ private handleSendMessage(frame: SendMessage) {
110
+ // Inject message into local agent session via openclaw CLI
111
+ const { target, message } = frame.payload;
112
+ const agent = this.config.agents.find((a) => {
113
+ if (target.startsWith("tags:")) {
114
+ return a.tags.includes(target.slice(5));
115
+ }
116
+ return a.id === target;
117
+ });
118
+
119
+ if (!agent) {
120
+ this.logger.warn(
121
+ `[clawmatrix] Received send for unknown target "${target}"`,
122
+ );
123
+ return;
124
+ }
125
+
126
+ // Fire-and-forget: inject message via openclaw agent
127
+ Bun.spawn(["openclaw", "agent", "--message", message], {
128
+ stdout: "ignore",
129
+ stderr: "ignore",
130
+ });
131
+ }
132
+ }
133
+
134
+ // ── Singleton accessor ───────────────────────────────────────────
135
+ let clusterRuntime: ClusterRuntime | null = null;
136
+
137
+ export function getClusterRuntime(): ClusterRuntime {
138
+ if (!clusterRuntime) {
139
+ throw new Error("ClawMatrix cluster runtime not initialized");
140
+ }
141
+ return clusterRuntime;
142
+ }
143
+
144
+ export function createClusterService(
145
+ config: ClawMatrixConfig,
146
+ ): OpenClawPluginService {
147
+ return {
148
+ id: "clawmatrix",
149
+ start(ctx: OpenClawPluginServiceContext) {
150
+ clusterRuntime = new ClusterRuntime(config, ctx.logger);
151
+ clusterRuntime.start();
152
+ },
153
+ async stop() {
154
+ if (clusterRuntime) {
155
+ await clusterRuntime.stop();
156
+ clusterRuntime = null;
157
+ }
158
+ },
159
+ };
160
+ }
package/src/config.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { z } from "zod/v4";
2
+
3
+ const AgentInfoSchema = z.object({
4
+ id: z.string(),
5
+ description: z.string(),
6
+ tags: z.array(z.string()).default([]),
7
+ });
8
+
9
+ const ModelInfoSchema = z.object({
10
+ id: z.string(),
11
+ provider: z.string(),
12
+ description: z.string().optional(),
13
+ });
14
+
15
+ const PeerConfigSchema = z.object({
16
+ nodeId: z.string(),
17
+ url: z.string(),
18
+ });
19
+
20
+ const ToolProxyConfigSchema = z.object({
21
+ enabled: z.boolean().default(false),
22
+ allow: z.array(z.enum(["exec", "read", "write", "ls"])).default([]),
23
+ deny: z.array(z.enum(["exec", "read", "write", "ls"])).default([]),
24
+ allowPaths: z.array(z.string()).default([]),
25
+ denyPaths: z.array(z.string()).default([]),
26
+ execAllowlist: z.array(z.string()).default([]),
27
+ maxOutputBytes: z.number().default(1_048_576),
28
+ });
29
+
30
+ export const ClawMatrixConfigSchema = z.object({
31
+ nodeId: z.string(),
32
+ secret: z.string(),
33
+ listen: z.boolean().default(false),
34
+ listenHost: z.string().default("0.0.0.0"),
35
+ listenPort: z.number().default(19000),
36
+ peers: z.array(PeerConfigSchema).default([]),
37
+ agents: z.array(AgentInfoSchema).default([]),
38
+ models: z.array(ModelInfoSchema).default([]),
39
+ tags: z.array(z.string()).default([]),
40
+ proxyPort: z.number().default(19001),
41
+ toolProxy: ToolProxyConfigSchema.optional(),
42
+ });
43
+
44
+ export type ClawMatrixConfig = z.infer<typeof ClawMatrixConfigSchema>;
45
+ export type PeerConfig = z.infer<typeof PeerConfigSchema>;
46
+ export type ToolProxyConfig = z.infer<typeof ToolProxyConfigSchema>;
47
+
48
+ export function parseConfig(raw: unknown): ClawMatrixConfig {
49
+ return ClawMatrixConfigSchema.parse(raw);
50
+ }