@reefclaw/connect 0.1.20 → 0.1.22
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/assets/bridge/bridge.d.ts +20 -5
- package/assets/bridge/bridge.js +29 -14
- package/assets/bridge/config.js +6 -0
- package/assets/bridge/gateway/gateway-config.d.ts +16 -5
- package/assets/bridge/gateway/gateway-config.js +68 -12
- package/assets/bridge/gateway/gateway-ws-client.d.ts +4 -1
- package/assets/bridge/gateway/gateway-ws-client.js +41 -11
- package/assets/bridge/gateway/poller.js +18 -8
- package/assets/bridge/providers/emergency-commands.d.ts +9 -1
- package/assets/bridge/providers/emergency-commands.js +38 -1
- package/assets/bridge/providers/gateway.d.ts +51 -1
- package/assets/bridge/providers/gateway.js +209 -22
- package/assets/bridge/providers/onboarding-commands.d.ts +11 -0
- package/assets/bridge/providers/onboarding-commands.js +5 -5
- package/assets/bridge/providers/risk-calculator.d.ts +61 -2
- package/assets/bridge/providers/risk-calculator.js +92 -20
- package/assets/bridge/utils/skill-signing.js +8 -3
- package/assets/plugin/ccxt/binance-public.d.ts +17 -5
- package/assets/plugin/ccxt/binance-public.js +31 -3
- package/assets/plugin/config/operator-provenance.d.ts +6 -0
- package/assets/plugin/config/operator-provenance.js +50 -0
- package/assets/plugin/config/plugin-config-io.d.ts +15 -1
- package/assets/plugin/config/plugin-config-io.js +29 -0
- package/assets/plugin/exchange-adapter.d.ts +13 -0
- package/assets/plugin/index.js +230 -176
- package/assets/plugin/ingest/event-loop-monitor.d.ts +22 -0
- package/assets/plugin/ingest/event-loop-monitor.js +190 -0
- package/assets/plugin/ingest/position-auto-capture.d.ts +5 -0
- package/assets/plugin/ingest/position-auto-capture.js +14 -5
- package/assets/plugin/ingest/readiness-reporter.d.ts +26 -6
- package/assets/plugin/ingest/readiness-reporter.js +137 -9
- package/assets/plugin/ingest/skill-version-reader.d.ts +16 -0
- package/assets/plugin/ingest/skill-version-reader.js +64 -0
- package/assets/plugin/live/approval-lifecycle.d.ts +30 -0
- package/assets/plugin/live/approval-lifecycle.js +80 -0
- package/assets/plugin/live/bracket-types.d.ts +9 -0
- package/assets/plugin/live/live-adapter.d.ts +0 -1
- package/assets/plugin/live/user-data-stream.js +10 -2
- package/assets/plugin/onboarding/runtime.d.ts +34 -1
- package/assets/plugin/onboarding/runtime.js +56 -5
- package/assets/plugin/openclaw.plugin.json +1 -1
- package/assets/plugin/risk/pre-trade-check.js +18 -5
- package/assets/plugin/simulator/exchange-simulator.d.ts +45 -2
- package/assets/plugin/simulator/exchange-simulator.js +96 -4
- package/assets/plugin/simulator/types.d.ts +17 -0
- package/assets/plugin/strategy/condition-registry.js +9 -2
- package/assets/plugin/strategy/evaluator.d.ts +5 -0
- package/assets/plugin/tools/attach-brackets.js +50 -1
- package/assets/plugin/tools/cancel-all-orders.js +9 -1
- package/assets/plugin/tools/create-order.js +18 -1
- package/assets/plugin/tools/get-bracket-config.d.ts +21 -2
- package/assets/plugin/tools/get-bracket-config.js +18 -2
- package/assets/plugin/tools/set-trading-mode.js +6 -3
- package/assets/plugin/venues/hyperliquid/hl-bracket-coordinator.d.ts +25 -1
- package/assets/plugin/venues/hyperliquid/hl-bracket-coordinator.js +57 -0
- package/assets/plugin/venues/hyperliquid/hl-brackets.d.ts +10 -0
- package/assets/plugin/venues/hyperliquid/hl-brackets.js +45 -13
- package/assets/plugin/venues/hyperliquid/hl-fill-ingest.d.ts +18 -0
- package/assets/plugin/venues/hyperliquid/hl-fill-ingest.js +88 -0
- package/assets/plugin/venues/hyperliquid/hl-live-adapter.d.ts +36 -0
- package/assets/plugin/venues/hyperliquid/hl-live-adapter.js +116 -7
- package/assets/plugin/venues/hyperliquid/hl-public.d.ts +12 -5
- package/assets/plugin/venues/hyperliquid/hl-public.js +24 -3
- package/assets/plugin/venues/hyperliquid/hl-user-stream.d.ts +13 -1
- package/assets/plugin/venues/hyperliquid/hl-user-stream.js +4 -1
- package/assets/plugin/venues/registry.js +8 -7
- package/assets/plugin/wave9/paper-admission-guard.d.ts +12 -1
- package/assets/plugin/wave9/paper-admission-guard.js +12 -1
- package/assets/shared/index.d.ts +3 -3
- package/assets/shared/index.js +6 -2
- package/assets/shared/readiness.d.ts +45 -2
- package/assets/shared/readiness.js +60 -8
- package/assets/skill/SKILL.md +6 -11
- package/dist/cli.js +3 -0
- package/package.json +1 -1
package/assets/plugin/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
|
},
|
|
@@ -812,6 +816,7 @@ const TOOL_PARAMS = {
|
|
|
812
816
|
test_exchange_credentials: {
|
|
813
817
|
type: 'object',
|
|
814
818
|
properties: {
|
|
819
|
+
operator_token: { type: 'string', description: 'Operator provenance — injected automatically by the ReefClaw dashboard path. Agent-initiated calls are refused without it.' },
|
|
815
820
|
apiKey: { type: 'string', description: 'Binance API key to verify (not persisted)' },
|
|
816
821
|
secret: { type: 'string', description: 'Binance API secret to verify (not persisted)' },
|
|
817
822
|
testnet: { type: 'boolean', description: 'Test against Binance testnet' },
|
|
@@ -821,6 +826,7 @@ const TOOL_PARAMS = {
|
|
|
821
826
|
clear_exchange_credentials: {
|
|
822
827
|
type: 'object',
|
|
823
828
|
properties: {
|
|
829
|
+
operator_token: { type: 'string', description: 'Operator provenance — injected automatically by the ReefClaw dashboard path. Agent-initiated calls are refused without it.' },
|
|
824
830
|
confirm: { type: 'boolean', description: 'Must be true — safety gate against accidental probe wipes' },
|
|
825
831
|
},
|
|
826
832
|
required: ['confirm'],
|
|
@@ -832,6 +838,7 @@ const TOOL_PARAMS = {
|
|
|
832
838
|
set_bracket_requirement: {
|
|
833
839
|
type: 'object',
|
|
834
840
|
properties: {
|
|
841
|
+
operator_token: { type: 'string', description: 'Operator provenance — injected automatically by the ReefClaw dashboard path. Agent-initiated calls are refused without it.' },
|
|
835
842
|
flag: { type: 'string', enum: ['requireStopLoss', 'requireTakeProfit'], description: 'Which requirement to flip' },
|
|
836
843
|
value: { type: 'boolean', description: 'New value. False disables the requirement and returns a warning banner copy.' },
|
|
837
844
|
},
|
|
@@ -1171,23 +1178,123 @@ const paperTradingPlugin = {
|
|
|
1171
1178
|
if (positionDecisionsClient) {
|
|
1172
1179
|
logger.info(TAG, `Journal close-on-reduce-only-fill ${closeOnReduceOnlyFill ? 'ENABLED' : 'disabled'}`);
|
|
1173
1180
|
}
|
|
1174
|
-
//
|
|
1175
|
-
|
|
1176
|
-
//
|
|
1177
|
-
//
|
|
1181
|
+
// Reconciler-driven close-bypass cleanup, installed on EVERY live adapter —
|
|
1182
|
+
// the boot one below AND every reconnect-built one (via
|
|
1183
|
+
// runtime.onAdapterCreated; audit 2026-07-26 F9 — previously boot-only, so
|
|
1184
|
+
// a dashboard mode/credential reconnect silently lost the journal cleanup).
|
|
1185
|
+
// The reconciler emits 'drift_detected' on every poll; we filter for
|
|
1186
|
+
// 'closed' drifts (position observed flat on exchange while local thinks
|
|
1187
|
+
// it's open). Fires for bracket SL/TP fills, opposite-side market orders,
|
|
1188
|
+
// and external closes — all paths that bypass close_position() and would
|
|
1189
|
+
// otherwise leave webapp DB + state-store stale.
|
|
1190
|
+
// See plugin/src/ingest/reconciler-cleanup.ts.
|
|
1191
|
+
const installDriftCleanup = (a) => {
|
|
1192
|
+
a.on('drift_detected', (result) => {
|
|
1193
|
+
for (const drift of result.drifts) {
|
|
1194
|
+
if (drift.type !== 'closed')
|
|
1195
|
+
continue;
|
|
1196
|
+
void onReconcilerObservedClose({
|
|
1197
|
+
decisionsClient: positionDecisionsClient,
|
|
1198
|
+
stateStore: positionStateStore,
|
|
1199
|
+
userId: positionDecisionsUserId,
|
|
1200
|
+
lastPriceFn: (sym) => a.getLastPrice(sym),
|
|
1201
|
+
}, {
|
|
1202
|
+
symbol: drift.symbol,
|
|
1203
|
+
lastContracts: drift.localContracts,
|
|
1204
|
+
observedAtMs: new Date(result.timestamp).getTime(),
|
|
1205
|
+
}).catch((err) => {
|
|
1206
|
+
logger.error(TAG, `reconciler cleanup failed for ${drift.symbol}: ${formatError(err)}`);
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
};
|
|
1211
|
+
// ---- Ingest + approval wiring (audit 2026-07-26 F10: BOOT-MODE-INDEPENDENT) ----
|
|
1212
|
+
// Built whenever credentials allow — NOT only on live boots. Every real
|
|
1213
|
+
// trader onboards in PAPER and goes live later from the dashboard; wiring
|
|
1214
|
+
// that only a live BOOT could construct silently never existed on their
|
|
1215
|
+
// box (no audit-trail ingest, no approval listener), and "restart the
|
|
1216
|
+
// container" is not an instruction we can hand a trader. These clients
|
|
1217
|
+
// are passive until a live adapter carries them — F9's runtime wiring
|
|
1218
|
+
// threads them into every adapter build, boot and reconnect alike.
|
|
1178
1219
|
let tradeIngest;
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1220
|
+
{
|
|
1221
|
+
const dbWriteMode = loadUserDataStreamDbWrite();
|
|
1222
|
+
let cfgForIngest;
|
|
1223
|
+
try {
|
|
1224
|
+
cfgForIngest = readPluginConfig();
|
|
1225
|
+
}
|
|
1226
|
+
catch {
|
|
1227
|
+
cfgForIngest = undefined;
|
|
1228
|
+
}
|
|
1229
|
+
const ingestToken = resolveIngestToken(cfgForIngest);
|
|
1230
|
+
const reefclawUserId = resolveReefclawUserId();
|
|
1231
|
+
// The Binance read path only runs when userDataStream.mode != 'off';
|
|
1232
|
+
// the HL user stream ALWAYS runs in live (no mode ladder), so on that
|
|
1233
|
+
// venue dbWrite + credentials alone decide (audit F26).
|
|
1234
|
+
const wsReadPathActive = venue === 'hyperliquid' || loadUserDataStreamMode() !== 'off';
|
|
1235
|
+
if (wsReadPathActive && dbWriteMode === 'on' && ingestToken && reefclawUserId) {
|
|
1236
|
+
const ingestBaseUrl = getUserDataStreamIngestBaseUrl(cfgForIngest);
|
|
1237
|
+
tradeIngest = {
|
|
1238
|
+
client: new TradeStoreClient({ baseUrl: ingestBaseUrl, ingestToken }),
|
|
1239
|
+
userId: reefclawUserId,
|
|
1240
|
+
// Venue-derived FillEvent.exchange — half of the audit-trail
|
|
1241
|
+
// idempotency key; sourced from fillExchangeId, never a literal.
|
|
1242
|
+
// (Reconnect builds re-stamp it from the TARGET venue — F9.)
|
|
1243
|
+
exchange: fillExchangeId(venue),
|
|
1244
|
+
};
|
|
1245
|
+
logger.info(TAG, `User-data stream dbWrite=on — WS audit-trail ingest wired to ${ingestBaseUrl} (userId=${reefclawUserId.slice(0, 8)}…)`);
|
|
1246
|
+
}
|
|
1247
|
+
else if (wsReadPathActive && dbWriteMode === 'on') {
|
|
1248
|
+
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`);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
// Approval mode is resolved PER READ (config + env), not snapshotted at
|
|
1252
|
+
// boot: a config change applies on the next create_order call and the
|
|
1253
|
+
// next adapter swap — no restart (F10; see docs/CLAUDE/approval-mode.md).
|
|
1254
|
+
// per_trade beats shadow when both are set (no point dual-writing shadow
|
|
1255
|
+
// rows while collecting real ones). See docs/APPROVAL_MODE_DESIGN.md §12.
|
|
1256
|
+
const approvalShadowEnabled = (process.env.APPROVAL_SHADOW_MODE ?? '').trim() === '1';
|
|
1257
|
+
const resolveApprovalMode = () => {
|
|
1258
|
+
try {
|
|
1259
|
+
if (readPluginConfig().approval?.mode === 'per_trade')
|
|
1260
|
+
return 'per_trade';
|
|
1261
|
+
}
|
|
1262
|
+
catch { /* config unreadable — env-only fallback below */ }
|
|
1263
|
+
return approvalShadowEnabled ? 'shadow' : 'off';
|
|
1264
|
+
};
|
|
1265
|
+
// Proposal manager + listener credentials: built whenever ingest
|
|
1266
|
+
// credentials exist (passive HTTP client), so a later approval.mode or
|
|
1267
|
+
// trading-mode change finds them ready instead of needing a restart.
|
|
1182
1268
|
let proposalManagerCtx;
|
|
1183
1269
|
let proposalListenerCredentials;
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1270
|
+
{
|
|
1271
|
+
let cfgForUrl;
|
|
1272
|
+
try {
|
|
1273
|
+
cfgForUrl = readPluginConfig();
|
|
1274
|
+
}
|
|
1275
|
+
catch {
|
|
1276
|
+
cfgForUrl = undefined;
|
|
1277
|
+
}
|
|
1278
|
+
const ingestToken = resolveIngestToken(cfgForUrl);
|
|
1279
|
+
const reefclawUserId = resolveReefclawUserId();
|
|
1280
|
+
const bootApprovalMode = resolveApprovalMode();
|
|
1281
|
+
if (ingestToken && reefclawUserId) {
|
|
1282
|
+
const ingestBaseUrl = getUserDataStreamIngestBaseUrl(cfgForUrl);
|
|
1283
|
+
proposalManagerCtx = {
|
|
1284
|
+
manager: new ProposalManager({ baseUrl: ingestBaseUrl, ingestToken }),
|
|
1285
|
+
userId: reefclawUserId,
|
|
1286
|
+
};
|
|
1287
|
+
proposalListenerCredentials = { ingestToken, userId: reefclawUserId };
|
|
1288
|
+
if (bootApprovalMode !== 'off') {
|
|
1289
|
+
logger.info(TAG, `Approval wiring active (mode=${bootApprovalMode}) → ${ingestBaseUrl} (userId=${reefclawUserId.slice(0, 8)}…)`);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
else if (bootApprovalMode !== 'off') {
|
|
1293
|
+
logger.warn(TAG, `approval mode=${bootApprovalMode} but ingest token / REEFCLAW_USER_ID missing — proposal path disabled (orders fire directly)`);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
// ---- Create exchange adapter based on trading mode ----
|
|
1297
|
+
let adapter;
|
|
1191
1298
|
if (tradingMode === 'MICRO_LIVE' || tradingMode === 'LIVE') {
|
|
1192
1299
|
// Per-VENUE credential presence (rehearsal find #4): this safety check
|
|
1193
1300
|
// predated the venue seam and tested only the Binance shape, so a
|
|
@@ -1203,21 +1310,9 @@ const paperTradingPlugin = {
|
|
|
1203
1310
|
tradingMode = 'PAPER';
|
|
1204
1311
|
}
|
|
1205
1312
|
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 */ }
|
|
1313
|
+
// Micro-live cap — same loader every runtime reconnect uses
|
|
1314
|
+
// (buildAdapter), so boot and reconnect can never disagree (F8).
|
|
1315
|
+
const microLiveConfig = loadMicroLiveConfig();
|
|
1221
1316
|
// Bracket-orders feature flag read from plugin-config at construction time.
|
|
1222
1317
|
// Default 'off' keeps legacy stop-watcher behaviour while the feature rolls out.
|
|
1223
1318
|
const bracketMode = loadBracketMode();
|
|
@@ -1232,95 +1327,8 @@ const paperTradingPlugin = {
|
|
|
1232
1327
|
if (userDataStreamMode !== 'off') {
|
|
1233
1328
|
logger.info(TAG, `User-data stream enabled in mode=${userDataStreamMode}`);
|
|
1234
1329
|
}
|
|
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
|
-
}
|
|
1330
|
+
// (Ingest + approval wiring is hoisted above the mode branch — F10:
|
|
1331
|
+
// it must exist on PAPER boots too, so a runtime live-flip finds it.)
|
|
1324
1332
|
// Venue-dispatched construction (multi-venue Phase 0). For 'binance'
|
|
1325
1333
|
// this is a pure pass-through to `new LiveAdapter(...)` — identical
|
|
1326
1334
|
// args, identical behavior; the unsupported-venue arm is unreachable
|
|
@@ -1334,6 +1342,10 @@ const paperTradingPlugin = {
|
|
|
1334
1342
|
credentials: hlCredentials,
|
|
1335
1343
|
mode: tradingMode,
|
|
1336
1344
|
marketSlippagePct: readPluginConfig().hl?.marketSlippagePct,
|
|
1345
|
+
microLive: microLiveConfig,
|
|
1346
|
+
// F26: same wiring object as the Binance arm — the SIGTERM
|
|
1347
|
+
// drain covers both venues because it drains this client.
|
|
1348
|
+
tradeIngest,
|
|
1337
1349
|
},
|
|
1338
1350
|
}
|
|
1339
1351
|
: {
|
|
@@ -1350,31 +1362,9 @@ const paperTradingPlugin = {
|
|
|
1350
1362
|
],
|
|
1351
1363
|
});
|
|
1352
1364
|
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
|
-
});
|
|
1365
|
+
// Journal close-bypass cleanup — shared installer (see its definition
|
|
1366
|
+
// above; the runtime re-installs it on every reconnect-built adapter).
|
|
1367
|
+
installDriftCleanup(liveAdapter);
|
|
1378
1368
|
// Post-registration async init (fire-and-forget — register must be sync)
|
|
1379
1369
|
liveAdapter.initialize().then(async () => {
|
|
1380
1370
|
// After init, run a one-shot startup reconciliation between
|
|
@@ -1556,6 +1546,24 @@ const paperTradingPlugin = {
|
|
|
1556
1546
|
});
|
|
1557
1547
|
};
|
|
1558
1548
|
wireWatcherJournalCapture(bootstrapWatcher);
|
|
1549
|
+
// Journal capture for the paper take-profit leg. Same contract as the
|
|
1550
|
+
// watcher wiring above: a target close bypasses close_position, so without
|
|
1551
|
+
// this it would leave a phantom-open journal row (the issue #199 class).
|
|
1552
|
+
// Wired once — the simulator instance outlives adapter swaps (every
|
|
1553
|
+
// PaperAdapter is constructed around this same object), unlike watchers.
|
|
1554
|
+
simulator.on('target_closed', (ev) => {
|
|
1555
|
+
onStopWatcherClose(autoCapture, {
|
|
1556
|
+
symbol: ev.symbol,
|
|
1557
|
+
leg: 'exchange_target',
|
|
1558
|
+
// The breached level and the price that breached it — the assessment
|
|
1559
|
+
// records both, so a gap past the target stays visible in the journal.
|
|
1560
|
+
stopPrice: ev.targetPrice,
|
|
1561
|
+
markPrice: ev.markPrice,
|
|
1562
|
+
order: ev.order,
|
|
1563
|
+
}).catch((err) => {
|
|
1564
|
+
logger.warn(TAG, `take-profit close capture failed: ${formatError(err)}`);
|
|
1565
|
+
});
|
|
1566
|
+
});
|
|
1559
1567
|
const runtime = new PluginRuntime({
|
|
1560
1568
|
adapter,
|
|
1561
1569
|
mode: tradingMode,
|
|
@@ -1564,6 +1572,16 @@ const paperTradingPlugin = {
|
|
|
1564
1572
|
marketFeed,
|
|
1565
1573
|
operationLock: wave9OperationLock,
|
|
1566
1574
|
onWatcherCreated: wireWatcherJournalCapture,
|
|
1575
|
+
// F9: reconnect-built adapters get the SAME boot wiring — drift-cleanup
|
|
1576
|
+
// listener + audit-trail ingest client + journal auto-capture — instead
|
|
1577
|
+
// of silently reverting to bare adapters until the next restart.
|
|
1578
|
+
onAdapterCreated: installDriftCleanup,
|
|
1579
|
+
liveWiring: {
|
|
1580
|
+
tradeIngestBase: tradeIngest
|
|
1581
|
+
? { client: tradeIngest.client, userId: tradeIngest.userId }
|
|
1582
|
+
: undefined,
|
|
1583
|
+
autoCapture,
|
|
1584
|
+
},
|
|
1567
1585
|
});
|
|
1568
1586
|
// Periodic DB-vs-exchange sweep (issues #199/#203): the boot-only reconcile
|
|
1569
1587
|
// left phantom-open journal rows alive for days between restarts. Runs for
|
|
@@ -1798,40 +1816,43 @@ const paperTradingPlugin = {
|
|
|
1798
1816
|
// If the ledger failed to load, Wave 9 admission is disabled and generic
|
|
1799
1817
|
// watcher behavior still starts normally here.
|
|
1800
1818
|
bootstrapWatcher.start();
|
|
1801
|
-
//
|
|
1802
|
-
//
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1819
|
+
// Approved-order listener lifecycle (F10): follows ADAPTER SWAPS, not boot
|
|
1820
|
+
// state. A PAPER-boot box that goes live from the dashboard starts the
|
|
1821
|
+
// listener at the flip; a live→PAPER flip stops it (previously it stayed
|
|
1822
|
+
// wired to the ORPHANED live adapter — an operator approval would have
|
|
1823
|
+
// fired a real exchange order while the dashboard said PAPER); a
|
|
1824
|
+
// credential swap rebinds it to the fresh adapter. Started only after
|
|
1825
|
+
// runtime, the shared mutation lock, and durable Wave 9 ownership state
|
|
1826
|
+
// are fully resolved — same ordering the boot-only version had.
|
|
1827
|
+
const approvalLifecycle = new ApprovalListenerLifecycle({
|
|
1828
|
+
resolveApprovalMode,
|
|
1829
|
+
hasWiring: () => proposalManagerCtx !== undefined && proposalListenerCredentials !== undefined,
|
|
1830
|
+
buildListener: (boundAdapter) => {
|
|
1831
|
+
let approvalCfg;
|
|
1809
1832
|
let cfgForUrl;
|
|
1810
1833
|
try {
|
|
1811
1834
|
cfgForUrl = readPluginConfig();
|
|
1835
|
+
approvalCfg = cfgForUrl.approval;
|
|
1812
1836
|
}
|
|
1813
1837
|
catch {
|
|
1814
1838
|
cfgForUrl = undefined;
|
|
1815
1839
|
}
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
baseUrl: ingestBaseUrl,
|
|
1840
|
+
return new ProposalDecisionListener({
|
|
1841
|
+
baseUrl: getUserDataStreamIngestBaseUrl(cfgForUrl),
|
|
1819
1842
|
ingestToken: proposalListenerCredentials.ingestToken,
|
|
1820
1843
|
userId: proposalListenerCredentials.userId,
|
|
1821
|
-
adapter:
|
|
1844
|
+
adapter: boundAdapter,
|
|
1822
1845
|
binanceApi,
|
|
1823
1846
|
autoCapture,
|
|
1824
1847
|
operationLock: wave9OperationLock,
|
|
1825
1848
|
checkWave9LiveSymbolOwnership,
|
|
1826
1849
|
pollIntervalMs: approvalCfg?.pollIntervalMs ?? 3_000,
|
|
1827
1850
|
});
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1851
|
+
},
|
|
1852
|
+
});
|
|
1853
|
+
runtime.setOnAdapterSwapped((a) => { void approvalLifecycle.onAdapterSwapped(a); });
|
|
1854
|
+
// Boot application — the same path every later swap takes.
|
|
1855
|
+
void approvalLifecycle.onAdapterSwapped(runtime.adapter);
|
|
1835
1856
|
const wave9ActivationClient = new IntelWave9ActivationClient(intelApiDeps);
|
|
1836
1857
|
const getWave9AccountSnapshot = async () => {
|
|
1837
1858
|
if (!runtime.adapter.isLive) {
|
|
@@ -2040,7 +2061,7 @@ const paperTradingPlugin = {
|
|
|
2040
2061
|
installSignalHandlers({
|
|
2041
2062
|
runtime,
|
|
2042
2063
|
tradeStore: tradeIngest?.client,
|
|
2043
|
-
proposalListener:
|
|
2064
|
+
proposalListener: approvalLifecycle,
|
|
2044
2065
|
stateManager,
|
|
2045
2066
|
});
|
|
2046
2067
|
// Warm the trading params cache (fire-and-forget — register must be sync)
|
|
@@ -2137,7 +2158,10 @@ const paperTradingPlugin = {
|
|
|
2137
2158
|
autoCapture,
|
|
2138
2159
|
proposalManager: proposalManagerCtx?.manager,
|
|
2139
2160
|
userId: proposalManagerCtx?.userId,
|
|
2140
|
-
|
|
2161
|
+
// Per-call read (F10): a config approval.mode change applies on
|
|
2162
|
+
// the NEXT create_order, not the next restart. The deps literal
|
|
2163
|
+
// is rebuilt per execute, so this re-resolves every call.
|
|
2164
|
+
approvalMode: resolveApprovalMode(),
|
|
2141
2165
|
wave9AdmissionGuard,
|
|
2142
2166
|
wave9ActivationCheck: () => wave9ActivationClient.getActivation(),
|
|
2143
2167
|
refreshPaperState: reloadState,
|
|
@@ -2615,44 +2639,74 @@ const paperTradingPlugin = {
|
|
|
2615
2639
|
{
|
|
2616
2640
|
name: 'set_exchange_credentials',
|
|
2617
2641
|
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.
|
|
2642
|
+
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
2643
|
parameters: TOOL_PARAMS.set_exchange_credentials,
|
|
2620
|
-
execute: async (_id, params) =>
|
|
2644
|
+
execute: async (_id, params) => {
|
|
2645
|
+
const prov = verifyOperatorProvenance(params.operator_token);
|
|
2646
|
+
if (!prov.ok)
|
|
2647
|
+
return jsonResult({ error: prov.error });
|
|
2648
|
+
return jsonResult(await setExchangeCredentialsTool(params, { runtime, adapterDeps }));
|
|
2649
|
+
},
|
|
2621
2650
|
},
|
|
2622
2651
|
{
|
|
2623
2652
|
name: 'set_trading_mode',
|
|
2624
2653
|
label: 'Set Trading Mode',
|
|
2625
|
-
description: 'Operator-only. Move the plugin between PAPER / MICRO_LIVE / LIVE. Enforces the one-rung-at-a-time ladder.
|
|
2654
|
+
description: 'Operator-only. Move the plugin between PAPER / MICRO_LIVE / LIVE. Enforces the one-rung-at-a-time ladder. Briefly reconnects the trading adapter (open positions keep their exchange-side protective brackets throughout). Refused without dashboard operator provenance.',
|
|
2626
2655
|
parameters: TOOL_PARAMS.set_trading_mode,
|
|
2627
|
-
execute: async (_id, params) =>
|
|
2656
|
+
execute: async (_id, params) => {
|
|
2657
|
+
const prov = verifyOperatorProvenance(params.operator_token);
|
|
2658
|
+
if (!prov.ok)
|
|
2659
|
+
return jsonResult({ error: prov.error });
|
|
2660
|
+
return jsonResult(await setTradingModeTool(params, { runtime, adapterDeps }));
|
|
2661
|
+
},
|
|
2628
2662
|
},
|
|
2629
2663
|
{
|
|
2630
2664
|
name: 'test_exchange_credentials',
|
|
2631
2665
|
label: 'Test Exchange Credentials',
|
|
2632
|
-
description: 'Operator-only. Verify a Binance API key + secret with a read-only call (fetchBalance) without persisting anything. Used by the dashboard pre-flight check before set_exchange_credentials.
|
|
2666
|
+
description: 'Operator-only. Verify a Binance API key + secret with a read-only call (fetchBalance) without persisting anything. Used by the dashboard pre-flight check before set_exchange_credentials. Refused without dashboard operator provenance.',
|
|
2633
2667
|
parameters: TOOL_PARAMS.test_exchange_credentials,
|
|
2634
|
-
|
|
2668
|
+
// Read-only, but provenance-gated anyway: it makes a live authenticated
|
|
2669
|
+
// exchange call with caller-supplied keys, which would otherwise hand a
|
|
2670
|
+
// prompt-injected agent a credential-validation oracle.
|
|
2671
|
+
execute: async (_id, params) => {
|
|
2672
|
+
const prov = verifyOperatorProvenance(params.operator_token);
|
|
2673
|
+
if (!prov.ok)
|
|
2674
|
+
return jsonResult({ error: prov.error });
|
|
2675
|
+
return jsonResult(await testExchangeCredentialsTool(params));
|
|
2676
|
+
},
|
|
2635
2677
|
},
|
|
2636
2678
|
{
|
|
2637
2679
|
name: 'clear_exchange_credentials',
|
|
2638
2680
|
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}.
|
|
2681
|
+
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
2682
|
parameters: TOOL_PARAMS.clear_exchange_credentials,
|
|
2641
|
-
execute: async (_id, params) =>
|
|
2683
|
+
execute: async (_id, params) => {
|
|
2684
|
+
const prov = verifyOperatorProvenance(params.operator_token);
|
|
2685
|
+
if (!prov.ok)
|
|
2686
|
+
return jsonResult({ error: prov.error });
|
|
2687
|
+
return jsonResult(await clearExchangeCredentialsTool(params, { runtime, adapterDeps }));
|
|
2688
|
+
},
|
|
2642
2689
|
},
|
|
2643
2690
|
{
|
|
2644
2691
|
name: 'get_bracket_config',
|
|
2645
2692
|
label: 'Get Bracket Config',
|
|
2646
2693
|
description: 'Operator-only. Returns the current bracket-orders configuration (mode + requireStopLoss + requireTakeProfit). Used by the dashboard Trading Parameters panel.',
|
|
2647
2694
|
parameters: TOOL_PARAMS.get_bracket_config,
|
|
2648
|
-
execute
|
|
2695
|
+
// Adapter read at execute time (not registration) so a PAPER→HL-live
|
|
2696
|
+
// swap changes the reported protection without a restart.
|
|
2697
|
+
execute: async () => jsonResult(getBracketConfigTool({}, { adapter: runtime.adapter })),
|
|
2649
2698
|
},
|
|
2650
2699
|
{
|
|
2651
2700
|
name: 'set_bracket_requirement',
|
|
2652
2701
|
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.
|
|
2702
|
+
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
2703
|
parameters: TOOL_PARAMS.set_bracket_requirement,
|
|
2655
|
-
execute: async (_id, params) =>
|
|
2704
|
+
execute: async (_id, params) => {
|
|
2705
|
+
const prov = verifyOperatorProvenance(params.operator_token);
|
|
2706
|
+
if (!prov.ok)
|
|
2707
|
+
return jsonResult({ error: prov.error });
|
|
2708
|
+
return jsonResult(setBracketRequirementTool(params));
|
|
2709
|
+
},
|
|
2656
2710
|
},
|
|
2657
2711
|
];
|
|
2658
2712
|
// ---- Tool gate (central per-user config, slice 1) ----
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
/** Time (ms) this process spent waiting for a CPU since the previous call, then
|
|
11
|
+
* re-baselines — so consecutive calls partition the timeline into the SAME
|
|
12
|
+
* non-overlapping windows as `sampleEventLoopDelayMs`, and the two readings of
|
|
13
|
+
* one cycle describe one window.
|
|
14
|
+
*
|
|
15
|
+
* ★ Call this EVERY cycle, not only when a stall was observed: the counter is
|
|
16
|
+
* cumulative since process start, so a first read taken at the moment of a
|
|
17
|
+
* freeze would report hours of ordinary scheduling as if it were the freeze.
|
|
18
|
+
*
|
|
19
|
+
* Returns null when unavailable or on the first (baseline-establishing) call. */
|
|
20
|
+
export declare function sampleRunqueueWaitMs(): number | null;
|
|
21
|
+
/** Test-only — drop the singleton so a fresh histogram is created. */
|
|
22
|
+
export declare function __resetEventLoopMonitorForTests(): void;
|