openclaw-bridge 0.3.2 → 0.4.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 +43 -16
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +809 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +64 -0
- package/dist/discovery.d.ts +4 -0
- package/dist/discovery.js +6 -0
- package/dist/file-ops.d.ts +22 -0
- package/dist/file-ops.js +253 -0
- package/dist/heartbeat.d.ts +21 -0
- package/dist/heartbeat.js +152 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +624 -0
- package/dist/manager/hub-client.d.ts +18 -0
- package/dist/manager/hub-client.js +89 -0
- package/dist/manager/local-manager.d.ts +12 -0
- package/dist/manager/local-manager.js +117 -0
- package/dist/manager/pm2-bridge.d.ts +17 -0
- package/dist/manager/pm2-bridge.js +113 -0
- package/dist/message-relay.d.ts +32 -0
- package/dist/message-relay.js +229 -0
- package/dist/permissions.d.ts +3 -0
- package/dist/permissions.js +14 -0
- package/dist/registry.d.ts +13 -0
- package/dist/registry.js +103 -0
- package/dist/restart.d.ts +15 -0
- package/dist/restart.js +107 -0
- package/dist/router.d.ts +11 -0
- package/dist/router.js +18 -0
- package/dist/session.d.ts +11 -0
- package/dist/session.js +21 -0
- package/dist/types.d.ts +90 -0
- package/dist/types.js +1 -0
- package/openclaw.plugin.json +6 -92
- package/package.json +15 -5
- package/src/cli.ts +0 -842
- package/src/config.ts +0 -72
- package/src/discovery.ts +0 -17
- package/src/file-ops.ts +0 -320
- package/src/heartbeat.ts +0 -196
- package/src/index.ts +0 -681
- package/src/manager/hub-client.ts +0 -114
- package/src/manager/local-manager.ts +0 -121
- package/src/manager/pm2-bridge.ts +0 -125
- package/src/message-relay.ts +0 -184
- package/src/permissions.ts +0 -18
- package/src/registry.ts +0 -107
- package/src/restart.ts +0 -137
- package/src/router.ts +0 -40
- package/src/session.ts +0 -33
- package/src/types.ts +0 -100
- package/tsconfig.json +0 -14
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import WebSocket from "ws";
|
|
2
|
-
import { hostname } from "node:os";
|
|
3
|
-
import type { PluginLogger } from "../types.js";
|
|
4
|
-
|
|
5
|
-
export class ManagerHubClient {
|
|
6
|
-
private hubUrl: string;
|
|
7
|
-
private apiKey: string;
|
|
8
|
-
private managerPass: string;
|
|
9
|
-
private machineId: string;
|
|
10
|
-
private ws: WebSocket | null = null;
|
|
11
|
-
private _connected = false;
|
|
12
|
-
private reconnectDelay = 1000;
|
|
13
|
-
private logger: PluginLogger;
|
|
14
|
-
onCommand: ((msg: any) => void) | null = null;
|
|
15
|
-
|
|
16
|
-
constructor(
|
|
17
|
-
hubUrl: string,
|
|
18
|
-
apiKey: string,
|
|
19
|
-
managerPass: string,
|
|
20
|
-
logger: PluginLogger,
|
|
21
|
-
) {
|
|
22
|
-
this.hubUrl = hubUrl;
|
|
23
|
-
this.apiKey = apiKey;
|
|
24
|
-
this.managerPass = managerPass;
|
|
25
|
-
this.machineId = hostname();
|
|
26
|
-
this.logger = logger;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
get connected(): boolean {
|
|
30
|
-
return this._connected;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async connect(): Promise<void> {
|
|
34
|
-
return new Promise((resolve, reject) => {
|
|
35
|
-
const wsUrl = this.hubUrl.replace(/^http/, "ws") + "/ws/manager";
|
|
36
|
-
this.ws = new WebSocket(wsUrl);
|
|
37
|
-
|
|
38
|
-
this.ws.on("open", () => {
|
|
39
|
-
this.ws!.send(
|
|
40
|
-
JSON.stringify({
|
|
41
|
-
type: "auth",
|
|
42
|
-
role: "manager",
|
|
43
|
-
machineId: this.machineId,
|
|
44
|
-
apiKey: this.apiKey,
|
|
45
|
-
managerPass: this.managerPass,
|
|
46
|
-
}),
|
|
47
|
-
);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
this.ws.on("message", (data) => {
|
|
51
|
-
const msg = JSON.parse(data.toString());
|
|
52
|
-
if (msg.type === "auth_ok") {
|
|
53
|
-
this._connected = true;
|
|
54
|
-
this.reconnectDelay = 1000;
|
|
55
|
-
this.logger.info(
|
|
56
|
-
`[local-manager] Hub connected as ${this.machineId}`,
|
|
57
|
-
);
|
|
58
|
-
resolve();
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
if (msg.type === "auth_error") {
|
|
62
|
-
reject(new Error(msg.reason));
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
if (msg.type === "manager_command" && this.onCommand) {
|
|
66
|
-
this.onCommand(msg);
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
this.ws.on("close", () => {
|
|
71
|
-
this._connected = false;
|
|
72
|
-
this.logger.warn(
|
|
73
|
-
`[local-manager] Hub disconnected, reconnecting in ${this.reconnectDelay}ms...`,
|
|
74
|
-
);
|
|
75
|
-
setTimeout(() => {
|
|
76
|
-
this.connect().catch(() => {
|
|
77
|
-
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30_000);
|
|
78
|
-
});
|
|
79
|
-
}, this.reconnectDelay);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
this.ws.on("error", (err) => {
|
|
83
|
-
this.logger.error(`[local-manager] Hub error: ${err.message}`);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
sendStatus(agents: any[], logs?: Record<string, string>): void {
|
|
89
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
90
|
-
this.ws.send(
|
|
91
|
-
JSON.stringify({
|
|
92
|
-
type: "manager_status",
|
|
93
|
-
machineId: this.machineId,
|
|
94
|
-
agents,
|
|
95
|
-
logs,
|
|
96
|
-
}),
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
sendResult(action: string, target: string, success: boolean): void {
|
|
102
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
103
|
-
this.ws.send(JSON.stringify({ type: "manager_result", action, target, success }));
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
disconnect(): void {
|
|
108
|
-
if (this.ws) {
|
|
109
|
-
this.ws.close();
|
|
110
|
-
this.ws = null;
|
|
111
|
-
this._connected = false;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import type { PluginLogger, LocalManagerConfig } from "../types.js";
|
|
5
|
-
import { ManagerHubClient } from "./hub-client.js";
|
|
6
|
-
import {
|
|
7
|
-
listProcesses,
|
|
8
|
-
getProcessLogs,
|
|
9
|
-
restartProcess,
|
|
10
|
-
stopProcess,
|
|
11
|
-
startProcess,
|
|
12
|
-
stopAll,
|
|
13
|
-
} from "./pm2-bridge.js";
|
|
14
|
-
|
|
15
|
-
const LOCK_FILE = join(tmpdir(), "openclaw-local-manager.lock");
|
|
16
|
-
|
|
17
|
-
function acquireLock(): boolean {
|
|
18
|
-
if (existsSync(LOCK_FILE)) {
|
|
19
|
-
try {
|
|
20
|
-
const pid = parseInt(readFileSync(LOCK_FILE, "utf-8").trim(), 10);
|
|
21
|
-
process.kill(pid, 0);
|
|
22
|
-
return false;
|
|
23
|
-
} catch {
|
|
24
|
-
// Process is dead, steal the lock
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
writeFileSync(LOCK_FILE, String(process.pid));
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function releaseLock(): void {
|
|
32
|
-
try { unlinkSync(LOCK_FILE); } catch {}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export class LocalManager {
|
|
36
|
-
private config: LocalManagerConfig;
|
|
37
|
-
private apiKey: string;
|
|
38
|
-
private logger: PluginLogger;
|
|
39
|
-
private hubClient: ManagerHubClient | null = null;
|
|
40
|
-
private statusTimer: ReturnType<typeof setInterval> | null = null;
|
|
41
|
-
private hasLock = false;
|
|
42
|
-
|
|
43
|
-
constructor(config: LocalManagerConfig, apiKey: string, logger: PluginLogger) {
|
|
44
|
-
this.config = config;
|
|
45
|
-
this.apiKey = apiKey;
|
|
46
|
-
this.logger = logger;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async start(): Promise<void> {
|
|
50
|
-
if (!this.config.enabled) return;
|
|
51
|
-
|
|
52
|
-
this.hasLock = acquireLock();
|
|
53
|
-
if (!this.hasLock) {
|
|
54
|
-
this.logger.info("[local-manager] Another instance is already managing this machine, skipping");
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
this.hubClient = new ManagerHubClient(
|
|
59
|
-
this.config.hubUrl, this.apiKey, this.config.managerPass, this.logger,
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
this.hubClient.onCommand = async (msg: any) => {
|
|
63
|
-
const processes = await listProcesses();
|
|
64
|
-
let target = msg.target;
|
|
65
|
-
|
|
66
|
-
if (msg.action !== "restart-all" && msg.action !== "stop-all") {
|
|
67
|
-
const match = processes.find((p) => p.agentId === msg.target || p.name === msg.target);
|
|
68
|
-
if (match) {
|
|
69
|
-
target = match.name;
|
|
70
|
-
} else {
|
|
71
|
-
this.logger.error(`[local-manager] No PM2 process found for: ${msg.target}`);
|
|
72
|
-
this.hubClient!.sendResult(msg.action, msg.target, false);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
this.logger.info(`[local-manager] ${msg.action} ${target}`);
|
|
78
|
-
try {
|
|
79
|
-
if (msg.action === "restart") await restartProcess(target);
|
|
80
|
-
else if (msg.action === "stop") await stopProcess(target);
|
|
81
|
-
else if (msg.action === "start") await startProcess(target);
|
|
82
|
-
else if (msg.action === "restart-all") await stopAll();
|
|
83
|
-
else if (msg.action === "stop-all") await stopAll();
|
|
84
|
-
this.hubClient!.sendResult(msg.action, msg.target, true);
|
|
85
|
-
} catch (err: any) {
|
|
86
|
-
this.logger.error(`[local-manager] Failed: ${err.message}`);
|
|
87
|
-
this.hubClient!.sendResult(msg.action, msg.target, false);
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
await this.hubClient.connect();
|
|
93
|
-
} catch (err: any) {
|
|
94
|
-
this.logger.warn(`[local-manager] Hub connect failed: ${err.message}. Will retry.`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
this.statusTimer = setInterval(async () => {
|
|
98
|
-
try {
|
|
99
|
-
const processes = await listProcesses();
|
|
100
|
-
const logs: Record<string, string> = {};
|
|
101
|
-
for (const proc of processes) {
|
|
102
|
-
logs[proc.name] = await getProcessLogs(proc.name);
|
|
103
|
-
}
|
|
104
|
-
if (this.hubClient?.connected) {
|
|
105
|
-
this.hubClient.sendStatus(processes, logs);
|
|
106
|
-
}
|
|
107
|
-
} catch (err: any) {
|
|
108
|
-
this.logger.warn(`[local-manager] Status tick failed: ${err.message}`);
|
|
109
|
-
}
|
|
110
|
-
}, 30_000);
|
|
111
|
-
|
|
112
|
-
this.logger.info("[local-manager] Started (reporting every 30s)");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async stop(): Promise<void> {
|
|
116
|
-
if (this.statusTimer) { clearInterval(this.statusTimer); this.statusTimer = null; }
|
|
117
|
-
if (this.hubClient) { this.hubClient.disconnect(); this.hubClient = null; }
|
|
118
|
-
if (this.hasLock) releaseLock();
|
|
119
|
-
this.logger.info("[local-manager] Stopped");
|
|
120
|
-
}
|
|
121
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { exec } from "node:child_process";
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
|
-
import { platform } from "node:os";
|
|
4
|
-
|
|
5
|
-
const _exec = promisify(exec);
|
|
6
|
-
const execAsync = (cmd: string, opts?: Record<string, unknown>) =>
|
|
7
|
-
_exec(cmd, { windowsHide: true, encoding: "utf-8", ...opts } as any);
|
|
8
|
-
const IS_WIN = platform() === "win32";
|
|
9
|
-
|
|
10
|
-
export interface PM2Process {
|
|
11
|
-
name: string;
|
|
12
|
-
agentId: string;
|
|
13
|
-
pid: number;
|
|
14
|
-
status: string;
|
|
15
|
-
memory: number;
|
|
16
|
-
cpu: number;
|
|
17
|
-
restarts: number;
|
|
18
|
-
uptime: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function killPort(port: number): Promise<void> {
|
|
22
|
-
if (!port) return;
|
|
23
|
-
try {
|
|
24
|
-
if (IS_WIN) {
|
|
25
|
-
const { stdout } = await execAsync(
|
|
26
|
-
`netstat -ano | findstr :${port} | findstr LISTENING`,
|
|
27
|
-
);
|
|
28
|
-
const pids = new Set<string>();
|
|
29
|
-
stdout.split("\n").forEach((line: string) => {
|
|
30
|
-
const pid = line.trim().split(/\s+/).pop();
|
|
31
|
-
if (pid && /^\d+$/.test(pid) && pid !== "0") pids.add(pid);
|
|
32
|
-
});
|
|
33
|
-
for (const pid of pids) {
|
|
34
|
-
await execAsync(`taskkill /F /PID ${pid}`).catch(() => {});
|
|
35
|
-
}
|
|
36
|
-
} else {
|
|
37
|
-
await execAsync(`lsof -ti:${port} | xargs kill -9`).catch(() => {});
|
|
38
|
-
}
|
|
39
|
-
} catch {
|
|
40
|
-
// No process on port
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function getProcessPort(name: string): Promise<number | null> {
|
|
45
|
-
try {
|
|
46
|
-
const { stdout } = await execAsync("pm2 jlist");
|
|
47
|
-
const apps = JSON.parse(stdout);
|
|
48
|
-
const app = apps.find((a: any) => a.name === name);
|
|
49
|
-
if (app?.pm2_env?.env?.OPENCLAW_GATEWAY_PORT) {
|
|
50
|
-
return parseInt(app.pm2_env.env.OPENCLAW_GATEWAY_PORT, 10);
|
|
51
|
-
}
|
|
52
|
-
if (app?.pm2_env?.pm_exec_path) {
|
|
53
|
-
const { readFileSync } = await import("node:fs");
|
|
54
|
-
const dir = app.pm2_env.pm_exec_path.replace(/[/\\]run\.sh$/, "");
|
|
55
|
-
const script = readFileSync(dir + "/run.sh", "utf-8");
|
|
56
|
-
const match = script.match(/OPENCLAW_GATEWAY_PORT="?(\d+)/);
|
|
57
|
-
if (match) return parseInt(match[1], 10);
|
|
58
|
-
}
|
|
59
|
-
} catch {}
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export async function listProcesses(): Promise<PM2Process[]> {
|
|
64
|
-
try {
|
|
65
|
-
const { stdout } = await execAsync("pm2 jlist");
|
|
66
|
-
const apps = JSON.parse(stdout);
|
|
67
|
-
return apps.map((p: any) => ({
|
|
68
|
-
name: p.name,
|
|
69
|
-
agentId: p.name.replace(/^gw-/, ""),
|
|
70
|
-
pid: p.pid,
|
|
71
|
-
status: p.pm2_env?.status || "unknown",
|
|
72
|
-
memory: p.monit?.memory || 0,
|
|
73
|
-
cpu: p.monit?.cpu || 0,
|
|
74
|
-
restarts: p.pm2_env?.restart_time || 0,
|
|
75
|
-
uptime: p.pm2_env?.pm_uptime ? Date.now() - p.pm2_env.pm_uptime : 0,
|
|
76
|
-
}));
|
|
77
|
-
} catch (err: any) {
|
|
78
|
-
return [];
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export async function getProcessLogs(name: string): Promise<string> {
|
|
83
|
-
try {
|
|
84
|
-
const { stdout } = await execAsync(
|
|
85
|
-
`pm2 logs ${name} --nostream --lines 100`,
|
|
86
|
-
{ timeout: 10_000 },
|
|
87
|
-
);
|
|
88
|
-
return stdout;
|
|
89
|
-
} catch {
|
|
90
|
-
return "(logs unavailable)";
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export async function restartProcess(name: string): Promise<void> {
|
|
95
|
-
await execAsync("pm2 stop " + name).catch(() => {});
|
|
96
|
-
const port = await getProcessPort(name);
|
|
97
|
-
if (port) await killPort(port);
|
|
98
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
99
|
-
await execAsync("pm2 restart " + name);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export async function stopProcess(name: string): Promise<void> {
|
|
103
|
-
await execAsync("pm2 stop " + name);
|
|
104
|
-
const port = await getProcessPort(name);
|
|
105
|
-
if (port) await killPort(port);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export async function startProcess(name: string): Promise<void> {
|
|
109
|
-
const port = await getProcessPort(name);
|
|
110
|
-
if (port) await killPort(port);
|
|
111
|
-
await execAsync("pm2 restart " + name);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export async function stopAll(): Promise<void> {
|
|
115
|
-
const procs = await listProcesses();
|
|
116
|
-
await execAsync("pm2 stop all");
|
|
117
|
-
for (const proc of procs) {
|
|
118
|
-
const port = await getProcessPort(proc.name);
|
|
119
|
-
if (port) await killPort(port);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export async function startAll(ecosystemPath: string): Promise<void> {
|
|
124
|
-
await execAsync("pm2 start " + ecosystemPath);
|
|
125
|
-
}
|
package/src/message-relay.ts
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import WebSocket from 'ws';
|
|
2
|
-
import type { PluginLogger, MessageRelayConfig } from './types.js';
|
|
3
|
-
|
|
4
|
-
type MessageHandler = (msg: any) => void;
|
|
5
|
-
|
|
6
|
-
export class MessageRelayClient {
|
|
7
|
-
private ws: WebSocket | null = null;
|
|
8
|
-
private agentId: string;
|
|
9
|
-
private config: MessageRelayConfig;
|
|
10
|
-
private logger: PluginLogger;
|
|
11
|
-
private handlers = new Map<string, MessageHandler[]>();
|
|
12
|
-
private reconnectDelay = 1000;
|
|
13
|
-
private maxReconnectDelay = 30000;
|
|
14
|
-
private shouldReconnect = true;
|
|
15
|
-
private authenticated = false;
|
|
16
|
-
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
17
|
-
private pendingCallbacks = new Map<string, {
|
|
18
|
-
resolve: (msg: any) => void;
|
|
19
|
-
reject: (err: Error) => void;
|
|
20
|
-
timer: ReturnType<typeof setTimeout>;
|
|
21
|
-
}>();
|
|
22
|
-
|
|
23
|
-
constructor(agentId: string, config: MessageRelayConfig, logger: PluginLogger) {
|
|
24
|
-
this.agentId = agentId;
|
|
25
|
-
this.config = config;
|
|
26
|
-
this.logger = logger;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async connect(): Promise<void> {
|
|
30
|
-
// Clean up any existing connection
|
|
31
|
-
if (this.ws) {
|
|
32
|
-
try { this.ws.removeAllListeners(); this.ws.close(); } catch {}
|
|
33
|
-
this.ws = null;
|
|
34
|
-
}
|
|
35
|
-
this.authenticated = false;
|
|
36
|
-
|
|
37
|
-
return new Promise((resolve, reject) => {
|
|
38
|
-
let settled = false;
|
|
39
|
-
const settle = (fn: Function, arg?: any) => {
|
|
40
|
-
if (!settled) { settled = true; fn(arg); }
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
this.ws = new WebSocket(this.config.url);
|
|
45
|
-
|
|
46
|
-
// Auth timeout — if no auth_ok within 10s, fail
|
|
47
|
-
const authTimer = setTimeout(() => {
|
|
48
|
-
settle(reject, new Error('Auth timeout'));
|
|
49
|
-
if (this.ws) { try { this.ws.close(); } catch {} }
|
|
50
|
-
}, 10_000);
|
|
51
|
-
|
|
52
|
-
this.ws.on('open', () => {
|
|
53
|
-
this.logger.info('Connected to Message Relay Hub');
|
|
54
|
-
this.ws!.send(JSON.stringify({
|
|
55
|
-
type: 'auth',
|
|
56
|
-
agentId: this.agentId,
|
|
57
|
-
apiKey: this.config.apiKey,
|
|
58
|
-
}));
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
this.ws.on('message', (data) => {
|
|
62
|
-
let msg: any;
|
|
63
|
-
try { msg = JSON.parse(data.toString()); } catch { return; }
|
|
64
|
-
|
|
65
|
-
if (msg.type === 'auth_ok') {
|
|
66
|
-
clearTimeout(authTimer);
|
|
67
|
-
this.authenticated = true;
|
|
68
|
-
this.reconnectDelay = 1000; // Reset on success
|
|
69
|
-
this.logger.info('Authenticated with Message Relay Hub');
|
|
70
|
-
settle(resolve);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (msg.type === 'auth_error') {
|
|
75
|
-
clearTimeout(authTimer);
|
|
76
|
-
this.logger.error(`Auth failed: ${msg.reason}`);
|
|
77
|
-
settle(reject, new Error(msg.reason));
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Check for pending request callbacks (replyTo or sessionId)
|
|
82
|
-
const callbackKey = msg.replyTo || msg.sessionId;
|
|
83
|
-
if (callbackKey && this.pendingCallbacks.has(callbackKey)) {
|
|
84
|
-
const cb = this.pendingCallbacks.get(callbackKey)!;
|
|
85
|
-
clearTimeout(cb.timer);
|
|
86
|
-
this.pendingCallbacks.delete(callbackKey);
|
|
87
|
-
cb.resolve(msg);
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Dispatch to type-based handlers
|
|
92
|
-
const handlers = this.handlers.get(msg.type) || [];
|
|
93
|
-
for (const handler of handlers) {
|
|
94
|
-
try { handler(msg); } catch (e: any) {
|
|
95
|
-
this.logger.error(`Handler error for ${msg.type}: ${e.message}`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
this.ws.on('close', (code) => {
|
|
101
|
-
clearTimeout(authTimer);
|
|
102
|
-
this.authenticated = false;
|
|
103
|
-
this.logger.info(`Disconnected from Hub (code: ${code})`);
|
|
104
|
-
settle(reject, new Error('Connection closed'));
|
|
105
|
-
if (this.shouldReconnect) {
|
|
106
|
-
this.scheduleReconnect();
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
this.ws.on('error', (err) => {
|
|
111
|
-
this.logger.error(`WebSocket error: ${err.message}`);
|
|
112
|
-
// Don't settle here — let 'close' event handle it
|
|
113
|
-
});
|
|
114
|
-
} catch (err: any) {
|
|
115
|
-
settle(reject, err);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
private scheduleReconnect(): void {
|
|
121
|
-
// Prevent multiple reconnect timers
|
|
122
|
-
if (this.reconnectTimer) return;
|
|
123
|
-
this.logger.info(`Reconnecting in ${this.reconnectDelay}ms...`);
|
|
124
|
-
this.reconnectTimer = setTimeout(async () => {
|
|
125
|
-
this.reconnectTimer = null;
|
|
126
|
-
try {
|
|
127
|
-
await this.connect();
|
|
128
|
-
this.logger.info('Reconnected to Message Relay Hub');
|
|
129
|
-
} catch {
|
|
130
|
-
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
|
|
131
|
-
// connect() failure triggers 'close' which calls scheduleReconnect again
|
|
132
|
-
}
|
|
133
|
-
}, this.reconnectDelay);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
on(type: string, handler: MessageHandler): void {
|
|
137
|
-
const existing = this.handlers.get(type) || [];
|
|
138
|
-
existing.push(handler);
|
|
139
|
-
this.handlers.set(type, existing);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
send(msg: any): void {
|
|
143
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN && this.authenticated) {
|
|
144
|
-
this.ws.send(JSON.stringify(msg));
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async sendAndWait(msg: any, timeoutMs = 60_000): Promise<any> {
|
|
149
|
-
return new Promise((resolve, reject) => {
|
|
150
|
-
const id = msg.id || msg.sessionId;
|
|
151
|
-
const timer = setTimeout(() => {
|
|
152
|
-
this.pendingCallbacks.delete(id);
|
|
153
|
-
reject(new Error(`Timeout waiting for reply to ${id}`));
|
|
154
|
-
}, timeoutMs);
|
|
155
|
-
|
|
156
|
-
this.pendingCallbacks.set(id, { resolve, reject, timer });
|
|
157
|
-
this.send(msg);
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async disconnect(): Promise<void> {
|
|
162
|
-
this.shouldReconnect = false;
|
|
163
|
-
if (this.reconnectTimer) {
|
|
164
|
-
clearTimeout(this.reconnectTimer);
|
|
165
|
-
this.reconnectTimer = null;
|
|
166
|
-
}
|
|
167
|
-
for (const [id, cb] of this.pendingCallbacks.entries()) {
|
|
168
|
-
clearTimeout(cb.timer);
|
|
169
|
-
cb.reject(new Error('Disconnecting'));
|
|
170
|
-
}
|
|
171
|
-
this.pendingCallbacks.clear();
|
|
172
|
-
|
|
173
|
-
if (this.ws) {
|
|
174
|
-
this.ws.removeAllListeners();
|
|
175
|
-
this.ws.close(1000, 'plugin deactivated');
|
|
176
|
-
this.ws = null;
|
|
177
|
-
}
|
|
178
|
-
this.authenticated = false;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
get isConnected(): boolean {
|
|
182
|
-
return this.ws !== null && this.ws.readyState === WebSocket.OPEN && this.authenticated;
|
|
183
|
-
}
|
|
184
|
-
}
|
package/src/permissions.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { BridgeConfig } from "./types.js";
|
|
2
|
-
|
|
3
|
-
const PUBLIC_ACTIONS = new Set(["discover", "whois", "send_file"]);
|
|
4
|
-
const SUPERUSER_ACTIONS = new Set(["read_file", "write_file", "restart"]);
|
|
5
|
-
|
|
6
|
-
export function checkPermission(action: string, config: BridgeConfig): boolean {
|
|
7
|
-
if (PUBLIC_ACTIONS.has(action)) return true;
|
|
8
|
-
if (SUPERUSER_ACTIONS.has(action)) return config.role === "superuser";
|
|
9
|
-
return false;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function assertPermission(action: string, config: BridgeConfig): void {
|
|
13
|
-
if (!checkPermission(action, config)) {
|
|
14
|
-
throw new Error(
|
|
15
|
-
`openclaw-bridge: permission denied — "${action}" requires superuser role, current role is "${config.role}"`,
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
}
|
package/src/registry.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import type { BridgeConfig, RegistryEntry, PluginLogger } from "./types.js";
|
|
2
|
-
|
|
3
|
-
export class BridgeRegistry {
|
|
4
|
-
private baseUrl: string;
|
|
5
|
-
private apiKey: string | undefined;
|
|
6
|
-
private logger: PluginLogger;
|
|
7
|
-
|
|
8
|
-
constructor(config: BridgeConfig, logger: PluginLogger) {
|
|
9
|
-
// Use fileRelay URL for registry (not OpenViking)
|
|
10
|
-
if (!config.fileRelay?.baseUrl) {
|
|
11
|
-
throw new Error("openclaw-bridge: fileRelay.baseUrl is required for registry");
|
|
12
|
-
}
|
|
13
|
-
this.baseUrl = config.fileRelay.baseUrl.replace(/\/+$/, "");
|
|
14
|
-
this.apiKey = config.fileRelay.apiKey;
|
|
15
|
-
this.logger = logger;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
private headers(): Record<string, string> {
|
|
19
|
-
const h: Record<string, string> = { "Content-Type": "application/json" };
|
|
20
|
-
if (this.apiKey) h["X-API-Key"] = this.apiKey;
|
|
21
|
-
return h;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async register(entry: RegistryEntry): Promise<void> {
|
|
25
|
-
try {
|
|
26
|
-
const res = await fetch(`${this.baseUrl}/api/v1/registry/register`, {
|
|
27
|
-
method: "POST",
|
|
28
|
-
headers: this.headers(),
|
|
29
|
-
body: JSON.stringify(entry),
|
|
30
|
-
});
|
|
31
|
-
if (!res.ok) {
|
|
32
|
-
throw new Error(`HTTP ${res.status}`);
|
|
33
|
-
}
|
|
34
|
-
this.logger.info(`openclaw-bridge: registered ${entry.agentId} to registry`);
|
|
35
|
-
} catch (err) {
|
|
36
|
-
this.logger.error(`openclaw-bridge: registration failed: ${String(err)}`);
|
|
37
|
-
throw err;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async update(entry: RegistryEntry): Promise<void> {
|
|
42
|
-
try {
|
|
43
|
-
const res = await fetch(`${this.baseUrl}/api/v1/registry/heartbeat`, {
|
|
44
|
-
method: "POST",
|
|
45
|
-
headers: this.headers(),
|
|
46
|
-
body: JSON.stringify(entry),
|
|
47
|
-
});
|
|
48
|
-
if (!res.ok) {
|
|
49
|
-
// Re-register if heartbeat fails
|
|
50
|
-
await this.register(entry);
|
|
51
|
-
}
|
|
52
|
-
} catch (err) {
|
|
53
|
-
this.logger.warn(`openclaw-bridge: heartbeat failed, re-registering: ${String(err)}`);
|
|
54
|
-
await this.register(entry);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async deregister(agentId: string): Promise<void> {
|
|
59
|
-
try {
|
|
60
|
-
await fetch(`${this.baseUrl}/api/v1/registry/${agentId}`, {
|
|
61
|
-
method: "DELETE",
|
|
62
|
-
headers: this.headers(),
|
|
63
|
-
});
|
|
64
|
-
this.logger.info(`openclaw-bridge: deregistered ${agentId}`);
|
|
65
|
-
} catch (err) {
|
|
66
|
-
this.logger.warn(`openclaw-bridge: deregistration failed: ${String(err)}`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async discover(offlineThresholdMs: number): Promise<RegistryEntry[]> {
|
|
71
|
-
try {
|
|
72
|
-
const res = await fetch(`${this.baseUrl}/api/v1/registry/discover`, {
|
|
73
|
-
headers: this.headers(),
|
|
74
|
-
});
|
|
75
|
-
if (!res.ok) return [];
|
|
76
|
-
|
|
77
|
-
const data = (await res.json()) as { agents?: RegistryEntry[] };
|
|
78
|
-
const now = Date.now();
|
|
79
|
-
|
|
80
|
-
return (data.agents ?? []).map((entry) => {
|
|
81
|
-
const lastBeat = new Date(entry.lastHeartbeat).getTime();
|
|
82
|
-
entry.status = (now - lastBeat > offlineThresholdMs) ? "offline" : "online";
|
|
83
|
-
return entry;
|
|
84
|
-
});
|
|
85
|
-
} catch (err) {
|
|
86
|
-
this.logger.error(`openclaw-bridge: discover failed: ${String(err)}`);
|
|
87
|
-
return [];
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async findAgent(agentId: string, offlineThresholdMs: number): Promise<RegistryEntry | null> {
|
|
92
|
-
try {
|
|
93
|
-
const res = await fetch(`${this.baseUrl}/api/v1/registry/whois/${agentId}`, {
|
|
94
|
-
headers: this.headers(),
|
|
95
|
-
});
|
|
96
|
-
if (!res.ok) return null;
|
|
97
|
-
|
|
98
|
-
const entry = (await res.json()) as RegistryEntry;
|
|
99
|
-
const now = Date.now();
|
|
100
|
-
const lastBeat = new Date(entry.lastHeartbeat).getTime();
|
|
101
|
-
entry.status = (now - lastBeat > offlineThresholdMs) ? "offline" : "online";
|
|
102
|
-
return entry;
|
|
103
|
-
} catch {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|