@reefclaw/openclaw-plugin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. package/.gitignore +8 -0
  2. package/audit/mode-transition-audit.d.ts +11 -0
  3. package/audit/mode-transition-audit.js +29 -0
  4. package/balance-utils.d.ts +36 -0
  5. package/balance-utils.js +98 -0
  6. package/bridge/bridge.d.ts +109 -0
  7. package/bridge/bridge.js +1036 -0
  8. package/bridge/config.d.ts +43 -0
  9. package/bridge/config.js +176 -0
  10. package/bridge/connector.d.ts +52 -0
  11. package/bridge/connector.js +293 -0
  12. package/bridge/event-replay-buffer.d.ts +43 -0
  13. package/bridge/event-replay-buffer.js +109 -0
  14. package/bridge/gateway/event-parser.d.ts +209 -0
  15. package/bridge/gateway/event-parser.js +793 -0
  16. package/bridge/gateway/gateway-config.d.ts +39 -0
  17. package/bridge/gateway/gateway-config.js +86 -0
  18. package/bridge/gateway/gateway-http-client.d.ts +50 -0
  19. package/bridge/gateway/gateway-http-client.js +165 -0
  20. package/bridge/gateway/gateway-ws-client.d.ts +116 -0
  21. package/bridge/gateway/gateway-ws-client.js +417 -0
  22. package/bridge/gateway/poller.d.ts +146 -0
  23. package/bridge/gateway/poller.js +505 -0
  24. package/bridge/gateway/tool-discovery.d.ts +25 -0
  25. package/bridge/gateway/tool-discovery.js +198 -0
  26. package/bridge/index.d.ts +2 -0
  27. package/bridge/index.js +253 -0
  28. package/bridge/logger.d.ts +2 -0
  29. package/bridge/logger.js +2 -0
  30. package/bridge/provider.d.ts +156 -0
  31. package/bridge/provider.js +2 -0
  32. package/bridge/providers/emergency-commands.d.ts +54 -0
  33. package/bridge/providers/emergency-commands.js +235 -0
  34. package/bridge/providers/gateway.d.ts +322 -0
  35. package/bridge/providers/gateway.js +2302 -0
  36. package/bridge/providers/mock.d.ts +37 -0
  37. package/bridge/providers/mock.js +385 -0
  38. package/bridge/providers/onboarding-commands.d.ts +83 -0
  39. package/bridge/providers/onboarding-commands.js +213 -0
  40. package/bridge/providers/risk-calculator.d.ts +96 -0
  41. package/bridge/providers/risk-calculator.js +369 -0
  42. package/bridge/setup.d.ts +32 -0
  43. package/bridge/setup.js +226 -0
  44. package/bridge/types.d.ts +584 -0
  45. package/bridge/types.js +50 -0
  46. package/bridge/utils/reconnect.d.ts +6 -0
  47. package/bridge/utils/reconnect.js +6 -0
  48. package/bridge/utils/skill-signing.d.ts +51 -0
  49. package/bridge/utils/skill-signing.js +138 -0
  50. package/bridge/utils/skill-version.d.ts +17 -0
  51. package/bridge/utils/skill-version.js +74 -0
  52. package/ccxt/binance-ban-gate.d.ts +47 -0
  53. package/ccxt/binance-ban-gate.js +409 -0
  54. package/ccxt/binance-private.d.ts +325 -0
  55. package/ccxt/binance-private.js +1415 -0
  56. package/ccxt/binance-public.d.ts +18 -0
  57. package/ccxt/binance-public.js +147 -0
  58. package/config/agent-config-client.d.ts +55 -0
  59. package/config/agent-config-client.js +145 -0
  60. package/config/agent-config-poller.d.ts +25 -0
  61. package/config/agent-config-poller.js +100 -0
  62. package/config/brackets-config.d.ts +22 -0
  63. package/config/brackets-config.js +58 -0
  64. package/config/gate-store.d.ts +18 -0
  65. package/config/gate-store.js +61 -0
  66. package/config/plugin-config-io.d.ts +181 -0
  67. package/config/plugin-config-io.js +84 -0
  68. package/config/position-review-config.d.ts +35 -0
  69. package/config/position-review-config.js +105 -0
  70. package/config/tool-gate.d.ts +53 -0
  71. package/config/tool-gate.js +125 -0
  72. package/config/user-data-stream-config.d.ts +85 -0
  73. package/config/user-data-stream-config.js +224 -0
  74. package/connector-supervisor.d.ts +36 -0
  75. package/connector-supervisor.js +149 -0
  76. package/exchange-adapter.d.ts +49 -0
  77. package/exchange-adapter.js +4 -0
  78. package/index.d.ts +30 -0
  79. package/index.js +2030 -0
  80. package/ingest/pending-entry-metadata.d.ts +52 -0
  81. package/ingest/pending-entry-metadata.js +182 -0
  82. package/ingest/position-auto-capture.d.ts +98 -0
  83. package/ingest/position-auto-capture.js +394 -0
  84. package/ingest/position-decisions-client.d.ts +318 -0
  85. package/ingest/position-decisions-client.js +296 -0
  86. package/ingest/reconcile-db-vs-exchange.d.ts +13 -0
  87. package/ingest/reconcile-db-vs-exchange.js +114 -0
  88. package/ingest/reconciler-cleanup.d.ts +37 -0
  89. package/ingest/reconciler-cleanup.js +147 -0
  90. package/ingest/rest-gap-filler.d.ts +191 -0
  91. package/ingest/rest-gap-filler.js +565 -0
  92. package/ingest/touched-symbols-store.d.ts +25 -0
  93. package/ingest/touched-symbols-store.js +96 -0
  94. package/ingest/trade-store-client.d.ts +40 -0
  95. package/ingest/trade-store-client.js +125 -0
  96. package/ingest/ws-ingest.d.ts +43 -0
  97. package/ingest/ws-ingest.js +126 -0
  98. package/learning/setup-family.d.ts +21 -0
  99. package/learning/setup-family.js +103 -0
  100. package/lifecycle/install-signal-handlers.d.ts +33 -0
  101. package/lifecycle/install-signal-handlers.js +112 -0
  102. package/lifecycle/shutdown-coordinator.d.ts +43 -0
  103. package/lifecycle/shutdown-coordinator.js +131 -0
  104. package/live/bracket-id.d.ts +18 -0
  105. package/live/bracket-id.js +81 -0
  106. package/live/bracket-ledger.d.ts +54 -0
  107. package/live/bracket-ledger.js +267 -0
  108. package/live/bracket-manager.d.ts +82 -0
  109. package/live/bracket-manager.js +478 -0
  110. package/live/bracket-params.d.ts +22 -0
  111. package/live/bracket-params.js +124 -0
  112. package/live/bracket-reconciler.d.ts +95 -0
  113. package/live/bracket-reconciler.js +573 -0
  114. package/live/bracket-types.d.ts +102 -0
  115. package/live/bracket-types.js +8 -0
  116. package/live/deposit-tracker.d.ts +62 -0
  117. package/live/deposit-tracker.js +97 -0
  118. package/live/emergency-controls.d.ts +32 -0
  119. package/live/emergency-controls.js +226 -0
  120. package/live/exchange-errors.d.ts +12 -0
  121. package/live/exchange-errors.js +130 -0
  122. package/live/exchange-info-cache.d.ts +35 -0
  123. package/live/exchange-info-cache.js +119 -0
  124. package/live/fact-subscriber.d.ts +78 -0
  125. package/live/fact-subscriber.js +182 -0
  126. package/live/intent-journal.d.ts +42 -0
  127. package/live/intent-journal.js +122 -0
  128. package/live/listen-key-manager.d.ts +70 -0
  129. package/live/listen-key-manager.js +169 -0
  130. package/live/live-adapter.d.ts +264 -0
  131. package/live/live-adapter.js +1665 -0
  132. package/live/live-balance-enricher.d.ts +32 -0
  133. package/live/live-balance-enricher.js +104 -0
  134. package/live/live-bracket-api.d.ts +13 -0
  135. package/live/live-bracket-api.js +20 -0
  136. package/live/live-state-store.d.ts +194 -0
  137. package/live/live-state-store.js +450 -0
  138. package/live/local-signal-service.d.ts +57 -0
  139. package/live/local-signal-service.js +146 -0
  140. package/live/local-strategy-evaluator.d.ts +62 -0
  141. package/live/local-strategy-evaluator.js +127 -0
  142. package/live/microstructure-assembler.d.ts +54 -0
  143. package/live/microstructure-assembler.js +148 -0
  144. package/live/order-poller.d.ts +29 -0
  145. package/live/order-poller.js +125 -0
  146. package/live/position-state-store.d.ts +83 -0
  147. package/live/position-state-store.js +237 -0
  148. package/live/proposal-decision-listener.d.ts +64 -0
  149. package/live/proposal-decision-listener.js +288 -0
  150. package/live/proposal-manager.d.ts +76 -0
  151. package/live/proposal-manager.js +140 -0
  152. package/live/rate-limiter.d.ts +47 -0
  153. package/live/rate-limiter.js +159 -0
  154. package/live/reconciler.d.ts +39 -0
  155. package/live/reconciler.js +175 -0
  156. package/live/setup-buckets.d.ts +7 -0
  157. package/live/setup-buckets.js +33 -0
  158. package/live/slippage-tracker.d.ts +45 -0
  159. package/live/slippage-tracker.js +78 -0
  160. package/live/stop-watcher.d.ts +34 -0
  161. package/live/stop-watcher.js +158 -0
  162. package/live/user-data-active-probe.d.ts +54 -0
  163. package/live/user-data-active-probe.js +180 -0
  164. package/live/user-data-stream-controller.d.ts +200 -0
  165. package/live/user-data-stream-controller.js +579 -0
  166. package/live/user-data-stream-ws.d.ts +22 -0
  167. package/live/user-data-stream-ws.js +63 -0
  168. package/live/user-data-stream.d.ts +243 -0
  169. package/live/user-data-stream.js +704 -0
  170. package/logger.d.ts +2 -0
  171. package/logger.js +2 -0
  172. package/mfe.d.ts +21 -0
  173. package/mfe.js +68 -0
  174. package/onboarding/mode-ladder.d.ts +1 -0
  175. package/onboarding/mode-ladder.js +3 -0
  176. package/onboarding/runtime.d.ts +71 -0
  177. package/onboarding/runtime.js +153 -0
  178. package/openclaw.plugin.json +94 -0
  179. package/package.json +27 -0
  180. package/paper-adapter.d.ts +24 -0
  181. package/paper-adapter.js +91 -0
  182. package/persistence/state-manager.d.ts +42 -0
  183. package/persistence/state-manager.js +164 -0
  184. package/pinned-plan.d.ts +9 -0
  185. package/pinned-plan.js +23 -0
  186. package/risk/pre-trade-check.d.ts +38 -0
  187. package/risk/pre-trade-check.js +345 -0
  188. package/risk/pre-trade-types.d.ts +60 -0
  189. package/risk/pre-trade-types.js +3 -0
  190. package/scripts/assemble.mjs +114 -0
  191. package/shadow/shadow-tracker.d.ts +36 -0
  192. package/shadow/shadow-tracker.js +151 -0
  193. package/shadow/types.d.ts +42 -0
  194. package/shadow/types.js +20 -0
  195. package/shared/indicators-extended.d.ts +52 -0
  196. package/shared/indicators-extended.js +291 -0
  197. package/shared/indicators.d.ts +15 -0
  198. package/shared/indicators.js +114 -0
  199. package/signals/conditions/registry.d.ts +16 -0
  200. package/signals/conditions/registry.js +1274 -0
  201. package/signals/conditions/types.d.ts +1 -0
  202. package/signals/conditions/types.js +4 -0
  203. package/signals/direction-rules.d.ts +3 -0
  204. package/signals/direction-rules.js +24 -0
  205. package/signals/entry-rules.d.ts +6 -0
  206. package/signals/entry-rules.js +33 -0
  207. package/signals/serialize-context.d.ts +4 -0
  208. package/signals/serialize-context.js +39 -0
  209. package/signals/stop-rules.d.ts +3 -0
  210. package/signals/stop-rules.js +48 -0
  211. package/signals/strategy-adapter.d.ts +14 -0
  212. package/signals/strategy-adapter.js +122 -0
  213. package/signals/types.d.ts +1 -0
  214. package/signals/types.js +8 -0
  215. package/simulator/exchange-simulator.d.ts +93 -0
  216. package/simulator/exchange-simulator.js +684 -0
  217. package/simulator/fill-engine.d.ts +53 -0
  218. package/simulator/fill-engine.js +276 -0
  219. package/simulator/paper-market-feed.d.ts +26 -0
  220. package/simulator/paper-market-feed.js +104 -0
  221. package/simulator/realistic-fills.d.ts +59 -0
  222. package/simulator/realistic-fills.js +175 -0
  223. package/simulator/types.d.ts +219 -0
  224. package/simulator/types.js +43 -0
  225. package/skills/reefclaw/SKILL.md +97 -0
  226. package/strategy/builtin-strategies.d.ts +2 -0
  227. package/strategy/builtin-strategies.js +109 -0
  228. package/strategy/condition-registry.d.ts +3 -0
  229. package/strategy/condition-registry.js +153 -0
  230. package/strategy/evaluator.d.ts +67 -0
  231. package/strategy/evaluator.js +93 -0
  232. package/tools/assessment-validation.d.ts +118 -0
  233. package/tools/assessment-validation.js +415 -0
  234. package/tools/attach-brackets.d.ts +34 -0
  235. package/tools/attach-brackets.js +363 -0
  236. package/tools/audit-bracket-protection.d.ts +49 -0
  237. package/tools/audit-bracket-protection.js +527 -0
  238. package/tools/cancel-all-orders.d.ts +7 -0
  239. package/tools/cancel-all-orders.js +5 -0
  240. package/tools/cancel-order.d.ts +10 -0
  241. package/tools/cancel-order.js +14 -0
  242. package/tools/check-position-health.d.ts +46 -0
  243. package/tools/check-position-health.js +194 -0
  244. package/tools/clear-exchange-credentials.d.ts +24 -0
  245. package/tools/clear-exchange-credentials.js +70 -0
  246. package/tools/close-position.d.ts +22 -0
  247. package/tools/close-position.js +449 -0
  248. package/tools/create-order.d.ts +54 -0
  249. package/tools/create-order.js +338 -0
  250. package/tools/exit-gate.d.ts +58 -0
  251. package/tools/exit-gate.js +162 -0
  252. package/tools/fetch-balance.d.ts +5 -0
  253. package/tools/fetch-balance.js +4 -0
  254. package/tools/fetch-ohlcv.d.ts +11 -0
  255. package/tools/fetch-ohlcv.js +8 -0
  256. package/tools/fetch-open-orders.d.ts +7 -0
  257. package/tools/fetch-open-orders.js +4 -0
  258. package/tools/fetch-positions.d.ts +7 -0
  259. package/tools/fetch-positions.js +4 -0
  260. package/tools/fetch-ticker.d.ts +11 -0
  261. package/tools/fetch-ticker.js +5 -0
  262. package/tools/get-agent-profile.d.ts +4 -0
  263. package/tools/get-agent-profile.js +6 -0
  264. package/tools/get-analytics.d.ts +6 -0
  265. package/tools/get-analytics.js +7 -0
  266. package/tools/get-backtest.d.ts +12 -0
  267. package/tools/get-backtest.js +91 -0
  268. package/tools/get-basis.d.ts +7 -0
  269. package/tools/get-basis.js +7 -0
  270. package/tools/get-bracket-config.d.ts +11 -0
  271. package/tools/get-bracket-config.js +24 -0
  272. package/tools/get-cascade-risk.d.ts +7 -0
  273. package/tools/get-cascade-risk.js +8 -0
  274. package/tools/get-crypto-metrics.d.ts +18 -0
  275. package/tools/get-crypto-metrics.js +45 -0
  276. package/tools/get-cvd.d.ts +6 -0
  277. package/tools/get-cvd.js +6 -0
  278. package/tools/get-divergences.d.ts +6 -0
  279. package/tools/get-divergences.js +6 -0
  280. package/tools/get-funding-context.d.ts +6 -0
  281. package/tools/get-funding-context.js +16 -0
  282. package/tools/get-liquidation-levels.d.ts +7 -0
  283. package/tools/get-liquidation-levels.js +7 -0
  284. package/tools/get-liquidation-pulse.d.ts +9 -0
  285. package/tools/get-liquidation-pulse.js +22 -0
  286. package/tools/get-market-breadth.d.ts +6 -0
  287. package/tools/get-market-breadth.js +8 -0
  288. package/tools/get-market-intel.d.ts +19 -0
  289. package/tools/get-market-intel.js +116 -0
  290. package/tools/get-market-structure.d.ts +47 -0
  291. package/tools/get-market-structure.js +198 -0
  292. package/tools/get-my-mined-patterns.d.ts +20 -0
  293. package/tools/get-my-mined-patterns.js +61 -0
  294. package/tools/get-my-proposed-learnings.d.ts +20 -0
  295. package/tools/get-my-proposed-learnings.js +55 -0
  296. package/tools/get-my-recent-reviews.d.ts +22 -0
  297. package/tools/get-my-recent-reviews.js +66 -0
  298. package/tools/get-orderbook.d.ts +21 -0
  299. package/tools/get-orderbook.js +32 -0
  300. package/tools/get-pattern-scan.d.ts +7 -0
  301. package/tools/get-pattern-scan.js +8 -0
  302. package/tools/get-regime.d.ts +6 -0
  303. package/tools/get-regime.js +7 -0
  304. package/tools/get-relevant-learnings.d.ts +21 -0
  305. package/tools/get-relevant-learnings.js +65 -0
  306. package/tools/get-resting-liquidity.d.ts +6 -0
  307. package/tools/get-resting-liquidity.js +11 -0
  308. package/tools/get-risk-scenario.d.ts +29 -0
  309. package/tools/get-risk-scenario.js +47 -0
  310. package/tools/get-risk-summary.d.ts +51 -0
  311. package/tools/get-risk-summary.js +118 -0
  312. package/tools/get-sentiment.d.ts +4 -0
  313. package/tools/get-sentiment.js +6 -0
  314. package/tools/get-session-review.d.ts +7 -0
  315. package/tools/get-session-review.js +8 -0
  316. package/tools/get-setup-detail.d.ts +7 -0
  317. package/tools/get-setup-detail.js +303 -0
  318. package/tools/get-signals.d.ts +15 -0
  319. package/tools/get-signals.js +54 -0
  320. package/tools/get-sizing.d.ts +6 -0
  321. package/tools/get-sizing.js +6 -0
  322. package/tools/get-trade-feedback.d.ts +7 -0
  323. package/tools/get-trade-feedback.js +8 -0
  324. package/tools/get-trade-flow.d.ts +7 -0
  325. package/tools/get-trade-flow.js +7 -0
  326. package/tools/get-volume-analysis.d.ts +21 -0
  327. package/tools/get-volume-analysis.js +74 -0
  328. package/tools/get-volume-profile.d.ts +7 -0
  329. package/tools/get-volume-profile.js +7 -0
  330. package/tools/helpers.d.ts +25 -0
  331. package/tools/helpers.js +38 -0
  332. package/tools/intel-api.d.ts +14 -0
  333. package/tools/intel-api.js +52 -0
  334. package/tools/intel-cache.d.ts +25 -0
  335. package/tools/intel-cache.js +133 -0
  336. package/tools/list-strategies.d.ts +7 -0
  337. package/tools/list-strategies.js +6 -0
  338. package/tools/modify-stop.d.ts +17 -0
  339. package/tools/modify-stop.js +81 -0
  340. package/tools/modify-target.d.ts +17 -0
  341. package/tools/modify-target.js +71 -0
  342. package/tools/propose-learning.d.ts +22 -0
  343. package/tools/propose-learning.js +65 -0
  344. package/tools/query-review-outcomes.d.ts +30 -0
  345. package/tools/query-review-outcomes.js +64 -0
  346. package/tools/query-trades.d.ts +21 -0
  347. package/tools/query-trades.js +37 -0
  348. package/tools/record-position-reviews.d.ts +38 -0
  349. package/tools/record-position-reviews.js +147 -0
  350. package/tools/save-strategy.d.ts +16 -0
  351. package/tools/save-strategy.js +46 -0
  352. package/tools/scan-pairs.d.ts +18 -0
  353. package/tools/scan-pairs.js +220 -0
  354. package/tools/score-setup.d.ts +31 -0
  355. package/tools/score-setup.js +268 -0
  356. package/tools/set-bracket-requirement.d.ts +18 -0
  357. package/tools/set-bracket-requirement.js +81 -0
  358. package/tools/set-exchange-credentials.d.ts +25 -0
  359. package/tools/set-exchange-credentials.js +80 -0
  360. package/tools/set-trading-mode.d.ts +26 -0
  361. package/tools/set-trading-mode.js +135 -0
  362. package/tools/test-exchange-credentials.d.ts +16 -0
  363. package/tools/test-exchange-credentials.js +100 -0
  364. package/tools/toggle-strategy.d.ts +8 -0
  365. package/tools/toggle-strategy.js +8 -0
  366. package/trading-params-cache.d.ts +26 -0
  367. package/trading-params-cache.js +52 -0
  368. package/types.d.ts +110 -0
  369. package/types.js +7 -0
  370. package/util/plugin-paths.d.ts +3 -0
  371. package/util/plugin-paths.js +15 -0
