@senpi/runtime 3.0.56 → 3.0.58
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/dist/cli/guide-commands.d.ts.map +1 -1
- package/dist/cli/guide-commands.js +28 -2
- package/dist/cli/guide-commands.js.map +1 -1
- package/dist/cli/senpi-commands.d.ts.map +1 -1
- package/dist/cli/senpi-commands.js +2 -0
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/cli/validate-command.d.ts +29 -0
- package/dist/cli/validate-command.d.ts.map +1 -0
- package/dist/cli/validate-command.js +89 -0
- package/dist/cli/validate-command.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +62 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/load-runtime-config.d.ts +38 -0
- package/dist/runtime/load-runtime-config.d.ts.map +1 -1
- package/dist/runtime/load-runtime-config.js +33 -25
- package/dist/runtime/load-runtime-config.js.map +1 -1
- package/dist/runtime/runtime-schema.d.ts +100 -100
- package/dist/scanners/external/scaffold/__main__.py +8 -0
- package/dist/scanners/external/scaffold/ctx.py +15 -1
- package/dist/scanners/external/scaffold/http_post.py +30 -0
- package/dist/scanners/external/scaffold/import_check.py +310 -0
- package/dist/scanners/external/scaffold/preflight.py +545 -0
- package/dist/scanners/external/scaffold/scaffold.py +19 -6
- package/dist/scanners/implementations/market-regime.d.ts +1 -1
- package/dist/telemetry/metrics.d.ts +6 -0
- package/dist/telemetry/metrics.d.ts.map +1 -1
- package/dist/telemetry/metrics.js +45 -0
- package/dist/telemetry/metrics.js.map +1 -1
- package/dist/utils/event-catalog.d.ts +40 -0
- package/dist/utils/event-catalog.d.ts.map +1 -1
- package/dist/utils/event-catalog.js +12 -0
- package/dist/utils/event-catalog.js.map +1 -1
- package/dist/validate/codes.d.ts +165 -0
- package/dist/validate/codes.d.ts.map +1 -0
- package/dist/validate/codes.js +381 -0
- package/dist/validate/codes.js.map +1 -0
- package/dist/validate/coverage.d.ts +34 -0
- package/dist/validate/coverage.d.ts.map +1 -0
- package/dist/validate/coverage.js +34 -0
- package/dist/validate/coverage.js.map +1 -0
- package/dist/validate/import-stage.d.ts +56 -0
- package/dist/validate/import-stage.d.ts.map +1 -0
- package/dist/validate/import-stage.js +244 -0
- package/dist/validate/import-stage.js.map +1 -0
- package/dist/validate/index.d.ts +28 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +27 -0
- package/dist/validate/index.js.map +1 -0
- package/dist/validate/install-gate.d.ts +44 -0
- package/dist/validate/install-gate.d.ts.map +1 -0
- package/dist/validate/install-gate.js +66 -0
- package/dist/validate/install-gate.js.map +1 -0
- package/dist/validate/live-stage.d.ts +134 -0
- package/dist/validate/live-stage.d.ts.map +1 -0
- package/dist/validate/live-stage.js +472 -0
- package/dist/validate/live-stage.js.map +1 -0
- package/dist/validate/proof.d.ts +104 -0
- package/dist/validate/proof.d.ts.map +1 -0
- package/dist/validate/proof.js +269 -0
- package/dist/validate/proof.js.map +1 -0
- package/dist/validate/recipe-checks.d.ts +79 -0
- package/dist/validate/recipe-checks.d.ts.map +1 -0
- package/dist/validate/recipe-checks.js +205 -0
- package/dist/validate/recipe-checks.js.map +1 -0
- package/dist/validate/recipe.d.ts +41 -0
- package/dist/validate/recipe.d.ts.map +1 -0
- package/dist/validate/recipe.js +215 -0
- package/dist/validate/recipe.js.map +1 -0
- package/dist/validate/report.d.ts +35 -0
- package/dist/validate/report.d.ts.map +1 -0
- package/dist/validate/report.js +80 -0
- package/dist/validate/report.js.map +1 -0
- package/dist/validate/run.d.ts +101 -0
- package/dist/validate/run.d.ts.map +1 -0
- package/dist/validate/run.js +471 -0
- package/dist/validate/run.js.map +1 -0
- package/dist/validate/signal-stage.d.ts +40 -0
- package/dist/validate/signal-stage.d.ts.map +1 -0
- package/dist/validate/signal-stage.js +116 -0
- package/dist/validate/signal-stage.js.map +1 -0
- package/dist/validate/target.d.ts +70 -0
- package/dist/validate/target.d.ts.map +1 -0
- package/dist/validate/target.js +231 -0
- package/dist/validate/target.js.map +1 -0
- package/dist/validate/telemetry.d.ts +24 -0
- package/dist/validate/telemetry.d.ts.map +1 -0
- package/dist/validate/telemetry.js +60 -0
- package/dist/validate/telemetry.js.map +1 -0
- package/dist/validate/types.d.ts +115 -0
- package/dist/validate/types.d.ts.map +1 -0
- package/dist/validate/types.js +31 -0
- package/dist/validate/types.js.map +1 -0
- package/dist/validate/wallet.d.ts +63 -0
- package/dist/validate/wallet.d.ts.map +1 -0
- package/dist/validate/wallet.js +66 -0
- package/dist/validate/wallet.js.map +1 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
"""Run one tick, observe everything, deliver nothing.
|
|
2
|
+
|
|
3
|
+
python3 -m scaffold.preflight <launch-config.json>
|
|
4
|
+
|
|
5
|
+
Runs the author's `scan()` exactly once against live data, through the same loop production uses,
|
|
6
|
+
and reports what happened as one JSON document, written to the `result_path` the caller names.
|
|
7
|
+
|
|
8
|
+
Signals are collected and returned rather than delivered — the runtime's own delivery funnel is
|
|
9
|
+
closed for the duration and raises if used. That covers the sanctioned path, which is the one
|
|
10
|
+
authored code is supposed to take; a scanner reaching for a socket directly is caught statically at
|
|
11
|
+
the import depth, not stopped here. So: nothing leaves by the route the scaffold owns.
|
|
12
|
+
|
|
13
|
+
## What this adds beyond running the loop
|
|
14
|
+
|
|
15
|
+
**Reads are counted.** A tick that returns nothing having read nothing is indistinguishable, from
|
|
16
|
+
the outside, from a healthy scanner with nothing to trade. Counting successful reads is what tells
|
|
17
|
+
those apart — and it is the difference between reporting a strategy as fine and reporting that
|
|
18
|
+
nothing about it was established.
|
|
19
|
+
|
|
20
|
+
**Suppressed exceptions are recorded.** The author contract tells scanners to return `[]` on any
|
|
21
|
+
failure rather than crash, which is right for production and blinding for validation: a scanner
|
|
22
|
+
whose every read fails looks exactly like one with no setups. Tracing catches exceptions as they
|
|
23
|
+
are raised, before the author's own handler swallows them.
|
|
24
|
+
|
|
25
|
+
**The early exit is located.** When a tick returns without reading, the line it returned from turns
|
|
26
|
+
"something stopped early" into an edit.
|
|
27
|
+
|
|
28
|
+
**Author output cannot corrupt the result.** The tick shares this process's stdout, so the result
|
|
29
|
+
does not travel on it: the caller names a `result_path` and the document is written there, atomically.
|
|
30
|
+
Author stdout is captured as evidence and never parsed. Redirecting `sys.stdout` alone would only
|
|
31
|
+
cover `print()` — `os.write(1, ...)`, a subprocess, or a C extension write to the same descriptor.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import io
|
|
35
|
+
import json
|
|
36
|
+
import math
|
|
37
|
+
import os
|
|
38
|
+
import sys
|
|
39
|
+
import time
|
|
40
|
+
import traceback
|
|
41
|
+
from typing import Any, Dict, List, Optional
|
|
42
|
+
|
|
43
|
+
from . import http_post
|
|
44
|
+
from .signal_envelope import candidate_to_signal
|
|
45
|
+
|
|
46
|
+
# Evidence is meant to be complete, but a scanner that prints in a loop will produce megabytes of
|
|
47
|
+
# it. Bounded with the ends kept and the middle marked, so the output stays honest about what was
|
|
48
|
+
# dropped rather than trailing off.
|
|
49
|
+
_STREAM_LIMIT = 32_768
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _bounded(text: str, limit: int = _STREAM_LIMIT) -> str:
|
|
53
|
+
if len(text) <= limit:
|
|
54
|
+
return text
|
|
55
|
+
half = limit // 2
|
|
56
|
+
dropped = len(text) - limit
|
|
57
|
+
return "{}\n… truncated {} characters …\n{}".format(text[:half], dropped, text[-half:])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class _Watched(dict):
|
|
61
|
+
"""A response that remembers whether the scanner looked inside it.
|
|
62
|
+
|
|
63
|
+
Fetching a payload and reading nothing out of it usually means the shape is not what the code
|
|
64
|
+
expects — a renamed key, a nesting level missed. That failure is silent: the read succeeded, the
|
|
65
|
+
scanner carried on, and the score was computed from a default. Recording access is what turns it
|
|
66
|
+
into something reportable.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self, source: dict) -> None:
|
|
70
|
+
super().__init__(source)
|
|
71
|
+
self.touched = False
|
|
72
|
+
|
|
73
|
+
def _touch(self) -> None:
|
|
74
|
+
self.touched = True
|
|
75
|
+
|
|
76
|
+
def __getitem__(self, key: Any) -> Any:
|
|
77
|
+
self._touch()
|
|
78
|
+
return super().__getitem__(key)
|
|
79
|
+
|
|
80
|
+
def get(self, key: Any, default: Any = None) -> Any:
|
|
81
|
+
self._touch()
|
|
82
|
+
return super().get(key, default)
|
|
83
|
+
|
|
84
|
+
def __contains__(self, key: Any) -> bool:
|
|
85
|
+
self._touch()
|
|
86
|
+
return super().__contains__(key)
|
|
87
|
+
|
|
88
|
+
def __iter__(self) -> Any:
|
|
89
|
+
self._touch()
|
|
90
|
+
return super().__iter__()
|
|
91
|
+
|
|
92
|
+
def keys(self) -> Any:
|
|
93
|
+
self._touch()
|
|
94
|
+
return super().keys()
|
|
95
|
+
|
|
96
|
+
def values(self) -> Any:
|
|
97
|
+
self._touch()
|
|
98
|
+
return super().values()
|
|
99
|
+
|
|
100
|
+
def items(self) -> Any:
|
|
101
|
+
self._touch()
|
|
102
|
+
return super().items()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class CountingMcp:
|
|
106
|
+
"""Records every call and forwards it unchanged.
|
|
107
|
+
|
|
108
|
+
A pass-through, not a stand-in: the real client is called, over the real network, and the real
|
|
109
|
+
payload is returned. Only the record is new. A substituted payload could not surface the type
|
|
110
|
+
and shape problems that make up most of what a live tick is for.
|
|
111
|
+
|
|
112
|
+
Nothing here decides what a scanner may call. The client refuses money- and state-changing
|
|
113
|
+
tools itself, before any transport is opened, and validation inherits that — which is the right
|
|
114
|
+
place for it, since the same boundary protects production.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(self, inner: Any) -> None:
|
|
118
|
+
self._inner = inner
|
|
119
|
+
self.calls: List[Dict[str, Any]] = []
|
|
120
|
+
self._watched: List[Dict[str, Any]] = []
|
|
121
|
+
|
|
122
|
+
def call_tool(self, name: str, args: Optional[dict] = None) -> Any:
|
|
123
|
+
started = time.monotonic()
|
|
124
|
+
try:
|
|
125
|
+
result = self._inner.call_tool(name, args)
|
|
126
|
+
except Exception as exc: # noqa: BLE001
|
|
127
|
+
# Deliberately not BaseException: a tick timeout unwinds through here and must not be
|
|
128
|
+
# recorded as a failed read or, worse, absorbed.
|
|
129
|
+
record = {
|
|
130
|
+
"tool": name,
|
|
131
|
+
"ms": round((time.monotonic() - started) * 1000),
|
|
132
|
+
"ok": False,
|
|
133
|
+
"error_type": type(exc).__name__,
|
|
134
|
+
"error": str(exc)[:500],
|
|
135
|
+
}
|
|
136
|
+
# The client refuses money- and state-changing tools before it opens a transport, and
|
|
137
|
+
# owns the list of which those are. Marked rather than re-derived here: a second
|
|
138
|
+
# opinion about what counts as a write would be a worse one, and would drift.
|
|
139
|
+
if isinstance(exc, PermissionError):
|
|
140
|
+
record["blocked"] = True
|
|
141
|
+
self.calls.append(record)
|
|
142
|
+
raise
|
|
143
|
+
|
|
144
|
+
record: Dict[str, Any] = {
|
|
145
|
+
"tool": name,
|
|
146
|
+
"ms": round((time.monotonic() - started) * 1000),
|
|
147
|
+
"ok": _payload_ok(result),
|
|
148
|
+
}
|
|
149
|
+
if isinstance(result, dict):
|
|
150
|
+
# Inspected before wrapping — reading keys here would otherwise count as the scanner
|
|
151
|
+
# having looked. Only recorded when it might explain something: a tick that read
|
|
152
|
+
# successfully and emitted nothing is the case where the shape is worth seeing.
|
|
153
|
+
record["response_keys"] = sorted(result.keys())[:20]
|
|
154
|
+
if "_cast_dropped" in result:
|
|
155
|
+
record["cast_dropped"] = True
|
|
156
|
+
if not record["ok"]:
|
|
157
|
+
record["error"] = _envelope_error(result)
|
|
158
|
+
self.calls.append(record)
|
|
159
|
+
|
|
160
|
+
if isinstance(result, dict) and record["ok"]:
|
|
161
|
+
watched = _Watched(result)
|
|
162
|
+
self._watched.append({"record": record, "response": watched})
|
|
163
|
+
return watched
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
def settle_unread(self) -> None:
|
|
167
|
+
"""Mark the responses nothing ever looked inside. Called once the tick is over."""
|
|
168
|
+
for entry in self._watched:
|
|
169
|
+
if not entry["response"].touched:
|
|
170
|
+
entry["record"]["unread"] = True
|
|
171
|
+
|
|
172
|
+
def __getattr__(self, name: str) -> Any:
|
|
173
|
+
# Keeps the wrapper transparent for anything beyond the documented surface.
|
|
174
|
+
return getattr(self._inner, name)
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def reads_ok(self) -> int:
|
|
178
|
+
return sum(1 for c in self.calls if c["ok"])
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def reads_failed(self) -> int:
|
|
182
|
+
return len(self.calls) - self.reads_ok
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _payload_ok(payload: Any) -> bool:
|
|
186
|
+
"""Whether a returned payload represents a successful read.
|
|
187
|
+
|
|
188
|
+
Not simply "did not raise". The transport raises on a protocol-level error, but an
|
|
189
|
+
application-level failure comes back as an ordinary response carrying a false success flag —
|
|
190
|
+
counting that as a read would reproduce, one layer down, exactly the confusion this is here to
|
|
191
|
+
remove. Shape-tolerant, because not every response carries the flag.
|
|
192
|
+
"""
|
|
193
|
+
return not (isinstance(payload, dict) and "success" in payload and not payload["success"])
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _envelope_error(payload: Any) -> str:
|
|
197
|
+
if isinstance(payload, dict):
|
|
198
|
+
err = payload.get("error")
|
|
199
|
+
if isinstance(err, dict):
|
|
200
|
+
return str(err.get("message") or err)
|
|
201
|
+
if err is not None:
|
|
202
|
+
return str(err)
|
|
203
|
+
return "the response reported failure"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class _Tracer:
|
|
207
|
+
"""Records exceptions raised inside the scanner's own files, and where it returned from.
|
|
208
|
+
|
|
209
|
+
A trace function sees an exception at the moment it is raised, not when it propagates — so it
|
|
210
|
+
sees the ones the author's handler is about to swallow, which is the entire point.
|
|
211
|
+
|
|
212
|
+
Three things this deliberately does NOT do:
|
|
213
|
+
|
|
214
|
+
**Line tracing.** Returning a handler from `'call'` arms per-line tracing for that frame, which
|
|
215
|
+
costs ~12x on compute-heavy code — enough to make a scanner blow a budget here that it meets in
|
|
216
|
+
production, which would be this command lying about production. `'exception'` and `'return'`
|
|
217
|
+
still fire with lines off.
|
|
218
|
+
|
|
219
|
+
**Module frames.** The tick runs after the entrypoint is imported, but the import itself is
|
|
220
|
+
traced, and `try: import optional_dep / except ImportError` is a completely ordinary module-level
|
|
221
|
+
idiom. Reporting it as a suppressed exception would flag a clean scanner.
|
|
222
|
+
|
|
223
|
+
**Unbounded growth.** A `try/except` inside a large loop raises once per row; the report caps at
|
|
224
|
+
50, but building 100k dicts to throw away is the sort of cost that only shows up on the scanner
|
|
225
|
+
least able to afford it.
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
#: Kept during the tick, not just at report time — see the class docstring.
|
|
229
|
+
LIMIT = 50
|
|
230
|
+
|
|
231
|
+
def __init__(self, root: str) -> None:
|
|
232
|
+
# The separator matters: a bare prefix test makes root `/x/scanner` claim
|
|
233
|
+
# `/x/scanner-utils/evil.py`, which is plausible with sibling per-instance scanner dirs.
|
|
234
|
+
self.root = os.path.abspath(root)
|
|
235
|
+
self._prefix = self.root.rstrip(os.sep) + os.sep
|
|
236
|
+
self.suppressed: List[Dict[str, Any]] = []
|
|
237
|
+
self.dropped = 0
|
|
238
|
+
self.last_return: Optional[Dict[str, Any]] = None
|
|
239
|
+
# Grouping is by object identity, held as a live reference — `id()` alone is unsound here,
|
|
240
|
+
# because CPython reuses an address as soon as the previous exception is collected, so two
|
|
241
|
+
# unrelated failures of the same class routinely share one. Exactly one reference is kept
|
|
242
|
+
# at a time (replaced on the next raise, dropped at the end of the tick), so this cannot
|
|
243
|
+
# pin a chain of tracebacks.
|
|
244
|
+
self._live_exc: Any = None
|
|
245
|
+
self._group = 0
|
|
246
|
+
|
|
247
|
+
def _own(self, frame: Any) -> bool:
|
|
248
|
+
return os.path.abspath(frame.f_code.co_filename).startswith(self._prefix)
|
|
249
|
+
|
|
250
|
+
def __call__(self, frame: Any, event: str, arg: Any) -> Any:
|
|
251
|
+
if event == "call":
|
|
252
|
+
if not self._own(frame) or frame.f_code.co_name == "<module>":
|
|
253
|
+
return None
|
|
254
|
+
# Exceptions and returns still fire; per-line events do not.
|
|
255
|
+
frame.f_trace_lines = False
|
|
256
|
+
return self
|
|
257
|
+
if event == "exception":
|
|
258
|
+
exc_type, exc_value, _tb = arg
|
|
259
|
+
# One exception propagating outward fires once per author frame. Consecutive events for
|
|
260
|
+
# the same object are one failure; a different object starts a new group.
|
|
261
|
+
if exc_value is not self._live_exc:
|
|
262
|
+
self._live_exc = exc_value
|
|
263
|
+
self._group += 1
|
|
264
|
+
if len(self.suppressed) >= self.LIMIT:
|
|
265
|
+
self.dropped += 1
|
|
266
|
+
return self
|
|
267
|
+
self.suppressed.append(
|
|
268
|
+
{
|
|
269
|
+
"file": os.path.relpath(frame.f_code.co_filename, self.root),
|
|
270
|
+
"line": frame.f_lineno,
|
|
271
|
+
"type": exc_type.__name__,
|
|
272
|
+
"message": str(exc_value)[:300],
|
|
273
|
+
"_group": self._group,
|
|
274
|
+
}
|
|
275
|
+
)
|
|
276
|
+
elif event == "return":
|
|
277
|
+
self.last_return = {
|
|
278
|
+
"file": os.path.relpath(frame.f_code.co_filename, self.root),
|
|
279
|
+
"line": frame.f_lineno,
|
|
280
|
+
"function": frame.f_code.co_name,
|
|
281
|
+
}
|
|
282
|
+
return self
|
|
283
|
+
|
|
284
|
+
def claim_ending(self, error_type: str) -> Optional[Dict[str, Any]]:
|
|
285
|
+
"""Take every entry belonging to the exception that ended the tick; return its raise site.
|
|
286
|
+
|
|
287
|
+
Two things this gets right that matching one entry by type does not.
|
|
288
|
+
|
|
289
|
+
The ending exception is not something the scanner "carried on from" — leaving *any* of its
|
|
290
|
+
entries in `suppressed` says the opposite of what happened. It fires once per author frame
|
|
291
|
+
it propagates through, so a helper raising into `scan()` leaves two, and removing one leaves
|
|
292
|
+
the other reading as a caught failure.
|
|
293
|
+
|
|
294
|
+
And of those entries, the FIRST is the raise site; the later ones are call sites further out.
|
|
295
|
+
Reporting a call site as the location points an edit a line above the defect. The tick ends
|
|
296
|
+
at the last exception raised, so the last entry of that type identifies it — and identity,
|
|
297
|
+
not type, gathers the rest of its frames.
|
|
298
|
+
"""
|
|
299
|
+
for entry in reversed(self.suppressed):
|
|
300
|
+
if entry["type"] != error_type:
|
|
301
|
+
continue
|
|
302
|
+
group = entry.get("_group")
|
|
303
|
+
mine = [e for e in self.suppressed if e.get("_group") == group]
|
|
304
|
+
self.suppressed = [e for e in self.suppressed if e.get("_group") != group]
|
|
305
|
+
return mine[0]
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
def release(self) -> None:
|
|
309
|
+
"""Drop the reference held for grouping, so no traceback outlives the tick."""
|
|
310
|
+
self._live_exc = None
|
|
311
|
+
|
|
312
|
+
def report(self) -> List[Dict[str, Any]]:
|
|
313
|
+
"""The suppressed list as it goes on the wire — identity stripped, it is bookkeeping."""
|
|
314
|
+
return [{k: v for k, v in e.items() if k != "_group"} for e in self.suppressed]
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class _CollectingSink:
|
|
318
|
+
"""Takes what the tick produced, and what went wrong, without sending either anywhere.
|
|
319
|
+
|
|
320
|
+
Both halves matter. The loop does not raise a scanner's own failure — it records the tick as
|
|
321
|
+
failed and reports it through the sink's error channel, so a preflight that only watched for an
|
|
322
|
+
exception would call a tick that raised "ok". Having the attribute is what opts into that
|
|
323
|
+
channel, which is why this is an object rather than a function.
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
def __init__(self) -> None:
|
|
327
|
+
self.signals: List[Any] = []
|
|
328
|
+
self.errors: List[Dict[str, Any]] = []
|
|
329
|
+
|
|
330
|
+
def __call__(self, candidates: Any) -> None:
|
|
331
|
+
self.signals.extend(candidates)
|
|
332
|
+
|
|
333
|
+
def post_error(self, error: Dict[str, Any]) -> None:
|
|
334
|
+
self.errors.append(error)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def run_preflight(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
338
|
+
"""Execute one tick and describe it. Never raises for a scanner's own failure."""
|
|
339
|
+
from . import scaffold as scaffold_mod
|
|
340
|
+
|
|
341
|
+
root = config["path"]
|
|
342
|
+
sink = _CollectingSink()
|
|
343
|
+
tracer = _Tracer(root)
|
|
344
|
+
|
|
345
|
+
inner = scaffold_mod._build_senpi_mcp() # noqa: SLF001 — the module's own construction seam
|
|
346
|
+
if inner is None:
|
|
347
|
+
return {
|
|
348
|
+
"ok": False,
|
|
349
|
+
"reason": "no_credentials",
|
|
350
|
+
"detail": "no data-client credentials are configured in this environment",
|
|
351
|
+
}
|
|
352
|
+
counter = CountingMcp(inner)
|
|
353
|
+
|
|
354
|
+
captured_out = io.StringIO()
|
|
355
|
+
captured_err = io.StringIO()
|
|
356
|
+
real_stdout, real_stderr = sys.stdout, sys.stderr
|
|
357
|
+
status = "ok"
|
|
358
|
+
failure: Optional[Dict[str, Any]] = None
|
|
359
|
+
|
|
360
|
+
http_post.PREFLIGHT = True
|
|
361
|
+
sys.stdout = captured_out
|
|
362
|
+
sys.stderr = captured_err
|
|
363
|
+
sys.settrace(tracer)
|
|
364
|
+
try:
|
|
365
|
+
scaffold_mod.run_scaffold(
|
|
366
|
+
root,
|
|
367
|
+
config["entrypoint"],
|
|
368
|
+
interval=float(config["interval_seconds"]),
|
|
369
|
+
timeout_seconds=config.get("timeout_seconds"),
|
|
370
|
+
inputs=config.get("inputs") or {},
|
|
371
|
+
state_path=config.get("state_path"),
|
|
372
|
+
scanner_name=config["scanner_name"],
|
|
373
|
+
wallet=config["wallet"],
|
|
374
|
+
default_signal_validity_seconds=int(config["default_signal_validity_seconds"]),
|
|
375
|
+
state_history_max_count=config.get("state_history_max_count") or 0,
|
|
376
|
+
sink=sink,
|
|
377
|
+
max_ticks=1,
|
|
378
|
+
install_signal_handlers=False,
|
|
379
|
+
senpi_mcp=counter,
|
|
380
|
+
dry_run=True,
|
|
381
|
+
)
|
|
382
|
+
except BaseException as exc: # noqa: BLE001 — a scanner may raise anything at all
|
|
383
|
+
# Only launch failures reach here: the loop handles a failing tick itself.
|
|
384
|
+
status = "error"
|
|
385
|
+
failure = {
|
|
386
|
+
"type": type(exc).__name__,
|
|
387
|
+
"message": str(exc)[:1000],
|
|
388
|
+
"traceback": _bounded("".join(traceback.format_exception(type(exc), exc, exc.__traceback__))),
|
|
389
|
+
}
|
|
390
|
+
finally:
|
|
391
|
+
sys.settrace(None)
|
|
392
|
+
sys.stdout, sys.stderr = real_stdout, real_stderr
|
|
393
|
+
http_post.PREFLIGHT = False
|
|
394
|
+
counter.settle_unread()
|
|
395
|
+
tracer.release()
|
|
396
|
+
|
|
397
|
+
# A tick that failed was reported through the sink rather than raised. The loop's error record
|
|
398
|
+
# carries the type and message but not the location — so it is composed with what the tracer
|
|
399
|
+
# saw. That gives a file and line, which is what an edit needs and a stack string is not.
|
|
400
|
+
if status == "ok" and sink.errors:
|
|
401
|
+
first = sink.errors[0]
|
|
402
|
+
status = "timeout" if first.get("type") == "timeout" else "error"
|
|
403
|
+
error_type = first.get("error_type") or first.get("type") or "tick_error"
|
|
404
|
+
|
|
405
|
+
# The loop reports a failed tick through the sink rather than raising it, and its record
|
|
406
|
+
# carries the type and message but not the location — so it is composed with what the
|
|
407
|
+
# tracer saw. A file and line is what an edit needs; a stack string is not.
|
|
408
|
+
where = tracer.claim_ending(error_type)
|
|
409
|
+
|
|
410
|
+
message = str(first.get("message", ""))[:1000]
|
|
411
|
+
if not message:
|
|
412
|
+
# A malformed return arrives with an empty message and its reason only in stderr, which
|
|
413
|
+
# is the one place a finding cannot quote. Recover what the loop knows.
|
|
414
|
+
message = str(first.get("error") or first.get("reason") or error_type)[:1000]
|
|
415
|
+
|
|
416
|
+
failure = {
|
|
417
|
+
"type": error_type,
|
|
418
|
+
"message": message,
|
|
419
|
+
"where": {"file": where["file"], "line": where["line"]} if where else None,
|
|
420
|
+
"reported_by": "scaffold",
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
"ok": True,
|
|
425
|
+
"status": status,
|
|
426
|
+
"failure": failure,
|
|
427
|
+
"reads": counter.calls,
|
|
428
|
+
"reads_ok": counter.reads_ok,
|
|
429
|
+
"reads_failed": counter.reads_failed,
|
|
430
|
+
"signals": _to_wire(sink.signals, config),
|
|
431
|
+
"suppressed": tracer.report(),
|
|
432
|
+
"suppressed_dropped": tracer.dropped,
|
|
433
|
+
"last_return": tracer.last_return,
|
|
434
|
+
"stdout": _bounded(captured_out.getvalue()),
|
|
435
|
+
"stderr": _bounded(captured_err.getvalue()),
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _to_wire(candidates: List[Any], config: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
440
|
+
"""Build each candidate into the shape intake would actually receive.
|
|
441
|
+
|
|
442
|
+
Uses the runtime's own envelope builder rather than describing what it would do. Judging a
|
|
443
|
+
candidate on its pre-envelope shape would answer a different question from "would this be
|
|
444
|
+
accepted", which is the only question worth asking here.
|
|
445
|
+
"""
|
|
446
|
+
produced_at = int(time.time() * 1000)
|
|
447
|
+
out: List[Dict[str, Any]] = []
|
|
448
|
+
for candidate in candidates:
|
|
449
|
+
rejections: List[Dict[str, Any]] = []
|
|
450
|
+
signal = candidate_to_signal(
|
|
451
|
+
candidate,
|
|
452
|
+
produced_at,
|
|
453
|
+
signal_data_schema=config.get("signal_data_schema"),
|
|
454
|
+
default_signal_validity_seconds=int(config["default_signal_validity_seconds"]),
|
|
455
|
+
on_reject=rejections.append,
|
|
456
|
+
)
|
|
457
|
+
out.append(
|
|
458
|
+
{
|
|
459
|
+
"emitted": candidate if isinstance(candidate, dict) else repr(candidate),
|
|
460
|
+
"wire": signal,
|
|
461
|
+
"rejected_by_scaffold": rejections,
|
|
462
|
+
}
|
|
463
|
+
)
|
|
464
|
+
return out
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _finite(value: Any) -> Any:
|
|
468
|
+
"""Replace non-finite floats with their text, recursively.
|
|
469
|
+
|
|
470
|
+
A 0/0 in scoring produces NaN, and `json.dumps` writes it as a bare `NaN` token that no strict
|
|
471
|
+
parser accepts — so the tick that surfaced the bug would be reported as the harness breaking.
|
|
472
|
+
Keeping the value as text says what happened instead of losing the document over it.
|
|
473
|
+
"""
|
|
474
|
+
if isinstance(value, float):
|
|
475
|
+
return value if math.isfinite(value) else repr(value)
|
|
476
|
+
if isinstance(value, dict):
|
|
477
|
+
return {k: _finite(v) for k, v in value.items()}
|
|
478
|
+
if isinstance(value, (list, tuple)):
|
|
479
|
+
return [_finite(v) for v in value]
|
|
480
|
+
return value
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _serialize(result: Dict[str, Any]) -> str:
|
|
484
|
+
"""One document, always parseable.
|
|
485
|
+
|
|
486
|
+
`allow_nan=False` turns a non-finite float into a raised error rather than a token that only
|
|
487
|
+
Python reads back. Anything still unserialisable after sanitising — a circular candidate, an
|
|
488
|
+
exotic object — must not cost the whole report, so the signals are dropped to text and the rest
|
|
489
|
+
survives.
|
|
490
|
+
"""
|
|
491
|
+
try:
|
|
492
|
+
return json.dumps(_finite(result), allow_nan=False, default=str)
|
|
493
|
+
except (ValueError, TypeError, RecursionError) as exc:
|
|
494
|
+
salvaged = dict(result)
|
|
495
|
+
salvaged["signals"] = [{"emitted": "<unserialisable>", "wire": None, "rejected_by_scaffold": []}]
|
|
496
|
+
salvaged["serialization_error"] = str(exc)[:300]
|
|
497
|
+
try:
|
|
498
|
+
return json.dumps(_finite(salvaged), allow_nan=False, default=str)
|
|
499
|
+
except Exception: # noqa: BLE001 — a report is worth more than the detail in it
|
|
500
|
+
return json.dumps({"ok": True, "status": result.get("status", "error"),
|
|
501
|
+
"serialization_error": str(exc)[:300]})
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _emit(result: Dict[str, Any], config: Dict[str, Any]) -> None:
|
|
505
|
+
"""Deliver the result where author code cannot reach it.
|
|
506
|
+
|
|
507
|
+
The tick runs in this process and shares its stdout. Swapping `sys.stdout` catches `print()`,
|
|
508
|
+
which is what authors do — but `os.write(1, ...)`, a subprocess, a C extension, or a thread
|
|
509
|
+
printing after the restore all write to the same descriptor, and the reader parses the whole
|
|
510
|
+
buffer. That fails closed (never a false pass) but reports a scanner's debug line as a broken
|
|
511
|
+
environment, which points at the wrong person entirely.
|
|
512
|
+
|
|
513
|
+
The caller names a file it owns; writing there makes the guarantee structural rather than
|
|
514
|
+
conventional. Stdout stays available as evidence, and is never parsed.
|
|
515
|
+
"""
|
|
516
|
+
document = _serialize(result)
|
|
517
|
+
path = config.get("result_path")
|
|
518
|
+
if path:
|
|
519
|
+
tmp = "{}.tmp".format(path)
|
|
520
|
+
with open(tmp, "w", encoding="utf-8") as handle:
|
|
521
|
+
handle.write(document)
|
|
522
|
+
os.replace(tmp, path) # the reader never sees a half-written document
|
|
523
|
+
return
|
|
524
|
+
# No path given: the caller is reading stdout (older harness, or a direct invocation).
|
|
525
|
+
sys.stdout.write(document)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
529
|
+
argv = list(sys.argv if argv is None else argv)
|
|
530
|
+
if len(argv) < 2:
|
|
531
|
+
sys.stderr.write("usage: python3 -m scaffold.preflight <launch-config.json>\n")
|
|
532
|
+
return 2
|
|
533
|
+
try:
|
|
534
|
+
with open(argv[1], "r", encoding="utf-8") as handle:
|
|
535
|
+
config = json.load(handle)
|
|
536
|
+
except (OSError, json.JSONDecodeError, UnicodeDecodeError) as exc:
|
|
537
|
+
sys.stdout.write(json.dumps({"ok": False, "reason": "bad_config", "detail": str(exc)}))
|
|
538
|
+
return 0
|
|
539
|
+
|
|
540
|
+
_emit(run_preflight(config), config)
|
|
541
|
+
return 0
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
if __name__ == "__main__":
|
|
545
|
+
sys.exit(main())
|
|
@@ -88,6 +88,8 @@ def run_scaffold(
|
|
|
88
88
|
sink: Optional[Callable] = None,
|
|
89
89
|
max_ticks: Optional[int] = None,
|
|
90
90
|
install_signal_handlers: bool = True,
|
|
91
|
+
senpi_mcp: Optional[Any] = None,
|
|
92
|
+
dry_run: bool = False,
|
|
91
93
|
) -> int:
|
|
92
94
|
"""Load the author's entrypoint and run its scan() on an interval loop (v2).
|
|
93
95
|
|
|
@@ -108,7 +110,11 @@ def run_scaffold(
|
|
|
108
110
|
Bound on the persisted state series. 0/unset = history disabled
|
|
109
111
|
(ctx.state.append raises).
|
|
110
112
|
sink: Callable(candidates) invoked after each clean tick. Default no-op.
|
|
111
|
-
max_ticks: Stop after this many ticks
|
|
113
|
+
max_ticks: Stop after this many ticks. None = unbounded. Used by tests and by a
|
|
114
|
+
validation run, which forces exactly one tick.
|
|
115
|
+
senpi_mcp: Data client to hand to scan(). Default: built from the launch environment.
|
|
116
|
+
Injected by a validation run so reads can be observed.
|
|
117
|
+
dry_run: Sets ctx.dry_run. True only during validation.
|
|
112
118
|
install_signal_handlers:
|
|
113
119
|
Install SIGTERM/SIGINT graceful-stop handlers. False for tests.
|
|
114
120
|
|
|
@@ -155,7 +161,9 @@ def run_scaffold(
|
|
|
155
161
|
# SENPI_API_KEY / SENPI_MCP_URL). With no creds the slot stays None and launch
|
|
156
162
|
# must NOT crash — the client is loud at FIRST USE, not at construction. Import
|
|
157
163
|
# is deferred so a stub patched onto scaffold.senpi_mcp in tests is honored.
|
|
158
|
-
|
|
164
|
+
# An injected client wins, so a validation run can observe what the tick reads without
|
|
165
|
+
# changing how it reads it. Absent one this behaves exactly as before.
|
|
166
|
+
senpi_mcp = senpi_mcp if senpi_mcp is not None else _build_senpi_mcp()
|
|
159
167
|
|
|
160
168
|
ctx = ScanContext(
|
|
161
169
|
state=state,
|
|
@@ -163,6 +171,7 @@ def run_scaffold(
|
|
|
163
171
|
scanner_name=scanner_name,
|
|
164
172
|
interval_seconds=interval_seconds,
|
|
165
173
|
senpi_mcp=senpi_mcp,
|
|
174
|
+
dry_run=dry_run,
|
|
166
175
|
)
|
|
167
176
|
|
|
168
177
|
# ---------- Lifecycle setup ----------
|
|
@@ -209,13 +218,17 @@ def run_scaffold(
|
|
|
209
218
|
# malformed (a tuple — the old v1 shape — included).
|
|
210
219
|
if not isinstance(result, list):
|
|
211
220
|
tick_status = "malformed"
|
|
221
|
+
malformed_reason = (
|
|
222
|
+
f"scan() returned {type(result).__name__!r}; expected a "
|
|
223
|
+
f"plain list[dict] (the v1 (candidates, state) tuple is gone)"
|
|
224
|
+
)
|
|
225
|
+
# Carried on the error record, not only in this log line: the sink is what
|
|
226
|
+
# a caller reads, and "malformed" without the reason is not actionable.
|
|
227
|
+
tick_error = {"type": "malformed", "message": malformed_reason}
|
|
212
228
|
log_event(
|
|
213
229
|
"scaffold_tick_malformed",
|
|
214
230
|
tick=tick_count,
|
|
215
|
-
reason=
|
|
216
|
-
f"scan() returned {type(result).__name__!r}; expected a "
|
|
217
|
-
f"plain list[dict] (the v1 (candidates, state) tuple is gone)"
|
|
218
|
-
),
|
|
231
|
+
reason=malformed_reason,
|
|
219
232
|
)
|
|
220
233
|
else:
|
|
221
234
|
candidates = result
|
|
@@ -41,9 +41,9 @@ export declare function analyzeAssetBias(candles: Candle[], emaFast: number, ema
|
|
|
41
41
|
* market intelligence.
|
|
42
42
|
*/
|
|
43
43
|
export declare function marketRegimeScanner(): import("../index.js").Scanner<{
|
|
44
|
-
assets?: string[] | undefined;
|
|
45
44
|
btcWeight?: number | undefined;
|
|
46
45
|
ethWeight?: number | undefined;
|
|
46
|
+
assets?: string[] | undefined;
|
|
47
47
|
emaFast?: number | undefined;
|
|
48
48
|
emaSlow?: number | undefined;
|
|
49
49
|
neutralThreshold?: number | undefined;
|
|
@@ -177,4 +177,10 @@ export interface DslActivePositionsSource {
|
|
|
177
177
|
* collection. Returns a disposer the runtime must call on stop to drop its observation.
|
|
178
178
|
*/
|
|
179
179
|
export declare function registerDslActivePositionsGauge(source: DslActivePositionsSource): () => void;
|
|
180
|
+
import type { ValidateResult } from "../validate/run.js";
|
|
181
|
+
import type { VerifyReason } from "../validate/proof.js";
|
|
182
|
+
/** One validate run and every finding it produced. Duration in milliseconds. */
|
|
183
|
+
export declare function recordValidateRun(result: ValidateResult, durationMs: number): void;
|
|
184
|
+
/** One proof verification, by outcome alone. */
|
|
185
|
+
export declare function recordProofVerification(outcome: "ok" | VerifyReason): void;
|
|
180
186
|
//# sourceMappingURL=metrics.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/telemetry/metrics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAKL,KAAK,KAAK,EAIX,MAAM,oBAAoB,CAAC;AAK5B,sFAAsF;AACtF,wBAAgB,QAAQ,IAAI,KAAK,CAEhC;
|
|
1
|
+
{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/telemetry/metrics.ts"],"names":[],"mappings":"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"}
|