@reefclaw/connect 0.1.2 → 0.1.3
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.
|
@@ -2,6 +2,12 @@ import type { TradingMode, ExchangeConfig } from '../types.js';
|
|
|
2
2
|
/** Full on-disk schema. Unknown keys are preserved on round-trip. */
|
|
3
3
|
export interface PluginConfigFile {
|
|
4
4
|
connectionToken?: string;
|
|
5
|
+
/** 'on' → the plugin spawns + restarts the relay connector as a child of
|
|
6
|
+
* the gateway process (no systemd — OpenClaw is the process manager).
|
|
7
|
+
* Written by the npx installer on fresh installs. MUST stay defaulted off:
|
|
8
|
+
* prod runs the bridge under systemd and a supervisor there would
|
|
9
|
+
* double-connect the relay room. Kill-switch: RC_CONNECTOR_SUPERVISOR=off. */
|
|
10
|
+
connectorSupervisor?: 'on' | 'off';
|
|
5
11
|
apiBaseUrl?: string;
|
|
6
12
|
intelligenceUrl?: string;
|
|
7
13
|
exchange?: ExchangeConfig;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
export interface ConnectorSupervisorOptions {
|
|
3
|
+
/** Directory holding the placed bridge (default ~/.reefclaw/bridge). */
|
|
4
|
+
bridgeDir?: string;
|
|
5
|
+
/** Injectable spawn for tests. */
|
|
6
|
+
spawnFn?: typeof spawn;
|
|
7
|
+
}
|
|
8
|
+
export interface ConnectorSupervisorHandle {
|
|
9
|
+
stop(): void;
|
|
10
|
+
/** Test/debug introspection. */
|
|
11
|
+
isRunning(): boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Start supervising the connector. Idempotent per process — repeat calls
|
|
15
|
+
* (OpenClaw can invoke register() more than once) return the existing handle.
|
|
16
|
+
* Returns null when the bridge isn't placed on disk (installer not run).
|
|
17
|
+
*/
|
|
18
|
+
export declare function startConnectorSupervisor(opts?: ConnectorSupervisorOptions): ConnectorSupervisorHandle | null;
|
|
19
|
+
/** Test-only: reset the module singleton. */
|
|
20
|
+
export declare function resetConnectorSupervisorForTest(): void;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Plugin-supervised ReefClaw connector (frictionless-onboarding phase 1).
|
|
2
|
+
//
|
|
3
|
+
// The plugin runs persistently inside the OpenClaw gateway process, so it can
|
|
4
|
+
// supervise the relay connector (the bridge) as a child process — making
|
|
5
|
+
// OpenClaw itself the process manager. This removes every host-level concern
|
|
6
|
+
// from onboarding: no systemd, no terminal, works inside containers, and the
|
|
7
|
+
// connector's lifetime is correctly tied to the gateway's (gateway down ⇒
|
|
8
|
+
// nothing to bridge anyway — the plugin holding the exchange keys lives in
|
|
9
|
+
// the same process).
|
|
10
|
+
//
|
|
11
|
+
// SAFETY: default OFF. Prod runs the bridge as `reefclaw-skill.service`
|
|
12
|
+
// (systemd) — a supervisor turned on there would double-connect the relay
|
|
13
|
+
// room. Only the fresh-install path (the npx installer) writes
|
|
14
|
+
// `connectorSupervisor: 'on'` into ~/.reefclaw/plugin-config.json.
|
|
15
|
+
// Kill-switch: RC_CONNECTOR_SUPERVISOR=off beats config.
|
|
16
|
+
import { spawn } from 'node:child_process';
|
|
17
|
+
import { existsSync } from 'node:fs';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { logger } from './logger.js';
|
|
21
|
+
const TAG = 'connector-supervisor';
|
|
22
|
+
const MIN_BACKOFF_MS = 5_000;
|
|
23
|
+
const MAX_BACKOFF_MS = 60_000;
|
|
24
|
+
/** A child that survives this long resets the backoff (it was healthy). */
|
|
25
|
+
const STABLE_RESET_MS = 5 * 60_000;
|
|
26
|
+
let singleton = null;
|
|
27
|
+
/**
|
|
28
|
+
* Start supervising the connector. Idempotent per process — repeat calls
|
|
29
|
+
* (OpenClaw can invoke register() more than once) return the existing handle.
|
|
30
|
+
* Returns null when the bridge isn't placed on disk (installer not run).
|
|
31
|
+
*/
|
|
32
|
+
export function startConnectorSupervisor(opts = {}) {
|
|
33
|
+
if (singleton)
|
|
34
|
+
return singleton;
|
|
35
|
+
const bridgeDir = opts.bridgeDir ?? join(homedir(), '.reefclaw', 'bridge');
|
|
36
|
+
const indexJs = join(bridgeDir, 'index.js');
|
|
37
|
+
if (!existsSync(indexJs)) {
|
|
38
|
+
logger.warn(TAG, `connector not found at ${indexJs} — supervisor idle (run the installer first)`);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const spawnFn = opts.spawnFn ?? spawn;
|
|
42
|
+
let child = null;
|
|
43
|
+
let stopped = false;
|
|
44
|
+
let backoffMs = MIN_BACKOFF_MS;
|
|
45
|
+
let restartTimer = null;
|
|
46
|
+
const launch = () => {
|
|
47
|
+
if (stopped)
|
|
48
|
+
return;
|
|
49
|
+
const startedAt = Date.now();
|
|
50
|
+
// NOT detached: the connector must die with the gateway (a gateway
|
|
51
|
+
// restart resurrects both, and an orphan bridge can never linger).
|
|
52
|
+
child = spawnFn(process.execPath, [indexJs, '--provider', 'gateway', '--log-level', 'info'], {
|
|
53
|
+
cwd: bridgeDir,
|
|
54
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
55
|
+
});
|
|
56
|
+
logger.info(TAG, `connector started (pid ${child.pid})`);
|
|
57
|
+
const forward = (stream, level) => {
|
|
58
|
+
stream?.on('data', (chunk) => {
|
|
59
|
+
for (const line of chunk.toString().split('\n')) {
|
|
60
|
+
if (line.trim())
|
|
61
|
+
logger[level](TAG, `[connector] ${line}`);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
forward(child.stdout, 'info');
|
|
66
|
+
forward(child.stderr, 'warn');
|
|
67
|
+
child.on('exit', (code, signal) => {
|
|
68
|
+
child = null;
|
|
69
|
+
if (stopped)
|
|
70
|
+
return;
|
|
71
|
+
const aliveMs = Date.now() - startedAt;
|
|
72
|
+
if (aliveMs >= STABLE_RESET_MS)
|
|
73
|
+
backoffMs = MIN_BACKOFF_MS;
|
|
74
|
+
// Exit is EXPECTED pre-token (the connector exits until the connect
|
|
75
|
+
// message is saved) — restart with backoff, exactly like systemd's
|
|
76
|
+
// Restart=always did.
|
|
77
|
+
logger.info(TAG, `connector exited (code=${code ?? 'null'} signal=${signal ?? 'null'} after ${Math.round(aliveMs / 1000)}s) — restarting in ${backoffMs / 1000}s`);
|
|
78
|
+
restartTimer = setTimeout(launch, backoffMs);
|
|
79
|
+
restartTimer.unref?.();
|
|
80
|
+
backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
|
|
81
|
+
});
|
|
82
|
+
child.on('error', (err) => {
|
|
83
|
+
logger.error(TAG, `connector spawn failed: ${err.message}`);
|
|
84
|
+
child = null;
|
|
85
|
+
if (stopped)
|
|
86
|
+
return;
|
|
87
|
+
restartTimer = setTimeout(launch, backoffMs);
|
|
88
|
+
restartTimer.unref?.();
|
|
89
|
+
backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
launch();
|
|
93
|
+
const handle = {
|
|
94
|
+
stop() {
|
|
95
|
+
stopped = true;
|
|
96
|
+
if (restartTimer)
|
|
97
|
+
clearTimeout(restartTimer);
|
|
98
|
+
if (child) {
|
|
99
|
+
try {
|
|
100
|
+
child.kill('SIGTERM');
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
/* already gone */
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
singleton = null;
|
|
107
|
+
},
|
|
108
|
+
isRunning() {
|
|
109
|
+
return child !== null;
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
singleton = handle;
|
|
113
|
+
return handle;
|
|
114
|
+
}
|
|
115
|
+
/** Test-only: reset the module singleton. */
|
|
116
|
+
export function resetConnectorSupervisorForTest() {
|
|
117
|
+
singleton = null;
|
|
118
|
+
}
|
package/assets/plugin/index.js
CHANGED
|
@@ -40,6 +40,7 @@ import { queryReviewOutcomesTool } from './tools/query-review-outcomes.js';
|
|
|
40
40
|
import { loadPositionReviewMode } from './config/position-review-config.js';
|
|
41
41
|
import { installSignalHandlers } from './lifecycle/install-signal-handlers.js';
|
|
42
42
|
import { readPluginConfig } from './config/plugin-config-io.js';
|
|
43
|
+
import { startConnectorSupervisor } from './connector-supervisor.js';
|
|
43
44
|
import { ToolGate } from './config/tool-gate.js';
|
|
44
45
|
import { gateStore } from './config/gate-store.js';
|
|
45
46
|
import { startAgentConfigPoller } from './config/agent-config-poller.js';
|
|
@@ -861,6 +862,13 @@ const paperTradingPlugin = {
|
|
|
861
862
|
if (pluginToolsFactory) {
|
|
862
863
|
api.registerTool(pluginToolsFactory, { names: pluginToolNames });
|
|
863
864
|
}
|
|
865
|
+
// The supervisor must be (re)evaluated on EVERY register call, not just
|
|
866
|
+
// the first: the first full register can run BEFORE the installer has
|
|
867
|
+
// written connectorSupervisor='on' (the gateway hot-reloads the plugin
|
|
868
|
+
// the moment `plugins install --link` records it), and OpenClaw's
|
|
869
|
+
// in-process restart re-invokes register() down THIS early-return path.
|
|
870
|
+
// startConnectorSupervisor() is itself a singleton — repeat calls no-op.
|
|
871
|
+
maybeStartConnectorSupervisor();
|
|
864
872
|
return;
|
|
865
873
|
}
|
|
866
874
|
logger.info(TAG, 'Initializing paper trading plugin...');
|
|
@@ -1984,6 +1992,31 @@ const paperTradingPlugin = {
|
|
|
1984
1992
|
pluginToolNames = toolNames;
|
|
1985
1993
|
pluginInitialised = true;
|
|
1986
1994
|
logger.info(TAG, `Registered ${gatedTools.length} tools (gate mode=${toolGate.getMode()}): ${toolNames.join(', ')}. Plugin v3.8.0 (${runtime.mode} mode)`);
|
|
1995
|
+
maybeStartConnectorSupervisor();
|
|
1987
1996
|
},
|
|
1988
1997
|
};
|
|
1998
|
+
/** Plugin-supervised connector (frictionless onboarding): when plugin-config
|
|
1999
|
+
* says connectorSupervisor='on' (written by the npx installer on fresh
|
|
2000
|
+
* installs — NEVER defaulted on, prod runs the bridge under systemd and
|
|
2001
|
+
* would double-connect the relay room), the plugin spawns + restarts the
|
|
2002
|
+
* relay connector as a child of the gateway process. OpenClaw is the process
|
|
2003
|
+
* manager: no systemd, works in containers, dies with the gateway.
|
|
2004
|
+
* Kill-switch: RC_CONNECTOR_SUPERVISOR=off. Called from EVERY register()
|
|
2005
|
+
* path (config may appear between calls); the supervisor itself is a
|
|
2006
|
+
* singleton so repeat calls no-op. */
|
|
2007
|
+
function maybeStartConnectorSupervisor() {
|
|
2008
|
+
try {
|
|
2009
|
+
const supervisorMode = readPluginConfig().connectorSupervisor;
|
|
2010
|
+
if (process.env.RC_CONNECTOR_SUPERVISOR === 'off') {
|
|
2011
|
+
if (supervisorMode === 'on')
|
|
2012
|
+
logger.warn(TAG, 'connector supervisor disabled by RC_CONNECTOR_SUPERVISOR=off');
|
|
2013
|
+
}
|
|
2014
|
+
else if (supervisorMode === 'on') {
|
|
2015
|
+
startConnectorSupervisor();
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
catch (err) {
|
|
2019
|
+
logger.warn(TAG, `connector supervisor init failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
1989
2022
|
export default paperTradingPlugin;
|
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { installPlugin } from './plugin.js';
|
|
11
11
|
import { installBridge } from './bridge.js';
|
|
12
12
|
import { installSkill } from './skill.js';
|
|
13
|
-
import {
|
|
13
|
+
import { enableConnectorSupervisor } from './supervisor-config.js';
|
|
14
14
|
import { checkGateway } from './validate.js';
|
|
15
15
|
import { readConfig, writeConfig, mergeReefClawConfig, gatewayAuthDowngradeNeeded, openClawInstalled, openClawConfigPath, readGatewayPort, } from './openclaw.js';
|
|
16
16
|
import { run, which } from './exec.js';
|
|
@@ -99,7 +99,10 @@ async function main() {
|
|
|
99
99
|
// invalid config.
|
|
100
100
|
const skill = installSkill();
|
|
101
101
|
installBridge();
|
|
102
|
-
|
|
102
|
+
// The plugin (inside the gateway) supervises the connector — no systemd/
|
|
103
|
+
// launchd/Task-Scheduler, works in containers. The gateway restart below is
|
|
104
|
+
// therefore also what STARTS the connector.
|
|
105
|
+
const supervised = enableConnectorSupervisor();
|
|
103
106
|
if (plugin.registered)
|
|
104
107
|
restartGateway();
|
|
105
108
|
await checkGateway(readGatewayPort(merged));
|
|
@@ -109,8 +112,8 @@ async function main() {
|
|
|
109
112
|
if (!skill.installed) {
|
|
110
113
|
warn('The agent skill was placed but not registered — see the message above to finish it.');
|
|
111
114
|
}
|
|
112
|
-
if (!
|
|
113
|
-
warn('
|
|
115
|
+
if (!supervised) {
|
|
116
|
+
warn('Connector supervision was not enabled — see the message above to finish it.');
|
|
114
117
|
}
|
|
115
118
|
nextSteps();
|
|
116
119
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Enable the plugin-supervised connector for this install: write
|
|
2
|
+
// `connectorSupervisor: 'on'` into ~/.reefclaw/plugin-config.json. The plugin
|
|
3
|
+
// (running inside the OpenClaw gateway) then spawns + restarts the connector
|
|
4
|
+
// itself — OpenClaw is the process manager. No systemd, no launchd, no Task
|
|
5
|
+
// Scheduler; works anywhere the gateway runs, including containers.
|
|
6
|
+
//
|
|
7
|
+
// Merge-preserving: the file may already exist (re-runs, or the operator's
|
|
8
|
+
// dashboard wrote exchange credentials); unknown keys survive.
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { REEFCLAW_HOME } from './paths.js';
|
|
12
|
+
import { step, ok, warn, info } from './ui.js';
|
|
13
|
+
export function enableConnectorSupervisor() {
|
|
14
|
+
step('Handing connector supervision to OpenClaw');
|
|
15
|
+
const path = join(REEFCLAW_HOME, 'plugin-config.json');
|
|
16
|
+
try {
|
|
17
|
+
let cfg = {};
|
|
18
|
+
if (existsSync(path)) {
|
|
19
|
+
cfg = JSON.parse(readFileSync(path, 'utf-8'));
|
|
20
|
+
}
|
|
21
|
+
cfg.connectorSupervisor = 'on';
|
|
22
|
+
mkdirSync(REEFCLAW_HOME, { recursive: true, mode: 0o700 });
|
|
23
|
+
writeFileSync(path, JSON.stringify(cfg, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
|
|
24
|
+
try {
|
|
25
|
+
chmodSync(path, 0o600); // mode is ignored when the file already existed
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
/* best-effort on non-POSIX filesystems */
|
|
29
|
+
}
|
|
30
|
+
ok('OpenClaw will start and supervise the connector (no service manager needed)');
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
warn(`could not write ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
35
|
+
info(`Add { "connectorSupervisor": "on" } to it yourself, then restart OpenClaw.`);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|