@reefclaw/openclaw-plugin 0.1.22 → 0.1.24

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.
Files changed (65) hide show
  1. package/bridge/bridge.js +110 -5
  2. package/bridge/connector.js +14 -2
  3. package/bridge/gateway/heartbeat-cron.js +30 -7
  4. package/bridge/gateway/poller.d.ts +5 -0
  5. package/bridge/gateway/poller.js +9 -0
  6. package/bridge/gateway/tool-discovery.d.ts +1 -1
  7. package/bridge/gateway/tool-discovery.js +4 -0
  8. package/bridge/provider.d.ts +36 -0
  9. package/bridge/providers/connector-update.d.ts +89 -0
  10. package/bridge/providers/connector-update.js +212 -0
  11. package/bridge/providers/emergency-commands.d.ts +36 -0
  12. package/bridge/providers/emergency-commands.js +91 -0
  13. package/bridge/providers/gateway.d.ts +37 -2
  14. package/bridge/providers/gateway.js +164 -9
  15. package/bridge/providers/mock.js +1 -0
  16. package/bridge/providers/onboarding-commands.d.ts +12 -1
  17. package/bridge/providers/onboarding-commands.js +25 -0
  18. package/bridge/types.d.ts +1 -1
  19. package/bridge/types.js +7 -0
  20. package/ccxt/binance-private.js +2 -1
  21. package/ccxt/binance-public.js +6 -1
  22. package/config/agent-config-client.d.ts +5 -2
  23. package/config/agent-config-client.js +13 -0
  24. package/config/agent-config-poller.js +5 -1
  25. package/config/gate-store.d.ts +9 -0
  26. package/config/gate-store.js +17 -2
  27. package/config/plugin-config-io.js +24 -2
  28. package/config/tool-gate.js +1 -0
  29. package/http/keepalive-fetch.d.ts +5 -0
  30. package/http/keepalive-fetch.js +50 -0
  31. package/index.js +73 -6
  32. package/ingest/position-decisions-client.d.ts +6 -0
  33. package/ingest/position-decisions-client.js +27 -9
  34. package/live/approval-lifecycle.d.ts +10 -0
  35. package/live/approval-lifecycle.js +16 -2
  36. package/live/microstructure-assembler.js +11 -2
  37. package/live/proposal-decision-listener.d.ts +21 -0
  38. package/live/proposal-decision-listener.js +39 -0
  39. package/live/proposal-manager.d.ts +12 -0
  40. package/live/proposal-manager.js +47 -0
  41. package/live/stop-watcher.d.ts +16 -1
  42. package/live/stop-watcher.js +48 -8
  43. package/openclaw.plugin.json +2 -1
  44. package/package.json +38 -38
  45. package/persistence/state-manager.d.ts +7 -0
  46. package/persistence/state-manager.js +28 -1
  47. package/simulator/exchange-simulator.d.ts +22 -0
  48. package/simulator/exchange-simulator.js +74 -32
  49. package/tools/audit-bracket-protection.js +11 -7
  50. package/tools/create-order.js +49 -7
  51. package/tools/get-funding-context.js +6 -1
  52. package/tools/get-liquidation-levels.js +5 -1
  53. package/tools/get-liquidation-pulse.js +7 -1
  54. package/tools/get-market-intel.js +2 -1
  55. package/tools/get-relevant-learnings.js +20 -1
  56. package/tools/get-resting-liquidity.js +6 -1
  57. package/tools/get-wave9-status.js +17 -0
  58. package/tools/hl-provision-agent-wallet.js +18 -0
  59. package/tools/hl-submit-agent-approval.d.ts +27 -0
  60. package/tools/hl-submit-agent-approval.js +140 -0
  61. package/tools/intel-api.d.ts +9 -0
  62. package/tools/intel-api.js +32 -1
  63. package/tools/record-position-reviews.js +2 -2
  64. package/tools/scan-pairs.js +20 -11
  65. package/types.d.ts +7 -0
