@reefclaw/openclaw-plugin 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bridge/bridge.js CHANGED
@@ -6,7 +6,7 @@ import { join } from 'path';
6
6
  import { homedir } from 'os';
7
7
  import { logger } from './logger.js';
8
8
  import { Connector } from './connector.js';
9
- import { readLocalSkillVersion, validateSkillContent, compareSemver } from './utils/skill-version.js';
9
+ import { readLocalSkillVersion, readAgentVisibleSkillVersions, validateSkillContent, compareSemver } from './utils/skill-version.js';
10
10
  import { verifySkillSignature, signatureRequired, readLastAppliedSignedAtMs, recordAppliedSignedAt, } from './utils/skill-signing.js';
11
11
  import { isTradingMode, validateModeTransition, redactTokens } from '@reefclaw/shared';
12
12
  import { OPERATOR_WRITE_METHODS } from './types.js';
@@ -837,6 +837,37 @@ export class Bridge {
837
837
  logger.warn(TAG, `Failed to update workspace skill SKILL.md: ${scanErr instanceof Error ? scanErr.message : String(scanErr)}`);
838
838
  }
839
839
  }
840
+ // 2d. Also write to the plugin-extension bundled skill directory — the
841
+ // chat-channel install layout (`/plugins install clawhub:...`) loads the
842
+ // agent-visible skill from ~/.openclaw/extensions/<plugin>/skills/, and a
843
+ // plugin (re)install resets that copy to the bundled bootstrap while the
844
+ // workspace copy above (our version tracker) survives. Without this
845
+ // write the agent stays on the bootstrap forever while the pull reports
846
+ // "already up to date" (observed live 2026-07-11).
847
+ const extensionsDir = join(homedir(), '.openclaw', 'extensions');
848
+ if (existsSync(extensionsDir)) {
849
+ try {
850
+ for (const ext of readdirSync(extensionsDir, { withFileTypes: true })) {
851
+ if (!ext.isDirectory() || !ext.name.includes('reefclaw'))
852
+ continue;
853
+ const extSkillsDir = join(extensionsDir, ext.name, 'skills');
854
+ if (!existsSync(extSkillsDir))
855
+ continue;
856
+ for (const skillEntry of readdirSync(extSkillsDir, { withFileTypes: true })) {
857
+ if (!skillEntry.isDirectory() || !skillEntry.name.includes('reefclaw'))
858
+ continue;
859
+ const extSkillMd = join(extSkillsDir, skillEntry.name, 'SKILL.md');
860
+ if (existsSync(extSkillMd)) {
861
+ writeFileSync(extSkillMd, content, 'utf-8');
862
+ logger.info(TAG, `Updated extension skill SKILL.md: ${extSkillMd}`);
863
+ }
864
+ }
865
+ }
866
+ }
867
+ catch (scanErr) {
868
+ logger.warn(TAG, `Failed to update extension skill SKILL.md: ${scanErr instanceof Error ? scanErr.message : String(scanErr)}`);
869
+ }
870
+ }
840
871
  // 3. Invalidate skillsSnapshot cache WITHOUT wiping sessions.json
841
872
  // Previously we wrote '{}' to sessions.json which destroyed OpenClaw chat history.
842
873
  // Now we surgically remove only the skillsSnapshot key from each session entry,
@@ -979,12 +1010,24 @@ export class Bridge {
979
1010
  return;
980
1011
  }
981
1012
  this.currentSkillVersion = readLocalSkillVersion();
