clawmatrix 0.1.1 → 0.1.2
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/llms.txt +17 -21
- package/openclaw.plugin.json +12 -5
- package/package.json +1 -1
- package/src/cluster-service.ts +18 -4
- package/src/config.ts +2 -5
- package/src/index.ts +4 -4
- package/src/tool-proxy.ts +50 -176
- package/src/tools/cluster-exec.ts +9 -5
- package/src/tools/cluster-read.ts +6 -3
- package/src/tools/cluster-tool.ts +63 -0
- package/src/tools/cluster-write.ts +7 -5
- package/src/types.ts +1 -1
- package/src/tools/cluster-ls.ts +0 -51
package/llms.txt
CHANGED
|
@@ -18,7 +18,7 @@ openclaw gateway restart
|
|
|
18
18
|
|
|
19
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
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** —
|
|
21
|
+
- **Tool Proxy** — Invoke any OpenClaw tool (exec, read, write, edit, web_search, etc.) on remote nodes without delegating the entire task.
|
|
22
22
|
- **Gossip Discovery** — Nodes discover each other via gossip protocol; no central registry needed.
|
|
23
23
|
- **Auto Failover** — If a node goes down, requests route to backup nodes automatically.
|
|
24
24
|
|
|
@@ -63,10 +63,8 @@ Add to `plugins.entries.clawmatrix.config` in your `openclaw.json`:
|
|
|
63
63
|
"tags": ["office", "gpu"],
|
|
64
64
|
"toolProxy": {
|
|
65
65
|
"enabled": true,
|
|
66
|
-
"allow": ["exec", "read", "
|
|
67
|
-
"deny": ["write"]
|
|
68
|
-
"allowPaths": ["/repo", "/data"],
|
|
69
|
-
"denyPaths": ["/etc", "/root"]
|
|
66
|
+
"allow": ["exec", "read", "edit", "web_search"],
|
|
67
|
+
"deny": ["write", "browser", "sessions_spawn"]
|
|
70
68
|
}
|
|
71
69
|
}
|
|
72
70
|
```
|
|
@@ -122,16 +120,13 @@ To use cluster models, set your agent's model to a cluster-proxied model:
|
|
|
122
120
|
| Field | Type | Default | Description |
|
|
123
121
|
|-------|------|---------|-------------|
|
|
124
122
|
| `enabled` | boolean | `false` | Allow remote tool execution on this node |
|
|
125
|
-
| `allow` | array | `[]` | Allowed
|
|
126
|
-
| `deny` | array | `[]` | Denied
|
|
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 |
|
|
123
|
+
| `allow` | array | `[]` | Allowed OpenClaw tool names (empty = all allowed) |
|
|
124
|
+
| `deny` | array | `[]` | Denied OpenClaw tool names (takes precedence over allow) |
|
|
130
125
|
| `maxOutputBytes` | number | `1048576` | Max output size per tool response (1 MB) |
|
|
131
126
|
|
|
132
127
|
## Agent tools
|
|
133
128
|
|
|
134
|
-
ClawMatrix registers 7 tools available to agents:
|
|
129
|
+
ClawMatrix registers 7 tools available to agents (tool proxy tools correspond to OpenClaw's built-in tools):
|
|
135
130
|
|
|
136
131
|
### cluster_peers
|
|
137
132
|
List all reachable peers, their agents, and available models.
|
|
@@ -148,27 +143,28 @@ Send a one-way message to another agent (injected into their session).
|
|
|
148
143
|
- `message`: Message content
|
|
149
144
|
|
|
150
145
|
### cluster_exec
|
|
151
|
-
Execute a shell command on a remote node.
|
|
146
|
+
Execute a shell command on a remote node (invokes OpenClaw `exec` tool).
|
|
152
147
|
- `node`: Target nodeId or `"tags:<tag>"`
|
|
153
148
|
- `command`: Shell command
|
|
154
|
-
- `
|
|
155
|
-
- `timeout`: Optional timeout in
|
|
149
|
+
- `workdir`: Optional working directory
|
|
150
|
+
- `timeout`: Optional timeout in seconds (default 1800)
|
|
156
151
|
|
|
157
152
|
### cluster_read
|
|
158
|
-
Read a file from a remote node.
|
|
153
|
+
Read a file from a remote node (invokes OpenClaw `read` tool).
|
|
159
154
|
- `node`: Target nodeId or `"tags:<tag>"`
|
|
160
|
-
- `path`:
|
|
155
|
+
- `path`: File path
|
|
161
156
|
|
|
162
157
|
### cluster_write
|
|
163
|
-
Write content to a file on a remote node.
|
|
158
|
+
Write content to a file on a remote node (invokes OpenClaw `write` tool).
|
|
164
159
|
- `node`: Target nodeId or `"tags:<tag>"`
|
|
165
|
-
- `path`:
|
|
160
|
+
- `path`: File path
|
|
166
161
|
- `content`: File content
|
|
167
162
|
|
|
168
|
-
###
|
|
169
|
-
|
|
163
|
+
### cluster_tool
|
|
164
|
+
Invoke any OpenClaw tool on a remote node (exec, read, write, edit, web_search, web_fetch, browser, process, etc.).
|
|
170
165
|
- `node`: Target nodeId or `"tags:<tag>"`
|
|
171
|
-
- `
|
|
166
|
+
- `tool`: OpenClaw tool name
|
|
167
|
+
- `args`: Tool arguments (tool-specific)
|
|
172
168
|
|
|
173
169
|
## Verify cluster status
|
|
174
170
|
|
package/openclaw.plugin.json
CHANGED
|
@@ -55,11 +55,18 @@
|
|
|
55
55
|
"type": "object",
|
|
56
56
|
"properties": {
|
|
57
57
|
"enabled": { "type": "boolean", "default": false },
|
|
58
|
-
"allow": {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
"allow": {
|
|
59
|
+
"type": "array",
|
|
60
|
+
"items": { "type": "string" },
|
|
61
|
+
"default": [],
|
|
62
|
+
"description": "Allowed OpenClaw tool names (e.g. exec, read, write, edit, web_search). Empty = all allowed."
|
|
63
|
+
},
|
|
64
|
+
"deny": {
|
|
65
|
+
"type": "array",
|
|
66
|
+
"items": { "type": "string" },
|
|
67
|
+
"default": [],
|
|
68
|
+
"description": "Denied OpenClaw tool names (takes precedence over allow)."
|
|
69
|
+
},
|
|
63
70
|
"maxOutputBytes": { "type": "number", "default": 1048576 }
|
|
64
71
|
}
|
|
65
72
|
}
|
package/package.json
CHANGED
package/src/cluster-service.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
OpenClawPluginService,
|
|
3
3
|
OpenClawPluginServiceContext,
|
|
4
|
+
OpenClawConfig,
|
|
4
5
|
PluginLogger,
|
|
5
6
|
} from "openclaw/plugin-sdk";
|
|
6
7
|
import type { ClawMatrixConfig } from "./config.ts";
|
|
@@ -8,7 +9,7 @@ import { spawnProcess } from "./compat.ts";
|
|
|
8
9
|
import { PeerManager } from "./peer-manager.ts";
|
|
9
10
|
import { HandoffManager } from "./handoff.ts";
|
|
10
11
|
import { ModelProxy } from "./model-proxy.ts";
|
|
11
|
-
import { ToolProxy } from "./tool-proxy.ts";
|
|
12
|
+
import { ToolProxy, type GatewayInfo } from "./tool-proxy.ts";
|
|
12
13
|
import type {
|
|
13
14
|
AnyClusterFrame,
|
|
14
15
|
HandoffRequest,
|
|
@@ -21,6 +22,18 @@ import type {
|
|
|
21
22
|
ToolProxyResponse,
|
|
22
23
|
} from "./types.ts";
|
|
23
24
|
|
|
25
|
+
function resolveGatewayInfo(openclawConfig: OpenClawConfig): GatewayInfo {
|
|
26
|
+
const port = openclawConfig.gateway?.port ?? 18789;
|
|
27
|
+
const auth = openclawConfig.gateway?.auth;
|
|
28
|
+
let authHeader: string | undefined;
|
|
29
|
+
if (auth?.token) {
|
|
30
|
+
authHeader = `Bearer ${auth.token}`;
|
|
31
|
+
} else if (auth?.password && typeof auth.password === "string") {
|
|
32
|
+
authHeader = `Bearer ${auth.password}`;
|
|
33
|
+
}
|
|
34
|
+
return { port, authHeader };
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
/** Singleton cluster state shared across plugin components. */
|
|
25
38
|
export class ClusterRuntime {
|
|
26
39
|
readonly config: ClawMatrixConfig;
|
|
@@ -30,13 +43,13 @@ export class ClusterRuntime {
|
|
|
30
43
|
readonly toolProxy: ToolProxy;
|
|
31
44
|
private logger: PluginLogger;
|
|
32
45
|
|
|
33
|
-
constructor(config: ClawMatrixConfig, logger: PluginLogger) {
|
|
46
|
+
constructor(config: ClawMatrixConfig, logger: PluginLogger, openclawConfig: OpenClawConfig) {
|
|
34
47
|
this.config = config;
|
|
35
48
|
this.logger = logger;
|
|
36
49
|
this.peerManager = new PeerManager(config);
|
|
37
50
|
this.handoffManager = new HandoffManager(config, this.peerManager);
|
|
38
51
|
this.modelProxy = new ModelProxy(config, this.peerManager);
|
|
39
|
-
this.toolProxy = new ToolProxy(config, this.peerManager);
|
|
52
|
+
this.toolProxy = new ToolProxy(config, this.peerManager, resolveGatewayInfo(openclawConfig));
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
start() {
|
|
@@ -144,11 +157,12 @@ export function getClusterRuntime(): ClusterRuntime {
|
|
|
144
157
|
|
|
145
158
|
export function createClusterService(
|
|
146
159
|
config: ClawMatrixConfig,
|
|
160
|
+
openclawConfig: OpenClawConfig,
|
|
147
161
|
): OpenClawPluginService {
|
|
148
162
|
return {
|
|
149
163
|
id: "clawmatrix",
|
|
150
164
|
start(ctx: OpenClawPluginServiceContext) {
|
|
151
|
-
clusterRuntime = new ClusterRuntime(config, ctx.logger);
|
|
165
|
+
clusterRuntime = new ClusterRuntime(config, ctx.logger, openclawConfig);
|
|
152
166
|
clusterRuntime.start();
|
|
153
167
|
},
|
|
154
168
|
async stop() {
|
package/src/config.ts
CHANGED
|
@@ -19,11 +19,8 @@ const PeerConfigSchema = z.object({
|
|
|
19
19
|
|
|
20
20
|
const ToolProxyConfigSchema = z.object({
|
|
21
21
|
enabled: z.boolean().default(false),
|
|
22
|
-
allow: z.array(z.
|
|
23
|
-
deny: z.array(z.
|
|
24
|
-
allowPaths: z.array(z.string()).default([]),
|
|
25
|
-
denyPaths: z.array(z.string()).default([]),
|
|
26
|
-
execAllowlist: z.array(z.string()).default([]),
|
|
22
|
+
allow: z.array(z.string()).default([]),
|
|
23
|
+
deny: z.array(z.string()).default([]),
|
|
27
24
|
maxOutputBytes: z.number().default(1_048_576),
|
|
28
25
|
});
|
|
29
26
|
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { createClusterPeersTool } from "./tools/cluster-peers.ts";
|
|
|
7
7
|
import { createClusterExecTool } from "./tools/cluster-exec.ts";
|
|
8
8
|
import { createClusterReadTool } from "./tools/cluster-read.ts";
|
|
9
9
|
import { createClusterWriteTool } from "./tools/cluster-write.ts";
|
|
10
|
-
import {
|
|
10
|
+
import { createClusterToolTool } from "./tools/cluster-tool.ts";
|
|
11
11
|
import { registerClusterCli } from "./cli.ts";
|
|
12
12
|
|
|
13
13
|
const plugin = {
|
|
@@ -39,7 +39,7 @@ const plugin = {
|
|
|
39
39
|
const config = parseConfig(api.pluginConfig);
|
|
40
40
|
|
|
41
41
|
// Background service: manages mesh connections, WS listener, heartbeat
|
|
42
|
-
api.registerService(createClusterService(config));
|
|
42
|
+
api.registerService(createClusterService(config, api.config));
|
|
43
43
|
|
|
44
44
|
// Model provider: register clawmatrix as a provider pointing to local HTTP proxy
|
|
45
45
|
api.registerProvider({
|
|
@@ -61,7 +61,7 @@ const plugin = {
|
|
|
61
61
|
api.registerTool(createClusterExecTool(), { optional: true });
|
|
62
62
|
api.registerTool(createClusterReadTool(), { optional: true });
|
|
63
63
|
api.registerTool(createClusterWriteTool(), { optional: true });
|
|
64
|
-
api.registerTool(
|
|
64
|
+
api.registerTool(createClusterToolTool(), { optional: true });
|
|
65
65
|
|
|
66
66
|
// CLI subcommand
|
|
67
67
|
api.registerCli(registerClusterCli, { commands: ["clawmatrix"] });
|
|
@@ -88,7 +88,7 @@ const plugin = {
|
|
|
88
88
|
lines.push(
|
|
89
89
|
"",
|
|
90
90
|
"Available cluster tools: cluster_handoff, cluster_send, cluster_peers, " +
|
|
91
|
-
"cluster_exec, cluster_read, cluster_write,
|
|
91
|
+
"cluster_exec, cluster_read, cluster_write, cluster_tool",
|
|
92
92
|
);
|
|
93
93
|
|
|
94
94
|
return { prependContext: lines.join("\n") };
|
package/src/tool-proxy.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import { readdir, stat } from "node:fs/promises";
|
|
2
|
-
import { join, resolve, normalize } from "node:path";
|
|
3
1
|
import type { PeerManager } from "./peer-manager.ts";
|
|
4
2
|
import type { ClawMatrixConfig, ToolProxyConfig } from "./config.ts";
|
|
5
|
-
import { spawnProcess, readFileText, writeFileText } from "./compat.ts";
|
|
6
3
|
import type { ToolProxyRequest, ToolProxyResponse } from "./types.ts";
|
|
7
4
|
|
|
8
5
|
const TOOL_TIMEOUT = 30_000;
|
|
@@ -13,57 +10,38 @@ interface PendingToolReq {
|
|
|
13
10
|
timer: ReturnType<typeof setTimeout>;
|
|
14
11
|
}
|
|
15
12
|
|
|
13
|
+
export interface GatewayInfo {
|
|
14
|
+
port: number;
|
|
15
|
+
authHeader?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
16
18
|
export class ToolProxy {
|
|
17
19
|
private config: ClawMatrixConfig;
|
|
18
20
|
private peerManager: PeerManager;
|
|
19
21
|
private pending = new Map<string, PendingToolReq>();
|
|
22
|
+
private gatewayInfo: GatewayInfo;
|
|
20
23
|
|
|
21
|
-
constructor(config: ClawMatrixConfig, peerManager: PeerManager) {
|
|
24
|
+
constructor(config: ClawMatrixConfig, peerManager: PeerManager, gatewayInfo: GatewayInfo) {
|
|
22
25
|
this.config = config;
|
|
23
26
|
this.peerManager = peerManager;
|
|
27
|
+
this.gatewayInfo = gatewayInfo;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
// ── Outbound: request remote execution ─────────────────────────
|
|
27
|
-
async
|
|
31
|
+
async invoke(
|
|
28
32
|
node: string,
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
tool: string,
|
|
34
|
+
params: Record<string, unknown>,
|
|
31
35
|
timeout?: number,
|
|
32
|
-
): Promise<
|
|
33
|
-
|
|
34
|
-
command,
|
|
35
|
-
cwd,
|
|
36
|
-
timeout: timeout ?? TOOL_TIMEOUT,
|
|
37
|
-
});
|
|
38
|
-
return result as { exitCode: number; stdout: string; stderr: string };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async read(node: string, path: string): Promise<{ content: string }> {
|
|
42
|
-
const result = await this.sendToolReq(node, "read", { path });
|
|
43
|
-
return result as { content: string };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async write(
|
|
47
|
-
node: string,
|
|
48
|
-
path: string,
|
|
49
|
-
content: string,
|
|
50
|
-
): Promise<{ success: boolean }> {
|
|
51
|
-
const result = await this.sendToolReq(node, "write", { path, content });
|
|
52
|
-
return result as { success: boolean };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async ls(
|
|
56
|
-
node: string,
|
|
57
|
-
path: string,
|
|
58
|
-
): Promise<{ entries: { name: string; type: "file" | "dir"; size: number }[] }> {
|
|
59
|
-
const result = await this.sendToolReq(node, "ls", { path });
|
|
60
|
-
return result as { entries: { name: string; type: "file" | "dir"; size: number }[] };
|
|
36
|
+
): Promise<Record<string, unknown>> {
|
|
37
|
+
return this.sendToolReq(node, tool, params, timeout);
|
|
61
38
|
}
|
|
62
39
|
|
|
63
40
|
private async sendToolReq(
|
|
64
41
|
node: string,
|
|
65
|
-
tool:
|
|
42
|
+
tool: string,
|
|
66
43
|
params: Record<string, unknown>,
|
|
44
|
+
timeout?: number,
|
|
67
45
|
): Promise<Record<string, unknown>> {
|
|
68
46
|
const route = this.peerManager.router.resolveNode(node);
|
|
69
47
|
if (!route) throw new Error(`Node "${node}" not reachable`);
|
|
@@ -79,7 +57,7 @@ export class ToolProxy {
|
|
|
79
57
|
};
|
|
80
58
|
|
|
81
59
|
return new Promise((resolve, reject) => {
|
|
82
|
-
const timeoutMs =
|
|
60
|
+
const timeoutMs = timeout ?? TOOL_TIMEOUT;
|
|
83
61
|
const timer = setTimeout(() => {
|
|
84
62
|
this.pending.delete(id);
|
|
85
63
|
this.peerManager.router.markFailed(id);
|
|
@@ -113,12 +91,11 @@ export class ToolProxy {
|
|
|
113
91
|
}
|
|
114
92
|
}
|
|
115
93
|
|
|
116
|
-
// ── Incoming request: execute
|
|
94
|
+
// ── Incoming request: execute via local Gateway ────────────────
|
|
117
95
|
async handleRequest(frame: ToolProxyRequest): Promise<void> {
|
|
118
96
|
const { id, from, payload } = frame;
|
|
119
97
|
const toolProxyConfig = this.config.toolProxy;
|
|
120
98
|
|
|
121
|
-
// Check if tool proxy is enabled
|
|
122
99
|
if (!toolProxyConfig?.enabled) {
|
|
123
100
|
this.sendResponse(id, from, {
|
|
124
101
|
success: false,
|
|
@@ -127,7 +104,6 @@ export class ToolProxy {
|
|
|
127
104
|
return;
|
|
128
105
|
}
|
|
129
106
|
|
|
130
|
-
// Check tool allowlist/denylist
|
|
131
107
|
if (!this.isToolAllowed(payload.tool, toolProxyConfig)) {
|
|
132
108
|
this.sendResponse(id, from, {
|
|
133
109
|
success: false,
|
|
@@ -137,25 +113,7 @@ export class ToolProxy {
|
|
|
137
113
|
}
|
|
138
114
|
|
|
139
115
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
switch (payload.tool) {
|
|
143
|
-
case "exec":
|
|
144
|
-
result = await this.executeExec(payload.params, toolProxyConfig);
|
|
145
|
-
break;
|
|
146
|
-
case "read":
|
|
147
|
-
result = await this.executeRead(payload.params, toolProxyConfig);
|
|
148
|
-
break;
|
|
149
|
-
case "write":
|
|
150
|
-
result = await this.executeWrite(payload.params, toolProxyConfig);
|
|
151
|
-
break;
|
|
152
|
-
case "ls":
|
|
153
|
-
result = await this.executeLs(payload.params, toolProxyConfig);
|
|
154
|
-
break;
|
|
155
|
-
default:
|
|
156
|
-
throw new Error(`Unknown tool: ${payload.tool}`);
|
|
157
|
-
}
|
|
158
|
-
|
|
116
|
+
const result = await this.executeViaGateway(payload.tool, payload.params);
|
|
159
117
|
this.sendResponse(id, from, { success: true, result });
|
|
160
118
|
} catch (err) {
|
|
161
119
|
this.sendResponse(id, from, {
|
|
@@ -180,140 +138,56 @@ export class ToolProxy {
|
|
|
180
138
|
} as ToolProxyResponse);
|
|
181
139
|
}
|
|
182
140
|
|
|
183
|
-
// ──
|
|
184
|
-
private async
|
|
141
|
+
// ── Gateway tool invocation ────────────────────────────────────
|
|
142
|
+
private async executeViaGateway(
|
|
143
|
+
tool: string,
|
|
185
144
|
params: Record<string, unknown>,
|
|
186
|
-
tpConfig: ToolProxyConfig,
|
|
187
145
|
): Promise<Record<string, unknown>> {
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const cwd = (params.cwd as string) ?? process.cwd();
|
|
200
|
-
const timeout = (params.timeout as number) ?? TOOL_TIMEOUT;
|
|
201
|
-
|
|
202
|
-
const proc = spawnProcess(["sh", "-c", command], {
|
|
203
|
-
cwd,
|
|
204
|
-
stdout: "pipe",
|
|
205
|
-
stderr: "pipe",
|
|
146
|
+
const { port, authHeader } = this.gatewayInfo;
|
|
147
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
148
|
+
if (authHeader) headers["Authorization"] = authHeader;
|
|
149
|
+
|
|
150
|
+
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers,
|
|
153
|
+
body: JSON.stringify({ tool, args: params }),
|
|
206
154
|
});
|
|
207
155
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
proc.stderr ? new Response(proc.stderr).text() : "",
|
|
213
|
-
]);
|
|
214
|
-
const exitCode = await proc.exited;
|
|
215
|
-
clearTimeout(timeoutId);
|
|
216
|
-
|
|
217
|
-
const maxBytes = tpConfig.maxOutputBytes;
|
|
218
|
-
return {
|
|
219
|
-
exitCode,
|
|
220
|
-
stdout: stdout.slice(0, maxBytes),
|
|
221
|
-
stderr: stderr.slice(0, maxBytes),
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
private async executeRead(
|
|
226
|
-
params: Record<string, unknown>,
|
|
227
|
-
tpConfig: ToolProxyConfig,
|
|
228
|
-
): Promise<Record<string, unknown>> {
|
|
229
|
-
const path = params.path as string;
|
|
230
|
-
if (!path) throw new Error("Missing path");
|
|
231
|
-
this.validatePath(path, tpConfig);
|
|
156
|
+
if (!res.ok) {
|
|
157
|
+
const text = await res.text();
|
|
158
|
+
throw new Error(`Gateway tool invocation failed (${res.status}): ${text}`);
|
|
159
|
+
}
|
|
232
160
|
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
161
|
+
const body = (await res.json()) as {
|
|
162
|
+
ok: boolean;
|
|
163
|
+
result?: unknown;
|
|
164
|
+
error?: { type: string; message: string };
|
|
236
165
|
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
private async executeWrite(
|
|
240
|
-
params: Record<string, unknown>,
|
|
241
|
-
tpConfig: ToolProxyConfig,
|
|
242
|
-
): Promise<Record<string, unknown>> {
|
|
243
|
-
const path = params.path as string;
|
|
244
|
-
const content = params.content as string;
|
|
245
|
-
if (!path || content === undefined) throw new Error("Missing path or content");
|
|
246
|
-
this.validatePath(path, tpConfig);
|
|
247
|
-
|
|
248
|
-
await writeFileText(path, content);
|
|
249
|
-
return { success: true };
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
private async executeLs(
|
|
253
|
-
params: Record<string, unknown>,
|
|
254
|
-
tpConfig: ToolProxyConfig,
|
|
255
|
-
): Promise<Record<string, unknown>> {
|
|
256
|
-
const path = params.path as string;
|
|
257
|
-
if (!path) throw new Error("Missing path");
|
|
258
|
-
this.validatePath(path, tpConfig);
|
|
259
166
|
|
|
260
|
-
|
|
261
|
-
|
|
167
|
+
if (!body.ok) {
|
|
168
|
+
throw new Error(body.error?.message ?? "Tool invocation failed");
|
|
169
|
+
}
|
|
262
170
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
entries.push({
|
|
268
|
-
name: entry.name,
|
|
269
|
-
type: entry.isDirectory() ? "dir" : "file",
|
|
270
|
-
size: Number(st.size),
|
|
271
|
-
});
|
|
272
|
-
} catch {
|
|
273
|
-
entries.push({
|
|
274
|
-
name: entry.name,
|
|
275
|
-
type: entry.isDirectory() ? "dir" : "file",
|
|
276
|
-
size: 0,
|
|
277
|
-
});
|
|
278
|
-
}
|
|
171
|
+
const maxBytes = this.config.toolProxy?.maxOutputBytes ?? 1_048_576;
|
|
172
|
+
const resultStr = JSON.stringify(body.result);
|
|
173
|
+
if (resultStr.length > maxBytes) {
|
|
174
|
+
return { truncated: true, result: resultStr.slice(0, maxBytes) };
|
|
279
175
|
}
|
|
280
176
|
|
|
281
|
-
return
|
|
177
|
+
return typeof body.result === "object" && body.result !== null
|
|
178
|
+
? (body.result as Record<string, unknown>)
|
|
179
|
+
: { result: body.result };
|
|
282
180
|
}
|
|
283
181
|
|
|
284
182
|
// ── Security ───────────────────────────────────────────────────
|
|
285
|
-
private isToolAllowed(
|
|
286
|
-
tool
|
|
287
|
-
tpConfig: ToolProxyConfig,
|
|
288
|
-
): boolean {
|
|
289
|
-
if (tpConfig.deny.includes(tool as "exec" | "read" | "write" | "ls")) return false;
|
|
183
|
+
private isToolAllowed(tool: string, tpConfig: ToolProxyConfig): boolean {
|
|
184
|
+
if (tpConfig.deny.includes(tool)) return false;
|
|
290
185
|
if (tpConfig.allow.length > 0) {
|
|
291
|
-
return tpConfig.allow.includes(tool
|
|
186
|
+
return tpConfig.allow.includes(tool);
|
|
292
187
|
}
|
|
293
188
|
return true;
|
|
294
189
|
}
|
|
295
190
|
|
|
296
|
-
private validatePath(path: string, tpConfig: ToolProxyConfig) {
|
|
297
|
-
const normalized = normalize(resolve(path));
|
|
298
|
-
|
|
299
|
-
// Check deny paths first
|
|
300
|
-
for (const denied of tpConfig.denyPaths) {
|
|
301
|
-
if (normalized.startsWith(normalize(resolve(denied)))) {
|
|
302
|
-
throw new Error(`Path "${path}" is in denied path prefix`);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Check allow paths
|
|
307
|
-
if (tpConfig.allowPaths.length > 0) {
|
|
308
|
-
const allowed = tpConfig.allowPaths.some((p) =>
|
|
309
|
-
normalized.startsWith(normalize(resolve(p))),
|
|
310
|
-
);
|
|
311
|
-
if (!allowed) {
|
|
312
|
-
throw new Error(`Path "${path}" is not in allowed path prefixes`);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
191
|
destroy() {
|
|
318
192
|
for (const [, pending] of this.pending) {
|
|
319
193
|
clearTimeout(pending.timer);
|
|
@@ -19,28 +19,32 @@ export function createClusterExecTool(): AnyAgentTool {
|
|
|
19
19
|
type: "string",
|
|
20
20
|
description: "Shell command to execute",
|
|
21
21
|
},
|
|
22
|
-
|
|
22
|
+
workdir: {
|
|
23
23
|
type: "string",
|
|
24
24
|
description: "Working directory (optional)",
|
|
25
25
|
},
|
|
26
26
|
timeout: {
|
|
27
27
|
type: "number",
|
|
28
|
-
description: "Timeout in
|
|
28
|
+
description: "Timeout in seconds (default 1800)",
|
|
29
29
|
},
|
|
30
30
|
},
|
|
31
31
|
required: ["node", "command"],
|
|
32
32
|
},
|
|
33
33
|
async execute(_toolCallId, params) {
|
|
34
|
-
const { node, command,
|
|
34
|
+
const { node, command, workdir, timeout } = params as {
|
|
35
35
|
node: string;
|
|
36
36
|
command: string;
|
|
37
|
-
|
|
37
|
+
workdir?: string;
|
|
38
38
|
timeout?: number;
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
try {
|
|
42
42
|
const runtime = getClusterRuntime();
|
|
43
|
-
const result = await runtime.toolProxy.
|
|
43
|
+
const result = await runtime.toolProxy.invoke(
|
|
44
|
+
node,
|
|
45
|
+
"exec",
|
|
46
|
+
{ command, workdir, timeout },
|
|
47
|
+
);
|
|
44
48
|
return {
|
|
45
49
|
content: [
|
|
46
50
|
{
|
|
@@ -15,7 +15,7 @@ export function createClusterReadTool(): AnyAgentTool {
|
|
|
15
15
|
},
|
|
16
16
|
path: {
|
|
17
17
|
type: "string",
|
|
18
|
-
description: "
|
|
18
|
+
description: "File path to read",
|
|
19
19
|
},
|
|
20
20
|
},
|
|
21
21
|
required: ["node", "path"],
|
|
@@ -25,10 +25,13 @@ export function createClusterReadTool(): AnyAgentTool {
|
|
|
25
25
|
|
|
26
26
|
try {
|
|
27
27
|
const runtime = getClusterRuntime();
|
|
28
|
-
const result = await runtime.toolProxy.
|
|
28
|
+
const result = await runtime.toolProxy.invoke(node, "read", { path });
|
|
29
29
|
return {
|
|
30
30
|
content: [
|
|
31
|
-
{
|
|
31
|
+
{
|
|
32
|
+
type: "text" as const,
|
|
33
|
+
text: JSON.stringify(result, null, 2),
|
|
34
|
+
},
|
|
32
35
|
],
|
|
33
36
|
details: result,
|
|
34
37
|
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AnyAgentTool } from "openclaw/plugin-sdk";
|
|
2
|
+
import { getClusterRuntime } from "../cluster-service.ts";
|
|
3
|
+
|
|
4
|
+
export function createClusterToolTool(): AnyAgentTool {
|
|
5
|
+
return {
|
|
6
|
+
name: "cluster_tool",
|
|
7
|
+
label: "Cluster Tool",
|
|
8
|
+
description:
|
|
9
|
+
"Invoke any OpenClaw tool on a remote cluster node. " +
|
|
10
|
+
"Supports all tools available on the remote node (exec, read, write, edit, " +
|
|
11
|
+
"web_search, web_fetch, browser, process, etc.). " +
|
|
12
|
+
"Use nodeId or 'tags:<tag>' to specify the target.",
|
|
13
|
+
parameters: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
node: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: 'Target nodeId or "tags:<tag>"',
|
|
19
|
+
},
|
|
20
|
+
tool: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Tool name to invoke (e.g. exec, read, write, edit, web_search, web_fetch, browser, process)",
|
|
23
|
+
},
|
|
24
|
+
args: {
|
|
25
|
+
type: "object",
|
|
26
|
+
description: "Tool arguments (tool-specific)",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
required: ["node", "tool", "args"],
|
|
30
|
+
},
|
|
31
|
+
async execute(_toolCallId, params) {
|
|
32
|
+
const { node, tool, args } = params as {
|
|
33
|
+
node: string;
|
|
34
|
+
tool: string;
|
|
35
|
+
args: Record<string, unknown>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const runtime = getClusterRuntime();
|
|
40
|
+
const result = await runtime.toolProxy.invoke(node, tool, args);
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: "text" as const,
|
|
45
|
+
text: JSON.stringify(result, null, 2),
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
details: result,
|
|
49
|
+
};
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: "text" as const,
|
|
55
|
+
text: `Tool error: ${err instanceof Error ? err.message : String(err)}`,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
details: { error: true },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -15,7 +15,7 @@ export function createClusterWriteTool(): AnyAgentTool {
|
|
|
15
15
|
},
|
|
16
16
|
path: {
|
|
17
17
|
type: "string",
|
|
18
|
-
description: "
|
|
18
|
+
description: "File path to write",
|
|
19
19
|
},
|
|
20
20
|
content: {
|
|
21
21
|
type: "string",
|
|
@@ -33,14 +33,16 @@ export function createClusterWriteTool(): AnyAgentTool {
|
|
|
33
33
|
|
|
34
34
|
try {
|
|
35
35
|
const runtime = getClusterRuntime();
|
|
36
|
-
const result = await runtime.toolProxy.
|
|
36
|
+
const result = await runtime.toolProxy.invoke(
|
|
37
|
+
node,
|
|
38
|
+
"write",
|
|
39
|
+
{ path, content },
|
|
40
|
+
);
|
|
37
41
|
return {
|
|
38
42
|
content: [
|
|
39
43
|
{
|
|
40
44
|
type: "text" as const,
|
|
41
|
-
text: result
|
|
42
|
-
? `Successfully wrote to ${path} on ${node}`
|
|
43
|
-
: `Write failed on ${node}`,
|
|
45
|
+
text: JSON.stringify(result, null, 2),
|
|
44
46
|
},
|
|
45
47
|
],
|
|
46
48
|
details: result,
|
package/src/types.ts
CHANGED
package/src/tools/cluster-ls.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import type { AnyAgentTool } from "openclaw/plugin-sdk";
|
|
2
|
-
import { getClusterRuntime } from "../cluster-service.ts";
|
|
3
|
-
|
|
4
|
-
export function createClusterLsTool(): AnyAgentTool {
|
|
5
|
-
return {
|
|
6
|
-
name: "cluster_ls",
|
|
7
|
-
label: "Cluster Ls",
|
|
8
|
-
description: "List directory contents on a remote cluster node.",
|
|
9
|
-
parameters: {
|
|
10
|
-
type: "object",
|
|
11
|
-
properties: {
|
|
12
|
-
node: {
|
|
13
|
-
type: "string",
|
|
14
|
-
description: 'Target nodeId or "tags:<tag>"',
|
|
15
|
-
},
|
|
16
|
-
path: {
|
|
17
|
-
type: "string",
|
|
18
|
-
description: "Absolute directory path to list",
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
required: ["node", "path"],
|
|
22
|
-
},
|
|
23
|
-
async execute(_toolCallId, params) {
|
|
24
|
-
const { node, path } = params as { node: string; path: string };
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
const runtime = getClusterRuntime();
|
|
28
|
-
const result = await runtime.toolProxy.ls(node, path);
|
|
29
|
-
return {
|
|
30
|
-
content: [
|
|
31
|
-
{
|
|
32
|
-
type: "text" as const,
|
|
33
|
-
text: JSON.stringify(result.entries, null, 2),
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
details: result,
|
|
37
|
-
};
|
|
38
|
-
} catch (err) {
|
|
39
|
-
return {
|
|
40
|
-
content: [
|
|
41
|
-
{
|
|
42
|
-
type: "text" as const,
|
|
43
|
-
text: `Ls error: ${err instanceof Error ? err.message : String(err)}`,
|
|
44
|
-
},
|
|
45
|
-
],
|
|
46
|
-
details: { error: true },
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
}
|