@senpi/runtime 3.0.0-dev.openclaw-upgrade-mai.20260623210452 → 3.0.0-dev.runtime-phase-2.20260623210537

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 (196) hide show
  1. package/README.md +9 -10
  2. package/dist/actions/decision-engine.d.ts.map +1 -1
  3. package/dist/actions/decision-engine.js +0 -3
  4. package/dist/actions/decision-engine.js.map +1 -1
  5. package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
  6. package/dist/actions/open-position/open-position.action.js +2 -6
  7. package/dist/actions/open-position/open-position.action.js.map +1 -1
  8. package/dist/cli/guide-commands.d.ts.map +1 -1
  9. package/dist/cli/guide-commands.js +56 -43
  10. package/dist/cli/guide-commands.js.map +1 -1
  11. package/dist/cli/senpi-commands.d.ts.map +1 -1
  12. package/dist/cli/senpi-commands.js +108 -0
  13. package/dist/cli/senpi-commands.js.map +1 -1
  14. package/dist/constants/dsl/index.d.ts +0 -2
  15. package/dist/constants/dsl/index.d.ts.map +1 -1
  16. package/dist/constants/dsl/index.js +0 -2
  17. package/dist/constants/dsl/index.js.map +1 -1
  18. package/dist/dsl/engine-client/config-mapping.d.ts +0 -7
  19. package/dist/dsl/engine-client/config-mapping.d.ts.map +1 -1
  20. package/dist/dsl/engine-client/config-mapping.js +2 -16
  21. package/dist/dsl/engine-client/config-mapping.js.map +1 -1
  22. package/dist/dsl/events/handlers.d.ts +2 -18
  23. package/dist/dsl/events/handlers.d.ts.map +1 -1
  24. package/dist/dsl/events/handlers.js +213 -302
  25. package/dist/dsl/events/handlers.js.map +1 -1
  26. package/dist/dsl/monitor/create-monitor.d.ts +0 -15
  27. package/dist/dsl/monitor/create-monitor.d.ts.map +1 -1
  28. package/dist/dsl/monitor/create-monitor.js +9 -44
  29. package/dist/dsl/monitor/create-monitor.js.map +1 -1
  30. package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
  31. package/dist/dsl/monitor/process-one-position.js +1 -24
  32. package/dist/dsl/monitor/process-one-position.js.map +1 -1
  33. package/dist/dsl/plugin/index.d.ts +1 -16
  34. package/dist/dsl/plugin/index.d.ts.map +1 -1
  35. package/dist/dsl/plugin/index.js +4 -35
  36. package/dist/dsl/plugin/index.js.map +1 -1
  37. package/dist/dsl/plugin/options.d.ts +0 -18
  38. package/dist/dsl/plugin/options.d.ts.map +1 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +0 -4
  41. package/dist/index.js.map +1 -1
  42. package/dist/runtime/create-scanner-module.d.ts +3 -4
  43. package/dist/runtime/create-scanner-module.d.ts.map +1 -1
  44. package/dist/runtime/create-scanner-module.js +5 -14
  45. package/dist/runtime/create-scanner-module.js.map +1 -1
  46. package/dist/runtime/create-scanner-providers-for-run.d.ts.map +1 -1
  47. package/dist/runtime/create-scanner-providers-for-run.js +1 -12
  48. package/dist/runtime/create-scanner-providers-for-run.js.map +1 -1
  49. package/dist/runtime/load-runtime-config.js +1 -1
  50. package/dist/runtime/load-runtime-config.js.map +1 -1
  51. package/dist/runtime/map-strategies-registration.js +8 -7
  52. package/dist/runtime/map-strategies-registration.js.map +1 -1
  53. package/dist/runtime/map-strategies.d.ts.map +1 -1
  54. package/dist/runtime/map-strategies.js +3 -12
  55. package/dist/runtime/map-strategies.js.map +1 -1
  56. package/dist/runtime/reconcile-state.d.ts +0 -44
  57. package/dist/runtime/reconcile-state.d.ts.map +1 -1
  58. package/dist/runtime/reconcile-state.js +44 -80
  59. package/dist/runtime/reconcile-state.js.map +1 -1
  60. package/dist/runtime/risk-guard.d.ts.map +1 -1
  61. package/dist/runtime/risk-guard.js +12 -17
  62. package/dist/runtime/risk-guard.js.map +1 -1
  63. package/dist/runtime/runtime-schema.d.ts +995 -2855
  64. package/dist/runtime/runtime-schema.d.ts.map +1 -1
  65. package/dist/runtime/runtime-schema.js +40 -133
  66. package/dist/runtime/runtime-schema.js.map +1 -1
  67. package/dist/runtime/runtime.d.ts +0 -26
  68. package/dist/runtime/runtime.d.ts.map +1 -1
  69. package/dist/runtime/runtime.js +5 -233
  70. package/dist/runtime/runtime.js.map +1 -1
  71. package/dist/runtime/scanner-registry.d.ts +1 -1
  72. package/dist/runtime/scanner-registry.d.ts.map +1 -1
  73. package/dist/runtime/scanner-registry.js +12 -3
  74. package/dist/runtime/scanner-registry.js.map +1 -1
  75. package/dist/runtime/supervisor/long-running-supervisor.d.ts.map +1 -1
  76. package/dist/runtime/supervisor/long-running-supervisor.js +1 -1
  77. package/dist/runtime/supervisor/long-running-supervisor.js.map +1 -1
  78. package/dist/runtime/supervisor/orphan-sweep.js +1 -1
  79. package/dist/runtime/supervisor/orphan-sweep.js.map +1 -1
  80. package/dist/runtime/supervisor/supervisor-utils.d.ts +1 -1
  81. package/dist/runtime/supervisor/supervisor-utils.d.ts.map +1 -1
  82. package/dist/runtime/supervisor/supervisor-utils.js +1 -1
  83. package/dist/runtime/supervisor/supervisor-utils.js.map +1 -1
  84. package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
  85. package/dist/runtime-api/intake/intake-mount.js +36 -28
  86. package/dist/runtime-api/intake/intake-mount.js.map +1 -1
  87. package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
  88. package/dist/runtime-api/intake/process-signal-post.js +0 -0
  89. package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
  90. package/dist/runtime-api/intake/scanner-launcher.d.ts +4 -18
  91. package/dist/runtime-api/intake/scanner-launcher.d.ts.map +1 -1
  92. package/dist/runtime-api/intake/scanner-launcher.js +3 -24
  93. package/dist/runtime-api/intake/scanner-launcher.js.map +1 -1
  94. package/dist/runtime-api/intake/types.d.ts +0 -6
  95. package/dist/runtime-api/intake/types.d.ts.map +1 -1
  96. package/dist/runtime-api/routes/signals.schema.d.ts +0 -1
  97. package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
  98. package/dist/runtime-api/routes/signals.schema.js +0 -5
  99. package/dist/runtime-api/routes/signals.schema.js.map +1 -1
  100. package/dist/scanners/external/liquidation-watchdog/README.md +107 -0
  101. package/dist/scanners/external/liquidation-watchdog/liquidation-watchdog.yaml +48 -0
  102. package/dist/scanners/external/liquidation-watchdog/producer.mjs +336 -0
  103. package/dist/scanners/external/momentum/README.md +148 -0
  104. package/dist/scanners/external/momentum/momentum.yaml +116 -0
  105. package/dist/scanners/external/momentum/producer.mjs +251 -0
  106. package/dist/scanners/external/scaffold/__main__.py +24 -65
  107. package/dist/scanners/external/scaffold/delivery.py +22 -56
  108. package/dist/scanners/external/scaffold/entrypoint_loader.py +48 -42
  109. package/dist/scanners/external/scaffold/journal.py +2 -13
  110. package/dist/scanners/external/scaffold/scaffold.py +128 -159
  111. package/dist/scanners/external/scaffold/signal_envelope.py +34 -177
  112. package/dist/scanners/external/shared/ingest.mjs +112 -0
  113. package/dist/scanners/external-scanner-receiver.d.ts +0 -6
  114. package/dist/scanners/external-scanner-receiver.d.ts.map +1 -1
  115. package/dist/scanners/external-scanner-receiver.js +1 -14
  116. package/dist/scanners/external-scanner-receiver.js.map +1 -1
  117. package/dist/scripts/run-dsl-standalone-live.d.ts +1 -1
  118. package/dist/scripts/run-dsl-standalone-live.js +3 -5
  119. package/dist/scripts/run-dsl-standalone-live.js.map +1 -1
  120. package/dist/senpi/client.d.ts +0 -12
  121. package/dist/senpi/client.d.ts.map +1 -1
  122. package/dist/senpi/client.js +2 -31
  123. package/dist/senpi/client.js.map +1 -1
  124. package/dist/state/state-manager.d.ts +0 -17
  125. package/dist/state/state-manager.d.ts.map +1 -1
  126. package/dist/state/state-manager.js +0 -28
  127. package/dist/state/state-manager.js.map +1 -1
  128. package/dist/strategy/strategy-state.d.ts.map +1 -1
  129. package/dist/strategy/strategy-state.js +1 -7
  130. package/dist/strategy/strategy-state.js.map +1 -1
  131. package/dist/tracking/constants.d.ts.map +1 -1
  132. package/dist/tracking/constants.js +0 -2
  133. package/dist/tracking/constants.js.map +1 -1
  134. package/dist/types/dsl/index.d.ts +1 -1
  135. package/dist/types/dsl/index.d.ts.map +1 -1
  136. package/dist/types/scanner.d.ts +0 -7
  137. package/dist/types/scanner.d.ts.map +1 -1
  138. package/examples/README.md +89 -0
  139. package/examples/scanners-consumer-quickstart.mjs +134 -0
  140. package/examples/scanners-emerging-movers-v4.mjs +287 -0
  141. package/examples/scanners-liquidation-watchdog.mjs +341 -0
  142. package/examples/scanners-live-mcp.mjs +208 -0
  143. package/examples/scanners-multi-strategy.mjs +165 -0
  144. package/examples/scanners-position-tracker-e2e.mjs +225 -0
  145. package/examples/scanners-scheduled-events.mjs +96 -0
  146. package/examples/scanners-senpi-provider-client.mjs +182 -0
  147. package/examples/scanners-single-strategy.mjs +198 -0
  148. package/examples/strategies/README.md +12 -17
  149. package/examples/strategies/dsl-showcase.yaml +68 -0
  150. package/examples/strategies/dsl.yaml +51 -0
  151. package/examples/strategies/fox-minimal-loosened.yaml +240 -0
  152. package/examples/strategies/fox-minimal.yaml +242 -0
  153. package/examples/strategies/minimal.yaml +46 -0
  154. package/examples/strategies/momentum/how-it-works.md +48 -0
  155. package/examples/strategies/momentum/momentum-guarded.yaml +161 -0
  156. package/examples/strategies/risk-showcase.yaml +93 -0
  157. package/examples/strategies/viper.yaml +67 -0
  158. package/examples/strategies/wolf.yaml +72 -0
  159. package/examples/yamls/sample-with-dsl-and-emerging-movers.yaml +123 -0
  160. package/package.json +1 -1
  161. package/dist/runtime/reconcile-decide.d.ts +0 -73
  162. package/dist/runtime/reconcile-decide.d.ts.map +0 -1
  163. package/dist/runtime/reconcile-decide.js +0 -166
  164. package/dist/runtime/reconcile-decide.js.map +0 -1
  165. package/dist/runtime/reconcile-run.d.ts +0 -62
  166. package/dist/runtime/reconcile-run.d.ts.map +0 -1
  167. package/dist/runtime/reconcile-run.js +0 -173
  168. package/dist/runtime/reconcile-run.js.map +0 -1
  169. package/dist/scanners/external/scaffold/ctx.py +0 -78
  170. package/dist/scanners/external/scaffold/requirements.txt +0 -11
  171. package/dist/scanners/external/scaffold/senpi_mcp.py +0 -250
  172. package/dist/scanners/external/scaffold/state_manager.py +0 -175
  173. package/dist/utils/keyed-mutex.d.ts +0 -37
  174. package/dist/utils/keyed-mutex.d.ts.map +0 -1
  175. package/dist/utils/keyed-mutex.js +0 -77
  176. package/dist/utils/keyed-mutex.js.map +0 -1
  177. package/examples/strategies/iguana/README.md +0 -135
  178. package/examples/strategies/iguana/recipe.yaml +0 -137
  179. package/examples/strategies/iguana/scanners/scan.py +0 -243
  180. package/examples/strategies/iguana/scanners/scoring.py +0 -152
  181. package/examples/strategies/raptor/README.md +0 -156
  182. package/examples/strategies/raptor/recipe.yaml +0 -154
  183. package/examples/strategies/raptor/scanners/scan.py +0 -349
  184. package/examples/strategies/raptor/scanners/scoring.py +0 -452
  185. package/examples/strategies/spider/README.md +0 -97
  186. package/examples/strategies/spider/scalp/recipe.yaml +0 -129
  187. package/examples/strategies/spider/scalp/scanners/scan.py +0 -219
  188. package/examples/strategies/spider/scalp/scanners/scoring.py +0 -179
  189. package/examples/strategies/spider/swing/recipe.yaml +0 -141
  190. package/examples/strategies/spider/swing/scanners/scan.py +0 -334
  191. package/examples/strategies/spider/swing/scanners/scoring.py +0 -184
  192. package/examples/strategies/turbine/README.md +0 -167
  193. package/examples/strategies/turbine/recipe-runners.yaml +0 -123
  194. package/examples/strategies/turbine/recipe-volume.yaml +0 -129
  195. package/examples/strategies/turbine/scanners/scan.py +0 -283
  196. package/examples/strategies/turbine/scanners/scoring.py +0 -190
