@reefclaw/connect 0.1.1 → 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
+ }
@@ -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;
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: reefclaw
3
- version: 0.0.1
3
+ version: 0.0.2
4
4
  description: ReefClaw trading control room — bootstrap (connects your agent; full trading instructions arrive automatically after first connect)
5
5
  author: ReefClaw
6
6
  homepage: https://reefclaw.com
@@ -29,7 +29,7 @@ config:
29
29
  connect instructions. The FULL trading instructions (versioned 2.x.y) are
30
30
  delivered automatically by the ReefClaw connector minutes after the first
31
31
  successful connect, over the authenticated update channel — they replace
32
- this file in place (version 0.0.1 always loses the semver comparison).
32
+ this file in place (a 0.0.x version always loses the semver comparison).
33
33
  The "Connecting (first run)" section below MUST stay in sync with the
34
34
  same section in skill/SKILL.md. -->
35
35
 
@@ -37,7 +37,7 @@ config:
37
37
 
38
38
  You are being connected to **ReefClaw**, a supervised trading control room. Your operator watches everything you do on the ReefClaw dashboard. This bootstrap file only covers **connecting**; your full trading instructions arrive automatically a few minutes after the connection succeeds.
39
39
 
40
- **Do not attempt to trade before the full instructions arrive.** If trading tools are visible but this file is still the bootstrap version (0.0.1), the connection has not completed — finish connecting first.
40
+ **Do not attempt to trade before the full instructions arrive.** If trading tools are visible but this file is still a bootstrap version (0.0.x), the connection has not completed — finish connecting first.
41
41
 
42
42
  ## Connecting (first run) — saving the connect message
43
43
 
@@ -66,8 +66,22 @@ When a user gives you this message (or just a bare `Here's my ReefClaw token: rc
66
66
  - After saving, the connector picks the config up within seconds (it restarts until a token appears). The user does not need to run any command.
67
67
  - If you cannot write the config yourself (e.g. exec or file access is restricted in this session), show the user this exact JSON snippet and where it goes, and offer to verify it after they save it.
68
68
 
69
+ ## If you cannot run the installer (elevated exec disabled)
70
+
71
+ Fresh OpenClaw installs deny elevated (host) exec from chat sessions. If `npx @reefclaw/connect` is blocked, do NOT dead-end — offer the user this choice:
72
+
73
+ 1. **Self-service (no terminal)**:
74
+ - Add the user's **sender id** (visible in this session's context) to `tools.elevated.allowFrom.<this channel>` in `~/.openclaw/openclaw.json`. This is an **array of sender ids per provider** — e.g. `"tools": { "elevated": { "allowFrom": { "telegram": ["12345678"] } } }` — NOT a boolean.
75
+ - Tell the user to type `/restart` in this chat (OpenClaw's built-in in-process restart).
76
+ - Then the user types `/elevated full` — elevated exec is **per-session and off until the user activates it**; OpenClaw confirms with "Elevated mode set to full". If it replies that the sender is not on the allowlist, re-check the allowFrom shape.
77
+ - **RETRY the install command — do not assume it is still blocked.**
78
+ - After the install succeeds: the user types `/elevated off` and you remove the allowFrom entry (the installed system never needs elevated exec — only the install did).
79
+ 2. **Manual (last resort)**: the user runs `npx @reefclaw/connect` themselves in a terminal on this machine — one command, nothing else.
80
+
81
+ Either way, **save the connection config regardless** — the connector picks it up automatically the moment the install completes.
82
+
69
83
  ## After connecting
70
84
 
71
85
  - The ReefClaw dashboard flips to **Connected** on its own — no manual verification step.
72
86
  - Your full trading instructions (SKILL.md version 2.x) are delivered automatically over the authenticated connection and replace this file. You will be notified in chat when that happens.
73
- - If more than ~10 minutes pass after a successful connect and this file is still version 0.0.1, tell the user to check the dashboard connection status.
87
+ - If more than ~10 minutes pass after a successful connect and this file is still a 0.0.x bootstrap version, tell the user to check the dashboard connection status.
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 { installDaemon } from './daemon.js';
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
- const startedService = installDaemon();
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 (!startedService) {
113
- warn('The connector was installed but not auto-started — see the run command above.');
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reefclaw/connect",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "One-command installer that connects your OpenClaw agent to ReefClaw (paper trading, no exchange keys).",
5
5
  "type": "module",
6
6
  "bin": {