@reefclaw/openclaw-plugin 0.1.15 → 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 +18 -6
- package/bridge/config.d.ts +18 -4
- package/bridge/config.js +70 -33
- package/bridge/index.js +1 -2
- package/bridge/providers/gateway.d.ts +10 -0
- package/bridge/providers/gateway.js +35 -0
- package/bridge/utils/skill-signing.d.ts +17 -8
- package/bridge/utils/skill-signing.js +23 -10
- package/ccxt/binance-private.js +19 -48
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -3
- package/skills/reefclaw/SKILL.md +12 -23
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
|
-
|
|
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
|
|
806
|
-
//
|
|
807
|
-
|
|
808
|
-
|
|
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
|
-
|
|
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,
|
package/bridge/config.d.ts
CHANGED
|
@@ -11,13 +11,27 @@ export declare function readOpenClawConfig(): {
|
|
|
11
11
|
userId?: string;
|
|
12
12
|
relayUrl?: string;
|
|
13
13
|
} | null;
|
|
14
|
-
/** Write/merge
|
|
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
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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
|
|
16
|
-
//
|
|
17
|
-
// during onboarding
|
|
18
|
-
//
|
|
19
|
-
//
|
|
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
|
|
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
|
-
//
|
|
76
|
-
//
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
*
|
|
134
|
-
*
|
|
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
|
|
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 (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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
|
|
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 **
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
-
//
|
|
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 **
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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 ?? '
|
|
75
|
+
return (process.env.SKILL_OTA_REQUIRE_SIGNATURE ?? 'on').toLowerCase() !== 'off';
|
|
63
76
|
}
|
|
64
77
|
function publicKeyFromRawB64(b64) {
|
|
65
78
|
try {
|
package/ccxt/binance-private.js
CHANGED
|
@@ -101,54 +101,25 @@ export class BinancePrivateApi {
|
|
|
101
101
|
if (this.testnet) {
|
|
102
102
|
this.exchange.setSandboxMode(true);
|
|
103
103
|
}
|
|
104
|
-
// ----
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
/**
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "reefclaw-paper-trading",
|
|
3
3
|
"name": "ReefClaw Trading",
|
|
4
|
-
"version": "0.1.
|
|
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.
|
|
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",
|
|
@@ -23,13 +23,15 @@
|
|
|
23
23
|
"node": ">=20"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@reefclaw/shared": "0.1.
|
|
26
|
+
"@reefclaw/shared": "0.1.4",
|
|
27
27
|
"ccxt": "4.5.37",
|
|
28
28
|
"json5": "2.2.3",
|
|
29
29
|
"ws": "8.21.1"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
|
-
"build": "node scripts/assemble.mjs"
|
|
32
|
+
"build": "node scripts/assemble.mjs",
|
|
33
|
+
"verify": "node scripts/verify-shared-contract.mjs",
|
|
34
|
+
"prepublishOnly": "node scripts/verify-shared-contract.mjs"
|
|
33
35
|
},
|
|
34
36
|
"license": "MIT",
|
|
35
37
|
"homepage": "https://reefclaw.com"
|
package/skills/reefclaw/SKILL.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|