@@ -0,0 +1,181 @@
1
+ import type { TradingMode, ExchangeConfig } from '../types.js';
2
+ /** Full on-disk schema. Unknown keys are preserved on round-trip. */
3
+ export interface PluginConfigFile {
4
+ connectionToken?: string;
5
+ /** 'on' → the plugin spawns + restarts the relay connector as a child of
6
+ * the gateway process (no systemd — OpenClaw is the process manager).
7
+ * Written by the npx installer on fresh installs. MUST stay defaulted off:
8
+ * prod runs the bridge under systemd and a supervisor there would
9
+ * double-connect the relay room. Kill-switch: RC_CONNECTOR_SUPERVISOR=off. */
10
+ connectorSupervisor?: 'on' | 'off';
11
+ apiBaseUrl?: string;
12
+ intelligenceUrl?: string;
13
+ exchange?: ExchangeConfig;
14
+ tradingMode?: TradingMode;
15
+ microLive?: {
16
+ sizeCapPercent?: number;
17
+ maxPositionUSDT?: number;
18
+ };
19
+ /** Exchange-native bracket orders (STOP_MARKET + TAKE_PROFIT_MARKET).
20
+ * mode='off' (default) keeps legacy stop-watcher behaviour.
21
+ * mode='observe' submits brackets AND keeps the watcher active (rollout stage).
22
+ * mode='enforce' disables watcher for live; brackets are the sole protection.
23
+ *
24
+ * requireStopLoss / requireTakeProfit (default both true) control whether
25
+ * the pre-trade gate rejects live entries that don't include the matching
26
+ * price. Operator can untick via the Trading Parameters panel; UI shows a
27
+ * red warning (stop off) or amber warning (target off) with typed confirm.
28
+ * Migration note: these flags move to intelligence DB per-user alongside
29
+ * Phase 2.5 (ledger mirror). Plugin-config is the temporary home. */
30
+ brackets?: {
31
+ mode?: 'off' | 'observe' | 'enforce';
32
+ requireStopLoss?: boolean;
33
+ requireTakeProfit?: boolean;
34
+ };
35
+ /** Facts-out signal evaluation (docs/FACTS_OUT_ARCHITECTURE.md step 5).
36
+ * evaluator='central' (default) → get_signals calls the intel engine as today.
37
+ * 'shadow' → also evaluate locally (via the SSE fact stream) and log the
38
+ * comparison; central is still authoritative (zero behaviour change).
39
+ * 'enforce' → get_signals returns the locally-computed snapshot (falling back
40
+ * to central when no fact has arrived for a symbol yet).
41
+ * `symbols` is the set the local service streams + evaluates; required when
42
+ * evaluator != 'central'. Requires the intel fact stream to be on
43
+ * (RC_INTEL_FACTS_STREAM=on). */
44
+ signals?: {
45
+ evaluator?: 'central' | 'shadow' | 'enforce';
46
+ symbols?: string[];
47
+ };
48
+ /** Binance user-data WebSocket stream (positions/balance/orders via push).
49
+ * mode='off' (default) keeps legacy REST polling authoritative.
50
+ * mode='shadow' runs the WS alongside REST and logs diffs; REST still authoritative.
51
+ * mode='observe' makes WS authoritative for reads; REST drops to 30-60s truth-check.
52
+ * mode='enforce' full cutover — skill-side pollers disabled, REST at 60s truth-check only.
53
+ *
54
+ * listenKeyRefreshMs / reconnectBaseMs / reconnectCapMs are escape-hatch tunables
55
+ * for runbook use; defaults are production-safe. staleEventWatchdogMs triggers a
56
+ * forced reconnect if no events arrive within the window (catches silent drops). */
57
+ userDataStream?: {
58
+ mode?: 'off' | 'shadow' | 'observe' | 'enforce';
59
+ listenKeyRefreshMs?: number;
60
+ reconnectBaseMs?: number;
61
+ reconnectCapMs?: number;
62
+ staleEventWatchdogMs?: number;
63
+ /** Interval for REST-refreshing algo orders into the store. Binance WS
64
+ * never pushes algo-order state changes until trigger, so this loop fills
65
+ * the gap. 0 = disabled. Default 15_000 (15s). */
66
+ algoRefreshMs?: number;
67
+ /** @deprecated since 2026-04-25 (ping-aware watchdog refactor). Field is
68
+ * still parsed and clamped so existing config files don't fail validation,
69
+ * but the runtime no longer reads it — the single `staleEventWatchdogMs`
70
+ * (default 6 min, ping-aware) plus the active REST probe replace it. */
71
+ activeStaleEventWatchdogMs?: number;
72
+ /** Active REST probe interval — polls fetchMyTrades for every open-position
73
+ * symbol; if a trade timestamp exceeds the WS lastEventAt, the WS missed
74
+ * a fill and the probe forces a reconnect. 0 = disabled. Default 60_000. */
75
+ activeProbeIntervalMs?: number;
76
+ /** Audit-trail write path (TRADE_AUDIT_TRAIL_PLAN Phase 1).
77
+ * 'off' (default) — WS ingest doesn't write to the trades table.
78
+ * 'on' — WsIngest forwards each TRADE event to the webapp's
79
+ * /api/internal/trades endpoint. Idempotent on the
80
+ * (exchange, exchange_trade_id) unique index, so it's
81
+ * safe to run alongside the legacy poller-write path.
82
+ * Orthogonal to `mode`: `mode` controls the read path; `dbWrite` controls
83
+ * the audit-write path. Both must be enabled (mode≠'off' AND dbWrite='on')
84
+ * for WS rows to land in the trades table. */
85
+ dbWrite?: 'off' | 'on';
86
+ /** Webapp base URL for the WS ingest POST. Defaults to the production
87
+ * reefclaw.com if absent; an explicit value overrides for staging /
88
+ * local-dev / on-prem deployments. */
89
+ ingestBaseUrl?: string;
90
+ };
91
+ /** Position Decision Journal — per-heartbeat-per-open-position review tool
92
+ * + stale-review gate. See docs/POSITION_DECISION_JOURNAL_PLAN.md.
93
+ *
94
+ * mode='off' (default) — `record_position_reviews` tool exists but no
95
+ * SKILL.md mandate; stale-gate is no-op.
96
+ * mode='shadow' — SKILL.md mandate live; per-heartbeat call required.
97
+ * No gate. Operator reads reviews to confirm quality.
98
+ * mode='observe' — `create_order` returns soft warning on stale review.
99
+ * mode='enforce' — `create_order` hard-rejects on stale review.
100
+ *
101
+ * staleAfterMs caps how old the most recent review can be before the gate
102
+ * fires. Default = 2 × heartbeatIntervalMs (60 min for a 30 min heartbeat).
103
+ *
104
+ * Entry/close auto-capture is always-on (independent of `mode`) once the
105
+ * webapp ingest URL + token are configured. */
106
+ positionReview?: {
107
+ mode?: 'off' | 'shadow' | 'observe' | 'enforce';
108
+ staleAfterMs?: number;
109
+ /** Exit-gate flag — prevents premature close_position calls per
110
+ * docs/EXIT_GATE_DESIGN.md. Orthogonal to `mode`: `mode` drives the
111
+ * per-heartbeat review requirement; `exitGate` drives the per-exit
112
+ * tier check.
113
+ *
114
+ * exitGate='off' (default) — gate never runs; behaviour identical to today.
115
+ * exitGate='shadow' — gate runs, result logged to close_assessment.metadata.gate_eval,
116
+ * `close_position` always proceeds.
117
+ * exitGate='observe' — gate runs; on BLOCK returns a soft warning in the
118
+ * tool result but `close_position` still proceeds.
119
+ * exitGate='enforce' — gate runs; on BLOCK rejects the tool call. */
120
+ exitGate?: 'off' | 'shadow' | 'observe' | 'enforce';
121
+ };
122
+ /** Approval Mode — per-user operator-approval-before-trade gate.
123
+ * Plugin emits a proposal via /api/internal/proposed_orders; operator decides
124
+ * in the browser; ProposalDecisionListener fires the order on approve via the
125
+ * existing create_order backend path. See docs/APPROVAL_MODE_DESIGN.md.
126
+ *
127
+ * mode='off' (default) — fully autonomous; create_order fires immediately.
128
+ * Toggle-off is byte-identical to today's behaviour.
129
+ * mode='per_trade' — every create_order emits a proposal and returns
130
+ * {status:'pending_approval'} to the agent. Listener
131
+ * fires the order when the operator clicks Approve.
132
+ *
133
+ * pollIntervalMs caps how often the listener checks for approved-but-unfired
134
+ * proposals via GET /api/internal/proposed_orders/pending-decisions. Default
135
+ * 3 sec — well under typical human approval latency. */
136
+ approval?: {
137
+ mode?: 'off' | 'per_trade';
138
+ pollIntervalMs?: number;
139
+ };
140
+ /** Public Binance market microstructure features (cross-market liquidation
141
+ * pulse + banded resting liquidity). Data layer lives on the intelligence
142
+ * VPS (separate workstream — see docs/MARKET_MICROSTRUCTURE_STREAMS.md);
143
+ * this flag controls whether the plugin attaches a per-symbol microstructure
144
+ * block to position-aware tool results (e.g. check_position_health) so the
145
+ * agent sees orderbook context every heartbeat without an extra tool call.
146
+ *
147
+ * heartbeatPush='off' (default) — no auto-attach. Agent can still call
148
+ * get_resting_liquidity / get_liquidation_pulse
149
+ * explicitly.
150
+ * heartbeatPush='on' — block attached when intel data is fresh. */
151
+ microstructure?: {
152
+ heartbeatPush?: 'off' | 'on';
153
+ };
154
+ /** EFFICIENCY_QUICK_WINS Change 1 — operator override for the legacy
155
+ * PositionWatcher poll cadence. Default 10000 (see stop-watcher.ts).
156
+ * Lets the operator revert the 3s→10s cut with zero deploy if a soak
157
+ * criterion fails. */
158
+ stopWatcher?: {
159
+ intervalMs?: number;
160
+ };
161
+ [extra: string]: unknown;
162
+ }
163
+ export declare function defaultConfigPath(): string;
164
+ /** Read the config file. Returns `{}` if the file doesn't exist.
165
+ * Throws if the file exists but is unreadable or not valid JSON — callers
166
+ * should treat that as an abort signal, not silently overwrite. */
167
+ export declare function readPluginConfig(path?: string): PluginConfigFile;
168
+ /** Apply a patch on top of the existing file and write atomically.
169
+ * - Unknown top-level keys in the on-disk file are preserved.
170
+ * - Unknown sub-keys of `microLive` are preserved.
171
+ * - Passing `undefined` as a top-level patch value deletes that key.
172
+ * - A `null` for `exchange` clears credentials.
173
+ *
174
+ * Atomicity: we write to `<path>.tmp` then rename. If the process crashes
175
+ * mid-write, the original file is untouched. A stale `.tmp` is overwritten
176
+ * on the next successful write.
177
+ */
178
+ export declare function updatePluginConfig(patch: Partial<PluginConfigFile>, path?: string): PluginConfigFile;
179
+ /** Redact a credential for logging — keeps first 4 and last 2 chars.
180
+ * Never log the full key/secret. */
181
+ export declare function redactCredential(value: string | undefined | null): string;
@@ -0,0 +1,84 @@
1
+ // Atomic read/write for ~/.reefclaw/plugin-config.json.
2
+ //
3
+ // Historically the plugin only READ this file at startup. As of PR1 of the
4
+ // dashboard onboarding feature, the plugin also WRITES it when the operator
5
+ // sets credentials or switches trading mode from the webapp. Writes must be
6
+ // crash-safe (no half-written JSON) and must preserve keys we don't understand
7
+ // (forwards compatibility — other tools may add fields we don't know about).
8
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
9
+ import { homedir } from 'node:os';
10
+ import { dirname, join } from 'node:path';
11
+ export function defaultConfigPath() {
12
+ return join(homedir(), '.reefclaw', 'plugin-config.json');
13
+ }
14
+ /** Read the config file. Returns `{}` if the file doesn't exist.
15
+ * Throws if the file exists but is unreadable or not valid JSON — callers
16
+ * should treat that as an abort signal, not silently overwrite. */
17
+ export function readPluginConfig(path = defaultConfigPath()) {
18
+ if (!existsSync(path))
19
+ return {};
20
+ const raw = readFileSync(path, 'utf-8');
21
+ const parsed = JSON.parse(raw);
22
+ if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {
23
+ throw new Error(`plugin-config.json root is not an object`);
24
+ }
25
+ return parsed;
26
+ }
27
+ /** Apply a patch on top of the existing file and write atomically.
28
+ * - Unknown top-level keys in the on-disk file are preserved.
29
+ * - Unknown sub-keys of `microLive` are preserved.
30
+ * - Passing `undefined` as a top-level patch value deletes that key.
31
+ * - A `null` for `exchange` clears credentials.
32
+ *
33
+ * Atomicity: we write to `<path>.tmp` then rename. If the process crashes
34
+ * mid-write, the original file is untouched. A stale `.tmp` is overwritten
35
+ * on the next successful write.
36
+ */
37
+ export function updatePluginConfig(patch, path = defaultConfigPath()) {
38
+ const dir = dirname(path);
39
+ if (!existsSync(dir)) {
40
+ mkdirSync(dir, { recursive: true });
41
+ }
42
+ const current = readPluginConfig(path);
43
+ const merged = { ...current };
44
+ for (const [key, value] of Object.entries(patch)) {
45
+ if (value === undefined) {
46
+ delete merged[key];
47
+ }
48
+ else if (key === 'microLive' && value && typeof value === 'object') {
49
+ merged.microLive = { ...(current.microLive ?? {}), ...value };
50
+ }
51
+ else if (key === 'brackets' && value && typeof value === 'object') {
52
+ merged.brackets = { ...(current.brackets ?? {}), ...value };
53
+ }
54
+ else if (key === 'userDataStream' && value && typeof value === 'object') {
55
+ merged.userDataStream = { ...(current.userDataStream ?? {}), ...value };
56
+ }
57
+ else if (key === 'positionReview' && value && typeof value === 'object') {
58
+ merged.positionReview = { ...(current.positionReview ?? {}), ...value };
59
+ }
60
+ else if (key === 'microstructure' && value && typeof value === 'object') {
61
+ merged.microstructure = { ...(current.microstructure ?? {}), ...value };
62
+ }
63
+ else if (key === 'approval' && value && typeof value === 'object') {
64
+ merged.approval = { ...(current.approval ?? {}), ...value };
65
+ }
66
+ else {
67
+ merged[key] = value;
68
+ }
69
+ }
70
+ const tmpPath = `${path}.tmp`;
71
+ const body = JSON.stringify(merged, null, 2) + '\n';
72
+ writeFileSync(tmpPath, body, { encoding: 'utf-8', mode: 0o600 });
73
+ renameSync(tmpPath, path);
74
+ return merged;
75
+ }
76
+ /** Redact a credential for logging — keeps first 4 and last 2 chars.
77
+ * Never log the full key/secret. */
78
+ export function redactCredential(value) {
79
+ if (!value)
80
+ return '(empty)';
81
+ if (value.length <= 8)
82
+ return '***';
83
+ return `${value.slice(0, 4)}…${value.slice(-2)}`;
84
+ }
@@ -0,0 +1,35 @@
1
+ import { type PluginConfigFile } from './plugin-config-io.js';
2
+ export type PositionReviewMode = 'off' | 'shadow' | 'observe' | 'enforce';
3
+ export declare function getPositionReviewMode(config?: PluginConfigFile): PositionReviewMode;
4
+ /** Resolve the position-review mode with the central-config precedence
5
+ * (TOOL_DISTRIBUTION_ARCHITECTURE.md §11 step 3, slice 3):
6
+ *
7
+ * central (agent_config.gates via gate-store) → plugin-config.json → 'off'
8
+ *
9
+ * The central value is already enum-validated (agent-config-client
10
+ * validateGates) and hot-applies on the next poll. `record_position_reviews`
11
+ * reads this per call (index.ts), so a dashboard/API flip needs NO restart —
12
+ * same mechanics as loadExitGateMode. Kill-switch RC_CENTRAL_GATES=off makes
13
+ * the store report null → the file rules again. (The startup log read at
14
+ * boot may still show the file value until the first poll applies central;
15
+ * it is cosmetic — the behavioural read is per-call.) */
16
+ export declare function loadPositionReviewMode(): PositionReviewMode;
17
+ export type ExitGateMode = 'off' | 'shadow' | 'observe' | 'enforce';
18
+ export declare function getExitGateMode(config?: PluginConfigFile): ExitGateMode;
19
+ /** Resolve the exit-gate mode with the central-config precedence
20
+ * (TOOL_DISTRIBUTION_ARCHITECTURE.md §11 step 3, slice 2):
21
+ *
22
+ * central (agent_config.gates via gate-store) → plugin-config.json → 'off'
23
+ *
24
+ * The central value is already enum-validated (agent-config-client
25
+ * validateGates) and hot-applies on the next poll — close-position.ts reads
26
+ * this per call, so a dashboard/API flip needs NO restart. `deps.exitGateMode`
27
+ * at the call site (tests) stays above all of this. Kill-switch
28
+ * RC_CENTRAL_GATES=off makes the store report null → the file rules again. */
29
+ export declare function loadExitGateMode(): ExitGateMode;
30
+ /** Stale threshold in ms. Default = 60 minutes (2 × 30-min heartbeat). */
31
+ export declare function getPositionReviewStaleAfterMs(config?: PluginConfigFile): number;
32
+ export declare function loadPositionReviewStaleAfterMs(): number;
33
+ export type MicrostructureHeartbeatPush = 'off' | 'on';
34
+ export declare function getMicrostructureHeartbeatPush(config?: PluginConfigFile): MicrostructureHeartbeatPush;
35
+ export declare function loadMicrostructureHeartbeatPush(): MicrostructureHeartbeatPush;
@@ -0,0 +1,105 @@
1
+ // Feature-flag readers for the Position Decision Journal.
2
+ //
3
+ // Two flags, both default 'off' so the system ships dead-code:
4
+ // - positionReview.mode drives the heartbeat-mandate + stale-gate
5
+ // - microstructure.heartbeatPush drives orderbook auto-attach to per-position tools
6
+ //
7
+ // See docs/POSITION_DECISION_JOURNAL_PLAN.md.
8
+ import { readPluginConfig } from './plugin-config-io.js';
9
+ import { gateStore } from './gate-store.js';
10
+ const VALID_REVIEW_MODES = new Set([
11
+ 'off',
12
+ 'shadow',
13
+ 'observe',
14
+ 'enforce',
15
+ ]);
16
+ export function getPositionReviewMode(config) {
17
+ const raw = config?.positionReview?.mode;
18
+ if (typeof raw === 'string' && VALID_REVIEW_MODES.has(raw)) {
19
+ return raw;
20
+ }
21
+ return 'off';
22
+ }
23
+ /** Resolve the position-review mode with the central-config precedence
24
+ * (TOOL_DISTRIBUTION_ARCHITECTURE.md §11 step 3, slice 3):
25
+ *
26
+ * central (agent_config.gates via gate-store) → plugin-config.json → 'off'
27
+ *
28
+ * The central value is already enum-validated (agent-config-client
29
+ * validateGates) and hot-applies on the next poll. `record_position_reviews`
30
+ * reads this per call (index.ts), so a dashboard/API flip needs NO restart —
31
+ * same mechanics as loadExitGateMode. Kill-switch RC_CENTRAL_GATES=off makes
32
+ * the store report null → the file rules again. (The startup log read at
33
+ * boot may still show the file value until the first poll applies central;
34
+ * it is cosmetic — the behavioural read is per-call.) */
35
+ export function loadPositionReviewMode() {
36
+ const central = gateStore.getPositionReviewMode();
37
+ if (central)
38
+ return central;
39
+ try {
40
+ return getPositionReviewMode(readPluginConfig());
41
+ }
42
+ catch {
43
+ return 'off';
44
+ }
45
+ }
46
+ const VALID_EXIT_GATE_MODES = new Set([
47
+ 'off',
48
+ 'shadow',
49
+ 'observe',
50
+ 'enforce',
51
+ ]);
52
+ export function getExitGateMode(config) {
53
+ const raw = config?.positionReview?.exitGate;
54
+ if (typeof raw === 'string' && VALID_EXIT_GATE_MODES.has(raw)) {
55
+ return raw;
56
+ }
57
+ return 'off';
58
+ }
59
+ /** Resolve the exit-gate mode with the central-config precedence
60
+ * (TOOL_DISTRIBUTION_ARCHITECTURE.md §11 step 3, slice 2):
61
+ *
62
+ * central (agent_config.gates via gate-store) → plugin-config.json → 'off'
63
+ *
64
+ * The central value is already enum-validated (agent-config-client
65
+ * validateGates) and hot-applies on the next poll — close-position.ts reads
66
+ * this per call, so a dashboard/API flip needs NO restart. `deps.exitGateMode`
67
+ * at the call site (tests) stays above all of this. Kill-switch
68
+ * RC_CENTRAL_GATES=off makes the store report null → the file rules again. */
69
+ export function loadExitGateMode() {
70
+ const central = gateStore.getExitGate();
71
+ if (central)
72
+ return central;
73
+ try {
74
+ return getExitGateMode(readPluginConfig());
75
+ }
76
+ catch {
77
+ return 'off';
78
+ }
79
+ }
80
+ /** Stale threshold in ms. Default = 60 minutes (2 × 30-min heartbeat). */
81
+ export function getPositionReviewStaleAfterMs(config) {
82
+ const raw = config?.positionReview?.staleAfterMs;
83
+ if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0)
84
+ return raw;
85
+ return 60 * 60 * 1000;
86
+ }
87
+ export function loadPositionReviewStaleAfterMs() {
88
+ try {
89
+ return getPositionReviewStaleAfterMs(readPluginConfig());
90
+ }
91
+ catch {
92
+ return 60 * 60 * 1000;
93
+ }
94
+ }
95
+ export function getMicrostructureHeartbeatPush(config) {
96
+ return config?.microstructure?.heartbeatPush === 'on' ? 'on' : 'off';
97
+ }
98
+ export function loadMicrostructureHeartbeatPush() {
99
+ try {
100
+ return getMicrostructureHeartbeatPush(readPluginConfig());
101
+ }
102
+ catch {
103
+ return 'off';
104
+ }
105
+ }
@@ -0,0 +1,53 @@
1
+ /** Safety-floor (core) tools + operator-control tools. Never governable. */
2
+ export declare const UNGOVERNABLE_TOOLS: ReadonlySet<string>;
3
+ export type ToolGateMode = 'enforce' | 'shadow' | 'off';
4
+ /** RC_TOOL_GATE: 'off' = kill-switch (gate never blocks), 'shadow' = log
5
+ * would-be blocks but execute anyway, anything else/default = 'enforce'.
6
+ * Kill-switch convention per the plugin's other RC_* flags: a systemd env
7
+ * override flips it instantly on restart, no redeploy. */
8
+ export declare function resolveToolGateMode(): ToolGateMode;
9
+ /** The structured, agent-visible result for a blocked call. Worded so the
10
+ * agent adapts instead of retrying. */
11
+ export declare function disabledToolResult(name: string): {
12
+ ok: false;
13
+ error: 'tool_disabled_by_operator';
14
+ tool: string;
15
+ message: string;
16
+ };
17
+ /** Minimal structural view of a registered plugin tool. `execute` is variadic
18
+ * (toolCallId, params, signal?) and typed loosely to match the plugin-boundary
19
+ * PluginTool interface in index.ts, which itself uses `any` for params. The
20
+ * generic in wrapTool preserves the concrete tool shape (label, description,
21
+ * parameters, …). */
22
+ export interface GateablePluginTool {
23
+ name: string;
24
+ execute: (...args: any[]) => any;
25
+ }
26
+ export declare class ToolGate {
27
+ private readonly mode;
28
+ private disabled;
29
+ private version;
30
+ /** Names already shadow-logged for the current version — avoids log spam. */
31
+ private shadowLogged;
32
+ constructor(mode?: ToolGateMode);
33
+ getMode(): ToolGateMode;
34
+ getVersion(): number;
35
+ getDisabled(): string[];
36
+ /** Apply a validated config. Strips ungovernable names (third enforcement
37
+ * layer — the webapp write route and internal read route already did), and
38
+ * logs only when the effective set actually changed. */
39
+ apply(config: {
40
+ version: number;
41
+ tools: {
42
+ disabled: string[];
43
+ };
44
+ }): void;
45
+ /** Should this call be blocked? Ungovernable names short-circuit FIRST —
46
+ * the safety floor wins over any disabled set, in any mode. */
47
+ isBlocked(name: string): boolean;
48
+ /** Wrap a tool's execute with the gate check. The returned tool is
49
+ * identical except `execute` consults the gate's live state per call —
50
+ * which is what makes a config change hot-apply without re-registration.
51
+ * All original args (toolCallId, params, abort signal) pass through. */
52
+ wrapTool<T extends GateablePluginTool>(tool: T, jsonResult: (data: unknown) => unknown): T;
53
+ }
@@ -0,0 +1,125 @@
1
+ // ToolGate — the plugin-side enforcement point for the Tools-tab OFF-list
2
+ // (docs/TOOL_DISTRIBUTION_ARCHITECTURE.md §5f). Every registered tool's
3
+ // `execute` is wrapped once at registration; the wrapper consults the gate's
4
+ // LIVE state on every invocation, so a config change applies on the next call
5
+ // with no re-registration and no restart (§5e hot-reload).
6
+ //
7
+ // ★ §5d SAFETY FLOOR — UNGOVERNABLE_TOOLS is hardcoded here and checked
8
+ // BEFORE the disabled set. Even if a compromised config server, a hand-edited
9
+ // DB row, or a poisoned cache ships `close_position` in the OFF-list, the
10
+ // gate never blocks it. This set is pinned by a test on this side AND a
11
+ // mirror pin in the webapp catalog (tools-catalog.test.ts) — change both
12
+ // deliberately or not at all.
13
+ import { logger } from '../logger.js';
14
+ const TAG = 'tool-gate';
15
+ /** Safety-floor (core) tools + operator-control tools. Never governable. */
16
+ export const UNGOVERNABLE_TOOLS = new Set([
17
+ // Core safety floor — protecting/exiting positions and the review capture.
18
+ 'create_order',
19
+ 'close_position',
20
+ 'modify_stop',
21
+ 'attach_brackets',
22
+ 'audit_bracket_protection',
23
+ 'record_position_reviews',
24
+ // Operator-control channel — disabling these would lock the operator out.
25
+ 'set_exchange_credentials',
26
+ 'test_exchange_credentials',
27
+ 'clear_exchange_credentials',
28
+ 'set_trading_mode',
29
+ 'get_bracket_config',
30
+ 'set_bracket_requirement',
31
+ ]);
32
+ /** RC_TOOL_GATE: 'off' = kill-switch (gate never blocks), 'shadow' = log
33
+ * would-be blocks but execute anyway, anything else/default = 'enforce'.
34
+ * Kill-switch convention per the plugin's other RC_* flags: a systemd env
35
+ * override flips it instantly on restart, no redeploy. */
36
+ export function resolveToolGateMode() {
37
+ const raw = (process.env.RC_TOOL_GATE ?? '').toLowerCase();
38
+ if (raw === 'off')
39
+ return 'off';
40
+ if (raw === 'shadow')
41
+ return 'shadow';
42
+ return 'enforce';
43
+ }
44
+ /** The structured, agent-visible result for a blocked call. Worded so the
45
+ * agent adapts instead of retrying. */
46
+ export function disabledToolResult(name) {
47
+ return {
48
+ ok: false,
49
+ error: 'tool_disabled_by_operator',
50
+ tool: name,
51
+ message: `The ${name} tool has been disabled by your operator in the ReefClaw dashboard Tools tab. ` +
52
+ `Do not retry this call — adapt your plan without it, and mention to your operator that ` +
53
+ `re-enabling it in the dashboard would help if you believe it is needed.`,
54
+ };
55
+ }
56
+ export class ToolGate {
57
+ mode;
58
+ disabled = new Set();
59
+ version = 0;
60
+ /** Names already shadow-logged for the current version — avoids log spam. */
61
+ shadowLogged = new Set();
62
+ constructor(mode = resolveToolGateMode()) {
63
+ this.mode = mode;
64
+ if (mode !== 'enforce')
65
+ logger.info(TAG, `mode=${mode}`);
66
+ }
67
+ getMode() {
68
+ return this.mode;
69
+ }
70
+ getVersion() {
71
+ return this.version;
72
+ }
73
+ getDisabled() {
74
+ return [...this.disabled].sort();
75
+ }
76
+ /** Apply a validated config. Strips ungovernable names (third enforcement
77
+ * layer — the webapp write route and internal read route already did), and
78
+ * logs only when the effective set actually changed. */
79
+ apply(config) {
80
+ const next = new Set(config.tools.disabled.filter((n) => !UNGOVERNABLE_TOOLS.has(n)));
81
+ const changed = next.size !== this.disabled.size || [...next].some((n) => !this.disabled.has(n));
82
+ this.version = config.version;
83
+ if (!changed)
84
+ return;
85
+ this.disabled = next;
86
+ this.shadowLogged.clear();
87
+ logger.info(TAG, `applied config v${config.version}: ${next.size} tool(s) disabled${next.size > 0 ? ` (${[...next].sort().join(', ')})` : ''}`);
88
+ }
89
+ /** Should this call be blocked? Ungovernable names short-circuit FIRST —
90
+ * the safety floor wins over any disabled set, in any mode. */
91
+ isBlocked(name) {
92
+ if (this.mode === 'off')
93
+ return false;
94
+ if (UNGOVERNABLE_TOOLS.has(name))
95
+ return false;
96
+ if (!this.disabled.has(name))
97
+ return false;
98
+ if (this.mode === 'shadow') {
99
+ if (!this.shadowLogged.has(name)) {
100
+ this.shadowLogged.add(name);
101
+ logger.info(TAG, `SHADOW: would block ${name} (config v${this.version})`);
102
+ }
103
+ return false;
104
+ }
105
+ return true;
106
+ }
107
+ /** Wrap a tool's execute with the gate check. The returned tool is
108
+ * identical except `execute` consults the gate's live state per call —
109
+ * which is what makes a config change hot-apply without re-registration.
110
+ * All original args (toolCallId, params, abort signal) pass through. */
111
+ wrapTool(tool, jsonResult) {
112
+ const originalExecute = tool.execute.bind(tool);
113
+ return {
114
+ ...tool,
115
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
116
+ execute: (...args) => {
117
+ if (this.isBlocked(tool.name)) {
118
+ logger.info(TAG, `blocked ${tool.name} (disabled by operator, config v${this.version})`);
119
+ return Promise.resolve(jsonResult(disabledToolResult(tool.name)));
120
+ }
121
+ return originalExecute(...args);
122
+ },
123
+ };
124
+ }
125
+ }