clawmatrix 0.2.9 → 0.3.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/LICENSE +27 -0
- package/README.md +123 -12
- package/package.json +2 -1
- package/src/acp-proxy.ts +433 -70
- package/src/cli.ts +478 -10
- package/src/cluster-service.ts +158 -14
- package/src/compat.ts +0 -6
- package/src/config.ts +17 -5
- package/src/connection.ts +61 -55
- package/src/e2e/helpers.ts +1 -5
- package/src/file-transfer.ts +721 -0
- package/src/handoff.ts +21 -8
- package/src/health-tracker.ts +6 -1
- package/src/index.ts +245 -11
- package/src/knowledge-sync.ts +74 -7
- package/src/model-proxy.ts +35 -10
- package/src/peer-manager.ts +84 -13
- package/src/rate-limiter.ts +16 -10
- package/src/router.ts +115 -33
- package/src/sentinel-manager.ts +59 -7
- package/src/sentinel.ts +13 -3
- package/src/terminal.ts +2 -1
- package/src/tool-proxy.ts +12 -4
- package/src/tools/cluster-diagnostic.ts +5 -2
- package/src/tools/cluster-edit.ts +2 -1
- package/src/tools/cluster-events.ts +3 -1
- package/src/tools/cluster-exec.ts +2 -0
- package/src/tools/cluster-handoff.ts +3 -1
- package/src/tools/cluster-peers.ts +3 -1
- package/src/tools/cluster-read.ts +4 -1
- package/src/tools/cluster-send.ts +2 -1
- package/src/tools/cluster-terminal.ts +4 -7
- package/src/tools/cluster-tool.ts +2 -2
- package/src/tools/cluster-transfer.ts +91 -0
- package/src/tools/cluster-write.ts +3 -1
- package/src/types.ts +191 -2
- package/src/web.ts +2 -10
- package/src/web-ui.ts +0 -1622
|
@@ -10,9 +10,10 @@ export function createClusterDiagnosticTool(): AnyAgentTool {
|
|
|
10
10
|
label: "Cluster Diagnostic",
|
|
11
11
|
description:
|
|
12
12
|
"Diagnose or execute commands on a remote node's sentinel process. " +
|
|
13
|
-
"Works even when the remote OpenClaw gateway is down. " +
|
|
13
|
+
"Works even when the remote OpenClaw gateway is down (sentinel is a lightweight always-on process). " +
|
|
14
14
|
"Use action 'status' to check if the remote gateway is alive, " +
|
|
15
|
-
"or 'exec' to run a shell command on the remote machine."
|
|
15
|
+
"or 'exec' to run a shell command on the remote machine. " +
|
|
16
|
+
"Note: exec runs commands without restrictions — only use in trusted networks.",
|
|
16
17
|
parameters: {
|
|
17
18
|
type: "object",
|
|
18
19
|
properties: {
|
|
@@ -88,6 +89,8 @@ export function createClusterDiagnosticTool(): AnyAgentTool {
|
|
|
88
89
|
};
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
// TODO(security): exec 允许任何已认证 peer 在远程 sentinel 执行任意命令,无 allowlist 或 capability check。
|
|
93
|
+
// 当前仅用于受信任网络。开放前需添加命令白名单或 per-peer 授权。
|
|
91
94
|
if (action === "exec") {
|
|
92
95
|
if (!command) {
|
|
93
96
|
return {
|
|
@@ -7,7 +7,8 @@ export function createClusterEditTool(): AnyAgentTool {
|
|
|
7
7
|
label: "Cluster Edit",
|
|
8
8
|
description:
|
|
9
9
|
"Edit a file on a remote cluster node by replacing exact text. " +
|
|
10
|
-
"
|
|
10
|
+
"IMPORTANT: Use cluster_read first to get exact file content — oldText must match precisely (including whitespace and indentation). " +
|
|
11
|
+
"For creating new files use cluster_write instead.",
|
|
11
12
|
parameters: {
|
|
12
13
|
type: "object",
|
|
13
14
|
properties: {
|
|
@@ -6,7 +6,9 @@ export function createClusterEventsTool(): AnyAgentTool {
|
|
|
6
6
|
name: "cluster_events",
|
|
7
7
|
label: "Cluster Events",
|
|
8
8
|
description:
|
|
9
|
-
"Query and consume events from external sources (
|
|
9
|
+
"Query and consume events from external sources (iOS Shortcuts, webhooks, etc.). " +
|
|
10
|
+
"Common types: message_received, location_change, battery_status. " +
|
|
11
|
+
"Events are persistent until consumed. Requires web.enabled in config.",
|
|
10
12
|
parameters: {
|
|
11
13
|
type: "object",
|
|
12
14
|
properties: {
|
|
@@ -7,6 +7,8 @@ export function createClusterExecTool(): AnyAgentTool {
|
|
|
7
7
|
label: "Cluster Exec",
|
|
8
8
|
description:
|
|
9
9
|
"Execute a shell command on a remote cluster node. " +
|
|
10
|
+
"Use for file/shell operations on remote machines. " +
|
|
11
|
+
"Returns {stdout, stderr, exitCode}. " +
|
|
10
12
|
"Use nodeId or 'tags:<tag>' to specify the target.",
|
|
11
13
|
parameters: {
|
|
12
14
|
type: "object",
|
|
@@ -6,7 +6,9 @@ export function createClusterHandoffTool(): AnyAgentTool {
|
|
|
6
6
|
name: "cluster_handoff",
|
|
7
7
|
label: "Cluster Handoff",
|
|
8
8
|
description:
|
|
9
|
-
"Hand off a task to another agent in the cluster and wait for the result. " +
|
|
9
|
+
"Hand off a complex task to another agent in the cluster and wait for the result. " +
|
|
10
|
+
"Use for multi-step tasks that require the remote agent's full capabilities. " +
|
|
11
|
+
"If the remote agent needs more info, response includes inputRequired=true — use cluster_handoff_reply to continue. " +
|
|
10
12
|
"Use agent ID (e.g. 'coder') or tag query (e.g. 'tags:coding') as target.",
|
|
11
13
|
parameters: {
|
|
12
14
|
type: "object",
|
|
@@ -6,7 +6,9 @@ export function createClusterPeersTool(): AnyAgentTool {
|
|
|
6
6
|
name: "cluster_peers",
|
|
7
7
|
label: "Cluster Peers",
|
|
8
8
|
description:
|
|
9
|
-
"List all
|
|
9
|
+
"List all peers in the cluster with their agents, models, tools, tags, and connection status. " +
|
|
10
|
+
"Call this first to discover what's available before using other cluster tools. " +
|
|
11
|
+
"Status: direct (connected), relay (via another node), sentinel-only (gateway down, sentinel up), satellite (HTTP polling device), unreachable.",
|
|
10
12
|
parameters: {
|
|
11
13
|
type: "object",
|
|
12
14
|
properties: {},
|
|
@@ -5,7 +5,10 @@ export function createClusterReadTool(): AnyAgentTool {
|
|
|
5
5
|
return {
|
|
6
6
|
name: "cluster_read",
|
|
7
7
|
label: "Cluster Read",
|
|
8
|
-
description:
|
|
8
|
+
description:
|
|
9
|
+
"Read a file from a remote cluster node. " +
|
|
10
|
+
"Returns {content} with the file text. " +
|
|
11
|
+
"Use nodeId or 'tags:<tag>' to specify the target.",
|
|
9
12
|
parameters: {
|
|
10
13
|
type: "object",
|
|
11
14
|
properties: {
|
|
@@ -6,7 +6,8 @@ export function createClusterSendTool(): AnyAgentTool {
|
|
|
6
6
|
name: "cluster_send",
|
|
7
7
|
label: "Cluster Send",
|
|
8
8
|
description:
|
|
9
|
-
"Send a one-way
|
|
9
|
+
"Send a one-way notification to a remote agent. Fire-and-forget: does not wait for a response. " +
|
|
10
|
+
"Use cluster_handoff instead when you need the remote agent to process a task and return a result.",
|
|
10
11
|
parameters: {
|
|
11
12
|
type: "object",
|
|
12
13
|
properties: {
|
|
@@ -7,13 +7,10 @@ export function createClusterTerminalTool(): AnyAgentTool {
|
|
|
7
7
|
label: "Cluster Terminal",
|
|
8
8
|
description:
|
|
9
9
|
"Interactive terminal (PTY) on a remote cluster node. " +
|
|
10
|
-
"
|
|
11
|
-
'"open"
|
|
12
|
-
'"
|
|
13
|
-
|
|
14
|
-
'"resize" — resize the terminal. ' +
|
|
15
|
-
'"list" — list active terminal sessions. ' +
|
|
16
|
-
'"close" — close a terminal session.',
|
|
10
|
+
"Workflow: open → input/read (repeat) → close. " +
|
|
11
|
+
'Actions: "open" (requires node), "input" (requires sessionId + data), "read" (requires sessionId), ' +
|
|
12
|
+
'"resize" (requires sessionId), "list" (no params), "close" (requires sessionId). ' +
|
|
13
|
+
"Prefer cluster_exec for one-shot commands; use this for interactive/stateful sessions.",
|
|
17
14
|
parameters: {
|
|
18
15
|
type: "object",
|
|
19
16
|
properties: {
|
|
@@ -7,8 +7,8 @@ export function createClusterToolInvokeTool(): AnyAgentTool {
|
|
|
7
7
|
label: "Cluster Tool Invoke",
|
|
8
8
|
description:
|
|
9
9
|
"Invoke any tool on a remote cluster node by name. " +
|
|
10
|
-
"
|
|
11
|
-
"
|
|
10
|
+
"Preferred for device-specific tools (screenshot, battery, location, clipboard, etc.). " +
|
|
11
|
+
"Run cluster_peers first to discover available tools, or use CLI: clawmatrix tools --describe <tool> for params. " +
|
|
12
12
|
"Use nodeId or 'tags:<tag>' to specify the target.",
|
|
13
13
|
parameters: {
|
|
14
14
|
type: "object",
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { AnyAgentTool } from "openclaw/plugin-sdk";
|
|
2
|
+
import { getClusterRuntime } from "../cluster-service.ts";
|
|
3
|
+
|
|
4
|
+
export function createClusterTransferTool(): AnyAgentTool {
|
|
5
|
+
return {
|
|
6
|
+
name: "cluster_transfer",
|
|
7
|
+
label: "Cluster File Transfer",
|
|
8
|
+
description:
|
|
9
|
+
"Transfer a file between the local node and a remote cluster node. " +
|
|
10
|
+
"Supports large files (up to 100MB) with chunked transfer and SHA-256 integrity check. " +
|
|
11
|
+
"Specify source_node to pull from remote, or target_node to push to remote.",
|
|
12
|
+
parameters: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
source_node: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "Source nodeId (omit for local). Exactly one of source_node or target_node must be provided.",
|
|
18
|
+
},
|
|
19
|
+
source_path: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "File path on the source node",
|
|
22
|
+
},
|
|
23
|
+
target_node: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "Target nodeId (omit for local). Exactly one of source_node or target_node must be provided.",
|
|
26
|
+
},
|
|
27
|
+
target_path: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "File path on the target node",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
required: ["source_path", "target_path"],
|
|
33
|
+
},
|
|
34
|
+
async execute(_toolCallId, params) {
|
|
35
|
+
const { source_node, source_path, target_node, target_path } = params as {
|
|
36
|
+
source_node?: string;
|
|
37
|
+
source_path: string;
|
|
38
|
+
target_node?: string;
|
|
39
|
+
target_path: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Validate: exactly one of source_node or target_node must be provided
|
|
43
|
+
if (source_node && target_node) {
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: "text" as const, text: "Error: Provide either source_node or target_node, not both." }],
|
|
46
|
+
details: { error: true },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (!source_node && !target_node) {
|
|
50
|
+
return {
|
|
51
|
+
content: [{ type: "text" as const, text: "Error: Provide either source_node (to pull) or target_node (to push)." }],
|
|
52
|
+
details: { error: true },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const runtime = getClusterRuntime();
|
|
58
|
+
const ftm = runtime.fileTransferManager;
|
|
59
|
+
if (!ftm) {
|
|
60
|
+
return {
|
|
61
|
+
content: [{ type: "text" as const, text: "Error: File transfer is not enabled on this node." }],
|
|
62
|
+
details: { error: true },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let result;
|
|
67
|
+
if (source_node) {
|
|
68
|
+
// Pull: remote → local
|
|
69
|
+
result = await ftm.pullFile(source_node, source_path, target_path);
|
|
70
|
+
} else {
|
|
71
|
+
// Push: local → remote
|
|
72
|
+
result = await ftm.pushFile(target_node!, source_path, target_path);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const text = result.success
|
|
76
|
+
? `Transfer complete: ${result.bytesTransferred} bytes transferred.`
|
|
77
|
+
: `Transfer failed: ${result.error}`;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
content: [{ type: "text" as const, text }],
|
|
81
|
+
details: result,
|
|
82
|
+
};
|
|
83
|
+
} catch (err) {
|
|
84
|
+
return {
|
|
85
|
+
content: [{ type: "text" as const, text: `Transfer error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
86
|
+
details: { error: true },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -5,7 +5,9 @@ export function createClusterWriteTool(): AnyAgentTool {
|
|
|
5
5
|
return {
|
|
6
6
|
name: "cluster_write",
|
|
7
7
|
label: "Cluster Write",
|
|
8
|
-
description:
|
|
8
|
+
description:
|
|
9
|
+
"Write content to a file on a remote cluster node (creates or overwrites). " +
|
|
10
|
+
"Use nodeId or 'tags:<tag>' to specify the target.",
|
|
9
11
|
parameters: {
|
|
10
12
|
type: "object",
|
|
11
13
|
properties: {
|
package/src/types.ts
CHANGED
|
@@ -317,6 +317,68 @@ export interface ToolBatchResponse extends ClusterFrame {
|
|
|
317
317
|
};
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
// ── File transfer ─────────────────────────────────────────────────
|
|
321
|
+
export interface FileTransferInit extends ClusterFrame {
|
|
322
|
+
type: "file_transfer_init";
|
|
323
|
+
id: string;
|
|
324
|
+
payload: {
|
|
325
|
+
sessionId: string;
|
|
326
|
+
direction: "push" | "pull";
|
|
327
|
+
filePath: string;
|
|
328
|
+
targetPath: string;
|
|
329
|
+
fileSize: number;
|
|
330
|
+
totalChunks: number;
|
|
331
|
+
chunkSize: number;
|
|
332
|
+
checksum: string; // SHA-256 hex
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export interface FileTransferAck extends ClusterFrame {
|
|
337
|
+
type: "file_transfer_ack";
|
|
338
|
+
id: string;
|
|
339
|
+
payload: {
|
|
340
|
+
sessionId: string;
|
|
341
|
+
accepted: boolean;
|
|
342
|
+
error?: string;
|
|
343
|
+
// Pull mode: responder includes file metadata
|
|
344
|
+
fileSize?: number;
|
|
345
|
+
totalChunks?: number;
|
|
346
|
+
checksum?: string;
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export interface FileTransferChunk extends ClusterFrame {
|
|
351
|
+
type: "file_transfer_chunk";
|
|
352
|
+
id: string;
|
|
353
|
+
payload: {
|
|
354
|
+
sessionId: string;
|
|
355
|
+
chunkIndex: number;
|
|
356
|
+
data: string; // base64-encoded
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export interface FileTransferChunkAck extends ClusterFrame {
|
|
361
|
+
type: "file_transfer_chunk_ack";
|
|
362
|
+
id: string;
|
|
363
|
+
payload: {
|
|
364
|
+
sessionId: string;
|
|
365
|
+
chunkIndex: number;
|
|
366
|
+
success: boolean;
|
|
367
|
+
error?: string;
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export interface FileTransferComplete extends ClusterFrame {
|
|
372
|
+
type: "file_transfer_complete";
|
|
373
|
+
id: string;
|
|
374
|
+
payload: {
|
|
375
|
+
sessionId: string;
|
|
376
|
+
success: boolean;
|
|
377
|
+
error?: string;
|
|
378
|
+
bytesTransferred?: number;
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
320
382
|
// ── Device info ───────────────────────────────────────────────────
|
|
321
383
|
export interface DeviceInfo {
|
|
322
384
|
os: string; // e.g. "Darwin 24.6.0", "Linux 6.1.0"
|
|
@@ -358,10 +420,20 @@ export interface ModelInfo {
|
|
|
358
420
|
compat?: ModelCompatInfo;
|
|
359
421
|
}
|
|
360
422
|
|
|
423
|
+
export interface ToolCatalogEntry {
|
|
424
|
+
name: string;
|
|
425
|
+
description: string;
|
|
426
|
+
usage?: string;
|
|
427
|
+
/** JSON Schema for the tool's input parameters (for LLM callers to construct invocations). */
|
|
428
|
+
inputSchema?: Record<string, unknown>;
|
|
429
|
+
}
|
|
430
|
+
|
|
361
431
|
export interface ToolProxyInfo {
|
|
362
432
|
enabled: boolean;
|
|
363
433
|
allow: string[];
|
|
364
434
|
deny: string[];
|
|
435
|
+
/** Optional tool catalog with descriptions and usage hints for remote callers. */
|
|
436
|
+
catalog?: ToolCatalogEntry[];
|
|
365
437
|
}
|
|
366
438
|
|
|
367
439
|
export interface AcpAgentInfo {
|
|
@@ -419,6 +491,24 @@ export interface HealthSyncFrame extends ClusterFrame {
|
|
|
419
491
|
};
|
|
420
492
|
}
|
|
421
493
|
|
|
494
|
+
export interface AvailabilityRequest extends ClusterFrame {
|
|
495
|
+
type: "availability_req";
|
|
496
|
+
id: string;
|
|
497
|
+
payload: {
|
|
498
|
+
range: "24h" | "7d" | "90d";
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export interface AvailabilityResponse extends ClusterFrame {
|
|
503
|
+
type: "availability_res";
|
|
504
|
+
id: string;
|
|
505
|
+
payload: {
|
|
506
|
+
success: boolean;
|
|
507
|
+
data?: unknown;
|
|
508
|
+
error?: string;
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
422
512
|
// ── Diagnostic (sentinel) ────────────────────────────────────────
|
|
423
513
|
export interface DiagnosticExec extends ClusterFrame {
|
|
424
514
|
type: "diagnostic_exec";
|
|
@@ -479,9 +569,10 @@ export interface AcpStreamChunk extends ClusterFrame {
|
|
|
479
569
|
id: string;
|
|
480
570
|
payload: {
|
|
481
571
|
delta: string;
|
|
482
|
-
event?: string; // "agent_message_chunk" | "tool_call" | etc.
|
|
572
|
+
event?: string; // "agent_message_chunk" | "tool_call" | "plan" | "available_commands" | "config_options" | "usage" | "session_info" | etc.
|
|
483
573
|
done: boolean;
|
|
484
574
|
sessionId?: string; // included for session watchers (multi-device sync)
|
|
575
|
+
data?: unknown; // structured data for rich events (plan entries, slash commands, config options, usage stats)
|
|
485
576
|
};
|
|
486
577
|
}
|
|
487
578
|
|
|
@@ -497,6 +588,10 @@ export interface AcpTaskResponse extends ClusterFrame {
|
|
|
497
588
|
acpSessionId?: string; // ACP-level session ID for future resume
|
|
498
589
|
stopReason?: string; // ACP stop reason
|
|
499
590
|
error?: string;
|
|
591
|
+
configOptions?: AcpConfigOption[]; // initial config options (model, thinking level, etc.)
|
|
592
|
+
slashCommands?: AcpSlashCommand[]; // available slash commands
|
|
593
|
+
availableModes?: AcpModeInfo[]; // available session modes
|
|
594
|
+
currentModeId?: string; // current mode
|
|
500
595
|
};
|
|
501
596
|
}
|
|
502
597
|
|
|
@@ -525,6 +620,7 @@ export interface AcpSessionInfo {
|
|
|
525
620
|
description?: string; // first user message (for display in session list)
|
|
526
621
|
updatedAt?: string;
|
|
527
622
|
agent?: string; // which ACP agent (claude, codex, etc.)
|
|
623
|
+
status?: "active" | "idle"; // active = in-memory session with daemon, idle = persisted on disk
|
|
528
624
|
}
|
|
529
625
|
|
|
530
626
|
export interface AcpListRequest extends ClusterFrame {
|
|
@@ -626,6 +722,86 @@ export interface AcpGetModesResponse extends ClusterFrame {
|
|
|
626
722
|
};
|
|
627
723
|
}
|
|
628
724
|
|
|
725
|
+
// ── ACP config options ───────────────────────────────────────────
|
|
726
|
+
|
|
727
|
+
export interface AcpConfigOption {
|
|
728
|
+
id: string;
|
|
729
|
+
name: string;
|
|
730
|
+
type: "select" | "boolean";
|
|
731
|
+
currentValue: string | boolean;
|
|
732
|
+
options?: Array<{ id: string; name: string; description?: string }>;
|
|
733
|
+
description?: string;
|
|
734
|
+
category?: string; // "mode" | "model" | "thought_level" | custom
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export interface AcpSetConfigRequest extends ClusterFrame {
|
|
738
|
+
type: "acp_set_config";
|
|
739
|
+
id: string;
|
|
740
|
+
payload: {
|
|
741
|
+
sessionId: string;
|
|
742
|
+
configId: string;
|
|
743
|
+
value: string | boolean;
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export interface AcpSetConfigResponse extends ClusterFrame {
|
|
748
|
+
type: "acp_set_config_res";
|
|
749
|
+
id: string;
|
|
750
|
+
payload: {
|
|
751
|
+
success: boolean;
|
|
752
|
+
configOptions?: AcpConfigOption[];
|
|
753
|
+
error?: string;
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ── ACP subscribe / observe ──────────────────────────────────────
|
|
758
|
+
|
|
759
|
+
export interface AcpSubscribeRequest extends ClusterFrame {
|
|
760
|
+
type: "acp_subscribe";
|
|
761
|
+
id: string;
|
|
762
|
+
payload: {
|
|
763
|
+
sessionId: string; // ClawMatrix session ID to observe
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
export interface AcpSubscribeResponse extends ClusterFrame {
|
|
768
|
+
type: "acp_subscribe_res";
|
|
769
|
+
id: string;
|
|
770
|
+
payload: {
|
|
771
|
+
success: boolean;
|
|
772
|
+
history?: ChatHistoryMessage[]; // snapshot at subscribe time
|
|
773
|
+
error?: string;
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
export interface AcpUnsubscribeRequest extends ClusterFrame {
|
|
778
|
+
type: "acp_unsubscribe";
|
|
779
|
+
payload: {
|
|
780
|
+
sessionId: string;
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** Pushed to all peers when a session is created, its title changes, or it completes. */
|
|
785
|
+
export interface AcpSessionNotify extends ClusterFrame {
|
|
786
|
+
type: "acp_session_notify";
|
|
787
|
+
payload: {
|
|
788
|
+
sessionId: string;
|
|
789
|
+
nodeId: string;
|
|
790
|
+
event: "created" | "updated" | "completed";
|
|
791
|
+
title?: string;
|
|
792
|
+
updatedAt?: string;
|
|
793
|
+
agent?: string;
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// ── ACP slash commands ──────────────────────────────────────────
|
|
798
|
+
|
|
799
|
+
export interface AcpSlashCommand {
|
|
800
|
+
name: string;
|
|
801
|
+
description: string;
|
|
802
|
+
input?: { hint?: string };
|
|
803
|
+
}
|
|
804
|
+
|
|
629
805
|
// ── Chat history ──────────────────────────────────────────────────
|
|
630
806
|
|
|
631
807
|
export interface ChatHistoryMessage {
|
|
@@ -828,6 +1004,12 @@ export type AnyClusterFrame =
|
|
|
828
1004
|
| AcpSetModeResponse
|
|
829
1005
|
| AcpGetModesRequest
|
|
830
1006
|
| AcpGetModesResponse
|
|
1007
|
+
| AcpSetConfigRequest
|
|
1008
|
+
| AcpSetConfigResponse
|
|
1009
|
+
| AcpSubscribeRequest
|
|
1010
|
+
| AcpSubscribeResponse
|
|
1011
|
+
| AcpUnsubscribeRequest
|
|
1012
|
+
| AcpSessionNotify
|
|
831
1013
|
| ChatHistoryRequest
|
|
832
1014
|
| ChatHistoryResponse
|
|
833
1015
|
| PeerApprovalNotify
|
|
@@ -840,4 +1022,11 @@ export type AnyClusterFrame =
|
|
|
840
1022
|
| TerminalResize
|
|
841
1023
|
| TerminalCloseRequest
|
|
842
1024
|
| TerminalCloseResponse
|
|
843
|
-
| HealthSyncFrame
|
|
1025
|
+
| HealthSyncFrame
|
|
1026
|
+
| AvailabilityRequest
|
|
1027
|
+
| AvailabilityResponse
|
|
1028
|
+
| FileTransferInit
|
|
1029
|
+
| FileTransferAck
|
|
1030
|
+
| FileTransferChunk
|
|
1031
|
+
| FileTransferChunkAck
|
|
1032
|
+
| FileTransferComplete;
|
package/src/web.ts
CHANGED
|
@@ -5,7 +5,6 @@ import type { ClawMatrixConfig } from "./config.ts";
|
|
|
5
5
|
import type { SatelliteContext, IngestedEvent } from "./types.ts";
|
|
6
6
|
import type { HealthTracker } from "./health-tracker.ts";
|
|
7
7
|
import { timingSafeEqual } from "./auth.ts";
|
|
8
|
-
import { renderDashboard } from "./web-ui.ts";
|
|
9
8
|
import { readBody } from "./http-utils.ts";
|
|
10
9
|
|
|
11
10
|
const COOKIE_NAME = "clawmatrix_token";
|
|
@@ -135,13 +134,6 @@ export class WebHandler {
|
|
|
135
134
|
return true;
|
|
136
135
|
}
|
|
137
136
|
|
|
138
|
-
// Serve dashboard HTML (auth checked client-side via API)
|
|
139
|
-
if (path === "/" && req.method === "GET") {
|
|
140
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
141
|
-
res.end(renderDashboard(this.config.nodeId));
|
|
142
|
-
return true;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
137
|
// All /api/* routes require auth (async)
|
|
146
138
|
if (path.startsWith("/api/")) {
|
|
147
139
|
this.handleAuthenticatedRoute(req, res, path);
|
|
@@ -224,7 +216,7 @@ export class WebHandler {
|
|
|
224
216
|
private async checkAuth(req: IncomingMessage): Promise<boolean> {
|
|
225
217
|
// Check Authorization header
|
|
226
218
|
const authHeader = req.headers.authorization;
|
|
227
|
-
if (authHeader?.startsWith("Bearer ") &&
|
|
219
|
+
if (authHeader?.startsWith("Bearer ") && timingSafeEqual(authHeader.slice(7), this.token)) {
|
|
228
220
|
return true;
|
|
229
221
|
}
|
|
230
222
|
|
|
@@ -658,7 +650,7 @@ export class WebHandler {
|
|
|
658
650
|
ssid: ctx.ssid || undefined,
|
|
659
651
|
ip: ctx.ip || undefined,
|
|
660
652
|
router: ctx.router || undefined,
|
|
661
|
-
cellular: !ctx.ssid,
|
|
653
|
+
cellular: typeof ctx.cellular === "boolean" ? ctx.cellular : !ctx.ssid,
|
|
662
654
|
country: ctx.country || undefined,
|
|
663
655
|
tools: Array.isArray(ctx.tools) ? ctx.tools : undefined,
|
|
664
656
|
ts: Date.now(),
|