package/bridge/bridge.js CHANGED
@@ -11,7 +11,7 @@
11
11
  import { writeFileSync, renameSync, existsSync, mkdirSync, unlinkSync, readdirSync } from 'fs';
12
12
  import { join } from 'path';
13
13
  import { homedir } from 'os';
14
- import { logger } from './logger.js';
14
+ import { logger, formatError } from './logger.js';
15
15
  import { Connector } from './connector.js';
16
16
  import { readLocalSkillVersion, readAgentVisibleSkillVersions, validateSkillContent, compareSemver } from './utils/skill-version.js';
17
17
  import { invalidateSkillsSnapshot } from './utils/skills-snapshot-invalidation.js';
@@ -53,6 +53,8 @@ const CRITICAL_EVENTS = new Set([
53
53
  'state_update', // Agent mode changes (ACTIVE/PAUSED/STOPPED)
54
54
  'trading_mode', // Paper/live mode changes
55
55
  'skill_update_applied', // OTA SKILL.md confirmation
56
+ 'connector_update', // Operator-triggered update progress — the frame right
57
+ // before the gateway restart must not be coalesced away.
56
58
  // All chat events are on the 'chat' channel and always sent immediately
57
59
  ]);
58
60
  /** How often to re-attempt the webapp SKILL.md pull while still on a bootstrap
@@ -249,6 +251,14 @@ export class Bridge {
249
251
  this.addListener('decisionTraceUpdate', (data) => {
250
252
  this.emit('agent_state', 'decision_trace', data);
251
253
  });
254
+ // Operator-triggered connector update: PTY output + status. CRITICAL
255
+ // priority so the operator watching an update in progress sees it live
256
+ // instead of behind the 500ms low-priority coalescer — and, more to the
257
+ // point, so the final frame before the gateway restart is not the one that
258
+ // gets dropped.
259
+ this.addListener('connectorUpdate', (data) => {
260
+ this.emit('agent_state', 'connector_update', data);
261
+ });
252
262
  }
253
263
  addListener(event, fn) {
254
264
  this.provider.on(event, fn);
@@ -313,10 +323,20 @@ export class Bridge {
313
323
  flushThrottleBuffer() {
314
324
  if (this.throttleBuffer.size === 0)
315
325
  return;
316
- for (const [, { channel, event, payload }] of this.throttleBuffer) {
317
- this.sendFrame(channel, event, payload);
326
+ // Per-entry guard + delete-as-sent: this runs bare inside setInterval, so
327
+ // an uncaught throw (e.g. JSON.stringify on a payload with a circular ref
328
+ // or BigInt leaking out of a tool result) would be a process-killing
329
+ // uncaught exception — and skipping clear() would re-flush the survivors
330
+ // next tick under NEW seq numbers (duplicate frames on the wire).
331
+ for (const [key, { channel, event, payload }] of this.throttleBuffer) {
332
+ this.throttleBuffer.delete(key);
333
+ try {
334
+ this.sendFrame(channel, event, payload);
335
+ }
336
+ catch (err) {
337
+ logger.warn(TAG, `flush dropped unserializable ${channel}:${event}: ${formatError(err)}`);
338
+ }
318
339
  }
319
- this.throttleBuffer.clear();
320
340
  }
321
341
  startThrottleTimer() {
322
342
  this.stopThrottleTimer();
@@ -361,7 +381,7 @@ export class Bridge {
361
381
  logger.info(TAG, 'Reconciliation requested');
362
382
  const snapshot = await this.provider.getSnapshot();
363
383
  const lastSequences = parseLastSequences(params);
364
- const gapFill = this.replayBuffer
384
+ let gapFill = this.replayBuffer
365
385
  .sinceMany(lastSequences)
366
386
  .map((e) => ({
367
387
  type: 'event',
@@ -370,7 +390,32 @@ export class Bridge {
370
390
  payload: e.payload,
371
391
  seq: e.seq,
372
392
  }));
393
+ // The whole reconcile response is ONE WS message; an unbounded replay
394
+ // blows the relay's frame limit and the browser then gets NO snapshot
395
+ // at all (observed live 2026-08-04: 369 replayed events → relay
396
+ // "Message too large"). Keep the NEWEST events under a byte budget —
397
+ // the snapshot already carries current state, so dropping the oldest
398
+ // stream events is strictly better than losing the entire response.
373
399
  if (gapFill.length > 0) {
400
+ const GAP_FILL_BYTE_BUDGET = 256 * 1024;
401
+ let bytes = 0;
402
+ let start = gapFill.length;
403
+ for (let i = gapFill.length - 1; i >= 0; i--) {
404
+ try {
405
+ bytes += JSON.stringify(gapFill[i]).length;
406
+ }
407
+ catch {
408
+ start = i + 1; // unserializable event — cut it and everything older
409
+ break;
410
+ }
411
+ if (bytes > GAP_FILL_BYTE_BUDGET)
412
+ break;
413
+ start = i;
414
+ }
415
+ if (start > 0) {
416
+ logger.warn(TAG, `gapFill capped: dropping ${start} oldest of ${gapFill.length} events to fit the relay frame limit`);
417
+ gapFill = gapFill.slice(start);
418
+ }
374
419
  logger.info(TAG, `gapFill: ${gapFill.length} events replayed (lastSeqs=${JSON.stringify(lastSequences)})`);
375
420
  }
376
421
  // Provider hardcodes gapFill: []; bridge owns the wire-side seq
@@ -505,6 +550,44 @@ export class Bridge {
505
550
  }
506
551
  return;
507
552
  }
553
+ if (method === 'hl_submit_agent_approval') {
554
+ const provider = this.provider;
555
+ if (!provider.submitHlAgentApproval) {
556
+ this.connector.sendResponse(id, true, {
557
+ ok: false,
558
+ message: 'Provider does not support hl_submit_agent_approval (likely mock provider)',
559
+ });
560
+ return;
561
+ }
562
+ const action = params?.action;
563
+ const nonce = params?.nonce;
564
+ const signature = params?.signature;
565
+ if (!action || typeof action !== 'object' ||
566
+ typeof nonce !== 'number' ||
567
+ !signature || typeof signature.r !== 'string' || typeof signature.s !== 'string' ||
568
+ typeof signature.v !== 'number') {
569
+ this.connector.sendResponse(id, false, undefined, {
570
+ code: 400,
571
+ message: 'hl_submit_agent_approval requires { action, nonce, signature:{r,s,v} }',
572
+ });
573
+ return;
574
+ }
575
+ // The plugin re-validates strictly (approveAgent only, own agent
576
+ // address only) — this layer just shape-checks and forwards.
577
+ audit('hl_submit_agent_approval.start', {
578
+ id,
579
+ chain: action.hyperliquidChain,
580
+ agentAddress: action.agentAddress,
581
+ });
582
+ const outcome = await provider.submitHlAgentApproval({
583
+ action: action,
584
+ nonce,
585
+ signature: { r: signature.r, s: signature.s, v: signature.v },
586
+ });
587
+ audit('hl_submit_agent_approval.complete', { id, ok: outcome.ok, hlStatus: outcome.hlStatus });
588
+ this.connector.sendResponse(id, true, outcome);
589
+ return;
590
+ }
508
591
  if (method === 'hl_agent_wallet_status') {
509
592
  const provider = this.provider;
510
593
  if (!provider.getHlAgentWalletStatus) {
@@ -519,6 +602,28 @@ export class Bridge {
519
602
  this.connector.sendResponse(id, true, result);
520
603
  return;
521
604
  }
605
+ if (method === 'connector.update') {
606
+ const provider = this.provider;
607
+ if (!provider.updateConnector) {
608
+ this.connector.sendResponse(id, true, {
609
+ ok: false,
610
+ status: 'failed',
611
+ message: 'Provider does not support connector.update (likely mock provider)',
612
+ });
613
+ return;
614
+ }
615
+ // ★ Deliberately reads ONE field. There is no command/version/args
616
+ // parameter to forward — the command is a constant inside the driver
617
+ // (providers/connector-update.ts). If a future change starts forwarding
618
+ // caller-supplied strings into that command, this stops being an update
619
+ // button and becomes a remote shell on the trader's machine.
620
+ const acknowledgeOpenPositions = params?.acknowledgeOpenPositions === true;
621
+ audit('connector_update.start', { id, acknowledgeOpenPositions });
622
+ const outcome = await provider.updateConnector({ acknowledgeOpenPositions });
623
+ audit('connector_update.dispatched', { id, ok: outcome.ok, status: outcome.status });
624
+ this.connector.sendResponse(id, true, outcome);
625
+ return;
626
+ }
522
627
  if (method === 'get_bracket_config') {
523
628
  if (!this.provider.getBracketConfig) {
524
629
  this.connector.sendResponse(id, false, undefined, {
@@ -78,10 +78,22 @@ export class Connector {
78
78
  this.rejectAllPending('Connection closed');
79
79
  if (this.destroyed)
80
80
  return;
81
- // Fatal codes: do not reconnect
81
+ // Fatal codes (4001 invalid/mismatched token): no fast reconnect — but
82
+ // not terminal-forever either. A 4001 can be transient (relay/DB blip,
83
+ // token-cache miss on a just-created token, revoke-then-restore), and
84
+ // the old return-and-never-retry left the dashboard dark until a manual
85
+ // bridge restart (residual half of the #312 gave-up-forever class). One
86
+ // probe per 15 min is harmless and self-heals the transient cases.
82
87
  if (FATAL_CLOSE_CODES.includes(code)) {
83
- logger.error(TAG, `Fatal close code ${code} not reconnecting`);
88
+ logger.error(TAG, `Fatal close code ${code} (invalid/mismatched token). If the token was rotated, ` +
89
+ `update the bridge .env (REEFCLAW_TOKEN) and restart. Probing again in 15 min ` +
90
+ `in case the rejection was transient.`);
84
91
  this.setState('failed');
92
+ this.attempt = 0;
93
+ this.reconnectTimer = setTimeout(() => {
94
+ this.reconnectTimer = null;
95
+ this.connect();
96
+ }, 15 * 60_000);
85
97
  return;
86
98
  }
87
99
  // 4002 = stale skill connection on relay. Wait for it to time out, then retry.
@@ -28,11 +28,34 @@ export const HEARTBEAT_EVERY_MS = 15 * 60 * 1000;
28
28
  // (verified empirically on 2026.7.1-2: a cron.add without it stores none) —
29
29
  // 780s is the field-proven value from the 2026-07-22 small-box mitigations.
30
30
  export const HEARTBEAT_TIMEOUT_SECONDS = 780;
31
- // Byte-identical to the message the setup-time CLI path created, so new jobs
32
- // match every existing install's job.
33
- export const HEARTBEAT_MESSAGE = 'Heartbeat. Run SESSION START mandatory checks, then the full Decision Loop (Steps 0-9) from SKILL.md. ' +
34
- 'Check tradingMode from fetch_balance it is your source of truth for paper vs live. ' +
35
- 'Use tools in parallel where possible.';
31
+ // The routine beat is HEARTBEAT.md (injected into every isolated cron session
32
+ // as workspace bootstrap context) — NOT the SKILL.md Decision Loop. The old
33
+ // message ("run the full Decision Loop (Steps 0-9) from SKILL.md") had two
34
+ // measured costs on prod (2026-08-04): the agent paged the whole ~110KB
35
+ // SKILL.md through sed/rg every beat (~100KB of tool output before any
36
+ // trading work), and Steps 0-9 predate the pinned-plan/exit-gate discipline —
37
+ // Step 8's exit rules are the scratch-the-winners instructions. SKILL.md is
38
+ // consulted only when preparing a NEW entry.
39
+ // NOTE: once-per-install marker semantics mean this message only reaches NEW
40
+ // installs; existing boxes need a one-time `openclaw cron edit <id> --message`
41
+ // (done on prod at deploy).
42
+ // ★ The mandatory items are ENUMERATED here on purpose. The first (softer)
43
+ // version of this message — "Run the HEARTBEAT.md checklist" — produced
44
+ // 85-second beats on gpt-5.4-mini that skipped record_position_reviews,
45
+ // get_wave9_status, and the liquidity batch entirely (observed on prod
46
+ // 2026-08-04 12:25/12:40 UTC): the checklist being in context is not enough,
47
+ // the beat prompt itself must name what cannot be skipped.
48
+ export const HEARTBEAT_MESSAGE = 'Heartbeat. Execute EVERY checkbox in the HEARTBEAT.md checklist (already injected in your context; ' +
49
+ 'do NOT re-read SKILL.md from disk unless preparing a NEW entry). ' +
50
+ 'NON-SKIPPABLE every beat: (1) query_trades({hours:1}) stop-watcher reconcile; ' +
51
+ '(2) get_wave9_status() once, unconditionally; ' +
52
+ '(3) if ANY position is open: get_my_recent_reviews() + get_relevant_learnings({applies_at: heartbeat}) in one batch, ' +
53
+ 'then record_position_reviews with ONE review per open position, ' +
54
+ 'plus get_resting_liquidity + get_liquidation_levels + get_liquidation_pulse for ALL positions in one parallel batch; ' +
55
+ '(4) the Market Assessment reads. ' +
56
+ 'Manage open positions by their pinned plan (invalidation_price / realization_rule) — do not re-argue the entry thesis each beat. ' +
57
+ 'Check tradingMode from fetch_balance — the source of truth for paper vs live. ' +
58
+ 'Batch independent tool calls in parallel. Trading tools only: no subagents, no web browsing.';
36
59
  /**
37
60
  * Whether an existing cron job counts as "the heartbeat already exists".
38
61
  * Union of the two matchers already in the codebase: the old setup path
@@ -89,8 +112,8 @@ export async function ensureHeartbeatCron(opts) {
89
112
  // cron.add on 2026.7.1-2). No toolsAllow: absent = all-tools-allowed,
90
113
  // which is future-proof — an explicit allowlist freezes at creation and
91
114
  // silently excludes every plugin tool shipped later. Beat tool
92
- // discipline (no subagents/web/media during heartbeats) lives in
93
- // SKILL.md, which updates on every bridge start.
115
+ // discipline (no subagents/web during heartbeats) is stated in
116
+ // HEARTBEAT_MESSAGE itself.
94
117
  payload: {
95
118
  kind: 'agentTurn',
96
119
  message: HEARTBEAT_MESSAGE,
@@ -114,6 +114,11 @@ export declare class Poller {
114
114
  * - Errors are logged and swallowed (never crash the provider)
115
115
  */