982
- if (compareSemver(body.version, this.currentSkillVersion) <= 0) {
1013
+ // The workspace tracker alone is NOT proof the agent has the full skill:
1014
+ // a plugin (re)install resets the agent-visible extension copy to the
1015
+ // bundled bootstrap while the workspace copy survives (observed live
1016
+ // 2026-07-11 — agent stranded on 0.0.5 with the tracker at 2.20.4). If
1017
+ // ANY agent-visible copy is behind the webapp version, re-apply anyway;
1018
+ // applySkillUpdate is idempotent across all copies.
1019
+ const staleAgentCopies = readAgentVisibleSkillVersions().filter((v) => compareSemver(body.version, v) > 0);
1020
+ if (compareSemver(body.version, this.currentSkillVersion) <= 0 && staleAgentCopies.length === 0) {
983
1021
  logger.info(TAG, `SKILL.md webapp pull: local v${this.currentSkillVersion ?? 'unknown'} already >= webapp v${body.version} — nothing to do`);
984
1022
  this.skillPullInFlight = false;
985
1023
  return;
986
1024
  }
987
- logger.info(TAG, `SKILL.md webapp pull: local v${this.currentSkillVersion ?? 'unknown'} behind webapp v${body.version} — applying`);
1025
+ if (staleAgentCopies.length > 0 && compareSemver(body.version, this.currentSkillVersion) <= 0) {
1026
+ logger.info(TAG, `SKILL.md webapp pull: agent-visible skill copy at v${staleAgentCopies[0]} behind webapp v${body.version} (plugin reinstall reset it) — re-applying`);
1027
+ }
1028
+ else {
1029
+ logger.info(TAG, `SKILL.md webapp pull: local v${this.currentSkillVersion ?? 'unknown'} behind webapp v${body.version} — applying`);
1030
+ }
988
1031
  const result = await this.applySkillUpdate({ version: body.version, content: body.content });
989
1032
  this.emit('agent_state', 'skill_update_applied', {
990
1033
  event: 'skill_update_applied',
@@ -2,6 +2,15 @@
2
2
  export declare function parseSkillVersion(content: string): string | null;
3
3
  /** Read current SKILL.md version from disk */
4
4
  export declare function readLocalSkillVersion(): string | null;
5
+ /** Versions of the AGENT-VISIBLE reefclaw SKILL.md copies under the
6
+ * plugin-extension layout (`~/.openclaw/extensions/<plugin>/skills/<skill>/`)
7
+ * — what OpenClaw's skill loader actually reads on chat-channel installs.
8
+ * A plugin (re)install resets these to the bundled bootstrap while the
9
+ * workspace copy (the version tracker above) survives, so the tracker alone
10
+ * cannot be trusted for "already up to date" (observed live 2026-07-11: agent
11
+ * stranded on bootstrap 0.0.5 while the tracker read 2.20.4). Fail-soft:
12
+ * unreadable dirs/files are skipped; no extensions dir → empty list. */
13
+ export declare function readAgentVisibleSkillVersions(): string[];
5
14
  /**
6
15
  * Compare two semver strings.
7
16
  * Returns 1 if a > b, -1 if a < b, 0 if equal.
@@ -1,5 +1,5 @@
1
1
  // SKILL.md version parsing utility for OTA updates
2
- import { readFileSync } from 'fs';
2
+ import { readFileSync, readdirSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { homedir } from 'os';
5
5
  const SKILL_MD_PATH = join(homedir(), '.openclaw', 'workspace', 'SKILL.md');
@@ -18,6 +18,50 @@ export function readLocalSkillVersion() {
18
18
  return null;
19
19
  }
20
20
  }
21
+ /** Versions of the AGENT-VISIBLE reefclaw SKILL.md copies under the
22
+ * plugin-extension layout (`~/.openclaw/extensions/<plugin>/skills/<skill>/`)
23
+ * — what OpenClaw's skill loader actually reads on chat-channel installs.
24
+ * A plugin (re)install resets these to the bundled bootstrap while the
25
+ * workspace copy (the version tracker above) survives, so the tracker alone
26
+ * cannot be trusted for "already up to date" (observed live 2026-07-11: agent
27
+ * stranded on bootstrap 0.0.5 while the tracker read 2.20.4). Fail-soft:
28
+ * unreadable dirs/files are skipped; no extensions dir → empty list. */
29
+ export function readAgentVisibleSkillVersions() {
30
+ const versions = [];
31
+ const extensionsDir = join(homedir(), '.openclaw', 'extensions');
32
+ let extEntries;
33
+ try {
34
+ extEntries = readdirSync(extensionsDir, { withFileTypes: true });
35
+ }
36
+ catch {
37
+ return versions;
38
+ }
39
+ for (const ext of extEntries) {
40
+ if (!ext.isDirectory() || !ext.name.includes('reefclaw'))
41
+ continue;
42
+ const skillsDir = join(extensionsDir, ext.name, 'skills');
43
+ let skillEntries;
44
+ try {
45
+ skillEntries = readdirSync(skillsDir, { withFileTypes: true });
46
+ }
47
+ catch {
48
+ continue;
49
+ }
50
+ for (const s of skillEntries) {
51
+ if (!s.isDirectory() || !s.name.includes('reefclaw'))
52
+ continue;
53
+ try {
54
+ const v = parseSkillVersion(readFileSync(join(skillsDir, s.name, 'SKILL.md'), 'utf-8'));
55
+ if (v)
56
+ versions.push(v);
57
+ }
58
+ catch {
59
+ // unreadable/absent copy — nothing to report for this entry
60
+ }
61
+ }
62
+ }
63
+ return versions;
64
+ }
21
65
  // 120KB — the full SKILL.md crossed 100KB at v2.20.0 (103.7KB), which silently
22
66
  // broke every OTA apply against the old 100KB cap. Keep comfortably under the
23
67
  // 128KiB Cloudflare Durable-Object per-value hard limit the relay stores into.
@@ -128,6 +128,8 @@ const CONTEXT_WEIGHT = {
128
128
  fetchOHLCV: 2,
129
129
  fetchFundingRate: 1,
130
130
  fetchOpenInterest: 1,
131
+ // Readiness gate's host→Binance reachability probe (fapiPublicGetTime, wt 1).
132
+ reachabilityProbe: 1,
131
133
  // ExchangeInfoCache boot load — ccxt loadMarkets() → GET /fapi/v1/exchangeInfo,
132
134
  // IP weight 1 (doc-verified developers.binance.com 2026-07-10). Was the
133
135
  // known "ungated_ip" ccxt-internal call named in the window-summary comment.
@@ -200,7 +202,7 @@ const NEVER_PACE = new Set([
200
202
  const SHED_FIRST = new Set([
201
203
  'fetchTodayIncomeBreakdown', 'fetchTransfers', 'fetchRecentTradedSymbols',
202
204
  'fetchTicker', 'fetchTickerRaw', 'fetchOHLCV', 'fetchOrderBook',
203
- 'fetchFundingRate', 'fetchOpenInterest',
205
+ 'fetchFundingRate', 'fetchOpenInterest', 'reachabilityProbe',
204
206
  ]);
205
207
  let weightWindowStart = 0;
206
208
  let weightUsed = 0;
@@ -15,4 +15,19 @@ export declare class BinancePublicApi {
15
15
  fetchOrderBook(symbol: string, limit?: number): Promise<OrderBookDepth | null>;
16
16
  /** Fetch OHLCV candles. Returns null on error. */
17
17
  fetchOHLCV(symbol: string, timeframe?: string, limit?: number): Promise<CcxtOHLCV[] | null>;
18
+ /** Probe Binance USD-M FUTURES reachability from this host — the readiness
19
+ * gate's core signal. Calls the futures-explicit implicit method so it hits
20
+ * `fapi.binance.com` (a bare `fetchTime()` on this instance resolves to spot
21
+ * `api.binance.com`, since the public client doesn't set defaultType:'future').
22
+ * HTTP 451 = Binance geo-restriction; the ban gate does NOT classify 451, so
23
+ * we inspect the message here. Ban-gate compliant (assertNotBanned/noteSuccess/
24
+ * noteBinanceError). Outcomes:
25
+ * - 'reachable' clean response (driftMs = serverTime − localTime)
26
+ * - 'geo_blocked' HTTP 451 — host is in a restricted region (actionable)
27
+ * - 'unknown' the ban/weight gate paused us — NOT a host problem
28
+ * - 'unreachable' network / DNS / timeout / other error */
29
+ probeReachability(): Promise<{
30
+ outcome: 'reachable' | 'geo_blocked' | 'unreachable' | 'unknown';
31
+ driftMs: number | null;
32
+ }>;
18
33
  }
@@ -2,7 +2,7 @@
2
2
  // No API keys required — only uses public market data.
3
3
  import { createRequire } from 'node:module';
4
4
  import { logger } from '../logger.js';
5
- import { assertNotBanned, noteBinanceError, noteSuccess } from './binance-ban-gate.js';
5
+ import { assertNotBanned, noteBinanceError, noteSuccess, BinanceBannedError } from './binance-ban-gate.js';
6
6
  const TAG = 'binance-public';
7
7
  // Load ccxt via CJS require — OpenClaw's ESM loader gives wrong module shape
8
8
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -144,4 +144,37 @@ export class BinancePublicApi {
144
144
  return null;
145
145
  }
146
146
  }
147
+ /** Probe Binance USD-M FUTURES reachability from this host — the readiness
148
+ * gate's core signal. Calls the futures-explicit implicit method so it hits
149
+ * `fapi.binance.com` (a bare `fetchTime()` on this instance resolves to spot
150
+ * `api.binance.com`, since the public client doesn't set defaultType:'future').
151
+ * HTTP 451 = Binance geo-restriction; the ban gate does NOT classify 451, so
152
+ * we inspect the message here. Ban-gate compliant (assertNotBanned/noteSuccess/
153
+ * noteBinanceError). Outcomes:
154
+ * - 'reachable' clean response (driftMs = serverTime − localTime)
155
+ * - 'geo_blocked' HTTP 451 — host is in a restricted region (actionable)
156
+ * - 'unknown' the ban/weight gate paused us — NOT a host problem
157
+ * - 'unreachable' network / DNS / timeout / other error */
158
+ async probeReachability() {
159
+ try {
160
+ assertNotBanned('reachabilityProbe');
161
+ const r = await this.exchange.fapiPublicGetTime({});
162
+ noteSuccess();
163
+ const serverTime = Number(r?.serverTime);
164
+ const driftMs = Number.isFinite(serverTime) ? serverTime - Date.now() : null;
165
+ return { outcome: 'reachable', driftMs };
166
+ }
167
+ catch (err) {
168
+ noteBinanceError(err);
169
+ if (err instanceof BinanceBannedError) {
170
+ // Gate paused us (418/429 backoff or weight pacer) — we didn't actually
171
+ // reach the host, so the result is UNKNOWN, not a reachability failure.
172
+ return { outcome: 'unknown', driftMs: null };
173
+ }
174
+ const msg = err instanceof Error ? err.message : String(err);
175
+ const geo = msg.includes('451');
176
+ logger.warn(TAG, `probeReachability failed${geo ? ' (HTTP 451 geo-block)' : ''}: ${msg}`);
177
+ return { outcome: geo ? 'geo_blocked' : 'unreachable', driftMs: null };
178
+ }
179
+ }
147
180
  }
package/index.js CHANGED
@@ -29,6 +29,7 @@ import { PositionStateStore } from './live/position-state-store.js';
29
29
  import { PendingEntryStore } from './ingest/pending-entry-metadata.js';
30
30
  import { onReconcilerObservedClose, reconcileStateStoreOnStartup } from './ingest/reconciler-cleanup.js';
31
31
  import { reconcileDbOpenVsExchange } from './ingest/reconcile-db-vs-exchange.js';
32
+ import { startReadinessReporter } from './ingest/readiness-reporter.js';
32
33
  import { IntelMicrostructureAssembler } from './live/microstructure-assembler.js';
33
34
  import { recordPositionReviewsTool } from './tools/record-position-reviews.js';
34
35
  import { getMyRecentReviewsTool } from './tools/get-my-recent-reviews.js';
@@ -2020,6 +2021,19 @@ const paperTradingPlugin = {
2020
2021
  pluginToolNames = toolNames;
2021
2022
  pluginInitialised = true;
2022
2023
  logger.info(TAG, `Registered ${gatedTools.length} tools (gate mode=${toolGate.getMode()}): ${toolNames.join(', ')}. Plugin v3.8.0 (${runtime.mode} mode)`);
2024
+ // Agent-readiness reporter (docs/AGENT_READINESS_GATE_PLAN.md Phase 1):
2025
+ // probe host→Binance reachability (HTTP 451 geo-block) + clock drift on the
2026
+ // host and POST a plain-English report to the webapp, so a silently-broken
2027
+ // agent shows an actionable dashboard alert instead of a false green.
2028
+ // Advisory + fire-and-forget; no token → no-op. Runs here once (guarded by
2029
+ // the pluginInitialised early-return → once per process) + on an unref'd
2030
+ // interval inside the reporter.
2031
+ startReadinessReporter({
2032
+ apiBaseUrl,
2033
+ token: resolveIngestToken({ connectionToken }),
2034
+ binanceApi,
2035
+ toolCount: toolNames.length,
2036
+ });
2023
2037
  maybeStartConnectorSupervisor();
2024
2038
  },
2025
2039
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reefclaw/openclaw-plugin",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "ReefClaw trading plugin for OpenClaw \u2014 paper trading with real Binance market data, plus the ReefClaw dashboard connector (supervised by OpenClaw, no service manager needed). Install: /plugins install clawhub:@reefclaw/openclaw-plugin",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -39,6 +39,12 @@ You are being connected to **ReefClaw**, a supervised trading control room. Your
39
39
 
40
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
+ ## Host requirements (check before connecting)
43
+
44
+ ReefClaw trades on Binance USD-M Futures, so **the machine running this OpenClaw must be able to reach Binance** (`fapi.binance.com`). Binance geo-restricts many locations: cheap US cloud regions — and some other jurisdictions — return **HTTP 451** ("restricted location"), and the agent then cannot fetch market data or trade. This applies even in **paper mode**, which uses live Binance prices.
45
+
46
+ If your market-data calls fail with `451`, this host is geo-blocked — move the agent to a Binance-permitted region (most EU and several Asia VPS regions work). This is a hosting requirement; ReefClaw cannot configure around it.
47
+
42
48
  ## Connecting (first run) — saving the connect message
43
49
 
44
50
  The onboarding page at [reefclaw.com/onboarding](https://reefclaw.com/onboarding) gives the user a single **connect message** that carries THREE fields: