@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.
- package/bridge/bridge.js +110 -5
- package/bridge/connector.js +14 -2
- package/bridge/gateway/heartbeat-cron.js +30 -7
- package/bridge/gateway/poller.d.ts +5 -0
- package/bridge/gateway/poller.js +9 -0
- package/bridge/gateway/tool-discovery.d.ts +1 -1
- package/bridge/gateway/tool-discovery.js +4 -0
- package/bridge/provider.d.ts +36 -0
- package/bridge/providers/connector-update.d.ts +89 -0
- package/bridge/providers/connector-update.js +212 -0
- package/bridge/providers/emergency-commands.d.ts +36 -0
- package/bridge/providers/emergency-commands.js +91 -0
- package/bridge/providers/gateway.d.ts +37 -2
- package/bridge/providers/gateway.js +164 -9
- package/bridge/providers/mock.js +1 -0
- package/bridge/providers/onboarding-commands.d.ts +12 -1
- package/bridge/providers/onboarding-commands.js +25 -0
- package/bridge/types.d.ts +1 -1
- package/bridge/types.js +7 -0
- package/ccxt/binance-private.js +2 -1
- package/ccxt/binance-public.js +6 -1
- package/config/agent-config-client.d.ts +5 -2
- package/config/agent-config-client.js +13 -0
- package/config/agent-config-poller.js +5 -1
- package/config/gate-store.d.ts +9 -0
- package/config/gate-store.js +17 -2
- package/config/plugin-config-io.js +24 -2
- package/config/tool-gate.js +1 -0
- package/http/keepalive-fetch.d.ts +5 -0
- package/http/keepalive-fetch.js +50 -0
- package/index.js +73 -6
- package/ingest/position-decisions-client.d.ts +6 -0
- package/ingest/position-decisions-client.js +27 -9
- package/live/approval-lifecycle.d.ts +10 -0
- package/live/approval-lifecycle.js +16 -2
- package/live/microstructure-assembler.js +11 -2
- package/live/proposal-decision-listener.d.ts +21 -0
- package/live/proposal-decision-listener.js +39 -0
- package/live/proposal-manager.d.ts +12 -0
- package/live/proposal-manager.js +47 -0
- package/live/stop-watcher.d.ts +16 -1
- package/live/stop-watcher.js +48 -8
- package/openclaw.plugin.json +2 -1
- package/package.json +38 -38
- package/persistence/state-manager.d.ts +7 -0
- package/persistence/state-manager.js +28 -1
- package/simulator/exchange-simulator.d.ts +22 -0
- package/simulator/exchange-simulator.js +74 -32
- package/tools/audit-bracket-protection.js +11 -7
- package/tools/create-order.js +49 -7
- package/tools/get-funding-context.js +6 -1
- package/tools/get-liquidation-levels.js +5 -1
- package/tools/get-liquidation-pulse.js +7 -1
- package/tools/get-market-intel.js +2 -1
- package/tools/get-relevant-learnings.js +20 -1
- package/tools/get-resting-liquidity.js +6 -1
- package/tools/get-wave9-status.js +17 -0
- package/tools/hl-provision-agent-wallet.js +18 -0
- package/tools/hl-submit-agent-approval.d.ts +27 -0
- package/tools/hl-submit-agent-approval.js +140 -0
- package/tools/intel-api.d.ts +9 -0
- package/tools/intel-api.js +32 -1
- package/tools/record-position-reviews.js +2 -2
- package/tools/scan-pairs.js +20 -11
- 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
|
-
|
|
317
|
-
|
|
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
|
-
|
|
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, {
|
package/bridge/connector.js
CHANGED
|
@@ -78,10 +78,22 @@ export class Connector {
|
|
|
78
78
|
this.rejectAllPending('Connection closed');
|
|
79
79
|
if (this.destroyed)
|
|
80
80
|
return;
|
|
81
|
-
// Fatal codes:
|
|
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}
|
|
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
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
93
|
-
//
|
|
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
|
*
|
package/bridge/gateway/poller.js
CHANGED
|
@@ -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 },
|
package/bridge/provider.d.ts
CHANGED
|
@@ -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>;
|