@@ -1,173 +0,0 @@
1
- /**
2
- * Impure reconcile executor: wires decideReconcile (pure) with the I/O side-effects
3
- * (adoptPosition, resizePosition, archiveGoneState).
4
- *
5
- * Design contract:
6
- * - decideReconcile is the SOLE decision source (no second gone-computation here).
7
- * - CH fetch returning null/undefined → success:false, zero mutations (R7 fail-safe).
8
- * - getOpenPositions undefined (CH never fetched) → success:false, zero mutations.
9
- * - Execution order within the serialize pass: archive gone FIRST, then adopt/resize
10
- * (frees state before re-adopting; mirrors monitor order).
11
- * - Each action is individually try/caught; one failure does not abort the rest.
12
- * - Only successfully applied actions count toward ReconcileCounts.
13
- */
14
- import { decideReconcile } from "./reconcile-decide.js";
15
- import { archiveGoneState } from "./reconcile-state.js";
16
- import { createLogger } from "../utils/logger.js";
17
- const logger = createLogger("[senpi-core][reconcile-run]");
18
- // ─── OpenPosition → PositionOpenedEvent mapping ───────────────────────────────
19
- /**
20
- * Map an OpenPosition (from clearinghouse) to the PositionOpenedEvent shape expected
21
- * by adoptPosition / resizePosition.
22
- *
23
- * Field mapping:
24
- * address → address (strategy wallet; same as deps.address)
25
- * wallet → position.wallet if set, else address (clearinghouse rows rarely carry wallet)
26
- * coin → position.coin
27
- * szi → String(position.szi) — signed; positive=LONG, negative=SHORT
28
- * entryPx → String(position.entryPx ?? 0)
29
- * leverage → position.leverage ?? 1
30
- * dex → position.dex if non-empty, else undefined (handlePositionOpened normalizes "")
31
- *
32
- * Note: PositionOpenedEvent.szi is a string (the handler parses it); OpenPosition.szi is a number.
33
- */
34
- export function openPositionToPayload(pos, address) {
35
- const dex = (pos.dex ?? "").trim();
36
- return {
37
- address,
38
- wallet: pos.wallet ?? address,
39
- coin: pos.coin,
40
- szi: String(pos.szi ?? 0),
41
- entryPx: String(pos.entryPx ?? 0),
42
- leverage: pos.leverage ?? 1,
43
- ...(dex ? { dex } : { dex: "" }),
44
- };
45
- }
46
- // ─── Executor ─────────────────────────────────────────────────────────────────
47
- export async function runReconcile(deps) {
48
- const { address, strategyState, senpi, stateManager, adopter, bus, serialize = (fn) => fn(), } = deps;
49
- const zeroCounts = { adopted: 0, resized: 0, archived: 0 };
50
- // Fetch the exchange snapshot + list DSL state + decide + execute, ALL inside the
51
- // serialize lock, so the exchange view and the DSL view are captured atomically vs
52
- // the trade loop.
53
- //
54
- // BUG 3 fix: list DSL state + decide INSIDE the lock so the plan is computed against
55
- // the same DSL snapshot the execute pass mutates.
56
- // BUG 5 fix (bugbot #156): fetch the exchange snapshot INSIDE the lock too. Capturing
57
- // it outside (a caller prefetch, or a pre-lock fetch) let a concurrent new-entry land
58
- // between snapshot and decision — reconcile then saw a DSL position absent from the
59
- // STALE exchange set, emitted "gone", and archived a LIVE managed position. Fetching
60
- // here means no trade can interleave between the exchange read and the DSL read. Cost:
61
- // one extra clearinghouse fetch per address per tick (the monitor's per-tick gate fetch
62
- // is the first; the dropped `prefetched` flag reused it, but that snapshot was the stale
63
- // one). Negligible against the monitor cadence (30s default, 5s floor).
64
- const outcome = await serialize(async () => {
65
- // PROPOSED (deferred — larger change, not implemented here):
66
- // This clearinghouse fetch runs INSIDE the reconcile lock and has no time limit. A
67
- // genuinely hung MCP connection (a freeze, not just slowness) would hold the lock
68
- // indefinitely and could stall stop() (which drains the consumer that contends this
69
- // lock). The risk is pre-existing and shared by EVERY in-lock exchange call (trade-path
70
- // create/close, etc.), not just this one — so the fix belongs at that whole layer.
71
- // Proposed policy (callTool already accepts an AbortSignal; we just don't pass one):
72
- // - while running: bound each in-lock exchange call with a timeout + up to 3 retries,
73
- // then give up. A clean give-up is safe — the gate stays closed and self-heals on
74
- // the next boot-retry / monitor tick once MCP recovers.
75
- // - on shutdown: give up after ~30s (no retries) so stop() can never hang.
76
- const ch = await strategyState.fetchClearinghouseState();
77
- if (ch == null) {
78
- logger.warn("reconcile-run: fetchClearinghouseState returned null/undefined — skipping (R7 fail-safe)", {
79
- address,
80
- });
81
- return { unavailable: true };
82
- }
83
- const exchangePositions = strategyState.getOpenPositions();
84
- if (exchangePositions === undefined) {
85
- logger.warn("reconcile-run: getOpenPositions returned undefined (clearinghouse never fetched) — skipping", {
86
- address,
87
- });
88
- return { unavailable: true };
89
- }
90
- const runtimeDslStates = await stateManager.listDslStates(address);
91
- const decision = decideReconcile({ address, exchangePositions, runtimeDslStates });
92
- const c = { adopted: 0, resized: 0, archived: 0 };
93
- // BUG 2 fix: track action failures so success reflects whether the reconcile
94
- // ACTIONS actually applied, not merely that CH was fetched + a decision ran.
95
- let f = 0;
96
- // Archive gone FIRST (frees state slots before re-adopting).
97
- for (const action of decision.actions) {
98
- if (action.kind !== "gone")
99
- continue;
100
- try {
101
- const outcome = await archiveGoneState(stateManager, senpi, action.state, {
102
- bus,
103
- strategyState,
104
- });
105
- if (outcome === "archived")
106
- c.archived++;
107
- // "skipped" is a by-design no-op (a different live position now holds the key, or
108
- // the state is already gone) — NOT a failure. Only "failed" blocks the gate.
109
- else if (outcome === "failed")
110
- f++;
111
- }
112
- catch (err) {
113
- f++;
114
- logger.warn("reconcile-run: archiveGoneState threw unexpectedly", {
115
- address,
116
- coin: action.coin,
117
- dex: action.dex,
118
- err,
119
- });
120
- }
121
- }
122
- // Adopt orphaned positions.
123
- for (const action of decision.actions) {
124
- if (action.kind !== "adopt")
125
- continue;
126
- try {
127
- await adopter.adoptPosition(openPositionToPayload(action.position, address));
128
- c.adopted++;
129
- }
130
- catch (err) {
131
- f++;
132
- logger.warn("reconcile-run: adoptPosition failed", {
133
- address,
134
- coin: action.coin,
135
- dex: action.dex,
136
- err,
137
- });
138
- }
139
- }
140
- // Resize drifted positions.
141
- for (const action of decision.actions) {
142
- if (action.kind !== "resize")
143
- continue;
144
- try {
145
- await adopter.resizePosition(openPositionToPayload(action.position, address));
146
- c.resized++;
147
- }
148
- catch (err) {
149
- f++;
150
- logger.warn("reconcile-run: resizePosition failed", {
151
- address,
152
- coin: action.coin,
153
- dex: action.dex,
154
- err,
155
- });
156
- }
157
- }
158
- return { unavailable: false, counts: c, failures: f, decided: decision.actions.length };
159
- });
160
- // CH unavailable / never fetched → R7 fail-safe (success:false, no mutation).
161
- if (outcome.unavailable) {
162
- return { success: false, counts: zeroCounts };
163
- }
164
- const { counts, failures, decided } = outcome;
165
- // success only when every decided action applied. Zero actions (converged) → success.
166
- const success = failures === 0;
167
- if (decided === 0) {
168
- return { success: true, counts: zeroCounts };
169
- }
170
- logger.info("reconcile-run: complete", { address, counts, failures, success });
171
- return { success, counts };
172
- }
173
- //# sourceMappingURL=reconcile-run.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"reconcile-run.js","sourceRoot":"","sources":["../../src/runtime/reconcile-run.ts"],"names":[],"mappings":"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"}
@@ -1,78 +0,0 @@
1
- """The author-facing `ctx` object handed to scan(inputs, ctx).
2
-
3
- `ScanContext` is the second argument to the author's `scan(inputs, ctx)`. It is
4
- two tiers (design §2.1):
5
-
6
- Capabilities you CALL:
7
- state — the StateManager (B2): a disk-persisted, bounded, transactional
8
- series. Passed in AS-IS (never wrapped/copied) so author mutations
9
- and the scaffold's commit/rollback hit the SAME object. May be None
10
- when history is disabled (state_history_max_count 0/unset).
11
- senpi_mcp — the MCP client capability. PRESENT but EMPTY this block (None);
12
- B5 fills it. The slot exists now so B5 is purely additive — no
13
- structural change to ctx later.
14
-
15
- Read-only metadata you READ:
16
- wallet — str, the strategy wallet (routing identity).
17
- scanner_name — str, this scanner's recipe name.
18
- interval_seconds — int, tick cadence in seconds.
19
-
20
- The metadata is FROZEN: mutating, deleting, or adding any attribute raises. The
21
- author can read `ctx.wallet` but cannot rebind it (or scribble a backdoor onto
22
- ctx). `state` and `senpi_mcp` are likewise immutable bindings on ctx — the author
23
- uses them through their own APIs, never by reassigning the slot.
24
- """
25
-
26
- from __future__ import annotations
27
-
28
- from typing import Any, Optional
29
-
30
-
31
- class ScanContext:
32
- """Frozen, read-only metadata + capability handles for scan(inputs, ctx).
33
-
34
- Construction seam the scaffold uses:
35
-
36
- ScanContext(
37
- *,
38
- state, # a StateManager instance (or None when history disabled)
39
- wallet: str,
40
- scanner_name: str,
41
- interval_seconds: int,
42
- senpi_mcp=None, # B5 fills this; None/absent this block
43
- )
44
- """
45
-
46
- # Lock the attribute set: no __dict__, so any unknown attribute (mutate / add /
47
- # delete) raises AttributeError. The five documented slots are the only ones.
48
- __slots__ = ("state", "senpi_mcp", "wallet", "scanner_name", "interval_seconds")
49
-
50
- def __init__(
51
- self,
52
- *,
53
- state: Any,
54
- wallet: str,
55
- scanner_name: str,
56
- interval_seconds: int,
57
- senpi_mcp: Optional[Any] = None,
58
- ) -> None:
59
- # Bypass our own frozen __setattr__ to populate the slots once at
60
- # construction. object.__setattr__ writes the slot directly.
61
- object.__setattr__(self, "state", state)
62
- object.__setattr__(self, "senpi_mcp", senpi_mcp)
63
- object.__setattr__(self, "wallet", wallet)
64
- object.__setattr__(self, "scanner_name", scanner_name)
65
- object.__setattr__(self, "interval_seconds", interval_seconds)
66
-
67
- # -- frozen: block every post-construction mutation --------------------
68
-
69
- def __setattr__(self, name: str, value: Any) -> None:
70
- raise AttributeError(
71
- f"ctx is read-only: cannot set {name!r}. The metadata "
72
- "(wallet/scanner_name/interval_seconds) is frozen, and the "
73
- "capability slots (state/senpi_mcp) are used through their own APIs, "
74
- "never reassigned."
75
- )
76
-
77
- def __delattr__(self, name: str) -> None:
78
- raise AttributeError(f"ctx is read-only: cannot delete {name!r}.")
@@ -1,11 +0,0 @@
1
- # Runtime dependencies for the scaffold harness (src/scanners/external/scaffold/).
2
- #
3
- # The scaffold is shipped to the scanner box and run via `python3 -m scaffold`.
4
- # `mcp` (the official Model Context Protocol Python SDK) backs ctx.senpi_mcp:
5
- # `streamablehttp_client` + a single persistent `ClientSession` (see senpi_mcp.py).
6
- # It is LAZY-IMPORTED at first use, so the package still imports where `mcp` is
7
- # absent (e.g. unit tests inject a fake SDK) — install this on the box that runs
8
- # scanners that call ctx.senpi_mcp.
9
- #
10
- # `mcp[cli]` is NOT needed — only the client transport + ClientSession surface.
11
- mcp>=1.9.0
@@ -1,250 +0,0 @@
1
- """`ctx.senpi_mcp` — the MCP client capability (runtime v2.0.0, Block B5).
2
-
3
- The author's `scan(inputs, ctx)` is SYNC. `ctx.senpi_mcp.call_tool(name, args)`
4
- is therefore a SYNC surface: the author never sees `await`. Internally it bridges
5
- the OFFICIAL MCP Python SDK (`mcp`), which is async-only:
6
-
7
- from mcp import ClientSession
8
- from mcp.client.streamable_http import streamablehttp_client
9
-
10
- async with streamablehttp_client(url, headers=...) as (read, write, _):
11
- async with ClientSession(read, write) as session:
12
- await session.initialize()
13
- result = await session.call_tool(name, arguments=args)
14
-
15
- DECISION (locked, execution-plan B5): use the SDK the STANDARD way — a SINGLE
16
- PERSISTENT `ClientSession` created ONCE (and `initialize()`d once), reused across
17
- every call. NO extra guards: no custom timeout, no reconnect/retry wrapper, no
18
- pooling toggle, no fresh-connection-per-call. The async context managers must
19
- STAY OPEN for the client's lifetime, so we drive them on a dedicated event loop
20
- running in a background thread; `call_tool` submits work onto that loop and blocks
21
- for the result (the sync↔async bridge).
22
-
23
- Response normalization MATCHES producer.mjs `createSenpiToolClient`:
24
- 1. isError truthy -> RAISE with the error text (never a silent None).
25
- 2. structuredContent present -> return it.
26
- 3. else first text content -> json.loads it (fallback to the raw text).
27
- 4. else -> return content as-is.
28
-
29
- The `mcp` package is LAZY-IMPORTED on first connect (NOT at module top), so
30
- `import scaffold.senpi_mcp` works even where `mcp` is not installed. Missing creds
31
- are LOUD AT FIRST USE: `call_tool` raises, construction never does.
32
-
33
- TEARDOWN (intentional, one-process-per-scanner model): there is NO explicit
34
- close()/shutdown for the persistent session. The background loop runs on a DAEMON
35
- thread, so it dies with the process. The scaffold runs exactly ONE client per
36
- process for that process's whole life — the supervisor restarts by spawning a
37
- FRESH child process (not by re-running run_scaffold in-place), so a single client
38
- is never replaced in-process and there is no loop/session leak across restart.
39
- On graceful stop (SIGTERM/SIGINT) run_scaffold exits its loop and the process
40
- returns; on a hard kill the OS reclaims the thread. We deliberately do NOT drive
41
- an AsyncExitStack.aclose() from a shutdown hook: it buys nothing over the dropped
42
- TCP connection (streamable-http is request/response, no protocol goodbye) and a
43
- cross-thread aclose() on a wedged transport could itself hang a path that today
44
- cannot hang.
45
- """
46
-
47
- from __future__ import annotations
48
-
49
- import asyncio
50
- import json
51
- import threading
52
- from typing import Any, Iterable, Optional
53
-
54
-
55
- # --------------------------------------------------------------------------
56
- # Producer MCP write boundary — read-only by default.
57
- # --------------------------------------------------------------------------
58
- #
59
- # A supervised producer's scan(inputs, ctx) runs in-process with the strategy's
60
- # full-capability SENPI_API_KEY. Without a boundary it could call ANY MCP tool —
61
- # including money/state mutations — bypassing every runtime guard (risk-guard,
62
- # DSL, slots, dedup). MUTATION_TOOLS enumerates those money/state-mutating tools;
63
- # call_tool BLOCKS them by default unless the EXACT tool name is on the client's
64
- # write_allowlist (least privilege — one permitted tool does not unlock others).
65
- #
66
- # Read tools (discovery_*, leaderboard_*, market_*, strategy_get*, strategy_list,
67
- # execution_get*, account_get*, user_get*, arena_*, audit_*, get_*,
68
- # list_senpi_guides, read_senpi_guide, ratchet_stop_get/list/events) are NOT in
69
- # this set and therefore always pass.
70
- MUTATION_TOOLS: frozenset[str] = frozenset(
71
- {
72
- "create_position",
73
- "close_position",
74
- "edit_position",
75
- "cancel_order",
76
- "send_usdc",
77
- "transfer_spot_to_perps",
78
- "strategy_create",
79
- "strategy_create_custom_strategy",
80
- "strategy_close",
81
- "strategy_close_positions",
82
- "strategy_update",
83
- "strategy_pause",
84
- "strategy_top_up",
85
- "strategy_withdraw_funds",
86
- "strategy_bridge_funds_from_hyperliquid_to_evm",
87
- "ratchet_stop_add",
88
- "ratchet_stop_edit",
89
- "ratchet_stop_delete",
90
- "user_claim_referral_rewards",
91
- }
92
- )
93
-
94
- # THE allowlist a scaffold maintainer edits to permit a mutation tool for
95
- # producers. EMPTY BY DEFAULT — every MUTATION_TOOLS entry is blocked. The ONLY
96
- # way to permit a mutation is to add its exact tool name to this frozenset
97
- # (e.g. {"create_position"}). This is scaffold-maintainer-owned source: it is
98
- # not operator/env-configurable and is not reachable by authored producer code.
99
- PRODUCER_WRITE_ALLOWLIST: frozenset[str] = frozenset()
100
-
101
-
102
- class SenpiMcpClient:
103
- """Sync MCP client over the official async SDK with one persistent session.
104
-
105
- Construction is cheap and never touches the network or imports `mcp`. The
106
- first `call_tool` lazily imports the SDK, spins up a private event loop on a
107
- background thread, opens the transport + ClientSession ONCE, and initializes
108
- it ONCE. Every subsequent call reuses that same session.
109
- """
110
-
111
- def __init__(
112
- self,
113
- api_key: Optional[str] = None,
114
- mcp_url: Optional[str] = None,
115
- write_allowlist: Iterable[str] = (),
116
- ) -> None:
117
- self._api_key = api_key
118
- self._mcp_url = mcp_url
119
- # Mutation tools the operator has explicitly permitted. EMPTY by default
120
- # → every MUTATION_TOOLS entry is blocked (read-only producer boundary).
121
- self._write_allowlist: frozenset[str] = frozenset(write_allowlist)
122
-
123
- # Persistent bridge state — created lazily on first call_tool.
124
- self._loop: Optional[asyncio.AbstractEventLoop] = None
125
- self._thread: Optional[threading.Thread] = None
126
- self._session: Any = None
127
- self._stack: Any = None
128
- self._connected = False
129
- self._connect_lock = threading.Lock()
130
-
131
- # -- public sync surface ----------------------------------------------
132
-
133
- def call_tool(self, name: str, args: Optional[dict] = None) -> Any:
134
- """Invoke an MCP tool synchronously; return the unwrapped result.
135
-
136
- Raises (loud, never a silent None) when the tool is a money/state
137
- mutation not on the write allowlist, when creds are missing, or when the
138
- tool reports `isError`.
139
- """
140
- # Read-only producer boundary, enforced PRE-CONNECT (no transport opened
141
- # for a blocked write). A mutation tool passes only if its exact name is
142
- # on the write allowlist (least privilege — one tool ≠ all).
143
- if name in MUTATION_TOOLS and name not in self._write_allowlist:
144
- raise PermissionError(
145
- f'senpi_mcp: tool "{name}" is a money/state-mutating MCP tool and '
146
- "is blocked — producers are READ-ONLY by default. The ONLY way to "
147
- f'permit it is to add "{name}" to the PRODUCER_WRITE_ALLOWLIST '
148
- "frozenset in src/scanners/external/scaffold/senpi_mcp.py "
149
- "(scaffold-maintainer-owned source; not operator/env-configurable; "
150
- "not reachable by authored producer code)."
151
- )
152
-
153
- self._ensure_connected()
154
- result = self._run(self._session.call_tool(name, arguments=args))
155
- return self._unwrap(name, result)
156
-
157
- # -- persistent session lifecycle -------------------------------------
158
-
159
- def _ensure_connected(self) -> None:
160
- if self._connected:
161
- return
162
- with self._connect_lock:
163
- if self._connected:
164
- return
165
-
166
- # Loud at first use: creds are required to reach the MCP server.
167
- if not self._api_key:
168
- raise RuntimeError(
169
- "senpi_mcp: SENPI_API_KEY is not set; cannot call MCP tools "
170
- "(creds are resolved at launch from SENPI_API_KEY/SENPI_MCP_URL)."
171
- )
172
- if not self._mcp_url:
173
- raise RuntimeError(
174
- "senpi_mcp: SENPI_MCP_URL is not set; cannot call MCP tools "
175
- "(creds are resolved at launch from SENPI_API_KEY/SENPI_MCP_URL)."
176
- )
177
-
178
- # Dedicated event loop on a background thread. The async context
179
- # managers (transport + session) must stay OPEN for the client's
180
- # lifetime, so we cannot use asyncio.run() per call — that would
181
- # tear the session down between calls.
182
- self._loop = asyncio.new_event_loop()
183
- self._thread = threading.Thread(
184
- target=self._loop.run_forever,
185
- name="senpi-mcp-loop",
186
- daemon=True,
187
- )
188
- self._thread.start()
189
-
190
- self._run(self._connect())
191
- self._connected = True
192
-
193
- async def _connect(self) -> None:
194
- """Open the transport + ClientSession ONCE and initialize ONCE.
195
-
196
- Lazy-import the SDK here so `import scaffold.senpi_mcp` succeeds in envs
197
- without `mcp` installed (tests inject a fake `mcp` into sys.modules).
198
- """
199
- from mcp import ClientSession
200
- from mcp.client.streamable_http import streamablehttp_client
201
-
202
- # Keep the async context managers alive by holding their __aenter__
203
- # values on self and only __aexit__-ing them at close. We use
204
- # AsyncExitStack so both nest correctly and unwind in order.
205
- from contextlib import AsyncExitStack
206
-
207
- self._stack = AsyncExitStack()
208
- headers = {"Authorization": f"Bearer {self._api_key}"}
209
- read, write, _ = await self._stack.enter_async_context(
210
- streamablehttp_client(self._mcp_url, headers=headers)
211
- )
212
- session = await self._stack.enter_async_context(ClientSession(read, write))
213
- await session.initialize()
214
- self._session = session
215
-
216
- def _run(self, coro: Any) -> Any:
217
- """Drive a coroutine to completion on the persistent loop, blocking."""
218
- future = asyncio.run_coroutine_threadsafe(coro, self._loop)
219
- return future.result()
220
-
221
- # -- response normalization (matches producer.mjs createSenpiToolClient) --
222
-
223
- @staticmethod
224
- def _unwrap(name: str, result: Any) -> Any:
225
- if getattr(result, "isError", False):
226
- content = getattr(result, "content", None)
227
- if (
228
- isinstance(content, list)
229
- and len(content) > 0
230
- and getattr(content[0], "type", None) == "text"
231
- ):
232
- error_text = content[0].text
233
- else:
234
- error_text = json.dumps(content, default=str)
235
- raise RuntimeError(f'MCP tool "{name}" returned error: {error_text}')
236
-
237
- structured = getattr(result, "structuredContent", None)
238
- if structured is not None:
239
- return structured
240
-
241
- content = getattr(result, "content", None)
242
- if isinstance(content, list) and len(content) > 0:
243
- first = content[0]
244
- if getattr(first, "type", None) == "text":
245
- try:
246
- return json.loads(first.text)
247
- except (ValueError, TypeError):
248
- return first.text
249
-
250
- return content