@reefclaw/openclaw-plugin 0.1.16 → 0.1.17

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/bridge/bridge.js CHANGED
@@ -788,7 +788,11 @@ export class Bridge {
788
788
  // 0. SIGNATURE GATE (C1) — verify BEFORE touching disk. The relay is a
789
789
  // dumb transport: only a payload signed by the operator's offline key
790
790
  // is applied. Fail closed when enforced.
791
- if (signatureRequired()) {
791
+ // A PRESENT signature is ALWAYS verified, enforcement flag or not: the
792
+ // break-glass exists to tolerate an update that arrives unsigned, never to
793
+ // accept one whose signature fails. Otherwise `=off` would downgrade a
794
+ // forged-signature attack into an applied update.
795
+ if (signatureRequired() || env.signature) {
792
796
  const verdict = verifySkillSignature(env);
793
797
  if (!verdict.ok) {
794
798
  logger.error(TAG, `OTA SKILL.md REJECTED (signature): ${verdict.reason}`);
@@ -802,10 +806,10 @@ export class Bridge {
802
806
  }
803
807
  signatureVerified = true;
804
808
  }
805
- else if (!env.signature) {
806
- // C1 (signed OTA) is ON HOLD → verification off by default, so OTA works
807
- // unsigned as before. To enforce: pin a key + SKILL_OTA_REQUIRE_SIGNATURE=on.
808
- logger.warn(TAG, 'OTA SKILL.md applied WITHOUT signature verification signed-OTA (C1) is disabled');
809
+ else {
810
+ // Unsigned AND the operator explicitly set SKILL_OTA_REQUIRE_SIGNATURE=off.
811
+ logger.warn(TAG, 'OTA SKILL.md applied WITHOUT signature verification SKILL_OTA_REQUIRE_SIGNATURE=off. ' +
812
+ 'This accepts instructions nobody signed; unset the flag to restore the default (enforced).');
809
813
  }
810
814
  // 1. Validate content
811
815
  const validation = validateSkillContent(content);
@@ -1043,7 +1047,15 @@ export class Bridge {
1043
1047
  else {
1044
1048
  logger.info(TAG, `SKILL.md webapp pull: local v${this.currentSkillVersion ?? 'unknown'} behind webapp v${body.version} — applying`);
1045
1049
  }
1046
- const result = await this.applySkillUpdate({ version: body.version, content: body.content });
1050
+ // Carry the offline signature through so the C1 gate can verify it. The
1051
+ // webapp holds no signing key — it only relays what was signed offline.
1052
+ const result = await this.applySkillUpdate({
1053
+ version: body.version,
1054
+ content: body.content,
1055
+ contentSha256: body.contentSha256,
1056
+ signedAt: body.signedAt,
1057
+ signature: body.signature,
1058
+ });
1047
1059
  this.emit('agent_state', 'skill_update_applied', {
1048
1060
  event: 'skill_update_applied',
1049
1061
  version: body.version,
@@ -11,13 +11,27 @@ export declare function readOpenClawConfig(): {
11
11
  userId?: string;
12
12
  relayUrl?: string;
13
13
  } | null;
14
- /** Write/merge token (and optional fields) into OpenClaw config */
14
+ /** Write/merge the ReefClaw connection (token + optional userId/relayUrl) into
15
+ * OpenClaw's config. Touches nothing else — see the scope contract above. */
15
16
  export declare function writeOpenClawConfig(token: string, userId?: string, relayUrl?: string): void;
17
+ /** True when the bridge lacks the write scope it needs to trade — i.e. the
18
+ * gateway granted a scope set without `operator.write`. Read from the
19
+ * handshake, so it reflects what the gateway ACTUALLY did, not a version
20
+ * guess. An empty/absent list is treated as "no evidence" (not a downgrade
21
+ * trigger): some builds omit the field, and acting on silence would recreate
22
+ * the speculative relaxation this replaced. */
23
+ export declare function handshakeLacksWriteScope(scopes: readonly string[] | undefined): boolean;
16
24
  /**
17
- * Check and fix gateway scope config on startup. Runs once — reads openclaw.json,
18
- * applies missing settings if needed, writes back. No-op if already correct.
25
+ * LAST RESORT: relax the local gateway's device-identity auth because a
26
+ * completed handshake proved the gateway is withholding `operator.write`
27
+ * (without it the bridge cannot place or cancel orders). Idempotent, and a
28
+ * no-op once the flags are set — so it warns exactly once per box, at the
29
+ * moment the need is demonstrated.
30
+ *
31
+ * Returns true if it changed the config (⇒ the gateway must be restarted for
32
+ * the flags to take effect; they are read at gateway boot).
19
33
  */
20
- export declare function ensureGatewayScopeConfigOnStartup(): void;
34
+ export declare function relaxGatewayDeviceAuth(reason: string): boolean;
21
35
  export interface CliArgs {
22
36
  provider: 'mock' | 'gateway';
23
37
  token?: string;
package/bridge/config.js CHANGED
@@ -12,11 +12,18 @@ const CONFIG_PATH = join(OPENCLAW_DIR, 'openclaw.json');
12
12
  const DEFAULT_RELAY_URL = 'wss://reefclaw.radunlupsa.partykit.dev';
13
13
  // ---- Read/write OpenClaw config ----
14
14
  //
15
- // Scope contract: these helpers touch ONLY the ReefClaw skill entry
16
- // (skills.entries.reefclaw.config the token/userId/relayUrl the USER pasted
17
- // during onboarding) and preserve every other key verbatim. They exist so the
18
- // connector can pick up a connection the user saved via chat and persist it
19
- // across restarts — not to manage OpenClaw's own configuration.
15
+ // Scope contract writeOpenClawConfig touches ONLY
16
+ // skills.entries.reefclaw.config (the token/userId/relayUrl the USER pasted
17
+ // during onboarding, so the connector can pick the connection up and persist
18
+ // it across restarts). Every other key in the file is preserved verbatim.
19
+ //
20
+ // It deliberately does NOT weaken the local gateway's auth. Until 2026-07-29
21
+ // it also set gateway.controlUi.{dangerouslyDisableDeviceAuth,
22
+ // allowInsecureAuth} = true on EVERY write — a security-relevant downgrade of
23
+ // the user's own gateway, applied pre-emptively whether or not their OpenClaw
24
+ // needed it. That relaxation now happens only on PROVEN need, via
25
+ // relaxGatewayDeviceAuth() below, which the bridge calls if and only if a
26
+ // completed handshake shows the gateway actually withheld `operator.write`.
20
27
  /** Read the OpenClaw config file and extract ReefClaw skill settings.
21
28
  * Prefers the schema-valid nested shape (entry.config.*), falls back to the
22
29
  * legacy flat shape per-field. */
@@ -40,7 +47,8 @@ export function readOpenClawConfig() {
40
47
  return null;
41
48
  }
42
49
  }
43
- /** Write/merge token (and optional fields) into OpenClaw config */
50
+ /** Write/merge the ReefClaw connection (token + optional userId/relayUrl) into
51
+ * OpenClaw's config. Touches nothing else — see the scope contract above. */
44
52
  export function writeOpenClawConfig(token, userId, relayUrl) {
45
53
  let config = {};
46
54
  // Read existing config to merge
@@ -72,9 +80,8 @@ export function writeOpenClawConfig(token, userId, relayUrl) {
72
80
  delete entry.token;
73
81
  delete entry.userId;
74
82
  delete entry.relayUrl;
75
- // Ensure gateway.controlUi settings for OpenClaw v2026.4.2+ scope compatibility.
76
- // Without these, the skill's WS connection gets scopes cleared (missing scope: operator.write).
77
- ensureGatewayScopeConfig(config);
83
+ // NOTE: no gateway.controlUi relaxation here. Saving a connection must not
84
+ // change the user's gateway security posture see relaxGatewayDeviceAuth().
78
85
  // Write — file holds the connection token, so restrict to owner.
79
86
  mkdirSync(OPENCLAW_DIR, { recursive: true, mode: 0o700 });
80
87
  writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
@@ -97,13 +104,26 @@ function restrictConfigPermissions() {
97
104
  logger.debug(TAG, `Could not chmod ${CONFIG_PATH} to 0600: ${err instanceof Error ? err.message : String(err)}`);
98
105
  }
99
106
  }
100
- // ---- Gateway scope compatibility (OpenClaw v2026.4.2+) ----
101
- /**
102
- * Ensure gateway.controlUi has the required settings for WS scope preservation.
103
- * Without these, OpenClaw v2026.4.2+ clears operator.write scopes on connections
104
- * without device identity, breaking mission presentation and chat RPCs.
105
- */
106
- function ensureGatewayScopeConfig(config) {
107
+ // ---- Gateway device-auth relaxation (LAST RESORT, proven need only) ----
108
+ //
109
+ // Some OpenClaw builds (observed on v2026.4.2) clear `operator.write` from a WS
110
+ // connection that carries no device identity the bridge then cannot place or
111
+ // cancel orders. The historical workaround was to set
112
+ // gateway.controlUi.{dangerouslyDisableDeviceAuth, allowInsecureAuth} = true
113
+ // on every install and every config save, whether or not the user's OpenClaw
114
+ // actually behaved that way. That is a real downgrade of the user's gateway
115
+ // applied on speculation, and the flag name says so.
116
+ //
117
+ // Since 2026-07-29 the relaxation is applied ONLY when a completed handshake
118
+ // proves it is needed: the bridge inspects the granted scopes from hello-ok and
119
+ // calls relaxGatewayDeviceAuth() if `operator.write` is missing. On the
120
+ // supported range (the plugin's compat floor is pluginApi >=2026.6.0) the
121
+ // handshake keeps its scopes and these flags are never written at all. Boxes
122
+ // that already have them keep them — nothing here removes an existing flag,
123
+ // because clearing it mid-session would strip the running bridge's scopes.
124
+ /** Set the two relaxation flags on an in-memory config. Returns true if either
125
+ * changed, so callers only write + disclose when something actually flipped. */
126
+ function applyGatewayAuthRelaxation(config) {
107
127
  if (!config.gateway)
108
128
  config.gateway = {};
109
129
  const gw = config.gateway;
@@ -119,32 +139,49 @@ function ensureGatewayScopeConfig(config) {
119
139
  cui.allowInsecureAuth = true;
120
140
  changed = true;
121
141
  }
122
- // Write-once: only flip (and disclose) when a flag was absent/false, so the
123
- // security-relevant downgrade is announced the one time it actually happens,
124
- // never on every boot. These flags relax the local gateway's device-identity
125
- // auth (required for OpenClaw v2026.4.2+ WS scope preservation) — the
126
- // operator should know they were set.
127
- if (changed) {
128
- logger.warn(TAG, 'Enabled gateway.controlUi.{dangerouslyDisableDeviceAuth,allowInsecureAuth} — this relaxes local gateway device-identity auth (needed for OpenClaw v2026.4.2+ WS scope preservation). Keep the gateway bound to localhost.');
129
- }
130
142
  return changed;
131
143
  }
144
+ /** True when the bridge lacks the write scope it needs to trade — i.e. the
145
+ * gateway granted a scope set without `operator.write`. Read from the
146
+ * handshake, so it reflects what the gateway ACTUALLY did, not a version
147
+ * guess. An empty/absent list is treated as "no evidence" (not a downgrade
148
+ * trigger): some builds omit the field, and acting on silence would recreate
149
+ * the speculative relaxation this replaced. */
150
+ export function handshakeLacksWriteScope(scopes) {
151
+ if (!Array.isArray(scopes) || scopes.length === 0)
152
+ return false;
153
+ return !scopes.includes('operator.write');
154
+ }
132
155
  /**
133
- * Check and fix gateway scope config on startup. Runs once — reads openclaw.json,
134
- * applies missing settings if needed, writes back. No-op if already correct.
156
+ * LAST RESORT: relax the local gateway's device-identity auth because a
157
+ * completed handshake proved the gateway is withholding `operator.write`
158
+ * (without it the bridge cannot place or cancel orders). Idempotent, and a
159
+ * no-op once the flags are set — so it warns exactly once per box, at the
160
+ * moment the need is demonstrated.
161
+ *
162
+ * Returns true if it changed the config (⇒ the gateway must be restarted for
163
+ * the flags to take effect; they are read at gateway boot).
135
164
  */
136
- export function ensureGatewayScopeConfigOnStartup() {
165
+ export function relaxGatewayDeviceAuth(reason) {
137
166
  try {
138
167
  const raw = readFileSync(CONFIG_PATH, 'utf-8');
139
168
  const config = JSON5.parse(raw);
140
- if (ensureGatewayScopeConfig(config)) {
141
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
142
- restrictConfigPermissions(); // mode above is ignored on an existing file
143
- logger.info(TAG, 'Applied gateway scope config for OpenClaw v2026.4.2+ compatibility');
144
- }
169
+ if (!applyGatewayAuthRelaxation(config))
170
+ return false; // already relaxed
171
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
172
+ restrictConfigPermissions(); // mode above is ignored on an existing file
173
+ logger.warn(TAG, `Enabled gateway.controlUi.{dangerouslyDisableDeviceAuth,allowInsecureAuth} in ` +
174
+ `${CONFIG_PATH} because ${reason}. This RELAXES your local gateway's ` +
175
+ `device-identity auth — it is the only way this OpenClaw build will keep ` +
176
+ `operator.write on the bridge connection, which trading requires. Keep the ` +
177
+ `gateway bound to localhost. RESTART the gateway for it to take effect. If ` +
178
+ `you would rather not run with it, upgrade OpenClaw (builds meeting the ` +
179
+ `plugin's compat floor keep the scope without this) and remove both flags.`);
180
+ return true;
145
181
  }
146
182
  catch {
147
- // Config doesn't exist or can't be parsed skip (setup hasn't run yet)
183
+ // Config missing/unparseable — setup hasn't run yet; nothing to relax.
184
+ return false;
148
185
  }
149
186
  }
150
187
  /**
package/bridge/index.js CHANGED
@@ -14,7 +14,7 @@ import { logger, setLogLevel } from './logger.js';
14
14
  import { Bridge } from './bridge.js';
15
15
  import { MockProvider } from './providers/mock.js';
16
16
  import { GatewayProvider } from './providers/gateway.js';
17
- import { resolveConfig, ensureGatewayScopeConfigOnStartup } from './config.js';
17
+ import { resolveConfig } from './config.js';
18
18
  import { resolveGatewayConfig, validateGatewayConfig } from './gateway/gateway-config.js';
19
19
  import { runSetup } from './setup.js';
20
20
  const TAG = 'main';
@@ -150,7 +150,6 @@ Gateway config resolution (for --provider gateway):
150
150
  // ---- Main ----
151
151
  async function main() {
152
152
  loadEnvFile();
153
- ensureGatewayScopeConfigOnStartup();
154
153
  const { cli: args, gateway: gatewayArgs } = parseArgs();
155
154
  if (args.logLevel) {
156
155
  setLogLevel(args.logLevel);
@@ -245,6 +245,16 @@ export declare class GatewayProvider implements OpenClawProvider {
245
245
  private startToolRetry;
246
246
  private createPoller;
247
247
  private wireWsEvents;
248
+ /** Whether we've already acted on a missing write scope this process. */
249
+ private writeScopeRelaxationApplied;
250
+ /** Least-privilege escalation, gated on evidence. Some OpenClaw builds strip
251
+ * `operator.write` from a WS connection with no device identity; without it
252
+ * the bridge cannot place or cancel orders. Rather than pre-emptively
253
+ * relaxing every user's gateway auth at install time (what we did until
254
+ * 2026-07-29), we read the scopes the gateway ACTUALLY granted and relax
255
+ * only when it demonstrably withheld the one we need. On the supported
256
+ * range the scope survives and nothing is written. */
257
+ private checkGrantedScopes;
248
258
  private onWsConnected;
249
259
  private onAgentEvent;
250
260
  private onPollerTicker;
@@ -6,6 +6,7 @@ import { join } from 'path';
6
6
  import { homedir } from 'os';
7
7
  import { logger, formatError } from '../logger.js';
8
8
  import { isTradingMode } from '../types.js';
9
+ import { handshakeLacksWriteScope, relaxGatewayDeviceAuth } from '../config.js';
9
10
  import { toIntelSymbol } from '@reefclaw/shared';
10
11
  import { GatewayHttpClient } from '../gateway/gateway-http-client.js';
11
12
  import { GatewayWsClient } from '../gateway/gateway-ws-client.js';
@@ -1001,6 +1002,10 @@ export class GatewayProvider {
1001
1002
  if (token) {
1002
1003
  logger.info(TAG, 'Got device token from WS handshake');
1003
1004
  }
1005
+ // FIRST handshake of the process — the one that matters on a fresh
1006
+ // install. onWsConnected() only fires on RECONNECT, so without this the
1007
+ // proven-need scope check would never run on a box that needs it.
1008
+ this.checkGrantedScopes(helloOk);
1004
1009
  settle(token);
1005
1010
  };
1006
1011
  const onFailed = () => {
@@ -1140,6 +1145,35 @@ export class GatewayProvider {
1140
1145
  });
1141
1146
  this.wsClient.on('agent', (payload) => this.onAgentEvent(payload));
1142
1147
  }
1148
+ /** Whether we've already acted on a missing write scope this process. */
1149
+ writeScopeRelaxationApplied = false;
1150
+ /** Least-privilege escalation, gated on evidence. Some OpenClaw builds strip
1151
+ * `operator.write` from a WS connection with no device identity; without it
1152
+ * the bridge cannot place or cancel orders. Rather than pre-emptively
1153
+ * relaxing every user's gateway auth at install time (what we did until
1154
+ * 2026-07-29), we read the scopes the gateway ACTUALLY granted and relax
1155
+ * only when it demonstrably withheld the one we need. On the supported
1156
+ * range the scope survives and nothing is written. */
1157
+ checkGrantedScopes(helloOk) {
1158
+ const scopes = helloOk.auth?.scopes;
1159
+ if (!handshakeLacksWriteScope(scopes))
1160
+ return;
1161
+ if (this.writeScopeRelaxationApplied)
1162
+ return;
1163
+ this.writeScopeRelaxationApplied = true;
1164
+ logger.warn(TAG, `Gateway granted [${(scopes ?? []).join(', ')}] — operator.write is MISSING, ` +
1165
+ `so order placement and cancellation would fail on this connection.`);
1166
+ const changed = relaxGatewayDeviceAuth('the gateway completed a handshake without granting operator.write');
1167
+ if (changed) {
1168
+ logger.warn(TAG, 'Gateway config updated — RESTART the OpenClaw gateway to regain operator.write. ' +
1169
+ 'Trading tools will keep failing until you do.');
1170
+ }
1171
+ else {
1172
+ logger.error(TAG, 'operator.write is missing and the relaxation flags are ALREADY set — this is ' +
1173
+ 'not the known device-identity case. Check gateway auth config; trading is ' +
1174
+ 'blocked until the gateway grants operator.write.');
1175
+ }
1176
+ }
1143
1177
  onWsConnected(helloOk) {
1144
1178
  if (!this.started)
1145
1179
  return; // Guard: stop() called before WS connected
@@ -1153,6 +1187,7 @@ export class GatewayProvider {
1153
1187
  if (helloOk.auth?.deviceToken && this.http) {
1154
1188
  this.http.setToken(helloOk.auth.deviceToken);
1155
1189
  }
1190
+ this.checkGrantedScopes(helloOk);
1156
1191
  // Resume poller on reconnect (initial poller creation is in initialize())
1157
1192
  if (this.poller) {
1158
1193
  this.poller.resume();
@@ -24,15 +24,24 @@ export declare function signedMessage(version: string, contentSha256: string, si
24
24
  /**
25
25
  * Is OTA signature verification enforced?
26
26
  *
27
- * Default **OFF** while C1 (signed OTA) is ON HOLD see
28
- * docs/PRE_LAUNCH_SECURITY_HARDENING.md §2.5. Keeping it off by default makes
29
- * this whole module DORMANT, so deploying skill/relay for UNRELATED reasons can
30
- * never break the existing (unsigned) OTA flow no env flag to remember.
27
+ * **Default ON since 2026-07-29** C1 is live. A SKILL.md update is applied
28
+ * only if it carries a valid Ed25519 signature from a key pinned in
29
+ * TRUSTED_SKILL_SIGNING_KEYS above, whose private half exists only on the
30
+ * operator's machine. Neither the relay, the webapp, nor CI can forge one, so a
31
+ * compromise of any of them cannot rewrite an agent's trading instructions.
31
32
  *
32
- * To ENABLE signed-OTA (when the agent-update mechanism is finalized): pin a
33
- * public key in TRUSTED_SKILL_SIGNING_KEYS **and** set
34
- * SKILL_OTA_REQUIRE_SIGNATURE=on. Both are required a pinned key alone does
35
- * nothing without the flag, and the flag alone fails closed (no key → reject).
33
+ * RELEASE ORDER (load-bearing): the content endpoint must be serving
34
+ * signatures BEFORE a build with this default reaches boxes. Deploy the webapp
35
+ * first (its build step verifies the signature and fails on mismatch), confirm
36
+ * GET /api/internal/skill-content returns `signature`, and only then publish the
37
+ * plugin/bridge. Ship them the other way round and every box rejects its
38
+ * instructions and stays on the bootstrap skill.
39
+ *
40
+ * Break-glass: SKILL_OTA_REQUIRE_SIGNATURE=off restores the old unsigned
41
+ * behaviour on a single box. It is a diagnostic escape hatch for the operator,
42
+ * not a supported mode — an unsigned update is exactly what this gate exists to
43
+ * refuse. Note the flag alone cannot weaken a *bad* signature: an update that
44
+ * carries an INVALID signature is refused regardless of this setting.
36
45
  */
37
46
  export declare function signatureRequired(): boolean;
38
47
  /**
@@ -34,7 +34,11 @@ import { parseSkillVersion } from './skill-version.js';
34
34
  * one — see the header for provisioning.
35
35
  */
36
36
  export const TRUSTED_SKILL_SIGNING_KEYS = [
37
- // 'Base64OfRaw32BytePublicKey=' paste from scripts/gen-skill-signing-key.py
37
+ // Pinned 2026-07-29. Private half lives ONLY in the operator's .env.deploy
38
+ // (gitignored, never on a server or in CI) — scripts/rotate-skill-signing-key.py
39
+ // writes it without printing it. Rotation: ADD the new key here, ship the
40
+ // build to every box, re-sign, then remove the old entry.
41
+ '3ptLQsSKT9/8GX4yTeRagGpXO5yCEdjVMnDex/qV66M=',
38
42
  ];
39
43
  const OTA_STATE_PATH = join(homedir(), '.openclaw', 'workspace', '.skill-ota-state.json');
40
44
  /** Lowercase hex SHA-256 of the UTF-8 content. MUST match the Python signer. */
@@ -48,18 +52,27 @@ export function signedMessage(version, contentSha256, signedAt) {
48
52
  /**
49
53
  * Is OTA signature verification enforced?
50
54
  *
51
- * Default **OFF** while C1 (signed OTA) is ON HOLD see
52
- * docs/PRE_LAUNCH_SECURITY_HARDENING.md §2.5. Keeping it off by default makes
53
- * this whole module DORMANT, so deploying skill/relay for UNRELATED reasons can
54
- * never break the existing (unsigned) OTA flow no env flag to remember.
55
+ * **Default ON since 2026-07-29** C1 is live. A SKILL.md update is applied
56
+ * only if it carries a valid Ed25519 signature from a key pinned in
57
+ * TRUSTED_SKILL_SIGNING_KEYS above, whose private half exists only on the
58
+ * operator's machine. Neither the relay, the webapp, nor CI can forge one, so a
59
+ * compromise of any of them cannot rewrite an agent's trading instructions.
55
60
  *
56
- * To ENABLE signed-OTA (when the agent-update mechanism is finalized): pin a
57
- * public key in TRUSTED_SKILL_SIGNING_KEYS **and** set
58
- * SKILL_OTA_REQUIRE_SIGNATURE=on. Both are required a pinned key alone does
59
- * nothing without the flag, and the flag alone fails closed (no key → reject).
61
+ * RELEASE ORDER (load-bearing): the content endpoint must be serving
62
+ * signatures BEFORE a build with this default reaches boxes. Deploy the webapp
63
+ * first (its build step verifies the signature and fails on mismatch), confirm
64
+ * GET /api/internal/skill-content returns `signature`, and only then publish the
65
+ * plugin/bridge. Ship them the other way round and every box rejects its
66
+ * instructions and stays on the bootstrap skill.
67
+ *
68
+ * Break-glass: SKILL_OTA_REQUIRE_SIGNATURE=off restores the old unsigned
69
+ * behaviour on a single box. It is a diagnostic escape hatch for the operator,
70
+ * not a supported mode — an unsigned update is exactly what this gate exists to
71
+ * refuse. Note the flag alone cannot weaken a *bad* signature: an update that
72
+ * carries an INVALID signature is refused regardless of this setting.
60
73
  */
61
74
  export function signatureRequired() {
62
- return (process.env.SKILL_OTA_REQUIRE_SIGNATURE ?? 'off').toLowerCase() === 'on';
75
+ return (process.env.SKILL_OTA_REQUIRE_SIGNATURE ?? 'on').toLowerCase() !== 'off';
63
76
  }
64
77
  function publicKeyFromRawB64(b64) {
65
78
  try {
@@ -101,54 +101,25 @@ export class BinancePrivateApi {
101
101
  if (this.testnet) {
102
102
  this.exchange.setSandboxMode(true);
103
103
  }
104
- // ---- WIDENED CANCEL TRACER (2026-05-16) ------------------------------
105
- // The narrow chokepoint (fapiPrivateDeleteAlgoOrder + the
106
- // cancelOrderByClientId ATTRIBUTION) is deployed and PROVEN working
107
- // (it caught the one bracket_integrity flush with a full stack) yet is
108
- // SILENT for the recurring ~20s rc-* bracket kills while a clean
109
- // NON-rc closePosition STOP+TP pair, identical in shape to the agent's,
110
- // survived 3.6 min untouched on the SAME symbol (controlled prod test
111
- // 2026-05-16). => the kill is keyed to the rc-* cid and BYPASSES every
112
- // BinancePrivateApi cancel method, i.e. something cancels directly on
113
- // the raw ccxt client. Wrap EVERY cancel surface on the raw instance so
114
- // no caller (this class, the reconciler, the agent, a ccxt cross-call,
115
- // anything holding `exchange`) can bypass it; log method + args + full
116
- // caller stack. Pure observability each wrapper calls straight through
117
- // (same `this`, same return Promise), zero behaviour change. Remove
118
- // once the proven source is fixed and soaked.
119
- {
120
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
121
- const ex = this.exchange;
122
- const cancelSurfaces = [
123
- 'cancelOrder', 'cancelOrders', 'cancelAllOrders', 'cancelOrdersForSymbols',
124
- 'fapiPrivateDeleteOrder', 'fapiPrivateDeleteAllOpenOrders',
125
- 'fapiPrivateDeleteAlgoOrder', 'fapiPrivateDeleteBatchOrders',
126
- 'fapiPrivateDeleteAlgoFuturesOrder',
127
- ];
128
- let armed = 0;
129
- for (const m of cancelSurfaces) {
130
- const orig = ex[m];
131
- if (typeof orig !== 'function')
132
- continue;
133
- ex[m] = (...args) => {
134
- let argStr;
135
- try {
136
- argStr = JSON.stringify(args).slice(0, 600);
137
- }
138
- catch {
139
- argStr = '(unserializable)';
140
- }
141
- const stack = new Error('wide-cancel-trace').stack ?? '(no stack)';
142
- logger.warn(TAG, `WIDE-CANCEL-TRACE — raw ccxt ${m}(${argStr}) invoked. If this ` +
143
- `carries an rc-*-{s,t} cid/algoId, THIS is the bracket ` +
144
- `stripper (the narrow chokepoint never sees it). Caller ` +
145
- `stack follows:\n${stack}`);
146
- return orig.apply(ex, args);
147
- };
148
- armed++;
149
- }
150
- logger.info(TAG, `WIDE-CANCEL-TRACE armed on ${armed} raw cancel surface(s)`);
151
- }
104
+ // ---- WIDE CANCEL TRACER: REMOVED 2026-07-29 --------------------------
105
+ // A temporary investigation tool (armed 2026-05-16) monkey-patched all
106
+ // nine raw ccxt cancel surfaces and logged `JSON.stringify(args)` plus a
107
+ // full caller stack on every cancel. It did its job: the cause was proven
108
+ // to be an EXTERNAL actor auto-cancelling any order with an `rc-`
109
+ // clientOrderId, fixed by renaming bracket cids to `bkt<16hex><role>`
110
+ // (docs/BRACKET_CANCEL_ROOT_CAUSE_2026-05-16.md). Its own note said
111
+ // "remove once the proven source is fixed and soaked" it then shipped
112
+ // for ten more weeks, logging order ids, symbols and quantities into
113
+ // every operator's journal on a live trading system.
114
+ //
115
+ // Deleted, not merely quieted: patching the shared ccxt instance is an
116
+ // invasive global side effect, and the two NARROW attribution tracers
117
+ // that remain (logAlgoCancelChokepoint + the cancelOrderByClientId
118
+ // bracket-cid trace) already cover the bracket-disappearance class. Those
119
+ // log only OUR OWN call stack — no order arguments — which is what makes
120
+ // them safe to keep. Do not re-add an args-logging wrapper; if a future
121
+ // investigation needs one, scope it to a symbol and gate it behind an
122
+ // explicit env flag with an expiry.
152
123
  logger.info(TAG, `Binance private API initialized (${this.testnet ? 'testnet' : 'mainnet'})`);
153
124
  }
154
125
  /**
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "reefclaw-paper-trading",
3
3
  "name": "ReefClaw Trading",
4
- "version": "0.1.16",
4
+ "version": "0.1.17",
5
5
  "description": "Supervised trading plugin for the ReefClaw dashboard: paper trading with real market data (no API keys required), and optional live trading on Binance or Hyperliquid behind explicit operator opt-in, exchange API credentials, and always-on protective stop brackets. Includes the dashboard connector bridge, heartbeat automation, and remote SKILL.md instruction updates from the ReefClaw webapp.",
6
6
  "author": "ReefClaw",
7
7
  "activation": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reefclaw/openclaw-plugin",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "ReefClaw supervised trading plugin for OpenClaw — paper trading with real market data, optional live trading on Binance or Hyperliquid (operator opt-in, API keys, always-on protective brackets), plus the ReefClaw dashboard connector with heartbeat automation and remote SKILL.md updates from the ReefClaw webapp. Install: /plugins install clawhub:@reefclaw/openclaw-plugin",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -30,7 +30,9 @@ config:
30
30
 
31
31
  This is the thin BOOTSTRAP copy shipped in the `@reefclaw/connect` npm package. It covers **installing the ReefClaw plugin and connecting to the user's ReefClaw account** — nothing else. The FULL trading instructions (versioned 2.x.y) are delivered automatically by the ReefClaw connector minutes after the first successful connect, over the authenticated update channel — they replace this file in place (a 0.0.x version always loses the semver comparison). Maintainers: the "Connecting (first run)" section below MUST stay in sync with the same section in `skill/SKILL.md`.
32
32
 
33
- You are being connected to **ReefClaw**, a supervised trading control room. Your operator watches everything you do on the ReefClaw dashboard. Some steps below change host configuration or install software; they exist solely to complete the setup **the user initiated by pasting their connect message**. Tell the user what each step changes before you do it, do it only for that user, and revert any temporary permission (like elevated exec) as soon as the install completes.
33
+ You are being connected to **ReefClaw**, a supervised trading control room. Your operator watches everything you do on the ReefClaw dashboard.
34
+
35
+ **Your part of the setup is one thing only: save the connection settings the user pastes to you.** You do not install software, change OpenClaw's security settings, grant yourself permissions, or restart anything. Installation is a human step, run deliberately in a terminal (below). If you cannot complete a step, say so and stop — never work around a permission boundary.
34
36
 
35
37
  **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.
36
38
 
@@ -67,34 +69,21 @@ When a user gives you this message (or just a bare `Here's my ReefClaw token: rc
67
69
  - 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.
68
70
  - 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.
69
71
 
70
- ## Installing without any terminal (preferred)
71
-
72
- OpenClaw can install the ReefClaw plugin through its own owner chat command — no exec, no terminal:
73
-
74
- 1. In `~/.openclaw/openclaw.json` set `commands.plugins: true` and add the user's **sender id** (visible in this session) to `commands.ownerAllowFrom`.
75
- 2. Tell the user to type `/restart` in this chat.
76
- 3. Then the user types: `/plugins install clawhub:@reefclaw/openclaw-plugin` — the install restarts OpenClaw automatically; the bundled connector starts by itself and the dashboard flips to Connected.
72
+ ## Installing the plugin — a human step, in a terminal
77
73
 
78
- If that install is **cancelled with a ClawHub security warning** ("Security scan: suspicious"): this is expected — ReefClaw is a trading plugin with disclosed live-trading capability, exchange-credential storage, and remote instruction updates, and ClawHub flags that capability set for review (the linked security page shows the details; VirusTotal reports it clean). **The chat command cannot acknowledge the warning — do NOT retry it in chat.** Two working paths, in order of preference:
74
+ If the ReefClaw trading tools are not present yet, the **user** installs them by running this once, in a terminal on the machine running OpenClaw (inside the container, if OpenClaw runs in Docker):
79
75
 
80
- 1. Run `npx --yes @reefclaw/connect` — it does not go through ClawHub. Use your exec tool if permitted (see the elevated-exec section below if it is not), or ask the user to run it once in a terminal.
81
- 2. A human who has reviewed the warning runs `openclaw plugins install clawhub:@reefclaw/openclaw-plugin --acknowledge-clawhub-risk` in a terminal (the acknowledgement is accepted from a trusted shell, only refused from chat).
82
-
83
- Only fall back to `npx @reefclaw/connect` (below) when the chat install is unavailable or cancelled as above.
76
+ ```
77
+ npx --yes @reefclaw/connect
78
+ ```
84
79
 
85
- ## If you cannot run the installer (elevated exec disabled)
80
+ That is the whole install. It places the plugin, sets up the connector, restarts the gateway, and prints what it changed. It needs no ReefClaw token — the connection is the separate paste step above, and the connector picks it up on its own.
86
81
 
87
- 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:
82
+ **Do not attempt to perform this install yourself, and do not work around it if it is unavailable to you.** Specifically: do not edit OpenClaw's configuration to enable chat install commands, do not add anyone to an owner or elevated-exec allowlist, do not enable elevated mode, and do not restart the gateway to widen your own permissions. If you cannot run the command, the correct outcome is to hand the user the one line above and let them run it deliberately. A trading integration is exactly the kind of software whose installation a human should perform knowingly.
88
83
 
89
- 1. **Self-service (no terminal)**:
90
- - 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.
91
- - Tell the user to type `/restart` in this chat (OpenClaw's built-in in-process restart).
92
- - 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.
93
- - **RETRY the install command — do not assume it is still blocked.**
94
- - 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).
95
- 2. **Manual (last resort)**: the user runs `npx @reefclaw/connect` themselves in a terminal on this machine — one command, nothing else.
84
+ **If the user installs via ClawHub instead** and sees a security warning ("Security scan: suspicious"): that is expected and honest — ReefClaw is a trading plugin, and it discloses live-trading capability, exchange-credential storage, and remote instruction updates, which ClawHub flags for review (its linked security page has the details; VirusTotal reports the artifact clean). The chat command cannot acknowledge that warning, so the user should either use the `npx` line above, or — having read the warning — run `openclaw plugins install clawhub:@reefclaw/openclaw-plugin --acknowledge-clawhub-risk` themselves in a terminal.
96
85
 
97
- Either way, **save the connection config regardless** — the connector picks it up automatically the moment the install completes.
86
+ Whatever the install path, **save the connection settings anyway** — the connector picks them up automatically the moment the plugin is running.
98
87
 
99
88
  ## After connecting
100
89