clawmatrix 0.1.23 → 0.2.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 +4 -1
- package/package.json +4 -2
- package/src/acp-proxy.ts +2073 -0
- package/src/audit.ts +42 -0
- package/src/auth.ts +2 -3
- package/src/cli.ts +76 -2
- package/src/cluster-service.ts +243 -3
- package/src/compat.ts +84 -3
- package/src/config.ts +117 -4
- package/src/connection.ts +288 -85
- package/src/crypto.ts +179 -0
- package/src/debug.ts +15 -2
- package/src/e2e/helpers.ts +318 -0
- package/src/handoff.ts +131 -86
- package/src/identity.ts +95 -0
- package/src/index.ts +467 -52
- package/src/knowledge-sync.ts +776 -207
- package/src/model-proxy.ts +144 -39
- package/src/peer-approval.ts +628 -0
- package/src/peer-manager.ts +261 -32
- package/src/rate-limiter.ts +88 -0
- package/src/router.ts +32 -10
- package/src/sentinel-manager.ts +142 -0
- package/src/sentinel.ts +618 -0
- package/src/task-activity.ts +74 -0
- package/src/terminal.ts +566 -0
- package/src/tool-proxy.ts +127 -3
- package/src/tools/cluster-acp.ts +237 -0
- package/src/tools/cluster-batch.ts +76 -0
- package/src/tools/cluster-diagnostic.ts +174 -0
- package/src/tools/cluster-edit.ts +70 -0
- package/src/tools/cluster-peers.ts +59 -14
- package/src/tools/cluster-terminal.ts +232 -0
- package/src/tools/cluster-tool.ts +26 -11
- package/src/types.ts +475 -3
- package/src/web.ts +2 -2
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { AnyAgentTool } from "openclaw/plugin-sdk";
|
|
2
|
+
import { getClusterRuntime } from "../cluster-service.ts";
|
|
3
|
+
|
|
4
|
+
export function createClusterEditTool(): AnyAgentTool {
|
|
5
|
+
return {
|
|
6
|
+
name: "cluster_edit",
|
|
7
|
+
label: "Cluster Edit",
|
|
8
|
+
description:
|
|
9
|
+
"Edit a file on a remote cluster node by replacing exact text. " +
|
|
10
|
+
"The oldText must match exactly (including whitespace).",
|
|
11
|
+
parameters: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
node: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: 'Target nodeId or "tags:<tag>"',
|
|
17
|
+
},
|
|
18
|
+
path: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "File path to edit (relative or absolute on the remote node)",
|
|
21
|
+
},
|
|
22
|
+
oldText: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "Exact text to find and replace (must match exactly)",
|
|
25
|
+
},
|
|
26
|
+
newText: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "New text to replace the old text with",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
required: ["node", "path", "oldText", "newText"],
|
|
32
|
+
},
|
|
33
|
+
async execute(_toolCallId, params) {
|
|
34
|
+
const { node, path, oldText, newText } = params as {
|
|
35
|
+
node: string;
|
|
36
|
+
path: string;
|
|
37
|
+
oldText: string;
|
|
38
|
+
newText: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const runtime = getClusterRuntime();
|
|
43
|
+
const result = await runtime.toolProxy.invoke(
|
|
44
|
+
node,
|
|
45
|
+
"edit",
|
|
46
|
+
{ path, oldText, newText },
|
|
47
|
+
);
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: "text" as const,
|
|
52
|
+
text: JSON.stringify(result),
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
details: result,
|
|
56
|
+
};
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return {
|
|
59
|
+
content: [
|
|
60
|
+
{
|
|
61
|
+
type: "text" as const,
|
|
62
|
+
text: `Edit error: ${err instanceof Error ? err.message : String(err)}`,
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
details: { error: true },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -14,19 +14,64 @@ export function createClusterPeersTool(): AnyAgentTool {
|
|
|
14
14
|
async execute() {
|
|
15
15
|
try {
|
|
16
16
|
const runtime = getClusterRuntime();
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
17
|
+
const allEntries = runtime.peerManager.router.getAllPeers();
|
|
18
|
+
|
|
19
|
+
// Separate sentinel peers from normal peers
|
|
20
|
+
const sentinelSet = new Set<string>();
|
|
21
|
+
for (const entry of allEntries) {
|
|
22
|
+
if (entry.nodeId.endsWith(":sentinel")) {
|
|
23
|
+
sentinelSet.add(entry.nodeId.replace(/:sentinel$/, ""));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const peers = allEntries
|
|
28
|
+
.filter((entry) => !entry.nodeId.endsWith(":sentinel"))
|
|
29
|
+
.map((entry) => {
|
|
30
|
+
const sentinelNodeId = `${entry.nodeId}:sentinel`;
|
|
31
|
+
const sentinelEntry = allEntries.find((e) => e.nodeId === sentinelNodeId);
|
|
32
|
+
const status = runtime.peerManager.router.getPeerStatus(entry);
|
|
33
|
+
const hasSentinel = !!sentinelEntry;
|
|
34
|
+
const sentinelStatus = sentinelEntry
|
|
35
|
+
? runtime.peerManager.router.getPeerStatus(sentinelEntry)
|
|
36
|
+
: undefined;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
nodeId: entry.nodeId,
|
|
40
|
+
agents: entry.agents.map((a) => ({
|
|
41
|
+
id: a.id,
|
|
42
|
+
description: a.description,
|
|
43
|
+
tags: a.tags,
|
|
44
|
+
})),
|
|
45
|
+
models: entry.models.map((m) => m.id),
|
|
46
|
+
tags: entry.tags,
|
|
47
|
+
tools: entry.toolProxy?.enabled ? (entry.toolProxy.allow ?? []) : [],
|
|
48
|
+
status,
|
|
49
|
+
latencyMs: entry.latencyMs,
|
|
50
|
+
// Sentinel info merged into the same row
|
|
51
|
+
...(hasSentinel ? {
|
|
52
|
+
sentinel: sentinelStatus === "direct" || sentinelStatus === "relay" ? "online" : "offline",
|
|
53
|
+
} : {}),
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Sentinel-only entries (main node not in routing table, but sentinel is)
|
|
58
|
+
for (const entry of allEntries) {
|
|
59
|
+
if (!entry.nodeId.endsWith(":sentinel")) continue;
|
|
60
|
+
const mainNodeId = entry.nodeId.replace(/:sentinel$/, "");
|
|
61
|
+
if (peers.some((p) => p.nodeId === mainNodeId)) continue;
|
|
62
|
+
// Main node is gone, only sentinel remains
|
|
63
|
+
const sentinelStatus = runtime.peerManager.router.getPeerStatus(entry);
|
|
64
|
+
peers.push({
|
|
65
|
+
nodeId: mainNodeId,
|
|
66
|
+
agents: [],
|
|
67
|
+
models: [],
|
|
68
|
+
tags: entry.tags.filter((t) => t !== "sentinel"),
|
|
69
|
+
tools: [],
|
|
70
|
+
status: "unreachable",
|
|
71
|
+
latencyMs: entry.latencyMs,
|
|
72
|
+
sentinel: sentinelStatus === "direct" || sentinelStatus === "relay" ? "online" : "offline",
|
|
73
|
+
} as (typeof peers)[number]);
|
|
74
|
+
}
|
|
30
75
|
|
|
31
76
|
// Include satellite nodes (minimal fields — no agents/models)
|
|
32
77
|
const satellites = runtime.webHandler?.getSatelliteContexts() ?? runtime.peerManager.satelliteContexts;
|
|
@@ -36,7 +81,7 @@ export function createClusterPeersTool(): AnyAgentTool {
|
|
|
36
81
|
nodeId: sat.nodeId,
|
|
37
82
|
tools: sat.tools ?? [],
|
|
38
83
|
status: "satellite",
|
|
39
|
-
} as (typeof peers)[number]);
|
|
84
|
+
} as unknown as (typeof peers)[number]);
|
|
40
85
|
}
|
|
41
86
|
|
|
42
87
|
return {
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { AnyAgentTool } from "openclaw/plugin-sdk";
|
|
2
|
+
import { getClusterRuntime } from "../cluster-service.ts";
|
|
3
|
+
|
|
4
|
+
export function createClusterTerminalTool(): AnyAgentTool {
|
|
5
|
+
return {
|
|
6
|
+
name: "cluster_terminal",
|
|
7
|
+
label: "Cluster Terminal",
|
|
8
|
+
description:
|
|
9
|
+
"Interactive terminal (PTY) on a remote cluster node. " +
|
|
10
|
+
"Actions: " +
|
|
11
|
+
'"open" — open a terminal session on a remote node. ' +
|
|
12
|
+
'"input" — send input (keystrokes/commands) to a session. ' +
|
|
13
|
+
'"read" — read buffered output from a session. ' +
|
|
14
|
+
'"resize" — resize the terminal. ' +
|
|
15
|
+
'"list" — list active terminal sessions. ' +
|
|
16
|
+
'"close" — close a terminal session.',
|
|
17
|
+
parameters: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
action: {
|
|
21
|
+
type: "string",
|
|
22
|
+
enum: ["open", "input", "read", "resize", "list", "close"],
|
|
23
|
+
description: 'Action to perform. Default: "open"',
|
|
24
|
+
},
|
|
25
|
+
node: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: 'Target node ID or "tags:<tag>" (required for open)',
|
|
28
|
+
},
|
|
29
|
+
sessionId: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "Session ID (required for input/read/resize/close)",
|
|
32
|
+
},
|
|
33
|
+
data: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "Input data to send (for input action). Supports \\n for newline, \\x03 for Ctrl-C, etc.",
|
|
36
|
+
},
|
|
37
|
+
shell: {
|
|
38
|
+
type: "string",
|
|
39
|
+
description: "Shell to use (default: $SHELL or /bin/sh)",
|
|
40
|
+
},
|
|
41
|
+
cols: {
|
|
42
|
+
type: "number",
|
|
43
|
+
description: "Terminal columns (default: 80)",
|
|
44
|
+
},
|
|
45
|
+
rows: {
|
|
46
|
+
type: "number",
|
|
47
|
+
description: "Terminal rows (default: 24)",
|
|
48
|
+
},
|
|
49
|
+
cwd: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Working directory on remote node",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
required: [],
|
|
55
|
+
},
|
|
56
|
+
async execute(_toolCallId, params) {
|
|
57
|
+
const {
|
|
58
|
+
action = "open",
|
|
59
|
+
node,
|
|
60
|
+
sessionId,
|
|
61
|
+
data,
|
|
62
|
+
shell,
|
|
63
|
+
cols,
|
|
64
|
+
rows,
|
|
65
|
+
cwd,
|
|
66
|
+
} = params as {
|
|
67
|
+
action?: "open" | "input" | "read" | "resize" | "list" | "close";
|
|
68
|
+
node?: string;
|
|
69
|
+
sessionId?: string;
|
|
70
|
+
data?: string;
|
|
71
|
+
shell?: string;
|
|
72
|
+
cols?: number;
|
|
73
|
+
rows?: number;
|
|
74
|
+
cwd?: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const runtime = getClusterRuntime();
|
|
79
|
+
const tm = runtime.terminalManager;
|
|
80
|
+
|
|
81
|
+
if (action === "list") {
|
|
82
|
+
const sessions = tm.listSessions();
|
|
83
|
+
return {
|
|
84
|
+
content: [{ type: "text" as const, text: JSON.stringify(sessions, null, 2) }],
|
|
85
|
+
details: sessions,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (action === "open") {
|
|
90
|
+
if (!node) {
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: "text" as const, text: "node is required for open action" }],
|
|
93
|
+
details: { error: true },
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const nodeId = resolveNodeId(runtime, node);
|
|
97
|
+
const sid = await tm.open(nodeId, { shell, cols, rows, cwd });
|
|
98
|
+
|
|
99
|
+
// Wait a moment for initial output (shell prompt)
|
|
100
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
101
|
+
const initial = tm.readOutput(sid);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
content: [{
|
|
105
|
+
type: "text" as const,
|
|
106
|
+
text: JSON.stringify({
|
|
107
|
+
sessionId: sid,
|
|
108
|
+
nodeId,
|
|
109
|
+
status: "open",
|
|
110
|
+
initialOutput: initial.data,
|
|
111
|
+
}, null, 2),
|
|
112
|
+
}],
|
|
113
|
+
details: { sessionId: sid, nodeId },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (action === "input") {
|
|
118
|
+
if (!sessionId) {
|
|
119
|
+
return {
|
|
120
|
+
content: [{ type: "text" as const, text: "sessionId is required for input action" }],
|
|
121
|
+
details: { error: true },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (!data) {
|
|
125
|
+
return {
|
|
126
|
+
content: [{ type: "text" as const, text: "data is required for input action" }],
|
|
127
|
+
details: { error: true },
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
tm.sendInput(sessionId, data);
|
|
131
|
+
|
|
132
|
+
// Wait for output response
|
|
133
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
134
|
+
const output = tm.readOutput(sessionId);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
content: [{
|
|
138
|
+
type: "text" as const,
|
|
139
|
+
text: JSON.stringify({
|
|
140
|
+
sessionId,
|
|
141
|
+
output: output.data,
|
|
142
|
+
closed: output.closed,
|
|
143
|
+
exitCode: output.exitCode,
|
|
144
|
+
}, null, 2),
|
|
145
|
+
}],
|
|
146
|
+
details: { output: output.data, closed: output.closed },
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (action === "read") {
|
|
151
|
+
if (!sessionId) {
|
|
152
|
+
return {
|
|
153
|
+
content: [{ type: "text" as const, text: "sessionId is required for read action" }],
|
|
154
|
+
details: { error: true },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const output = tm.readOutput(sessionId);
|
|
158
|
+
return {
|
|
159
|
+
content: [{
|
|
160
|
+
type: "text" as const,
|
|
161
|
+
text: JSON.stringify({
|
|
162
|
+
sessionId,
|
|
163
|
+
output: output.data,
|
|
164
|
+
closed: output.closed,
|
|
165
|
+
exitCode: output.exitCode,
|
|
166
|
+
}, null, 2),
|
|
167
|
+
}],
|
|
168
|
+
details: output,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (action === "resize") {
|
|
173
|
+
if (!sessionId) {
|
|
174
|
+
return {
|
|
175
|
+
content: [{ type: "text" as const, text: "sessionId is required for resize action" }],
|
|
176
|
+
details: { error: true },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
tm.resize(sessionId, cols ?? 80, rows ?? 24);
|
|
180
|
+
return {
|
|
181
|
+
content: [{ type: "text" as const, text: `Resized to ${cols ?? 80}x${rows ?? 24}` }],
|
|
182
|
+
details: { success: true },
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (action === "close") {
|
|
187
|
+
if (!sessionId) {
|
|
188
|
+
return {
|
|
189
|
+
content: [{ type: "text" as const, text: "sessionId is required for close action" }],
|
|
190
|
+
details: { error: true },
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
tm.close(sessionId);
|
|
194
|
+
return {
|
|
195
|
+
content: [{ type: "text" as const, text: `Session ${sessionId} closed` }],
|
|
196
|
+
details: { success: true },
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
content: [{ type: "text" as const, text: `Unknown action: ${action}` }],
|
|
202
|
+
details: { error: true },
|
|
203
|
+
};
|
|
204
|
+
} catch (err) {
|
|
205
|
+
return {
|
|
206
|
+
content: [{
|
|
207
|
+
type: "text" as const,
|
|
208
|
+
text: `Terminal error: ${err instanceof Error ? err.message : String(err)}`,
|
|
209
|
+
}],
|
|
210
|
+
details: { error: true },
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function resolveNodeId(
|
|
218
|
+
runtime: ReturnType<typeof getClusterRuntime>,
|
|
219
|
+
node: string,
|
|
220
|
+
): string {
|
|
221
|
+
const peers = runtime.peerManager.router.getAllPeers();
|
|
222
|
+
const direct = peers.find((p) => p.nodeId === node);
|
|
223
|
+
if (direct) return direct.nodeId;
|
|
224
|
+
|
|
225
|
+
if (node.startsWith("tags:")) {
|
|
226
|
+
const tag = node.slice(5);
|
|
227
|
+
const match = peers.find((p) => p.tags.includes(tag));
|
|
228
|
+
if (match) return match.nodeId;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return node;
|
|
232
|
+
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { AnyAgentTool } from "openclaw/plugin-sdk";
|
|
2
2
|
import { getClusterRuntime } from "../cluster-service.ts";
|
|
3
3
|
|
|
4
|
-
export function
|
|
4
|
+
export function createClusterToolInvokeTool(): AnyAgentTool {
|
|
5
5
|
return {
|
|
6
6
|
name: "cluster_tool",
|
|
7
|
-
label: "Cluster Tool",
|
|
7
|
+
label: "Cluster Tool Invoke",
|
|
8
8
|
description:
|
|
9
|
-
"Invoke any
|
|
9
|
+
"Invoke any tool on a remote cluster node by name. " +
|
|
10
|
+
"Use this for device-specific tools (e.g. screenshot, battery, location on mobile nodes). " +
|
|
11
|
+
"Use cluster_peers to discover available tools on each peer. " +
|
|
12
|
+
"Use nodeId or 'tags:<tag>' to specify the target.",
|
|
10
13
|
parameters: {
|
|
11
14
|
type: "object",
|
|
12
15
|
properties: {
|
|
@@ -16,25 +19,37 @@ export function createClusterToolTool(): AnyAgentTool {
|
|
|
16
19
|
},
|
|
17
20
|
tool: {
|
|
18
21
|
type: "string",
|
|
19
|
-
description: "
|
|
22
|
+
description: "Remote tool name to invoke (e.g. screenshot, battery, location)",
|
|
20
23
|
},
|
|
21
|
-
|
|
24
|
+
params: {
|
|
22
25
|
type: "object",
|
|
23
|
-
description: "
|
|
26
|
+
description: "Parameters to pass to the remote tool (tool-specific)",
|
|
27
|
+
additionalProperties: true,
|
|
28
|
+
},
|
|
29
|
+
timeout: {
|
|
30
|
+
type: "number",
|
|
31
|
+
description: "Timeout in seconds (default 30)",
|
|
24
32
|
},
|
|
25
33
|
},
|
|
26
|
-
required: ["node", "tool"
|
|
34
|
+
required: ["node", "tool"],
|
|
27
35
|
},
|
|
28
36
|
async execute(_toolCallId, params) {
|
|
29
|
-
const { node, tool,
|
|
37
|
+
const { node, tool, params: toolParams, timeout } = params as {
|
|
30
38
|
node: string;
|
|
31
39
|
tool: string;
|
|
32
|
-
|
|
40
|
+
params?: Record<string, unknown>;
|
|
41
|
+
timeout?: number;
|
|
33
42
|
};
|
|
34
43
|
|
|
35
44
|
try {
|
|
36
45
|
const runtime = getClusterRuntime();
|
|
37
|
-
const
|
|
46
|
+
const timeoutMs = (timeout ?? 30) * 1000;
|
|
47
|
+
const result = await runtime.toolProxy.invoke(
|
|
48
|
+
node,
|
|
49
|
+
tool,
|
|
50
|
+
toolParams ?? {},
|
|
51
|
+
timeoutMs,
|
|
52
|
+
);
|
|
38
53
|
return {
|
|
39
54
|
content: [
|
|
40
55
|
{
|
|
@@ -49,7 +64,7 @@ export function createClusterToolTool(): AnyAgentTool {
|
|
|
49
64
|
content: [
|
|
50
65
|
{
|
|
51
66
|
type: "text" as const,
|
|
52
|
-
text: `Tool error: ${err instanceof Error ? err.message : String(err)}`,
|
|
67
|
+
text: `Tool invoke error: ${err instanceof Error ? err.message : String(err)}`,
|
|
53
68
|
},
|
|
54
69
|
],
|
|
55
70
|
details: { error: true },
|