116
116
  private guardedExec;
117
+ /** Arm the circuit-breaker for a Binance-bound call made OUTSIDE
118
+ * guardedExec (e.g. the gateway's paper mark-to-market ticker fan-out).
119
+ * Those calls used to swallow 418/429s entirely — exactly the
120
+ * ban-extending failure mode the gate exists to prevent. */
121
+ noteExternalRateLimit(label: string, msg: string): void;
117
122
  /** Inspect a failed poll's error for Binance rate-limit signals and arm
118
123
  * the process-wide circuit-breaker accordingly.
119
124
  *
@@ -266,6 +266,15 @@ export class Poller {
266
266
  this.inflight.delete(label);
267
267
  }
268
268
  }
269
+ /** Arm the circuit-breaker for a Binance-bound call made OUTSIDE
270
+ * guardedExec (e.g. the gateway's paper mark-to-market ticker fan-out).
271
+ * Those calls used to swallow 418/429s entirely — exactly the
272
+ * ban-extending failure mode the gate exists to prevent. */
273
+ noteExternalRateLimit(label, msg) {
274
+ if (isRateLimitShapedError(msg)) {
275
+ this.applyRateLimitBackoff(label, msg);
276
+ }
277
+ }
269
278
  /** Inspect a failed poll's error for Binance rate-limit signals and arm
270
279
  * the process-wide circuit-breaker accordingly.
271
280
  *
@@ -3,7 +3,7 @@ import { GatewayHttpClient, GatewayHttpError } from './gateway-http-client.js';
3
3
  * Logical tool names used by ReefClaw internally.
4
4
  * Each maps to one or more candidate actual tool names on the gateway.
5
5
  */
