clawmatrix 0.1.2 → 0.1.5
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 +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/cluster-service.ts +5 -4
- package/src/config.ts +28 -1
- package/src/connection.ts +39 -8
- package/src/handoff.ts +4 -4
- package/src/index.ts +44 -21
- package/src/model-proxy.ts +14 -5
- package/src/peer-manager.ts +3 -0
- package/src/tool-proxy.ts +2 -4
- package/src/types.ts +7 -1
package/llms.txt
CHANGED
|
@@ -120,7 +120,7 @@ To use cluster models, set your agent's model to a cluster-proxied model:
|
|
|
120
120
|
| Field | Type | Default | Description |
|
|
121
121
|
|-------|------|---------|-------------|
|
|
122
122
|
| `enabled` | boolean | `false` | Allow remote tool execution on this node |
|
|
123
|
-
| `allow` | array | `[]` | Allowed OpenClaw tool names
|
|
123
|
+
| `allow` | array | `[]` | Allowed OpenClaw tool names. `["*"]` or `[]` = all allowed |
|
|
124
124
|
| `deny` | array | `[]` | Denied OpenClaw tool names (takes precedence over allow) |
|
|
125
125
|
| `maxOutputBytes` | number | `1048576` | Max output size per tool response (1 MB) |
|
|
126
126
|
|
package/openclaw.plugin.json
CHANGED
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"type": "array",
|
|
60
60
|
"items": { "type": "string" },
|
|
61
61
|
"default": [],
|
|
62
|
-
"description": "Allowed OpenClaw tool names
|
|
62
|
+
"description": "Allowed OpenClaw tool names. Use [\"*\"] for all. Empty or [\"*\"] = all allowed."
|
|
63
63
|
},
|
|
64
64
|
"deny": {
|
|
65
65
|
"type": "array",
|
package/package.json
CHANGED
package/src/cluster-service.ts
CHANGED
|
@@ -46,10 +46,11 @@ export class ClusterRuntime {
|
|
|
46
46
|
constructor(config: ClawMatrixConfig, logger: PluginLogger, openclawConfig: OpenClawConfig) {
|
|
47
47
|
this.config = config;
|
|
48
48
|
this.logger = logger;
|
|
49
|
+
const gatewayInfo = resolveGatewayInfo(openclawConfig);
|
|
49
50
|
this.peerManager = new PeerManager(config);
|
|
50
51
|
this.handoffManager = new HandoffManager(config, this.peerManager);
|
|
51
|
-
this.modelProxy = new ModelProxy(config, this.peerManager);
|
|
52
|
-
this.toolProxy = new ToolProxy(config, this.peerManager,
|
|
52
|
+
this.modelProxy = new ModelProxy(config, this.peerManager, gatewayInfo);
|
|
53
|
+
this.toolProxy = new ToolProxy(config, this.peerManager, gatewayInfo);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
start() {
|
|
@@ -137,8 +138,8 @@ export class ClusterRuntime {
|
|
|
137
138
|
return;
|
|
138
139
|
}
|
|
139
140
|
|
|
140
|
-
// Fire-and-forget: inject message via openclaw agent
|
|
141
|
-
spawnProcess(["openclaw", "agent", "--message", message], {
|
|
141
|
+
// Fire-and-forget: inject message via openclaw agent CLI
|
|
142
|
+
spawnProcess(["openclaw", "agent", "--agent", agent.id, "--message", message], {
|
|
142
143
|
stdout: "ignore",
|
|
143
144
|
stderr: "ignore",
|
|
144
145
|
});
|
package/src/config.ts
CHANGED
|
@@ -6,10 +6,29 @@ const AgentInfoSchema = z.object({
|
|
|
6
6
|
tags: z.array(z.string()).default([]),
|
|
7
7
|
});
|
|
8
8
|
|
|
9
|
+
const ModelParamsSchema = {
|
|
10
|
+
api: z.enum([
|
|
11
|
+
"openai-completions", "openai-responses", "openai-codex-responses",
|
|
12
|
+
"anthropic-messages", "google-generative-ai", "github-copilot",
|
|
13
|
+
"bedrock-converse-stream", "ollama",
|
|
14
|
+
]).optional(),
|
|
15
|
+
contextWindow: z.number().optional(),
|
|
16
|
+
maxTokens: z.number().optional(),
|
|
17
|
+
reasoning: z.boolean().optional(),
|
|
18
|
+
input: z.array(z.enum(["text", "image"])).optional(),
|
|
19
|
+
cost: z.object({
|
|
20
|
+
input: z.number(),
|
|
21
|
+
output: z.number(),
|
|
22
|
+
cacheRead: z.number(),
|
|
23
|
+
cacheWrite: z.number(),
|
|
24
|
+
}).optional(),
|
|
25
|
+
};
|
|
26
|
+
|
|
9
27
|
const ModelInfoSchema = z.object({
|
|
10
28
|
id: z.string(),
|
|
11
29
|
provider: z.string(),
|
|
12
30
|
description: z.string().optional(),
|
|
31
|
+
...ModelParamsSchema,
|
|
13
32
|
});
|
|
14
33
|
|
|
15
34
|
const PeerConfigSchema = z.object({
|
|
@@ -24,15 +43,23 @@ const ToolProxyConfigSchema = z.object({
|
|
|
24
43
|
maxOutputBytes: z.number().default(1_048_576),
|
|
25
44
|
});
|
|
26
45
|
|
|
46
|
+
const ProxyModelSchema = z.object({
|
|
47
|
+
id: z.string(),
|
|
48
|
+
nodeId: z.string().optional(),
|
|
49
|
+
description: z.string().optional(),
|
|
50
|
+
...ModelParamsSchema,
|
|
51
|
+
});
|
|
52
|
+
|
|
27
53
|
export const ClawMatrixConfigSchema = z.object({
|
|
28
54
|
nodeId: z.string(),
|
|
29
|
-
secret: z.string(),
|
|
55
|
+
secret: z.string().min(16, "secret must be at least 16 characters"),
|
|
30
56
|
listen: z.boolean().default(false),
|
|
31
57
|
listenHost: z.string().default("0.0.0.0"),
|
|
32
58
|
listenPort: z.number().default(19000),
|
|
33
59
|
peers: z.array(PeerConfigSchema).default([]),
|
|
34
60
|
agents: z.array(AgentInfoSchema).default([]),
|
|
35
61
|
models: z.array(ModelInfoSchema).default([]),
|
|
62
|
+
proxyModels: z.array(ProxyModelSchema).default([]),
|
|
36
63
|
tags: z.array(z.string()).default([]),
|
|
37
64
|
proxyPort: z.number().default(19001),
|
|
38
65
|
toolProxy: ToolProxyConfigSchema.optional(),
|
package/src/connection.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import type {
|
|
3
|
+
AgentInfo,
|
|
3
4
|
AnyClusterFrame,
|
|
4
5
|
AuthChallenge,
|
|
5
6
|
AuthOk,
|
|
6
7
|
AuthFail,
|
|
7
8
|
ClusterFrame,
|
|
9
|
+
ModelInfo,
|
|
8
10
|
NodeCapabilities,
|
|
9
11
|
} from "./types.ts";
|
|
10
12
|
import { generateNonce, computeHmac, verifyHmac } from "./auth.ts";
|
|
@@ -12,6 +14,7 @@ import { generateNonce, computeHmac, verifyHmac } from "./auth.ts";
|
|
|
12
14
|
const HEARTBEAT_BASE = 12_000;
|
|
13
15
|
const HEARTBEAT_JITTER = 6_000;
|
|
14
16
|
const HEARTBEAT_TIMEOUT_COUNT = 3;
|
|
17
|
+
const AUTH_TIMEOUT = 10_000;
|
|
15
18
|
|
|
16
19
|
export type ConnectionRole = "inbound" | "outbound";
|
|
17
20
|
|
|
@@ -41,6 +44,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
41
44
|
authenticated = false;
|
|
42
45
|
|
|
43
46
|
private heartbeatTimer: ReturnType<typeof setTimeout> | null = null;
|
|
47
|
+
private authTimer: ReturnType<typeof setTimeout> | null = null;
|
|
44
48
|
private missedPongs = 0;
|
|
45
49
|
private pendingNonce: string | null = null;
|
|
46
50
|
private closed = false;
|
|
@@ -61,6 +65,11 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
61
65
|
|
|
62
66
|
if (role === "inbound") {
|
|
63
67
|
this.sendAuthChallenge();
|
|
68
|
+
this.authTimer = setTimeout(() => {
|
|
69
|
+
if (!this.authenticated) {
|
|
70
|
+
this.close(4003, "auth timeout");
|
|
71
|
+
}
|
|
72
|
+
}, AUTH_TIMEOUT);
|
|
64
73
|
}
|
|
65
74
|
}
|
|
66
75
|
|
|
@@ -142,7 +151,13 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
142
151
|
private async handleAuthMessage(frame: AnyClusterFrame) {
|
|
143
152
|
if (this.role === "inbound") {
|
|
144
153
|
if (frame.type !== "auth") return;
|
|
145
|
-
const { nodeId, sig } = frame.payload as {
|
|
154
|
+
const { nodeId, sig, agents, models, tags } = frame.payload as {
|
|
155
|
+
nodeId: string;
|
|
156
|
+
sig: string;
|
|
157
|
+
agents?: AgentInfo[];
|
|
158
|
+
models?: ModelInfo[];
|
|
159
|
+
tags?: string[];
|
|
160
|
+
};
|
|
146
161
|
if (!this.pendingNonce) return;
|
|
147
162
|
|
|
148
163
|
const valid = await verifyHmac(this.pendingNonce, this.secret, sig);
|
|
@@ -160,7 +175,14 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
160
175
|
}
|
|
161
176
|
|
|
162
177
|
this.remoteNodeId = nodeId;
|
|
178
|
+
this.remoteCapabilities = {
|
|
179
|
+
nodeId,
|
|
180
|
+
agents: agents ?? [],
|
|
181
|
+
models: models ?? [],
|
|
182
|
+
tags: tags ?? [],
|
|
183
|
+
};
|
|
163
184
|
this.authenticated = true;
|
|
185
|
+
this.clearAuthTimer();
|
|
164
186
|
|
|
165
187
|
this.sendRaw({
|
|
166
188
|
type: "auth_ok",
|
|
@@ -175,12 +197,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
175
197
|
} satisfies AuthOk);
|
|
176
198
|
|
|
177
199
|
this.startHeartbeat();
|
|
178
|
-
this.emit("authenticated",
|
|
179
|
-
nodeId,
|
|
180
|
-
agents: [],
|
|
181
|
-
models: [],
|
|
182
|
-
tags: [],
|
|
183
|
-
});
|
|
200
|
+
this.emit("authenticated", this.remoteCapabilities);
|
|
184
201
|
} else {
|
|
185
202
|
if (frame.type === "auth_challenge") {
|
|
186
203
|
const { nonce } = (frame as AuthChallenge).payload;
|
|
@@ -189,7 +206,13 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
189
206
|
type: "auth",
|
|
190
207
|
from: this.nodeId,
|
|
191
208
|
timestamp: Date.now(),
|
|
192
|
-
payload: {
|
|
209
|
+
payload: {
|
|
210
|
+
nodeId: this.nodeId,
|
|
211
|
+
sig,
|
|
212
|
+
agents: this.localCapabilities.agents,
|
|
213
|
+
models: this.localCapabilities.models,
|
|
214
|
+
tags: this.localCapabilities.tags,
|
|
215
|
+
},
|
|
193
216
|
});
|
|
194
217
|
return;
|
|
195
218
|
}
|
|
@@ -217,6 +240,13 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
217
240
|
}
|
|
218
241
|
}
|
|
219
242
|
|
|
243
|
+
private clearAuthTimer() {
|
|
244
|
+
if (this.authTimer) {
|
|
245
|
+
clearTimeout(this.authTimer);
|
|
246
|
+
this.authTimer = null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
220
250
|
// ── Heartbeat ──────────────────────────────────────────────────
|
|
221
251
|
private startHeartbeat() {
|
|
222
252
|
const scheduleNext = () => {
|
|
@@ -243,6 +273,7 @@ export class Connection extends EventEmitter<ConnectionEvents> {
|
|
|
243
273
|
close(code = 1000, reason = "normal") {
|
|
244
274
|
if (this.closed) return;
|
|
245
275
|
this.closed = true;
|
|
276
|
+
this.clearAuthTimer();
|
|
246
277
|
if (this.heartbeatTimer) {
|
|
247
278
|
clearTimeout(this.heartbeatTimer);
|
|
248
279
|
this.heartbeatTimer = null;
|
package/src/handoff.ts
CHANGED
|
@@ -148,10 +148,10 @@ export class HandoffManager {
|
|
|
148
148
|
? `${payload.task}\n\nContext:\n${payload.context}`
|
|
149
149
|
: payload.task;
|
|
150
150
|
|
|
151
|
-
const proc = spawnProcess(
|
|
152
|
-
|
|
153
|
-
stderr: "pipe",
|
|
154
|
-
|
|
151
|
+
const proc = spawnProcess(
|
|
152
|
+
["openclaw", "agent", "--agent", agent.id, "--message", message],
|
|
153
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
154
|
+
);
|
|
155
155
|
|
|
156
156
|
const stdout = proc.stdout ? await new Response(proc.stdout).text() : "";
|
|
157
157
|
const exitCode = await proc.exited;
|
package/src/index.ts
CHANGED
|
@@ -74,21 +74,39 @@ const plugin = {
|
|
|
74
74
|
if (peers.length === 0) return;
|
|
75
75
|
|
|
76
76
|
const lines = [
|
|
77
|
-
`[ClawMatrix Cluster]
|
|
78
|
-
`
|
|
77
|
+
`[ClawMatrix Cluster]`,
|
|
78
|
+
`You are on node "${config.nodeId}"${config.tags.length ? ` (tags: ${config.tags.join(", ")})` : ""}.`,
|
|
79
79
|
];
|
|
80
|
+
|
|
81
|
+
if (config.agents.length > 0) {
|
|
82
|
+
const localAgent = config.agents[0]!;
|
|
83
|
+
lines.push(`Your role: ${localAgent.description}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
lines.push("", "Remote nodes in the cluster:");
|
|
80
87
|
for (const peer of peers) {
|
|
81
|
-
const agents = peer.agents.map((a) => a.id).join(", ") || "none";
|
|
82
|
-
const models = peer.models.map((m) => m.id).join(", ") || "none";
|
|
83
88
|
const status = peer.connection?.isOpen ? "connected" : "via relay";
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
)
|
|
89
|
+
const tags = peer.tags.length ? ` [${peer.tags.join(", ")}]` : "";
|
|
90
|
+
lines.push(` - ${peer.nodeId} (${status})${tags}`);
|
|
91
|
+
for (const agent of peer.agents) {
|
|
92
|
+
lines.push(` agent "${agent.id}": ${agent.description}`);
|
|
93
|
+
}
|
|
94
|
+
if (peer.models.length > 0) {
|
|
95
|
+
lines.push(` models: ${peer.models.map((m) => m.id).join(", ")}`);
|
|
96
|
+
}
|
|
87
97
|
}
|
|
98
|
+
|
|
88
99
|
lines.push(
|
|
89
100
|
"",
|
|
90
|
-
"
|
|
91
|
-
"
|
|
101
|
+
"When a task involves resources on a remote node (files, commands, services), " +
|
|
102
|
+
"use cluster tools to operate there directly:",
|
|
103
|
+
" - cluster_exec / cluster_read / cluster_write — run commands, read/write files on a remote node",
|
|
104
|
+
" - cluster_tool — invoke any OpenClaw tool on a remote node",
|
|
105
|
+
" - cluster_handoff — delegate a complex task to a remote agent for autonomous execution",
|
|
106
|
+
" - cluster_send — send a one-way message to a remote agent",
|
|
107
|
+
" - cluster_peers — inspect cluster topology",
|
|
108
|
+
"Use the node's description and tags to decide which node to target. " +
|
|
109
|
+
"For simple operations, prefer cluster_exec/read/write; for complex multi-step tasks, prefer cluster_handoff.",
|
|
92
110
|
);
|
|
93
111
|
|
|
94
112
|
return { prependContext: lines.join("\n") };
|
|
@@ -100,19 +118,24 @@ const plugin = {
|
|
|
100
118
|
};
|
|
101
119
|
|
|
102
120
|
function getAllClusterModels(config: ReturnType<typeof parseConfig>) {
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
121
|
+
// models = models this node serves to the cluster
|
|
122
|
+
// proxyModels = remote models this node wants to consume from the cluster
|
|
123
|
+
// Both need to be registered with the OpenClaw provider so it routes requests to our local proxy.
|
|
124
|
+
const seen = new Set<string>();
|
|
125
|
+
const all = [...config.models, ...config.proxyModels].filter((m) => {
|
|
126
|
+
if (seen.has(m.id)) return false;
|
|
127
|
+
seen.add(m.id);
|
|
128
|
+
return true;
|
|
129
|
+
});
|
|
130
|
+
return all.map((m) => ({
|
|
108
131
|
id: m.id,
|
|
109
|
-
name: m.id,
|
|
110
|
-
api: "openai-completions" as const,
|
|
111
|
-
reasoning: false,
|
|
112
|
-
input: ["text" as const],
|
|
113
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
114
|
-
contextWindow: 128_000,
|
|
115
|
-
maxTokens: 4096,
|
|
132
|
+
name: m.description ?? m.id,
|
|
133
|
+
api: m.api ?? ("openai-completions" as const),
|
|
134
|
+
reasoning: m.reasoning ?? false,
|
|
135
|
+
input: (m.input as Array<"text" | "image">) ?? ["text" as const],
|
|
136
|
+
cost: m.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
137
|
+
contextWindow: m.contextWindow ?? 128_000,
|
|
138
|
+
maxTokens: m.maxTokens ?? 4096,
|
|
116
139
|
}));
|
|
117
140
|
}
|
|
118
141
|
|
package/src/model-proxy.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createServer, type Server } from "node:http";
|
|
2
2
|
import type { PeerManager } from "./peer-manager.ts";
|
|
3
3
|
import type { ClawMatrixConfig } from "./config.ts";
|
|
4
|
+
import type { GatewayInfo } from "./tool-proxy.ts";
|
|
4
5
|
import type {
|
|
5
6
|
ModelRequest,
|
|
6
7
|
ModelResponse,
|
|
@@ -23,10 +24,12 @@ export class ModelProxy {
|
|
|
23
24
|
private peerManager: PeerManager;
|
|
24
25
|
private pending = new Map<string, PendingModelReq>();
|
|
25
26
|
private httpServer: Server | null = null;
|
|
27
|
+
private gatewayInfo: GatewayInfo;
|
|
26
28
|
|
|
27
|
-
constructor(config: ClawMatrixConfig, peerManager: PeerManager) {
|
|
29
|
+
constructor(config: ClawMatrixConfig, peerManager: PeerManager, gatewayInfo: GatewayInfo) {
|
|
28
30
|
this.config = config;
|
|
29
31
|
this.peerManager = peerManager;
|
|
32
|
+
this.gatewayInfo = gatewayInfo;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
/** Start the local HTTP proxy server for OpenAI-compatible requests. */
|
|
@@ -113,7 +116,11 @@ export class ModelProxy {
|
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
const modelId = body.model;
|
|
116
|
-
|
|
119
|
+
// Check proxyModels for a nodeId routing hint
|
|
120
|
+
const proxyModel = this.config.proxyModels.find((m) => m.id === modelId);
|
|
121
|
+
const route = proxyModel?.nodeId
|
|
122
|
+
? this.peerManager.router.getRoute(proxyModel.nodeId)
|
|
123
|
+
: this.peerManager.router.resolveModel(modelId);
|
|
117
124
|
if (!route) {
|
|
118
125
|
return {
|
|
119
126
|
status: 404,
|
|
@@ -372,11 +379,13 @@ export class ModelProxy {
|
|
|
372
379
|
}
|
|
373
380
|
|
|
374
381
|
try {
|
|
375
|
-
const
|
|
382
|
+
const { port, authHeader } = this.gatewayInfo;
|
|
383
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
384
|
+
if (authHeader) headers["Authorization"] = authHeader;
|
|
376
385
|
|
|
377
|
-
const response = await fetch(
|
|
386
|
+
const response = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
|
|
378
387
|
method: "POST",
|
|
379
|
-
headers
|
|
388
|
+
headers,
|
|
380
389
|
body: JSON.stringify({
|
|
381
390
|
model: `${model.provider}/${model.id}`,
|
|
382
391
|
messages: payload.messages,
|
package/src/peer-manager.ts
CHANGED
|
@@ -256,6 +256,9 @@ export class PeerManager extends EventEmitter<PeerManagerEvents> {
|
|
|
256
256
|
|
|
257
257
|
// ── Message handling ───────────────────────────────────────────
|
|
258
258
|
private onFrame(frame: AnyClusterFrame, from: Connection) {
|
|
259
|
+
// Validate from field: must be the direct peer or a known node (relayed)
|
|
260
|
+
if (frame.from && frame.from !== from.remoteNodeId && !this.router.getRoute(frame.from)) return;
|
|
261
|
+
|
|
259
262
|
if (frame.id && this.router.isDuplicate(frame.id)) return;
|
|
260
263
|
|
|
261
264
|
if (frame.type === "peer_sync") {
|
package/src/tool-proxy.ts
CHANGED
|
@@ -182,10 +182,8 @@ export class ToolProxy {
|
|
|
182
182
|
// ── Security ───────────────────────────────────────────────────
|
|
183
183
|
private isToolAllowed(tool: string, tpConfig: ToolProxyConfig): boolean {
|
|
184
184
|
if (tpConfig.deny.includes(tool)) return false;
|
|
185
|
-
if (tpConfig.allow.length
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
return true;
|
|
185
|
+
if (tpConfig.allow.length === 0 || tpConfig.allow.includes("*")) return true;
|
|
186
|
+
return tpConfig.allow.includes(tool);
|
|
189
187
|
}
|
|
190
188
|
|
|
191
189
|
destroy() {
|
package/src/types.ts
CHANGED
|
@@ -17,7 +17,13 @@ export interface AuthChallenge extends ClusterFrame {
|
|
|
17
17
|
|
|
18
18
|
export interface AuthRequest extends ClusterFrame {
|
|
19
19
|
type: "auth";
|
|
20
|
-
payload: {
|
|
20
|
+
payload: {
|
|
21
|
+
nodeId: string;
|
|
22
|
+
sig: string;
|
|
23
|
+
agents?: AgentInfo[];
|
|
24
|
+
models?: ModelInfo[];
|
|
25
|
+
tags?: string[];
|
|
26
|
+
};
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
export interface AuthOk extends ClusterFrame {
|