@reefclaw/openclaw-plugin 0.1.12 → 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/event-parser.d.ts +19 -0
- package/bridge/gateway/event-parser.js +52 -0
- package/bridge/gateway/gateway-config.d.ts +16 -5
- package/bridge/gateway/gateway-config.js +68 -12
- package/bridge/gateway/heartbeat-cron.d.ts +1 -0
- package/bridge/gateway/heartbeat-cron.js +22 -1
- 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 +72 -1
- package/bridge/providers/gateway.js +241 -29
- package/bridge/providers/onboarding-commands.d.ts +8 -0
- package/bridge/providers/onboarding-commands.js +4 -4
- package/bridge/types.d.ts +30 -0
- package/bridge/utils/identity-name.d.ts +24 -0
- package/bridge/utils/identity-name.js +54 -0
- package/ccxt/binance-public.d.ts +21 -7
- package/ccxt/binance-public.js +70 -6
- 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/live/proposal-decision-listener.d.ts +20 -0
- package/live/proposal-decision-listener.js +211 -48
- 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/tools/create-order.d.ts +11 -0
- package/tools/create-order.js +23 -2
- package/tools/get-risk-summary.d.ts +4 -0
- package/tools/get-risk-summary.js +62 -23
- package/venues/hyperliquid/hl-bracket-coordinator.d.ts +29 -1
- package/venues/hyperliquid/hl-bracket-coordinator.js +59 -2
- 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 +36 -0
- package/venues/hyperliquid/hl-live-adapter.js +155 -12
- package/venues/hyperliquid/hl-order.d.ts +35 -0
- package/venues/hyperliquid/hl-order.js +123 -0
- package/venues/hyperliquid/hl-position.d.ts +36 -0
- package/venues/hyperliquid/hl-position.js +127 -0
- package/venues/hyperliquid/hl-private.d.ts +20 -3
- package/venues/hyperliquid/hl-private.js +37 -6
- 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/index.js
CHANGED
|
@@ -29,6 +29,7 @@ import { loadUserDataStreamMode, loadUserDataStreamTunables, loadUserDataStreamD
|
|
|
29
29
|
import { TradeStoreClient } from './ingest/trade-store-client.js';
|
|
30
30
|
import { ProposalManager } from './live/proposal-manager.js';
|
|
31
31
|
import { ProposalDecisionListener } from './live/proposal-decision-listener.js';
|
|
32
|
+
import { ApprovalListenerLifecycle } from './live/approval-lifecycle.js';
|
|
32
33
|
import { PositionDecisionsClient } from './ingest/position-decisions-client.js';
|
|
33
34
|
import { PositionStateStore } from './live/position-state-store.js';
|
|
34
35
|
import { PendingEntryStore } from './ingest/pending-entry-metadata.js';
|
|
@@ -48,7 +49,8 @@ import { queryReviewOutcomesTool } from './tools/query-review-outcomes.js';
|
|
|
48
49
|
import { loadPositionReviewMode } from './config/position-review-config.js';
|
|
49
50
|
import { installSignalHandlers } from './lifecycle/install-signal-handlers.js';
|
|
50
51
|
import { SerialTradingOperationLock } from './lifecycle/trading-operation-lock.js';
|
|
51
|
-
import { readPluginConfig, readOpenClawConnection, loadStopWatcherIntervalMs } from './config/plugin-config-io.js';
|
|
52
|
+
import { readPluginConfig, readOpenClawConnection, loadMicroLiveConfig, loadStopWatcherIntervalMs } from './config/plugin-config-io.js';
|
|
53
|
+
import { verifyOperatorProvenance } from './config/operator-provenance.js';
|
|
52
54
|
import { startConnectorSupervisor, hasBundledBridge } from './connector-supervisor.js';
|
|
53
55
|
import { ToolGate } from './config/tool-gate.js';
|
|
54
56
|
import { gateStore } from './config/gate-store.js';
|
|
@@ -795,6 +797,7 @@ const TOOL_PARAMS = {
|
|
|
795
797
|
set_exchange_credentials: {
|
|
796
798
|
type: 'object',
|
|
797
799
|
properties: {
|
|
800
|
+
operator_token: { type: 'string', description: 'Operator provenance — injected automatically by the ReefClaw dashboard path. Agent-initiated calls are refused without it.' },
|
|
798
801
|
apiKey: { type: 'string', description: 'Binance API key' },
|
|
799
802
|
secret: { type: 'string', description: 'Binance API secret' },
|
|
800
803
|
testnet: { type: 'boolean', description: 'Use Binance testnet (default: false)' },
|
|
@@ -804,6 +807,7 @@ const TOOL_PARAMS = {
|
|
|
804
807
|
set_trading_mode: {
|
|
805
808
|
type: 'object',
|
|
806
809
|
properties: {
|
|
810
|
+
operator_token: { type: 'string', description: 'Operator provenance — injected automatically by the ReefClaw dashboard path. Agent-initiated calls are refused without it.' },
|
|
807
811
|
mode: { type: 'string', enum: ['PAPER', 'MICRO_LIVE', 'LIVE'], description: 'Target trading mode' },
|
|
808
812
|
acknowledged: { type: 'boolean', description: 'Required when moving to LIVE — explicit operator acknowledgment' },
|
|
809
813
|
},
|
|
@@ -821,6 +825,7 @@ const TOOL_PARAMS = {
|
|
|
821
825
|
clear_exchange_credentials: {
|
|
822
826
|
type: 'object',
|
|
823
827
|
properties: {
|
|
828
|
+
operator_token: { type: 'string', description: 'Operator provenance — injected automatically by the ReefClaw dashboard path. Agent-initiated calls are refused without it.' },
|
|
824
829
|
confirm: { type: 'boolean', description: 'Must be true — safety gate against accidental probe wipes' },
|
|
825
830
|
},
|
|
826
831
|
required: ['confirm'],
|
|
@@ -832,6 +837,7 @@ const TOOL_PARAMS = {
|
|
|
832
837
|
set_bracket_requirement: {
|
|
833
838
|
type: 'object',
|
|
834
839
|
properties: {
|
|
840
|
+
operator_token: { type: 'string', description: 'Operator provenance — injected automatically by the ReefClaw dashboard path. Agent-initiated calls are refused without it.' },
|
|
835
841
|
flag: { type: 'string', enum: ['requireStopLoss', 'requireTakeProfit'], description: 'Which requirement to flip' },
|
|
836
842
|
value: { type: 'boolean', description: 'New value. False disables the requirement and returns a warning banner copy.' },
|
|
837
843
|
},
|
|
@@ -1171,23 +1177,123 @@ const paperTradingPlugin = {
|
|
|
1171
1177
|
if (positionDecisionsClient) {
|
|
1172
1178
|
logger.info(TAG, `Journal close-on-reduce-only-fill ${closeOnReduceOnlyFill ? 'ENABLED' : 'disabled'}`);
|
|
1173
1179
|
}
|
|
1174
|
-
//
|
|
1175
|
-
|
|
1176
|
-
//
|
|
1177
|
-
//
|
|
1180
|
+
// Reconciler-driven close-bypass cleanup, installed on EVERY live adapter —
|
|
1181
|
+
// the boot one below AND every reconnect-built one (via
|
|
1182
|
+
// runtime.onAdapterCreated; audit 2026-07-26 F9 — previously boot-only, so
|
|
1183
|
+
// a dashboard mode/credential reconnect silently lost the journal cleanup).
|
|
1184
|
+
// The reconciler emits 'drift_detected' on every poll; we filter for
|
|
1185
|
+
// 'closed' drifts (position observed flat on exchange while local thinks
|
|
1186
|
+
// it's open). Fires for bracket SL/TP fills, opposite-side market orders,
|
|
1187
|
+
// and external closes — all paths that bypass close_position() and would
|
|
1188
|
+
// otherwise leave webapp DB + state-store stale.
|
|
1189
|
+
// See plugin/src/ingest/reconciler-cleanup.ts.
|
|
1190
|
+
const installDriftCleanup = (a) => {
|
|
1191
|
+
a.on('drift_detected', (result) => {
|
|
1192
|
+
for (const drift of result.drifts) {
|
|
1193
|
+
if (drift.type !== 'closed')
|
|
1194
|
+
continue;
|
|
1195
|
+
void onReconcilerObservedClose({
|
|
1196
|
+
decisionsClient: positionDecisionsClient,
|
|
1197
|
+
stateStore: positionStateStore,
|
|
1198
|
+
userId: positionDecisionsUserId,
|
|
1199
|
+
lastPriceFn: (sym) => a.getLastPrice(sym),
|
|
1200
|
+
}, {
|
|
1201
|
+
symbol: drift.symbol,
|
|
1202
|
+
lastContracts: drift.localContracts,
|
|
1203
|
+
observedAtMs: new Date(result.timestamp).getTime(),
|
|
1204
|
+
}).catch((err) => {
|
|
1205
|
+
logger.error(TAG, `reconciler cleanup failed for ${drift.symbol}: ${formatError(err)}`);
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
};
|
|
1210
|
+
// ---- Ingest + approval wiring (audit 2026-07-26 F10: BOOT-MODE-INDEPENDENT) ----
|
|
1211
|
+
// Built whenever credentials allow — NOT only on live boots. Every real
|
|
1212
|
+
// trader onboards in PAPER and goes live later from the dashboard; wiring
|
|
1213
|
+
// that only a live BOOT could construct silently never existed on their
|
|
1214
|
+
// box (no audit-trail ingest, no approval listener), and "restart the
|
|
1215
|
+
// container" is not an instruction we can hand a trader. These clients
|
|
1216
|
+
// are passive until a live adapter carries them — F9's runtime wiring
|
|
1217
|
+
// threads them into every adapter build, boot and reconnect alike.
|
|
1178
1218
|
let tradeIngest;
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1219
|
+
{
|
|
1220
|
+
const dbWriteMode = loadUserDataStreamDbWrite();
|
|
1221
|
+
let cfgForIngest;
|
|
1222
|
+
try {
|
|
1223
|
+
cfgForIngest = readPluginConfig();
|
|
1224
|
+
}
|
|
1225
|
+
catch {
|
|
1226
|
+
cfgForIngest = undefined;
|
|
1227
|
+
}
|
|
1228
|
+
const ingestToken = resolveIngestToken(cfgForIngest);
|
|
1229
|
+
const reefclawUserId = resolveReefclawUserId();
|
|
1230
|
+
// The Binance read path only runs when userDataStream.mode != 'off';
|
|
1231
|
+
// the HL user stream ALWAYS runs in live (no mode ladder), so on that
|
|
1232
|
+
// venue dbWrite + credentials alone decide (audit F26).
|
|
1233
|
+
const wsReadPathActive = venue === 'hyperliquid' || loadUserDataStreamMode() !== 'off';
|
|
1234
|
+
if (wsReadPathActive && dbWriteMode === 'on' && ingestToken && reefclawUserId) {
|
|
1235
|
+
const ingestBaseUrl = getUserDataStreamIngestBaseUrl(cfgForIngest);
|
|
1236
|
+
tradeIngest = {
|
|
1237
|
+
client: new TradeStoreClient({ baseUrl: ingestBaseUrl, ingestToken }),
|
|
1238
|
+
userId: reefclawUserId,
|
|
1239
|
+
// Venue-derived FillEvent.exchange — half of the audit-trail
|
|
1240
|
+
// idempotency key; sourced from fillExchangeId, never a literal.
|
|
1241
|
+
// (Reconnect builds re-stamp it from the TARGET venue — F9.)
|
|
1242
|
+
exchange: fillExchangeId(venue),
|
|
1243
|
+
};
|
|
1244
|
+
logger.info(TAG, `User-data stream dbWrite=on — WS audit-trail ingest wired to ${ingestBaseUrl} (userId=${reefclawUserId.slice(0, 8)}…)`);
|
|
1245
|
+
}
|
|
1246
|
+
else if (wsReadPathActive && dbWriteMode === 'on') {
|
|
1247
|
+
logger.warn(TAG, `User-data stream dbWrite=on but ingest token (plugin-config connectionToken / WEBAPP_INGEST_TOKEN) or REEFCLAW_USER_ID missing — audit-trail ingest disabled`);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
// Approval mode is resolved PER READ (config + env), not snapshotted at
|
|
1251
|
+
// boot: a config change applies on the next create_order call and the
|
|
1252
|
+
// next adapter swap — no restart (F10; see docs/CLAUDE/approval-mode.md).
|
|
1253
|
+
// per_trade beats shadow when both are set (no point dual-writing shadow
|
|
1254
|
+
// rows while collecting real ones). See docs/APPROVAL_MODE_DESIGN.md §12.
|
|
1255
|
+
const approvalShadowEnabled = (process.env.APPROVAL_SHADOW_MODE ?? '').trim() === '1';
|
|
1256
|
+
const resolveApprovalMode = () => {
|
|
1257
|
+
try {
|
|
1258
|
+
if (readPluginConfig().approval?.mode === 'per_trade')
|
|
1259
|
+
return 'per_trade';
|
|
1260
|
+
}
|
|
1261
|
+
catch { /* config unreadable — env-only fallback below */ }
|
|
1262
|
+
return approvalShadowEnabled ? 'shadow' : 'off';
|
|
1263
|
+
};
|
|
1264
|
+
// Proposal manager + listener credentials: built whenever ingest
|
|
1265
|
+
// credentials exist (passive HTTP client), so a later approval.mode or
|
|
1266
|
+
// trading-mode change finds them ready instead of needing a restart.
|
|
1182
1267
|
let proposalManagerCtx;
|
|
1183
1268
|
let proposalListenerCredentials;
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1269
|
+
{
|
|
1270
|
+
let cfgForUrl;
|
|
1271
|
+
try {
|
|
1272
|
+
cfgForUrl = readPluginConfig();
|
|
1273
|
+
}
|
|
1274
|
+
catch {
|
|
1275
|
+
cfgForUrl = undefined;
|
|
1276
|
+
}
|
|
1277
|
+
const ingestToken = resolveIngestToken(cfgForUrl);
|
|
1278
|
+
const reefclawUserId = resolveReefclawUserId();
|
|
1279
|
+
const bootApprovalMode = resolveApprovalMode();
|
|
1280
|
+
if (ingestToken && reefclawUserId) {
|
|
1281
|
+
const ingestBaseUrl = getUserDataStreamIngestBaseUrl(cfgForUrl);
|
|
1282
|
+
proposalManagerCtx = {
|
|
1283
|
+
manager: new ProposalManager({ baseUrl: ingestBaseUrl, ingestToken }),
|
|
1284
|
+
userId: reefclawUserId,
|
|
1285
|
+
};
|
|
1286
|
+
proposalListenerCredentials = { ingestToken, userId: reefclawUserId };
|
|
1287
|
+
if (bootApprovalMode !== 'off') {
|
|
1288
|
+
logger.info(TAG, `Approval wiring active (mode=${bootApprovalMode}) → ${ingestBaseUrl} (userId=${reefclawUserId.slice(0, 8)}…)`);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
else if (bootApprovalMode !== 'off') {
|
|
1292
|
+
logger.warn(TAG, `approval mode=${bootApprovalMode} but ingest token / REEFCLAW_USER_ID missing — proposal path disabled (orders fire directly)`);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
// ---- Create exchange adapter based on trading mode ----
|
|
1296
|
+
let adapter;
|
|
1191
1297
|
if (tradingMode === 'MICRO_LIVE' || tradingMode === 'LIVE') {
|
|
1192
1298
|
// Per-VENUE credential presence (rehearsal find #4): this safety check
|
|
1193
1299
|
// predated the venue seam and tested only the Binance shape, so a
|
|
@@ -1203,21 +1309,9 @@ const paperTradingPlugin = {
|
|
|
1203
1309
|
tradingMode = 'PAPER';
|
|
1204
1310
|
}
|
|
1205
1311
|
else {
|
|
1206
|
-
//
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
const rcConfigPath = join(homedir(), '.reefclaw', 'plugin-config.json');
|
|
1210
|
-
if (existsSync(rcConfigPath)) {
|
|
1211
|
-
const rcConfig = JSON.parse(readFileSync(rcConfigPath, 'utf-8'));
|
|
1212
|
-
if (rcConfig?.microLive) {
|
|
1213
|
-
microLiveConfig = {
|
|
1214
|
-
sizeCapPercent: rcConfig.microLive.sizeCapPercent,
|
|
1215
|
-
maxPositionUSDT: rcConfig.microLive.maxPositionUSDT,
|
|
1216
|
-
};
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
catch { /* already read above, best-effort */ }
|
|
1312
|
+
// Micro-live cap — same loader every runtime reconnect uses
|
|
1313
|
+
// (buildAdapter), so boot and reconnect can never disagree (F8).
|
|
1314
|
+
const microLiveConfig = loadMicroLiveConfig();
|
|
1221
1315
|
// Bracket-orders feature flag read from plugin-config at construction time.
|
|
1222
1316
|
// Default 'off' keeps legacy stop-watcher behaviour while the feature rolls out.
|
|
1223
1317
|
const bracketMode = loadBracketMode();
|
|
@@ -1232,95 +1326,8 @@ const paperTradingPlugin = {
|
|
|
1232
1326
|
if (userDataStreamMode !== 'off') {
|
|
1233
1327
|
logger.info(TAG, `User-data stream enabled in mode=${userDataStreamMode}`);
|
|
1234
1328
|
}
|
|
1235
|
-
//
|
|
1236
|
-
//
|
|
1237
|
-
// - userDataStream.mode != 'off' (read path running)
|
|
1238
|
-
// - userDataStream.dbWrite = 'on' (operator opted in)
|
|
1239
|
-
// - ingest token resolvable (plugin-config connectionToken,
|
|
1240
|
-
// else legacy WEBAPP_INGEST_TOKEN env)
|
|
1241
|
-
// - REEFCLAW_USER_ID env set (trader identity)
|
|
1242
|
-
// Any missing field → no ingest wired (silent dead-code default).
|
|
1243
|
-
const dbWriteMode = loadUserDataStreamDbWrite();
|
|
1244
|
-
let cfgForIngest;
|
|
1245
|
-
try {
|
|
1246
|
-
cfgForIngest = readPluginConfig();
|
|
1247
|
-
}
|
|
1248
|
-
catch {
|
|
1249
|
-
cfgForIngest = undefined;
|
|
1250
|
-
}
|
|
1251
|
-
const ingestToken = resolveIngestToken(cfgForIngest);
|
|
1252
|
-
const reefclawUserId = resolveReefclawUserId();
|
|
1253
|
-
if (userDataStreamMode !== 'off' &&
|
|
1254
|
-
dbWriteMode === 'on' &&
|
|
1255
|
-
ingestToken &&
|
|
1256
|
-
reefclawUserId) {
|
|
1257
|
-
const ingestBaseUrl = getUserDataStreamIngestBaseUrl(cfgForIngest);
|
|
1258
|
-
tradeIngest = {
|
|
1259
|
-
client: new TradeStoreClient({ baseUrl: ingestBaseUrl, ingestToken }),
|
|
1260
|
-
userId: reefclawUserId,
|
|
1261
|
-
// Venue-derived FillEvent.exchange — 'binance_futures' for the
|
|
1262
|
-
// binance venue, i.e. byte-identical to the pre-venue literal
|
|
1263
|
-
// WsIngest defaulted to. Half of the (exchange, exchange_trade_id)
|
|
1264
|
-
// audit-trail idempotency key; never a fresh string literal.
|
|
1265
|
-
exchange: fillExchangeId(venue),
|
|
1266
|
-
};
|
|
1267
|
-
logger.info(TAG, `User-data stream dbWrite=on — WS audit-trail ingest wired to ${ingestBaseUrl} (userId=${reefclawUserId.slice(0, 8)}…)`);
|
|
1268
|
-
}
|
|
1269
|
-
else if (userDataStreamMode !== 'off' && dbWriteMode === 'on') {
|
|
1270
|
-
logger.warn(TAG, `User-data stream dbWrite=on but ingest token (plugin-config connectionToken / WEBAPP_INGEST_TOKEN) or REEFCLAW_USER_ID missing — audit-trail ingest disabled`);
|
|
1271
|
-
}
|
|
1272
|
-
// ---- Approval-mode wiring ----
|
|
1273
|
-
// Two switches gate proposal-related behaviour:
|
|
1274
|
-
//
|
|
1275
|
-
// 1. config approval.mode='per_trade' → THE feature. create_order
|
|
1276
|
-
// returns pending_approval (does NOT fire). ProposalDecisionListener
|
|
1277
|
-
// polls /pending-decisions and fires when the operator approves.
|
|
1278
|
-
//
|
|
1279
|
-
// 2. env APPROVAL_SHADOW_MODE=1 → Phase A telemetry only.
|
|
1280
|
-
// create_order dual-writes a shadow proposal row AND fires the
|
|
1281
|
-
// real order. Used to collect proposal-shape distribution before
|
|
1282
|
-
// flipping per_trade. Retired once per_trade has soaked.
|
|
1283
|
-
//
|
|
1284
|
-
// If both are set, per_trade wins — no point dual-writing shadow rows
|
|
1285
|
-
// when we're collecting real ones. The ProposalManager itself is the
|
|
1286
|
-
// same instance either way; `origin` on each insert distinguishes them.
|
|
1287
|
-
//
|
|
1288
|
-
// See docs/APPROVAL_MODE_DESIGN.md §12.
|
|
1289
|
-
const approvalShadowEnabled = (process.env.APPROVAL_SHADOW_MODE ?? '').trim() === '1';
|
|
1290
|
-
try {
|
|
1291
|
-
const cfg = readPluginConfig();
|
|
1292
|
-
if (cfg.approval?.mode === 'per_trade') {
|
|
1293
|
-
approvalModeForTool = 'per_trade';
|
|
1294
|
-
}
|
|
1295
|
-
else if (approvalShadowEnabled) {
|
|
1296
|
-
approvalModeForTool = 'shadow';
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
catch {
|
|
1300
|
-
// Config read failed; fall back to env-only.
|
|
1301
|
-
if (approvalShadowEnabled)
|
|
1302
|
-
approvalModeForTool = 'shadow';
|
|
1303
|
-
}
|
|
1304
|
-
if (approvalModeForTool !== 'off' && ingestToken && reefclawUserId) {
|
|
1305
|
-
let cfgForUrl;
|
|
1306
|
-
try {
|
|
1307
|
-
cfgForUrl = readPluginConfig();
|
|
1308
|
-
}
|
|
1309
|
-
catch {
|
|
1310
|
-
cfgForUrl = undefined;
|
|
1311
|
-
}
|
|
1312
|
-
const ingestBaseUrl = getUserDataStreamIngestBaseUrl(cfgForUrl);
|
|
1313
|
-
proposalManagerCtx = {
|
|
1314
|
-
manager: new ProposalManager({ baseUrl: ingestBaseUrl, ingestToken }),
|
|
1315
|
-
userId: reefclawUserId,
|
|
1316
|
-
};
|
|
1317
|
-
proposalListenerCredentials = { ingestToken, userId: reefclawUserId };
|
|
1318
|
-
logger.info(TAG, `Approval wiring active (mode=${approvalModeForTool}) → ${ingestBaseUrl} (userId=${reefclawUserId.slice(0, 8)}…)`);
|
|
1319
|
-
}
|
|
1320
|
-
else if (approvalModeForTool !== 'off') {
|
|
1321
|
-
logger.warn(TAG, `approval mode=${approvalModeForTool} but WEBAPP_INGEST_TOKEN or REEFCLAW_USER_ID env missing — proposal path disabled`);
|
|
1322
|
-
approvalModeForTool = 'off';
|
|
1323
|
-
}
|
|
1329
|
+
// (Ingest + approval wiring is hoisted above the mode branch — F10:
|
|
1330
|
+
// it must exist on PAPER boots too, so a runtime live-flip finds it.)
|
|
1324
1331
|
// Venue-dispatched construction (multi-venue Phase 0). For 'binance'
|
|
1325
1332
|
// this is a pure pass-through to `new LiveAdapter(...)` — identical
|
|
1326
1333
|
// args, identical behavior; the unsupported-venue arm is unreachable
|
|
@@ -1334,6 +1341,10 @@ const paperTradingPlugin = {
|
|
|
1334
1341
|
credentials: hlCredentials,
|
|
1335
1342
|
mode: tradingMode,
|
|
1336
1343
|
marketSlippagePct: readPluginConfig().hl?.marketSlippagePct,
|
|
1344
|
+
microLive: microLiveConfig,
|
|
1345
|
+
// F26: same wiring object as the Binance arm — the SIGTERM
|
|
1346
|
+
// drain covers both venues because it drains this client.
|
|
1347
|
+
tradeIngest,
|
|
1337
1348
|
},
|
|
1338
1349
|
}
|
|
1339
1350
|
: {
|
|
@@ -1350,31 +1361,9 @@ const paperTradingPlugin = {
|
|
|
1350
1361
|
],
|
|
1351
1362
|
});
|
|
1352
1363
|
adapter = liveAdapter;
|
|
1353
|
-
//
|
|
1354
|
-
//
|
|
1355
|
-
|
|
1356
|
-
// Fires for bracket SL/TP fills, opposite-side market orders, and
|
|
1357
|
-
// external closes — all paths that bypass close_position() and
|
|
1358
|
-
// would otherwise leave webapp DB + state-store stale.
|
|
1359
|
-
// See plugin/src/ingest/reconciler-cleanup.ts.
|
|
1360
|
-
liveAdapter.on('drift_detected', (result) => {
|
|
1361
|
-
for (const drift of result.drifts) {
|
|
1362
|
-
if (drift.type !== 'closed')
|
|
1363
|
-
continue;
|
|
1364
|
-
void onReconcilerObservedClose({
|
|
1365
|
-
decisionsClient: positionDecisionsClient,
|
|
1366
|
-
stateStore: positionStateStore,
|
|
1367
|
-
userId: positionDecisionsUserId,
|
|
1368
|
-
lastPriceFn: (sym) => liveAdapter.getLastPrice(sym),
|
|
1369
|
-
}, {
|
|
1370
|
-
symbol: drift.symbol,
|
|
1371
|
-
lastContracts: drift.localContracts,
|
|
1372
|
-
observedAtMs: new Date(result.timestamp).getTime(),
|
|
1373
|
-
}).catch((err) => {
|
|
1374
|
-
logger.error(TAG, `reconciler cleanup failed for ${drift.symbol}: ${formatError(err)}`);
|
|
1375
|
-
});
|
|
1376
|
-
}
|
|
1377
|
-
});
|
|
1364
|
+
// Journal close-bypass cleanup — shared installer (see its definition
|
|
1365
|
+
// above; the runtime re-installs it on every reconnect-built adapter).
|
|
1366
|
+
installDriftCleanup(liveAdapter);
|
|
1378
1367
|
// Post-registration async init (fire-and-forget — register must be sync)
|
|
1379
1368
|
liveAdapter.initialize().then(async () => {
|
|
1380
1369
|
// After init, run a one-shot startup reconciliation between
|
|
@@ -1556,6 +1545,24 @@ const paperTradingPlugin = {
|
|
|
1556
1545
|
});
|
|
1557
1546
|
};
|
|
1558
1547
|
wireWatcherJournalCapture(bootstrapWatcher);
|
|
1548
|
+
// Journal capture for the paper take-profit leg. Same contract as the
|
|
1549
|
+
// watcher wiring above: a target close bypasses close_position, so without
|
|
1550
|
+
// this it would leave a phantom-open journal row (the issue #199 class).
|
|
1551
|
+
// Wired once — the simulator instance outlives adapter swaps (every
|
|
1552
|
+
// PaperAdapter is constructed around this same object), unlike watchers.
|
|
1553
|
+
simulator.on('target_closed', (ev) => {
|
|
1554
|
+
onStopWatcherClose(autoCapture, {
|
|
1555
|
+
symbol: ev.symbol,
|
|
1556
|
+
leg: 'exchange_target',
|
|
1557
|
+
// The breached level and the price that breached it — the assessment
|
|
1558
|
+
// records both, so a gap past the target stays visible in the journal.
|
|
1559
|
+
stopPrice: ev.targetPrice,
|
|
1560
|
+
markPrice: ev.markPrice,
|
|
1561
|
+
order: ev.order,
|
|
1562
|
+
}).catch((err) => {
|
|
1563
|
+
logger.warn(TAG, `take-profit close capture failed: ${formatError(err)}`);
|
|
1564
|
+
});
|
|
1565
|
+
});
|
|
1559
1566
|
const runtime = new PluginRuntime({
|
|
1560
1567
|
adapter,
|
|
1561
1568
|
mode: tradingMode,
|
|
@@ -1564,6 +1571,16 @@ const paperTradingPlugin = {
|
|
|
1564
1571
|
marketFeed,
|
|
1565
1572
|
operationLock: wave9OperationLock,
|
|
1566
1573
|
onWatcherCreated: wireWatcherJournalCapture,
|
|
1574
|
+
// F9: reconnect-built adapters get the SAME boot wiring — drift-cleanup
|
|
1575
|
+
// listener + audit-trail ingest client + journal auto-capture — instead
|
|
1576
|
+
// of silently reverting to bare adapters until the next restart.
|
|
1577
|
+
onAdapterCreated: installDriftCleanup,
|
|
1578
|
+
liveWiring: {
|
|
1579
|
+
tradeIngestBase: tradeIngest
|
|
1580
|
+
? { client: tradeIngest.client, userId: tradeIngest.userId }
|
|
1581
|
+
: undefined,
|
|
1582
|
+
autoCapture,
|
|
1583
|
+
},
|
|
1567
1584
|
});
|
|
1568
1585
|
// Periodic DB-vs-exchange sweep (issues #199/#203): the boot-only reconcile
|
|
1569
1586
|
// left phantom-open journal rows alive for days between restarts. Runs for
|
|
@@ -1798,40 +1815,43 @@ const paperTradingPlugin = {
|
|
|
1798
1815
|
// If the ledger failed to load, Wave 9 admission is disabled and generic
|
|
1799
1816
|
// watcher behavior still starts normally here.
|
|
1800
1817
|
bootstrapWatcher.start();
|
|
1801
|
-
//
|
|
1802
|
-
//
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1818
|
+
// Approved-order listener lifecycle (F10): follows ADAPTER SWAPS, not boot
|
|
1819
|
+
// state. A PAPER-boot box that goes live from the dashboard starts the
|
|
1820
|
+
// listener at the flip; a live→PAPER flip stops it (previously it stayed
|
|
1821
|
+
// wired to the ORPHANED live adapter — an operator approval would have
|
|
1822
|
+
// fired a real exchange order while the dashboard said PAPER); a
|
|
1823
|
+
// credential swap rebinds it to the fresh adapter. Started only after
|
|
1824
|
+
// runtime, the shared mutation lock, and durable Wave 9 ownership state
|
|
1825
|
+
// are fully resolved — same ordering the boot-only version had.
|
|
1826
|
+
const approvalLifecycle = new ApprovalListenerLifecycle({
|
|
1827
|
+
resolveApprovalMode,
|
|
1828
|
+
hasWiring: () => proposalManagerCtx !== undefined && proposalListenerCredentials !== undefined,
|
|
1829
|
+
buildListener: (boundAdapter) => {
|
|
1830
|
+
let approvalCfg;
|
|
1809
1831
|
let cfgForUrl;
|
|
1810
1832
|
try {
|
|
1811
1833
|
cfgForUrl = readPluginConfig();
|
|
1834
|
+
approvalCfg = cfgForUrl.approval;
|
|
1812
1835
|
}
|
|
1813
1836
|
catch {
|
|
1814
1837
|
cfgForUrl = undefined;
|
|
1815
1838
|
}
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
baseUrl: ingestBaseUrl,
|
|
1839
|
+
return new ProposalDecisionListener({
|
|
1840
|
+
baseUrl: getUserDataStreamIngestBaseUrl(cfgForUrl),
|
|
1819
1841
|
ingestToken: proposalListenerCredentials.ingestToken,
|
|
1820
1842
|
userId: proposalListenerCredentials.userId,
|
|
1821
|
-
adapter:
|
|
1843
|
+
adapter: boundAdapter,
|
|
1822
1844
|
binanceApi,
|
|
1823
1845
|
autoCapture,
|
|
1824
1846
|
operationLock: wave9OperationLock,
|
|
1825
1847
|
checkWave9LiveSymbolOwnership,
|
|
1826
1848
|
pollIntervalMs: approvalCfg?.pollIntervalMs ?? 3_000,
|
|
1827
1849
|
});
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1850
|
+
},
|
|
1851
|
+
});
|
|
1852
|
+
runtime.setOnAdapterSwapped((a) => { void approvalLifecycle.onAdapterSwapped(a); });
|
|
1853
|
+
// Boot application — the same path every later swap takes.
|
|
1854
|
+
void approvalLifecycle.onAdapterSwapped(runtime.adapter);
|
|
1835
1855
|
const wave9ActivationClient = new IntelWave9ActivationClient(intelApiDeps);
|
|
1836
1856
|
const getWave9AccountSnapshot = async () => {
|
|
1837
1857
|
if (!runtime.adapter.isLive) {
|
|
@@ -2040,7 +2060,7 @@ const paperTradingPlugin = {
|
|
|
2040
2060
|
installSignalHandlers({
|
|
2041
2061
|
runtime,
|
|
2042
2062
|
tradeStore: tradeIngest?.client,
|
|
2043
|
-
proposalListener:
|
|
2063
|
+
proposalListener: approvalLifecycle,
|
|
2044
2064
|
stateManager,
|
|
2045
2065
|
});
|
|
2046
2066
|
// Warm the trading params cache (fire-and-forget — register must be sync)
|
|
@@ -2137,7 +2157,10 @@ const paperTradingPlugin = {
|
|
|
2137
2157
|
autoCapture,
|
|
2138
2158
|
proposalManager: proposalManagerCtx?.manager,
|
|
2139
2159
|
userId: proposalManagerCtx?.userId,
|
|
2140
|
-
|
|
2160
|
+
// Per-call read (F10): a config approval.mode change applies on
|
|
2161
|
+
// the NEXT create_order, not the next restart. The deps literal
|
|
2162
|
+
// is rebuilt per execute, so this re-resolves every call.
|
|
2163
|
+
approvalMode: resolveApprovalMode(),
|
|
2141
2164
|
wave9AdmissionGuard,
|
|
2142
2165
|
wave9ActivationCheck: () => wave9ActivationClient.getActivation(),
|
|
2143
2166
|
refreshPaperState: reloadState,
|
|
@@ -2615,16 +2638,26 @@ const paperTradingPlugin = {
|
|
|
2615
2638
|
{
|
|
2616
2639
|
name: 'set_exchange_credentials',
|
|
2617
2640
|
label: 'Set Exchange Credentials',
|
|
2618
|
-
description: 'Operator-only. Store Binance API key + secret in ~/.reefclaw/plugin-config.json. If current mode is live, reconnects the adapter.
|
|
2641
|
+
description: 'Operator-only. Store Binance API key + secret in ~/.reefclaw/plugin-config.json. If current mode is live, reconnects the adapter. Refused without dashboard operator provenance.',
|
|
2619
2642
|
parameters: TOOL_PARAMS.set_exchange_credentials,
|
|
2620
|
-
execute: async (_id, params) =>
|
|
2643
|
+
execute: async (_id, params) => {
|
|
2644
|
+
const prov = verifyOperatorProvenance(params.operator_token);
|
|
2645
|
+
if (!prov.ok)
|
|
2646
|
+
return jsonResult({ error: prov.error });
|
|
2647
|
+
return jsonResult(await setExchangeCredentialsTool(params, { runtime, adapterDeps }));
|
|
2648
|
+
},
|
|
2621
2649
|
},
|
|
2622
2650
|
{
|
|
2623
2651
|
name: 'set_trading_mode',
|
|
2624
2652
|
label: 'Set Trading Mode',
|
|
2625
|
-
description: 'Operator-only. Move the plugin between PAPER / MICRO_LIVE / LIVE. Enforces the one-rung-at-a-time ladder.
|
|
2653
|
+
description: 'Operator-only. Move the plugin between PAPER / MICRO_LIVE / LIVE. Enforces the one-rung-at-a-time ladder. Refused without dashboard operator provenance.',
|
|
2626
2654
|
parameters: TOOL_PARAMS.set_trading_mode,
|
|
2627
|
-
execute: async (_id, params) =>
|
|
2655
|
+
execute: async (_id, params) => {
|
|
2656
|
+
const prov = verifyOperatorProvenance(params.operator_token);
|
|
2657
|
+
if (!prov.ok)
|
|
2658
|
+
return jsonResult({ error: prov.error });
|
|
2659
|
+
return jsonResult(await setTradingModeTool(params, { runtime, adapterDeps }));
|
|
2660
|
+
},
|
|
2628
2661
|
},
|
|
2629
2662
|
{
|
|
2630
2663
|
name: 'test_exchange_credentials',
|
|
@@ -2636,9 +2669,14 @@ const paperTradingPlugin = {
|
|
|
2636
2669
|
{
|
|
2637
2670
|
name: 'clear_exchange_credentials',
|
|
2638
2671
|
label: 'Clear Exchange Credentials',
|
|
2639
|
-
description: 'Operator-only. Remove stored Binance credentials from plugin-config.json and de-escalate to PAPER mode if currently running in MICRO_LIVE/LIVE. Requires {confirm: true}.
|
|
2672
|
+
description: 'Operator-only. Remove stored Binance credentials from plugin-config.json and de-escalate to PAPER mode if currently running in MICRO_LIVE/LIVE. Requires {confirm: true}. Refused without dashboard operator provenance.',
|
|
2640
2673
|
parameters: TOOL_PARAMS.clear_exchange_credentials,
|
|
2641
|
-
execute: async (_id, params) =>
|
|
2674
|
+
execute: async (_id, params) => {
|
|
2675
|
+
const prov = verifyOperatorProvenance(params.operator_token);
|
|
2676
|
+
if (!prov.ok)
|
|
2677
|
+
return jsonResult({ error: prov.error });
|
|
2678
|
+
return jsonResult(await clearExchangeCredentialsTool(params, { runtime, adapterDeps }));
|
|
2679
|
+
},
|
|
2642
2680
|
},
|
|
2643
2681
|
{
|
|
2644
2682
|
name: 'get_bracket_config',
|
|
@@ -2650,9 +2688,14 @@ const paperTradingPlugin = {
|
|
|
2650
2688
|
{
|
|
2651
2689
|
name: 'set_bracket_requirement',
|
|
2652
2690
|
label: 'Set Bracket Requirement',
|
|
2653
|
-
description: 'Operator-only. Flip requireStopLoss or requireTakeProfit in plugin-config. Returns a warning copy when a flag is disabled so the dashboard can render the confirmation banner.
|
|
2691
|
+
description: 'Operator-only. Flip requireStopLoss or requireTakeProfit in plugin-config. Returns a warning copy when a flag is disabled so the dashboard can render the confirmation banner. Refused without dashboard operator provenance.',
|
|
2654
2692
|
parameters: TOOL_PARAMS.set_bracket_requirement,
|
|
2655
|
-
execute: async (_id, params) =>
|
|
2693
|
+
execute: async (_id, params) => {
|
|
2694
|
+
const prov = verifyOperatorProvenance(params.operator_token);
|
|
2695
|
+
if (!prov.ok)
|
|
2696
|
+
return jsonResult({ error: prov.error });
|
|
2697
|
+
return jsonResult(setBracketRequirementTool(params));
|
|
2698
|
+
},
|
|
2656
2699
|
},
|
|
2657
2700
|
];
|
|
2658
2701
|
// ---- Tool gate (central per-user config, slice 1) ----
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Start sampling. Safe to call repeatedly (idempotent). */
|
|
2
|
+
export declare function startEventLoopMonitor(): void;
|
|
3
|
+
/** Worst event-loop delay (ms) observed since the previous call, then reset —
|
|
4
|
+
* so consecutive calls partition the timeline into non-overlapping windows.
|
|
5
|
+
* Returns null when the monitor is unavailable or has no sample yet.
|
|
6
|
+
*
|
|
7
|
+
* The histogram stores nanoseconds; `max` is Infinity-safe but can read as a
|
|
8
|
+
* sentinel before the first sample lands, so anything non-finite → null. */
|
|
9
|
+
export declare function sampleEventLoopDelayMs(): number | null;
|
|
10
|
+
/** Test-only — drop the singleton so a fresh histogram is created. */
|
|
11
|
+
export declare function __resetEventLoopMonitorForTests(): void;
|