6
- export type LogicalTool = 'fetch_ticker' | 'fetch_balance' | 'fetch_ohlcv' | 'fetch_positions' | 'fetch_open_orders' | 'cancel_all_orders' | 'cancel_order' | 'close_position' | 'create_order' | 'get_market_structure' | 'get_crypto_metrics' | 'get_market_intel' | 'set_trading_mode' | 'set_exchange_credentials' | 'test_exchange_credentials' | 'clear_exchange_credentials' | 'hl_provision_agent_wallet' | 'hl_agent_wallet_status' | 'get_bracket_config' | 'set_bracket_requirement';
6
+ export type LogicalTool = 'fetch_ticker' | 'fetch_balance' | 'fetch_ohlcv' | 'fetch_positions' | 'fetch_open_orders' | 'cancel_all_orders' | 'cancel_order' | 'close_position' | 'create_order' | 'get_market_structure' | 'get_crypto_metrics' | 'get_market_intel' | 'set_trading_mode' | 'set_exchange_credentials' | 'test_exchange_credentials' | 'clear_exchange_credentials' | 'hl_provision_agent_wallet' | 'hl_agent_wallet_status' | 'hl_submit_agent_approval' | 'get_bracket_config' | 'set_bracket_requirement';
7
7
  /**
8
8
  * Build the test args used when probing each logical tool.
9
9
  *
@@ -22,6 +22,7 @@ const OPTIONAL_TOOLS = [
22
22
  'clear_exchange_credentials',
23
23
  'hl_provision_agent_wallet',
24
24
  'hl_agent_wallet_status',
25
+ 'hl_submit_agent_approval',
25
26
  'get_bracket_config',
26
27
  'set_bracket_requirement',
27
28
  ];
@@ -48,6 +49,7 @@ const CANDIDATES = {
48
49
  clear_exchange_credentials: ['clear_exchange_credentials'],
49
50
  hl_provision_agent_wallet: ['hl_provision_agent_wallet'],
50
51
  hl_agent_wallet_status: ['hl_agent_wallet_status'],
52
+ hl_submit_agent_approval: ['hl_submit_agent_approval'],
51
53
  get_bracket_config: ['get_bracket_config'],
52
54
  set_bracket_requirement: ['set_bracket_requirement'],
53
55
  };
@@ -119,6 +121,8 @@ export function buildProbeArgs(symbol = DEFAULT_PROBE_SYMBOL) {
119
121
  // timeout and mark the tool missing forever).
120
122
  hl_provision_agent_wallet: { walletAddress: '' },
121
123
  hl_agent_wallet_status: { probe: true },
124
+ // Same dedicated probe path: a real submit would POST to Hyperliquid.
125
+ hl_submit_agent_approval: { probe: true },
122
126
  // Bracket-config probes: no-arg read; set with an invalid flag triggers early-return.
123
127
  get_bracket_config: {},
124
128
  set_bracket_requirement: { flag: '__probe__', value: false },
@@ -1,5 +1,6 @@
1
1
  import type { EmergencyAction, ReconciliationSnapshot, TickerData, CandleData, OrderUpdatePayload, AgentStateData, RiskUpdatePayload, ChatMessage, MarketStructureData, CryptoMetricsData, VolumeAnalysisData, TradeJournalEntry, RegimeData, SignalData, MissionData, AnalyticsData, ShadowComparisonData, TradingModeData, DecisionTraceData } from './types.js';
2
2
  import type { TradingMode } from '@reefclaw/shared';
3
+ import type { ConnectorUpdateOutcome } from './providers/connector-update.js';
3
4
  /** Per-venue credential shapes for the operator set/test RPCs. Binance is an
4
5
  * HMAC key pair; Hyperliquid is a MASTER account address (public — queries)
5
6
  * plus an approved AGENT wallet private key (signs only, cannot withdraw).
@@ -39,6 +40,18 @@ export interface HlProvisionOutcome {
39
40
  currentVenue?: string;
40
41
  currentMode?: string;
41
42
  }
43
+ /** Result of hl_submit_agent_approval — the box relays the wallet-signed
44
+ * approveAgent action to Hyperliquid (browsers are often blocked). */
45
+ export interface HlSubmitApprovalOutcome {
46
+ ok: boolean;
47
+ message: string;
48
+ hlStatus?: string;
49
+ chain?: string;
50
+ agentAddress?: string;
51
+ }
52
+ /** Progress of an operator-triggered connector update. Re-exported from the
53
+ * driver so callers get the status union without importing the PTY module. */
54
+ export type { ConnectorUpdateOutcome, ConnectorUpdateStatus, } from './providers/connector-update.js';
42
55
  /** Result of hl_agent_wallet_status — approval polling for the guided flow. */
