@reefclaw/openclaw-plugin 0.1.13 → 0.1.14
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/gateway/gateway-config.d.ts +16 -5
- package/bridge/gateway/gateway-config.js +68 -12
- package/bridge/gateway/poller.js +18 -8
- package/bridge/providers/emergency-commands.d.ts +9 -1
- package/bridge/providers/emergency-commands.js +38 -1
- package/bridge/providers/gateway.d.ts +23 -1
- package/bridge/providers/gateway.js +79 -17
- package/bridge/providers/onboarding-commands.d.ts +8 -0
- package/bridge/providers/onboarding-commands.js +4 -4
- package/ccxt/binance-public.d.ts +17 -5
- package/ccxt/binance-public.js +31 -3
- package/config/operator-provenance.d.ts +6 -0
- package/config/operator-provenance.js +50 -0
- package/config/plugin-config-io.d.ts +15 -1
- package/config/plugin-config-io.js +24 -0
- package/index.js +216 -173
- package/ingest/event-loop-monitor.d.ts +11 -0
- package/ingest/event-loop-monitor.js +113 -0
- package/ingest/position-auto-capture.d.ts +5 -0
- package/ingest/position-auto-capture.js +14 -5
- package/ingest/readiness-reporter.d.ts +17 -6
- package/ingest/readiness-reporter.js +88 -9
- package/ingest/skill-version-reader.d.ts +16 -0
- package/ingest/skill-version-reader.js +64 -0
- package/live/approval-lifecycle.d.ts +30 -0
- package/live/approval-lifecycle.js +80 -0
- package/live/bracket-types.d.ts +9 -0
- package/live/live-adapter.d.ts +0 -1
- package/onboarding/runtime.d.ts +34 -1
- package/onboarding/runtime.js +56 -5
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/simulator/exchange-simulator.d.ts +45 -2
- package/simulator/exchange-simulator.js +96 -4
- package/simulator/types.d.ts +17 -0
- package/tools/attach-brackets.js +50 -1
- package/venues/hyperliquid/hl-bracket-coordinator.d.ts +25 -1
- package/venues/hyperliquid/hl-bracket-coordinator.js +57 -0
- package/venues/hyperliquid/hl-brackets.d.ts +10 -0
- package/venues/hyperliquid/hl-brackets.js +45 -13
- package/venues/hyperliquid/hl-fill-ingest.d.ts +18 -0
- package/venues/hyperliquid/hl-fill-ingest.js +69 -0
- package/venues/hyperliquid/hl-live-adapter.d.ts +32 -0
- package/venues/hyperliquid/hl-live-adapter.js +112 -7
- package/venues/hyperliquid/hl-public.d.ts +12 -5
- package/venues/hyperliquid/hl-public.js +24 -3
- package/venues/hyperliquid/hl-user-stream.d.ts +13 -1
- package/venues/hyperliquid/hl-user-stream.js +4 -1
package/ccxt/binance-public.js
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
import { createRequire } from 'node:module';
|
|
34
34
|
import { logger } from '../logger.js';
|
|
35
35
|
import { assertNotBanned, noteBinanceError, noteSuccess, BinanceBannedError } from './binance-ban-gate.js';
|
|
36
|
+
import { REACHABILITY_STALL_FACTOR } from '@reefclaw/shared';
|
|
36
37
|
const TAG = 'binance-public';
|
|
37
38
|
// Load ccxt via CJS require — OpenClaw's ESM loader gives wrong module shape
|
|
38
39
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -190,8 +191,19 @@ export class BinancePublicApi {
|
|
|
190
191
|
* - 'reachable' clean response (driftMs = serverTime − localTime)
|
|
191
192
|
* - 'geo_blocked' HTTP 451 — host is in a restricted region (actionable)
|
|
192
193
|
* - 'unknown' the ban/weight gate paused us — NOT a host problem
|
|
193
|
-
* - '
|
|
194
|
+
* - 'stalled' THIS process was starved — we learned nothing (issue #265)
|
|
195
|
+
* - 'unreachable' network / DNS / timeout / other error
|
|
196
|
+
*
|
|
197
|
+
* ★ Self-stall detection: an error is evidence about BINANCE only if it
|
|
198
|
+
* arrived on schedule. When the host starves the process the loop stops
|
|
199
|
+
* running, ccxt's own timeout lands minutes late, and the old code blamed the
|
|
200
|
+
* network — producing a confident "you are geo-blocked / check your firewall"
|
|
201
|
+
* banner while the agent was in fact trading normally. Past
|
|
202
|
+
* REACHABILITY_STALL_FACTOR× the request budget we report `stalled` and the
|
|
203
|
+
* reporter renders it `unknown`. A 451 still wins: the server ANSWERED, so
|
|
204
|
+
* that classification stands however late we noticed it. */
|
|
194
205
|
async probeReachability() {
|
|
206
|
+
const startedAt = Date.now();
|
|
195
207
|
try {
|
|
196
208
|
assertNotBanned('reachabilityProbe');
|
|
197
209
|
const r = await this.exchange.fapiPublicGetTime({});
|
|
@@ -209,8 +221,24 @@ export class BinancePublicApi {
|
|
|
209
221
|
}
|
|
210
222
|
const msg = err instanceof Error ? err.message : String(err);
|
|
211
223
|
const geo = msg.includes('451');
|
|
212
|
-
|
|
213
|
-
|
|
224
|
+
if (geo) {
|
|
225
|
+
logger.warn(TAG, `probeReachability failed (HTTP 451 geo-block): ${msg}`);
|
|
226
|
+
return { outcome: 'geo_blocked', driftMs: null };
|
|
227
|
+
}
|
|
228
|
+
const elapsedMs = Date.now() - startedAt;
|
|
229
|
+
if (elapsedMs > this.probeTimeoutMs() * REACHABILITY_STALL_FACTOR) {
|
|
230
|
+
logger.warn(TAG, `probeReachability inconclusive: this process was starved — a ${this.probeTimeoutMs()}ms probe took ${Math.round(elapsedMs / 1000)}s (${msg}). Reporting reachability as unknown, NOT as a Binance failure.`);
|
|
231
|
+
return { outcome: 'stalled', driftMs: null, stallMs: elapsedMs };
|
|
232
|
+
}
|
|
233
|
+
logger.warn(TAG, `probeReachability failed: ${msg}`);
|
|
234
|
+
return { outcome: 'unreachable', driftMs: null };
|
|
214
235
|
}
|
|
215
236
|
}
|
|
237
|
+
/** The request budget the stall yardstick is measured against. ccxt owns the
|
|
238
|
+
* actual timeout, so read it from the instance rather than hardcoding a
|
|
239
|
+
* second copy that could silently drift from it. */
|
|
240
|
+
probeTimeoutMs() {
|
|
241
|
+
const t = Number(this.exchange?.timeout);
|
|
242
|
+
return Number.isFinite(t) && t > 0 ? t : 10_000;
|
|
243
|
+
}
|
|
216
244
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Operator-provenance gate for the state-mutating operator tools
|
|
2
|
+
// (audit 2026-07-26 F12).
|
|
3
|
+
//
|
|
4
|
+
// "Operator-only. Never called by the agent." was PROSE in four tool
|
|
5
|
+
// descriptions — nothing enforced it, and `acknowledged:true` is
|
|
6
|
+
// caller-supplied, so a tool-capable agent could walk the PAPER→LIVE ladder,
|
|
7
|
+
// clear the exchange credentials, or disable the stop-loss requirement with
|
|
8
|
+
// one impulsive call. The dashboard path (browser → relay → skill bridge →
|
|
9
|
+
// gateway) now proves provenance: the bridge injects the rc_* connection
|
|
10
|
+
// token it already authenticates with, and the plugin compares it against
|
|
11
|
+
// its own copy. The chat redaction guards keep rc_* tokens OUT of the
|
|
12
|
+
// agent's context, so the agent cannot produce one conversationally.
|
|
13
|
+
//
|
|
14
|
+
// THREAT-MODEL HONESTY: the agent runs on the operator's own box and could
|
|
15
|
+
// read plugin-config with exec tools — this gate defends against one-call
|
|
16
|
+
// LLM impulsivity (the realistic failure mode), not a deliberately
|
|
17
|
+
// adversarial agent, which owns the box and could edit the config directly.
|
|
18
|
+
//
|
|
19
|
+
// Fail direction: OPEN when no connection token is configured (a pure-local
|
|
20
|
+
// dev box has no dashboard to inject provenance — same
|
|
21
|
+
// infrastructure-absent ⇒ allow direction as the entitlement gate);
|
|
22
|
+
// CLOSED on a missing or mismatched token when one IS configured.
|
|
23
|
+
// Read per call (no restart needed after a token rotation — F10 philosophy).
|
|
24
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
25
|
+
import { readPluginConfig } from './plugin-config-io.js';
|
|
26
|
+
import { resolveIngestToken } from './user-data-stream-config.js';
|
|
27
|
+
const REFUSAL = 'operator_only: this tool changes safety-critical operator settings and accepts requests ' +
|
|
28
|
+
'ONLY from the ReefClaw dashboard (operator provenance token missing or invalid). Do not ' +
|
|
29
|
+
'retry and do not attempt to obtain the token — if this change is wanted, the OPERATOR ' +
|
|
30
|
+
'makes it from the dashboard Settings panel.';
|
|
31
|
+
export function verifyOperatorProvenance(provided, configPath) {
|
|
32
|
+
let expected;
|
|
33
|
+
try {
|
|
34
|
+
expected = resolveIngestToken(readPluginConfig(configPath))?.trim() || undefined;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
expected = undefined;
|
|
38
|
+
}
|
|
39
|
+
if (!expected)
|
|
40
|
+
return { ok: true }; // unconfigured box — nothing to prove against
|
|
41
|
+
const given = typeof provided === 'string' ? provided.trim() : '';
|
|
42
|
+
if (given.length === 0)
|
|
43
|
+
return { ok: false, error: REFUSAL };
|
|
44
|
+
const a = Buffer.from(given, 'utf8');
|
|
45
|
+
const b = Buffer.from(expected, 'utf8');
|
|
46
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
|
47
|
+
return { ok: false, error: REFUSAL };
|
|
48
|
+
}
|
|
49
|
+
return { ok: true };
|
|
50
|
+
}
|
|
@@ -46,8 +46,12 @@ export interface PluginConfigFile {
|
|
|
46
46
|
hl?: {
|
|
47
47
|
marketSlippagePct?: number;
|
|
48
48
|
};
|
|
49
|
+
/** MICRO_LIVE per-order notional cap in quote-USD (USDT on Binance, USDC on
|
|
50
|
+
* Hyperliquid). Default 50 when the mode is MICRO_LIVE and no value is set.
|
|
51
|
+
* `sizeCapPercent` used to be accepted here but was NEVER enforced by any
|
|
52
|
+
* adapter (dead config implying protection it didn't provide) — it is no
|
|
53
|
+
* longer read; loadMicroLiveConfig warns when it is present. */
|
|
49
54
|
microLive?: {
|
|
50
|
-
sizeCapPercent?: number;
|
|
51
55
|
maxPositionUSDT?: number;
|
|
52
56
|
};
|
|
53
57
|
/** Exchange-native bracket orders (STOP_MARKET + TAKE_PROFIT_MARKET).
|
|
@@ -202,6 +206,16 @@ export declare function defaultConfigPath(): string;
|
|
|
202
206
|
* the override to the default (same read-at-build-time pattern as
|
|
203
207
|
* loadBracketMode). */
|
|
204
208
|
export declare function loadStopWatcherIntervalMs(path?: string): number | undefined;
|
|
209
|
+
/** Best-effort read of the operator's micro-live notional cap
|
|
210
|
+
* (`microLive.maxPositionUSDT`). Read at every adapter CONSTRUCTION — boot
|
|
211
|
+
* AND runtime reconnects — so a mode flip or credential save can't silently
|
|
212
|
+
* reset a raised OR lowered cap back to the $50 default (audit 2026-07-26
|
|
213
|
+
* F8; same read-at-build-time pattern as loadBracketMode /
|
|
214
|
+
* loadStopWatcherIntervalMs). Returns undefined when unset/garbage — the
|
|
215
|
+
* adapters then apply their own MICRO_LIVE default. */
|
|
216
|
+
export declare function loadMicroLiveConfig(path?: string): {
|
|
217
|
+
maxPositionUSDT?: number;
|
|
218
|
+
} | undefined;
|
|
205
219
|
/** Read the config file. Returns `{}` if the file doesn't exist.
|
|
206
220
|
* Throws if the file exists but is unreadable or not valid JSON — callers
|
|
207
221
|
* should treat that as an abort signal, not silently overwrite. */
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
|
|
9
9
|
import { homedir } from 'node:os';
|
|
10
10
|
import { dirname, join } from 'node:path';
|
|
11
|
+
import { logger } from '../logger.js';
|
|
11
12
|
/** The connection the AGENT saves during onboarding lives in OpenClaw's own
|
|
12
13
|
* config (`skills.entries.reefclaw.config` in ~/.openclaw/openclaw.json) —
|
|
13
14
|
* the connector reads it there, and since the chat-install flow never writes
|
|
@@ -53,6 +54,29 @@ export function loadStopWatcherIntervalMs(path) {
|
|
|
53
54
|
catch { /* best-effort — default applies */ }
|
|
54
55
|
return undefined;
|
|
55
56
|
}
|
|
57
|
+
/** Best-effort read of the operator's micro-live notional cap
|
|
58
|
+
* (`microLive.maxPositionUSDT`). Read at every adapter CONSTRUCTION — boot
|
|
59
|
+
* AND runtime reconnects — so a mode flip or credential save can't silently
|
|
60
|
+
* reset a raised OR lowered cap back to the $50 default (audit 2026-07-26
|
|
61
|
+
* F8; same read-at-build-time pattern as loadBracketMode /
|
|
62
|
+
* loadStopWatcherIntervalMs). Returns undefined when unset/garbage — the
|
|
63
|
+
* adapters then apply their own MICRO_LIVE default. */
|
|
64
|
+
export function loadMicroLiveConfig(path) {
|
|
65
|
+
try {
|
|
66
|
+
const ml = readPluginConfig(path).microLive;
|
|
67
|
+
if (!ml || typeof ml !== 'object')
|
|
68
|
+
return undefined;
|
|
69
|
+
if (ml.sizeCapPercent !== undefined) {
|
|
70
|
+
logger.warn('plugin-config', 'microLive.sizeCapPercent is set but has NEVER been enforced by any adapter — it has no effect. ' +
|
|
71
|
+
'Remove it; microLive.maxPositionUSDT is the enforced per-order notional cap.');
|
|
72
|
+
}
|
|
73
|
+
const cap = ml.maxPositionUSDT;
|
|
74
|
+
if (typeof cap === 'number' && Number.isFinite(cap) && cap > 0)
|
|
75
|
+
return { maxPositionUSDT: cap };
|
|
76
|
+
}
|
|
77
|
+
catch { /* best-effort — adapter default applies */ }
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
56
80
|
/** Read the config file. Returns `{}` if the file doesn't exist.
|
|
57
81
|
* Throws if the file exists but is unreadable or not valid JSON — callers
|
|
58
82
|
* should treat that as an abort signal, not silently overwrite. */
|