43
56
  export interface HlAgentWalletStatusOutcome {
44
57
  ok: boolean;
@@ -91,6 +104,8 @@ export interface ProviderEvents {
91
104
  id: string;
92
105
  toolName: string;
93
106
  }) => void;
107
+ /** Progress of an operator-triggered connector update (PTY output + status). */
108
+ connectorUpdate: (data: ConnectorUpdateOutcome) => void;
94
109
  marketStructure: (data: MarketStructureData) => void;
95
110
  cryptoMetrics: (data: CryptoMetricsData) => void;
96
111
  volumeAnalysis: (data: VolumeAnalysisData) => void;
@@ -196,6 +211,27 @@ export interface OpenClawProvider {
196
211
  /** Operator-only: approval/balance status of the box's provisioned HL agent
197
212
  * wallet (polled by the dashboard's guided flow after the wallet signature). */
198
213
  getHlAgentWalletStatus?(): Promise<HlAgentWalletStatusOutcome>;
214
+ /** Operator-only: submit the wallet-signed approveAgent action to
215
+ * Hyperliquid FROM THE BOX. Carries no secret — only a signature the
216
+ * operator's wallet already produced. */
217
+ submitHlAgentApproval?(args: {
218
+ action: Record<string, unknown>;
219
+ nonce: number;
220
+ signature: {
221
+ r: string;
222
+ s: string;
223
+ v: number;
224
+ };
225
+ }): Promise<HlSubmitApprovalOutcome>;
226
+ /** Operator-only: update the ReefClaw connector on this box to the latest
227
+ * published version, driven over the gateway's terminal.* PTY methods.
228
+ *
229
+ * There is no command parameter, by design — see the security note at the
230
+ * top of providers/connector-update.ts. The only input is the operator's
231
+ * acknowledgement that the resulting restart may interrupt an open book. */
232
+ updateConnector?(args: {
233
+ acknowledgeOpenPositions?: boolean;
234
+ }): Promise<ConnectorUpdateOutcome>;
199
235
  /** Operator-only: remove stored credentials from the plugin config and
200
236
  * de-escalate to PAPER mode if currently running in a live mode. */
201
237
  clearExchangeCredentials?(): Promise<{
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The ONE command this feature can ever run.
3
+ *
4
+ * `-y` so npx never blocks on its install prompt; `@latest` because this is an
5
+ * update. No user input, no interpolation, no exceptions.
6
+ */
7
+ export declare const CONNECTOR_UPDATE_COMMAND = "npx -y @reefclaw/connect@latest";
8
+ /** Marker printed after the command so we can detect completion + exit code. */
9
+ export declare const DONE_SENTINEL = "__RC_CONNECTOR_UPDATE_DONE__";
10
+ /**
11
+ * The exact line written to the PTY. Built once, from constants only.
12
+ * `$?` is expanded by the remote shell, not by us.
13
+ */
14
+ export declare function buildUpdateCommandLine(): string;
15
+ /** Minimal RPC surface this module needs (injected — keeps it unit-testable). */
16
+ export type TerminalRpc = (method: string, params: Record<string, unknown>) => Promise<unknown>;
17
+ export type ConnectorUpdateStatus =
18
+ /** Command written to the PTY; output is streaming. */
19
+ 'started'
20
+ /** DONE sentinel observed — we have a real exit code. */
21
+ | 'completed'
22
+ /** Transport died after the command started = the gateway restart landed. */
23
+ | 'restarting'
24
+ /** Refused before anything ran (terminal disabled / sandboxed / open positions). */
25
+ | 'blocked'
26
+ /** Something genuinely went wrong. */
27
+ | 'failed';
28
+ export interface ConnectorUpdateOutcome {
29
+ ok: boolean;
30
+ status: ConnectorUpdateStatus;
31
+ message: string;
32
+ sessionId?: string;
33
+ exitCode?: number | null;
34
+ /** Accumulated screen text, for the dashboard's output pane. */
35
+ output?: string;
36
+ /** True when the caller must re-submit with acknowledgeOpenPositions. */
37
+ requiresPositionAck?: boolean;
38
+ }
39
+ /**
40
+ * Map a gateway error from `terminal.open` onto an operator-readable outcome.
41
+ *
42
+ * The two refusals we know the gateway can produce are worth distinct copy —
43
+ * both are configuration facts on the trader's box that no retry will fix:
44
+ * - "terminal is not available" / "terminal is disabled"
45
+ * - agent runs in a sandbox → in-sandbox terminals are unsupported
46
+ */
47
+ export declare function describeOpenFailure(err: unknown): ConnectorUpdateOutcome;
48
+ /**
49
+ * Once the command is running, losing the transport is EXPECTED — the installer
50
+ * restarts the gateway, which is the process hosting both the PTY and our own
51
+ * connection. Treating that as a failure would report a successful update as
52
+ * broken, so the distinction is explicit and tested.
53
+ */
54
+ export declare function classifyTransportLoss(started: boolean): ConnectorUpdateStatus;
55
+ /** Extract the exit code that follows the DONE sentinel, if it has appeared. */
56
+ export declare function parseDoneSentinel(screen: string): {
57
+ done: boolean;
58
+ exitCode: number | null;
59
+ };
60
+ /**
61
+ * The sentinel is echoed by the shell as part of the command line BEFORE the
62
+ * command runs, so a naive `includes()` reports "done" immediately. Strip the
63
+ * echoed command line first: completion is only credible after the printf has
64
+ * actually executed, which is the LAST occurrence and is followed by a digit.
65
+ */
66
+ export declare function isCredibleCompletion(screen: string): boolean;
67
+ export interface StartOptions {
68
+ /** Open positions the operator has been warned about and accepted. */
69
+ acknowledgeOpenPositions?: boolean;
70
+ /** Count of currently-open positions, as the skill sees them. */
71
+ openPositionCount: number;
72
+ }
73
+ /**
74
+ * Guard the restart hazard. The installer restarts the gateway; with positions
75
+ * open that means the agent stops managing them for the duration (exchange-side
76
+ * brackets still protect the book — they are enforced by the venue, not by us —
77
+ * but there is a known startup window where protective closes are refused).
78
+ * So: never silently update a live book. Demand an explicit acknowledgement.
79
+ */
80
+ export declare function checkPositionGuard(opts: StartOptions): ConnectorUpdateOutcome | null;
81
+ /**
82
+ * Open a PTY and write the update command to it. Returns as soon as the command
83
+ * is running — the caller polls `readTerminalText` for output.
84
+ */
85
+ export declare function startConnectorUpdate(rpc: TerminalRpc, opts: StartOptions): Promise<ConnectorUpdateOutcome>;
86
+ /** Read the current screen contents. Returns null when the session is gone. */
87
+ export declare function readTerminalText(rpc: TerminalRpc, sessionId: string): Promise<string | null>;
88
+ /** Best-effort PTY cleanup. Never throws — cleanup failure must not mask an outcome. */
89
+ export declare function closeTerminal(rpc: TerminalRpc, sessionId: string): Promise<void>;