@senpi/runtime 3.0.59 → 3.0.63
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/README.md +73 -2
- package/dist/actions/decision-engine.d.ts +0 -2
- package/dist/actions/decision-engine.d.ts.map +1 -1
- package/dist/actions/decision-engine.js +1 -79
- package/dist/actions/decision-engine.js.map +1 -1
- package/dist/cli/deploy-render.d.ts +91 -0
- package/dist/cli/deploy-render.d.ts.map +1 -0
- package/dist/cli/deploy-render.js +216 -0
- package/dist/cli/deploy-render.js.map +1 -0
- package/dist/cli/guide-commands.d.ts.map +1 -1
- package/dist/cli/guide-commands.js +78 -6
- package/dist/cli/guide-commands.js.map +1 -1
- package/dist/cli/senpi-commands.d.ts +6 -1
- package/dist/cli/senpi-commands.d.ts.map +1 -1
- package/dist/cli/senpi-commands.js +244 -8
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/cli/validate-command.d.ts.map +1 -1
- package/dist/cli/validate-command.js +32 -1
- package/dist/cli/validate-command.js.map +1 -1
- package/dist/deploy/deadlines.d.ts +47 -0
- package/dist/deploy/deadlines.d.ts.map +1 -0
- package/dist/deploy/deadlines.js +56 -0
- package/dist/deploy/deadlines.js.map +1 -0
- package/dist/deploy/error-text.d.ts +25 -0
- package/dist/deploy/error-text.d.ts.map +1 -0
- package/dist/deploy/error-text.js +42 -0
- package/dist/deploy/error-text.js.map +1 -0
- package/dist/deploy/funding.d.ts +87 -0
- package/dist/deploy/funding.d.ts.map +1 -0
- package/dist/deploy/funding.js +229 -0
- package/dist/deploy/funding.js.map +1 -0
- package/dist/deploy/install-gate-runner.d.ts +12 -0
- package/dist/deploy/install-gate-runner.d.ts.map +1 -0
- package/dist/deploy/install-gate-runner.js +17 -0
- package/dist/deploy/install-gate-runner.js.map +1 -0
- package/dist/deploy/job.d.ts +199 -0
- package/dist/deploy/job.d.ts.map +1 -0
- package/dist/deploy/job.js +463 -0
- package/dist/deploy/job.js.map +1 -0
- package/dist/deploy/min-budget.d.ts +68 -0
- package/dist/deploy/min-budget.d.ts.map +1 -0
- package/dist/deploy/min-budget.js +297 -0
- package/dist/deploy/min-budget.js.map +1 -0
- package/dist/deploy/orchestrator.d.ts +305 -0
- package/dist/deploy/orchestrator.d.ts.map +1 -0
- package/dist/deploy/orchestrator.js +2910 -0
- package/dist/deploy/orchestrator.js.map +1 -0
- package/dist/deploy/package.d.ts +130 -0
- package/dist/deploy/package.d.ts.map +1 -0
- package/dist/deploy/package.js +351 -0
- package/dist/deploy/package.js.map +1 -0
- package/dist/deploy/proof-gate.d.ts +82 -0
- package/dist/deploy/proof-gate.d.ts.map +1 -0
- package/dist/deploy/proof-gate.js +269 -0
- package/dist/deploy/proof-gate.js.map +1 -0
- package/dist/deploy/python-float.d.ts +34 -0
- package/dist/deploy/python-float.d.ts.map +1 -0
- package/dist/deploy/python-float.js +46 -0
- package/dist/deploy/python-float.js.map +1 -0
- package/dist/deploy/recheck-commands.d.ts +50 -0
- package/dist/deploy/recheck-commands.d.ts.map +1 -0
- package/dist/deploy/recheck-commands.js +59 -0
- package/dist/deploy/recheck-commands.js.map +1 -0
- package/dist/deploy/register.d.ts +99 -0
- package/dist/deploy/register.d.ts.map +1 -0
- package/dist/deploy/register.js +689 -0
- package/dist/deploy/register.js.map +1 -0
- package/dist/deploy/taxonomy.d.ts +24 -0
- package/dist/deploy/taxonomy.d.ts.map +1 -0
- package/dist/deploy/taxonomy.js +31 -0
- package/dist/deploy/taxonomy.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +244 -132
- package/dist/index.js.map +1 -1
- package/dist/runtime/strategy-registry.d.ts +8 -0
- package/dist/runtime/strategy-registry.d.ts.map +1 -1
- package/dist/runtime/strategy-registry.js +14 -1
- package/dist/runtime/strategy-registry.js.map +1 -1
- package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
- package/dist/runtime-api/intake/intake-mount.js +7 -4
- package/dist/runtime-api/intake/intake-mount.js.map +1 -1
- package/dist/scanners/implementations/market-regime.d.ts +1 -1
- package/dist/senpi/client.d.ts +58 -4
- package/dist/senpi/client.d.ts.map +1 -1
- package/dist/senpi/client.js +192 -15
- package/dist/senpi/client.js.map +1 -1
- package/dist/senpi/strategy-status.d.ts +32 -0
- package/dist/senpi/strategy-status.d.ts.map +1 -0
- package/dist/senpi/strategy-status.js +70 -0
- package/dist/senpi/strategy-status.js.map +1 -0
- package/dist/senpi/types.d.ts +47 -3
- package/dist/senpi/types.d.ts.map +1 -1
- package/dist/skills-manager/manifest.js +1 -1
- package/dist/skills-manager/skills-manifest.json +1 -1
- package/dist/telemetry/metrics.d.ts +23 -0
- package/dist/telemetry/metrics.d.ts.map +1 -1
- package/dist/telemetry/metrics.js +65 -0
- package/dist/telemetry/metrics.js.map +1 -1
- package/dist/types/strategy.d.ts +9 -1
- package/dist/types/strategy.d.ts.map +1 -1
- package/dist/universe/package-universe.d.ts +97 -0
- package/dist/universe/package-universe.d.ts.map +1 -0
- package/dist/universe/package-universe.js +275 -0
- package/dist/universe/package-universe.js.map +1 -0
- package/dist/validate/codes.d.ts +20 -0
- package/dist/validate/codes.d.ts.map +1 -1
- package/dist/validate/codes.js +36 -0
- package/dist/validate/codes.js.map +1 -1
- package/dist/validate/import-stage.js +5 -5
- package/dist/validate/import-stage.js.map +1 -1
- package/dist/validate/index.d.ts +1 -1
- package/dist/validate/index.d.ts.map +1 -1
- package/dist/validate/index.js.map +1 -1
- package/dist/validate/install-gate.d.ts.map +1 -1
- package/dist/validate/install-gate.js +10 -2
- package/dist/validate/install-gate.js.map +1 -1
- package/dist/validate/live-instruments.d.ts +44 -0
- package/dist/validate/live-instruments.d.ts.map +1 -0
- package/dist/validate/live-instruments.js +71 -0
- package/dist/validate/live-instruments.js.map +1 -0
- package/dist/validate/live-stage.js +5 -5
- package/dist/validate/live-stage.js.map +1 -1
- package/dist/validate/proof.d.ts +11 -1
- package/dist/validate/proof.d.ts.map +1 -1
- package/dist/validate/proof.js +7 -6
- package/dist/validate/proof.js.map +1 -1
- package/dist/validate/recipe-checks.d.ts.map +1 -1
- package/dist/validate/recipe-checks.js +6 -4
- package/dist/validate/recipe-checks.js.map +1 -1
- package/dist/validate/run.d.ts +5 -0
- package/dist/validate/run.d.ts.map +1 -1
- package/dist/validate/run.js +10 -1
- package/dist/validate/run.js.map +1 -1
- package/dist/validate/target.d.ts +41 -0
- package/dist/validate/target.d.ts.map +1 -1
- package/dist/validate/target.js +192 -16
- package/dist/validate/target.js.map +1 -1
- package/dist/validate/universe-stage.d.ts +71 -0
- package/dist/validate/universe-stage.d.ts.map +1 -0
- package/dist/validate/universe-stage.js +183 -0
- package/dist/validate/universe-stage.js.map +1 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/dist/runtime/managed-wallet-guard.d.ts +0 -47
- package/dist/runtime/managed-wallet-guard.d.ts.map +0 -1
- package/dist/runtime/managed-wallet-guard.js +0 -106
- package/dist/runtime/managed-wallet-guard.js.map +0 -1
- package/dist/runtime/protection-nudge.d.ts +0 -67
- package/dist/runtime/protection-nudge.d.ts.map +0 -1
- package/dist/runtime/protection-nudge.js +0 -244
- package/dist/runtime/protection-nudge.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
import { readFileSync } from "node:fs";
|
|
15
15
|
import { join, dirname } from "node:path";
|
|
16
16
|
import { SenpiClient } from "./senpi/client.js";
|
|
17
|
+
import { buildWalletStatusHint } from "./senpi/strategy-status.js";
|
|
18
|
+
import { DeployJobController } from "./deploy/job.js";
|
|
19
|
+
import { registerDeployGatewayMethods } from "./deploy/register.js";
|
|
17
20
|
import { AuditQueryError } from "./runtime/audit-query.types.js";
|
|
18
21
|
import { buildRuntimeStateDir, archiveStateDir } from "./runtime/state-layout.js";
|
|
19
22
|
import { registerSenpiCli } from "./cli/senpi-commands.js";
|
|
@@ -35,8 +38,6 @@ import { resolvePackageName } from "./runtime/auto-update/resolve-package-name.j
|
|
|
35
38
|
import { runOpenClawPluginsUpdate } from "./runtime/auto-update/run-update-command.js";
|
|
36
39
|
import { registerSpanHooks } from "./telemetry/spans/index.js";
|
|
37
40
|
import { registerSessionContextSeedHook } from "./prompt/session-context-seed-hook.js";
|
|
38
|
-
import { createManagedWalletGuard } from "./runtime/managed-wallet-guard.js";
|
|
39
|
-
import { createProtectionGate, createProtectionNudge, createProtectionSummaryCapture, } from "./runtime/protection-nudge.js";
|
|
40
41
|
import { initDefaultPiiRedactor } from "./telemetry/spans/redact-pii.js";
|
|
41
42
|
import { initOtelSdk, shutdownOtelSdk } from "./telemetry/otel-sdk.js";
|
|
42
43
|
import { getLogLevel, getLogLevelSource, parseLogLevel, setLogLevel, } from "./utils/logger.js";
|
|
@@ -245,49 +246,6 @@ const senpiRuntimePlugin = {
|
|
|
245
246
|
});
|
|
246
247
|
}
|
|
247
248
|
const runtimeHandles = new Map();
|
|
248
|
-
// Position-tool guards on ONE before_tool_call registration, composed in
|
|
249
|
-
// order. The managed-wallet hard block runs first: it vetoes agent-issued
|
|
250
|
-
// create_position calls into wallets an installed runtime manages (checking
|
|
251
|
-
// live handles — wallet parsed from YAML, covering registry entries with
|
|
252
|
-
// missing/stale wallet fields — then the registry read from the resolved
|
|
253
|
-
// `baseStateDir`, not the raw openclaw stateDir). A wallet-guard block
|
|
254
|
-
// short-circuits; only if it allows does the protection gate run, which on
|
|
255
|
-
// a manual (non-runtime) wallet vetoes an unprotected create_position and a
|
|
256
|
-
// stop-loss-cancel edit unless the agent opted out with protection:"none".
|
|
257
|
-
// The gate stashes each allowed create_position's protection summary into a
|
|
258
|
-
// shared capture that the tool_result_persist nudge reads back — the nudge
|
|
259
|
-
// event carries no params. `on`'s typing is void-only, so the
|
|
260
|
-
// veto/message-returning handlers are cast at this boundary.
|
|
261
|
-
try {
|
|
262
|
-
const managedWalletGuard = createManagedWalletGuard({
|
|
263
|
-
resolveStateDir: () => baseStateDir,
|
|
264
|
-
getLiveWallets: () => Array.from(runtimeHandles, ([id, handle]) => ({ id, wallet: handle.wallet })),
|
|
265
|
-
logger: createLogger("[senpi-core][managed-wallet-guard]"),
|
|
266
|
-
});
|
|
267
|
-
const protectionCapture = createProtectionSummaryCapture();
|
|
268
|
-
const protectionGate = createProtectionGate({
|
|
269
|
-
logger: createLogger("[senpi-core][protection-gate]"),
|
|
270
|
-
capture: protectionCapture,
|
|
271
|
-
});
|
|
272
|
-
const beforeToolCall = async (event) => {
|
|
273
|
-
const walletVerdict = await managedWalletGuard(event);
|
|
274
|
-
if (walletVerdict)
|
|
275
|
-
return walletVerdict;
|
|
276
|
-
return protectionGate(event);
|
|
277
|
-
};
|
|
278
|
-
api.on("before_tool_call", beforeToolCall);
|
|
279
|
-
const protectionNudge = createProtectionNudge({
|
|
280
|
-
logger: createLogger("[senpi-core][protection-nudge]"),
|
|
281
|
-
capture: protectionCapture,
|
|
282
|
-
});
|
|
283
|
-
api.on("tool_result_persist", protectionNudge);
|
|
284
|
-
pluginLogger.info("[senpi-runtime] position-tool guards registered (before_tool_call: managed-wallet + protection gate; tool_result_persist: protection nudge)");
|
|
285
|
-
}
|
|
286
|
-
catch (err) {
|
|
287
|
-
pluginLogger.warn("[senpi-runtime] failed to register position-tool guards", {
|
|
288
|
-
err: err instanceof Error ? err.message : String(err),
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
249
|
let apiHandle;
|
|
292
250
|
// Supervised scanner state — module-scoped so stop() can reach them.
|
|
293
251
|
// Created eagerly at register() time (not deferred to start()) so that
|
|
@@ -427,9 +385,23 @@ const senpiRuntimePlugin = {
|
|
|
427
385
|
restartPrefixes: [`plugins.entries.${api.id}.config.autoUpdate.lastRestartTrigger`],
|
|
428
386
|
});
|
|
429
387
|
}
|
|
388
|
+
// ── senpi deploy (detached job) ───────────────────────────────────────────
|
|
389
|
+
// Constructed here, ahead of registerService, because the service `start` hook below runs its
|
|
390
|
+
// boot scan: declaring it after would leave that reference in the temporal dead zone for any
|
|
391
|
+
// future refactor that starts the service earlier. The rest of the deploy wiring lives in
|
|
392
|
+
// src/deploy/register.ts behind an explicit context; this closure contributes collaborators only.
|
|
393
|
+
const deployController = new DeployJobController({
|
|
394
|
+
deploysDir: join(baseStateDir, "deploys"),
|
|
395
|
+
now: () => Date.now(),
|
|
396
|
+
log: (line) => api.logger.info(`[senpi-runtime] ${line}`),
|
|
397
|
+
});
|
|
430
398
|
api.registerService({
|
|
431
399
|
id: "senpi-runtime",
|
|
432
400
|
async start() {
|
|
401
|
+
// Deploy-journal janitor: stamp anything a restart left unterminated as `interrupted`.
|
|
402
|
+
// It never re-enters a deploy state machine — discovery is pull-based via
|
|
403
|
+
// `senpi deploy status`.
|
|
404
|
+
await deployController.scanOnBoot();
|
|
433
405
|
await autoUpdateCoordinator.start();
|
|
434
406
|
// SENPI_RUNTIME_API_DISABLED=true also disables the skills coordinator.
|
|
435
407
|
// Skills bootstrap performs a real `git clone` synchronously during
|
|
@@ -741,13 +713,15 @@ const senpiRuntimePlugin = {
|
|
|
741
713
|
pluginLogger.info("[senpi-runtime] CLI registrar callback invoked — attaching senpi subcommands to program");
|
|
742
714
|
registerSenpiCli(ctx.program);
|
|
743
715
|
}, { commands: ["senpi"] });
|
|
744
|
-
/**
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
716
|
+
/**
|
|
717
|
+
* The whole `senpi.installRuntime` behaviour, callable without a gateway ctx so
|
|
718
|
+
* `senpi deploy` installs through the SAME gates (wallet ownership, one-runtime-per-wallet,
|
|
719
|
+
* scanner-path resolution) instead of a parallel path that could drift.
|
|
720
|
+
*/
|
|
721
|
+
async function installRuntimeCore(params) {
|
|
748
722
|
const toError = (message) => ({ code: "INVALID_REQUEST", message });
|
|
749
723
|
const toUnavailable = (message) => ({ code: "UNAVAILABLE", message });
|
|
750
|
-
const { readRegistry, writeRegistry, validateEntry, hasId, deriveRuntimeId, sanitizeId } = await import("./runtime/strategy-registry.js");
|
|
724
|
+
const { readRegistry, writeRegistry, validateEntry, hasId, findByWallet, deriveRuntimeId, sanitizeId } = await import("./runtime/strategy-registry.js");
|
|
751
725
|
const id = typeof params?.id === "string" ? params.id : undefined;
|
|
752
726
|
const runtimeYamlPath = typeof params?.runtimeYamlPath === "string" ? params.runtimeYamlPath : undefined;
|
|
753
727
|
const runtimeYamlContent = typeof params?.runtimeYamlContent === "string" ? params.runtimeYamlContent : undefined;
|
|
@@ -760,12 +734,10 @@ const senpiRuntimePlugin = {
|
|
|
760
734
|
const hasPath = runtimeYamlPath !== undefined && runtimeYamlPath.length > 0;
|
|
761
735
|
const hasContent = runtimeYamlContent !== undefined;
|
|
762
736
|
if (!hasPath && !hasContent) {
|
|
763
|
-
|
|
764
|
-
return;
|
|
737
|
+
return { ok: false, error: toError("Please provide your runtime YAML: use the create command with --path <file> or --content \"<yaml text>\".") };
|
|
765
738
|
}
|
|
766
739
|
if (hasPath && hasContent) {
|
|
767
|
-
|
|
768
|
-
return;
|
|
740
|
+
return { ok: false, error: toError("Please provide either a file path (--path) or YAML content (--content), not both.") };
|
|
769
741
|
}
|
|
770
742
|
const pathOrContent = runtimeYamlPath ?? runtimeYamlContent;
|
|
771
743
|
// Parse YAML first to extract name, version, and wallet for ID derivation.
|
|
@@ -784,10 +756,15 @@ const senpiRuntimePlugin = {
|
|
|
784
756
|
}
|
|
785
757
|
catch (err) {
|
|
786
758
|
const msg = err instanceof Error ? err.message : String(err);
|
|
787
|
-
|
|
788
|
-
return;
|
|
759
|
+
return { ok: false, error: toUnavailable(`Could not start the runtime. This is usually due to invalid or incomplete YAML.\n\n${msg}`) };
|
|
789
760
|
}
|
|
790
761
|
// ── install gate ────────────────────────────────────────────────────
|
|
762
|
+
// The cwd-resolution trap (M226926 inline -c, M279357 --path) is closed by this gate's
|
|
763
|
+
// E_VALIDATE_UNRESOLVABLE_SCANNER_PATH: a relative scanner path with no source dir would
|
|
764
|
+
// silently resolve against the process cwd (intake-mount.ts selectScanners baseDir=undefined)
|
|
765
|
+
// and mount a dead scanner. A second check for the same condition used to sit immediately
|
|
766
|
+
// above this one; two gates on one boundary meant the message a caller saw depended on which
|
|
767
|
+
// ran first, so it was folded into the finding's own `fix`.
|
|
791
768
|
// The schema accepts recipes that cannot trade: an entry action with no
|
|
792
769
|
// reachable margin source declines every signal it will ever see, one with
|
|
793
770
|
// no guardrails trades with no daily-loss ceiling, and a declared scanner
|
|
@@ -797,9 +774,12 @@ const senpiRuntimePlugin = {
|
|
|
797
774
|
// running runtime is untouched. Deliberately NOT done at boot: that loop
|
|
798
775
|
// stops every started runtime on a throw, so refusing there would take the
|
|
799
776
|
// box down over one bad recipe.
|
|
800
|
-
// Decide inside the guard, act outside it.
|
|
801
|
-
//
|
|
802
|
-
//
|
|
777
|
+
// Decide inside the guard, act outside it. The catch below exists to keep a
|
|
778
|
+
// defect in the CHECKS from failing an install, and it cannot tell one of
|
|
779
|
+
// those from a fault raised while acting on a refusal it already decided.
|
|
780
|
+
// Returning after the guard keeps the two apart, so no future statement
|
|
781
|
+
// added on the refusal path can be swallowed into "the gate could not run"
|
|
782
|
+
// and let the install proceed.
|
|
803
783
|
let gateRefusal;
|
|
804
784
|
try {
|
|
805
785
|
const { collectRecipeFindings } = await import("./validate/recipe.js");
|
|
@@ -829,6 +809,13 @@ const senpiRuntimePlugin = {
|
|
|
829
809
|
// themselves — log it and let the install proceed rather than blocking a
|
|
830
810
|
// good recipe on our own bug.
|
|
831
811
|
//
|
|
812
|
+
// This side fails OPEN and `runDeploy`'s pre-money install gate
|
|
813
|
+
// (deploy/orchestrator.ts, the `deploy.install_gate` span) fails CLOSED on
|
|
814
|
+
// the SAME fault, deliberately: refusing a good recipe HERE blocks a
|
|
815
|
+
// working install, while refusing a deploy costs only a re-run, before any
|
|
816
|
+
// money has moved. Two boundaries, one predicate — neither branch may be
|
|
817
|
+
// flipped on its own.
|
|
818
|
+
//
|
|
832
819
|
// But say so on the ledger, not only in a log line. Proceeding ungated is
|
|
833
820
|
// silent by construction: the install succeeds, nothing looks wrong, and
|
|
834
821
|
// protection has reverted to what it was before the gate existed — for
|
|
@@ -845,25 +832,67 @@ const senpiRuntimePlugin = {
|
|
|
845
832
|
pluginLogger.warn(appendCause("[senpi-runtime] install gate could not run — proceeding without it", gateErr));
|
|
846
833
|
}
|
|
847
834
|
if (gateRefusal !== undefined) {
|
|
848
|
-
|
|
849
|
-
return;
|
|
835
|
+
return { ok: false, error: toError(gateRefusal) };
|
|
850
836
|
}
|
|
851
837
|
const registry = await readRegistry(baseStateDir);
|
|
852
838
|
const resolvedId = id && id.length > 0
|
|
853
839
|
? (sanitizeId(id) || "runtime")
|
|
854
840
|
: deriveRuntimeId(runtimeName, wallet);
|
|
855
841
|
if (hasId(registry, resolvedId)) {
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
842
|
+
return { ok: false, error: toError(`A runtime with id "${resolvedId}" already exists. Use a different id (e.g. --runtime-id another-name) or remove the existing one first: openclaw senpi runtime delete ${resolvedId}`) };
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* One runtime per wallet address — asked of PERSISTED state, not only of this process.
|
|
846
|
+
*
|
|
847
|
+
* Two engines on one wallet is not a tidiness problem: each keeps its own high-water state,
|
|
848
|
+
* so they disagree about what is protected and stop each other's positions out on different
|
|
849
|
+
* floors, against one margin account.
|
|
850
|
+
*
|
|
851
|
+
* The live-handle half alone could not carry that guarantee, because it is blind in exactly
|
|
852
|
+
* the window where it is needed. A runtime that is INSTALLED but not running right now — its
|
|
853
|
+
* boot failed, or the box came up and it never started — has a registry row and no handle,
|
|
854
|
+
* so the loop below sees nothing and lets a second row land on the same wallet. Nothing
|
|
855
|
+
* reconciles the two afterwards: the registry has no uniqueness on `wallet`, and the boot
|
|
856
|
+
* loop starts every row it can resolve. The restart that fixes the first runtime is the one
|
|
857
|
+
* that brings up both.
|
|
858
|
+
*
|
|
859
|
+
* Both halves are kept because they answer different questions and the answers differ:
|
|
860
|
+
* a live handle proves a runtime is trading NOW (and names the stop-it-first step), while a
|
|
861
|
+
* registry row proves one is installed and will be started at the next boot. Case-folded on
|
|
862
|
+
* both, because the registry stores `strategy.wallet` verbatim from the YAML and a
|
|
863
|
+
* checksummed address is the same margin account as its lowercase spelling.
|
|
864
|
+
*/
|
|
860
865
|
if (wallet) {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
+
const walletKey = wallet.toLowerCase();
|
|
867
|
+
const liveId = [...runtimeHandles].find(([, handle]) => (handle.wallet ?? "").toLowerCase() === walletKey)?.[0];
|
|
868
|
+
// Which runtime holds this wallet, from either read. The live half matches on
|
|
869
|
+
// `handle.wallet`, the registry half on `entry.wallet`, so the id is what they have in
|
|
870
|
+
// common — and it is the id, not which read matched, that the two messages differ on.
|
|
871
|
+
const holderId = liveId ?? findByWallet(registry, wallet)?.id;
|
|
872
|
+
if (holderId !== undefined) {
|
|
873
|
+
// "running" vs "installed and not running" is asked of THAT id — the same predicate
|
|
874
|
+
// `senpi.listRuntimes` renders "stopped" from — rather than inferred from which of the
|
|
875
|
+
// two reads found it. They agree in practice; the refusal has to agree with the surface
|
|
876
|
+
// it sends the reader to, because that is where the reader checks it.
|
|
877
|
+
if (runtimeHandles.has(holderId)) {
|
|
878
|
+
return { ok: false, error: toError(`A runtime for wallet "${wallet}" is already running (runtime "${holderId}"). ` +
|
|
879
|
+
`Stop it first: openclaw senpi runtime delete --id ${holderId}`) };
|
|
866
880
|
}
|
|
881
|
+
return { ok: false, error: toError(
|
|
882
|
+
// What is CLAIMED here is only what the two reads above prove: a registry row exists,
|
|
883
|
+
// and this gateway has no live handle under its id. Not "it crashed", not "it is
|
|
884
|
+
// broken" — neither was read, and the boot loop will not necessarily manage to start
|
|
885
|
+
// it either (a boot that already failed once fails again), so the claim is that the
|
|
886
|
+
// next restart TRIES. The next step leads with the read, because the operator has to
|
|
887
|
+
// find out which of the two runtimes they actually want before removing either, and
|
|
888
|
+
// deleting the wrong one leaves a funded wallet watched by nothing.
|
|
889
|
+
`A runtime for wallet "${wallet}" is already installed (runtime "${holderId}"). ` +
|
|
890
|
+
`It is not running in this gateway right now, but it is in the registry, so the next ` +
|
|
891
|
+
`restart will try to start it — installing a second runtime on the same wallet would ` +
|
|
892
|
+
`put two exit engines on one wallet's margin, each tracking its own high-water floor. ` +
|
|
893
|
+
`Nothing was installed. See what that runtime is before removing anything: ` +
|
|
894
|
+
`openclaw senpi runtime list. If it is genuinely unwanted, remove it first: ` +
|
|
895
|
+
`openclaw senpi runtime delete --id ${holderId}`) };
|
|
867
896
|
}
|
|
868
897
|
}
|
|
869
898
|
// Best-effort wallet ownership validation: calls the MCP server to check
|
|
@@ -882,48 +911,21 @@ const senpiRuntimePlugin = {
|
|
|
882
911
|
const match = strategies.find((s) => s.strategyWalletAddress.toLowerCase() === wallet.toLowerCase());
|
|
883
912
|
if (!match) {
|
|
884
913
|
pluginLogger.info("[senpi-runtime] Wallet validation failed: not owned", { wallet });
|
|
885
|
-
respond(false, undefined, toError(`Strategy wallet "${wallet}" (YAML field: strategy.wallet) was not found in your Senpi account's strategies. ` +
|
|
886
|
-
`Check the address for typos. ` +
|
|
887
|
-
`To list all your strategies and their wallet addresses, call the MCP tool: strategy_list (no arguments needed). ` +
|
|
888
|
-
`Each result includes "strategyWalletAddress" — use that value as strategy.wallet in your YAML.`));
|
|
889
914
|
// `return` triggers the inner `finally` (disconnect) before exiting.
|
|
890
|
-
return
|
|
915
|
+
return { ok: false, error: toError(`Strategy wallet "${wallet}" (YAML field: strategy.wallet) was not found in your Senpi account's strategies. ` +
|
|
916
|
+
`Check the address for typos. ` +
|
|
917
|
+
`To list all your strategies and their wallet addresses, call the MCP tool: strategy_list (no arguments needed). ` +
|
|
918
|
+
`Each result includes "strategyWalletAddress" — use that value as strategy.wallet in your YAML.`) };
|
|
891
919
|
}
|
|
892
920
|
if (match.status !== "ACTIVE") {
|
|
893
921
|
pluginLogger.info("[senpi-runtime] Wallet validation failed: strategy not active", { wallet, status: match.status, strategyId: match.id });
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
"UNSUBSCRIBE_TRADER", "FUND_SOURCE_WALLET", "INACTIVE",
|
|
902
|
-
]);
|
|
903
|
-
let hint;
|
|
904
|
-
if (initializingStatuses.has(match.status)) {
|
|
905
|
-
hint =
|
|
906
|
-
`The strategy is still initializing (${match.status}). ` +
|
|
907
|
-
`It will reach ACTIVE automatically — wait a few minutes, then retry. ` +
|
|
908
|
-
`Initialization order: CREATE_WALLET → FUND_WALLET → INITIALIZE_POSITIONS → ACTIVE.`;
|
|
909
|
-
}
|
|
910
|
-
else if (match.status === "PAUSED") {
|
|
911
|
-
hint =
|
|
912
|
-
`The strategy is paused and cannot be used to create a runtime — please create a new strategy instead.`;
|
|
913
|
-
}
|
|
914
|
-
else if (terminalStatuses.has(match.status)) {
|
|
915
|
-
hint =
|
|
916
|
-
`The strategy is closing or already closed (${match.status}). ` +
|
|
917
|
-
`A runtime cannot be created for a closed strategy — please create a new strategy instead.`;
|
|
918
|
-
}
|
|
919
|
-
else {
|
|
920
|
-
hint = `Only ACTIVE strategies can run a runtime.`;
|
|
921
|
-
}
|
|
922
|
-
return (`Strategy wallet "${wallet}" (YAML field: strategy.wallet) has status "${match.status}" — a runtime requires ACTIVE status. ` +
|
|
923
|
-
hint +
|
|
924
|
-
` To check the latest status, call the MCP tool: strategy_list.`);
|
|
925
|
-
})()));
|
|
926
|
-
return;
|
|
922
|
+
return { ok: false, error: toError(
|
|
923
|
+
// H7/D-10: the status vocabulary lives in one module, shared with deploy's
|
|
924
|
+
// reconcile. This gate's private copy was already missing TERMINATED and
|
|
925
|
+
// CLOSING_DONE, so a gone strategy fell to the generic "only ACTIVE" hint.
|
|
926
|
+
`Strategy wallet "${wallet}" (YAML field: strategy.wallet) has status "${match.status}" — a runtime requires ACTIVE status. ` +
|
|
927
|
+
buildWalletStatusHint(match.status) +
|
|
928
|
+
` To check the latest status, call the MCP tool: strategy_list.`) };
|
|
927
929
|
}
|
|
928
930
|
pluginLogger.info("[senpi-runtime] Wallet validation passed", { wallet, strategyId: match.id, status: match.status });
|
|
929
931
|
}
|
|
@@ -952,11 +954,17 @@ const senpiRuntimePlugin = {
|
|
|
952
954
|
};
|
|
953
955
|
const valid = validateEntry(entry);
|
|
954
956
|
if (!valid.ok) {
|
|
955
|
-
|
|
956
|
-
return;
|
|
957
|
+
return { ok: false, error: toError(valid.error) };
|
|
957
958
|
}
|
|
958
959
|
registry.runtimes.push(entry);
|
|
959
960
|
await writeRegistry(baseStateDir, registry);
|
|
961
|
+
/**
|
|
962
|
+
* The unwired verdict for THIS install, tracked locally rather than read back out of
|
|
963
|
+
* `unwiredRuntimes`, so the result reports what this call did and never a leftover verdict
|
|
964
|
+
* from an earlier runtime that happened to share the id. Set at each of the three sites that
|
|
965
|
+
* leave the runtime without live entry scanners; cleared by the wire-success path.
|
|
966
|
+
*/
|
|
967
|
+
let unwiredPhase;
|
|
960
968
|
try {
|
|
961
969
|
const scannerProviders = await createScannerProvidersForRun({
|
|
962
970
|
mcpTransport,
|
|
@@ -1013,6 +1021,7 @@ const senpiRuntimePlugin = {
|
|
|
1013
1021
|
await hotLauncher.start();
|
|
1014
1022
|
launchers.set(resolvedId, hotLauncher);
|
|
1015
1023
|
unwiredRuntimes.delete(resolvedId);
|
|
1024
|
+
unwiredPhase = undefined;
|
|
1016
1025
|
runtimeHandles.get(resolvedId)?.attachSupervisionSource(() => ({
|
|
1017
1026
|
unwired: false,
|
|
1018
1027
|
rows: hotLauncher.describeSupervision(),
|
|
@@ -1021,6 +1030,7 @@ const senpiRuntimePlugin = {
|
|
|
1021
1030
|
catch (startErr) {
|
|
1022
1031
|
await hotLauncher.stop().catch(() => { });
|
|
1023
1032
|
unwiredRuntimes.set(resolvedId, "install_launch");
|
|
1033
|
+
unwiredPhase = "install_launch";
|
|
1024
1034
|
runtimeHandles.get(resolvedId)?.attachSupervisionSource(() => ({
|
|
1025
1035
|
unwired: true,
|
|
1026
1036
|
unwiredPhase: "install_launch",
|
|
@@ -1031,8 +1041,10 @@ const senpiRuntimePlugin = {
|
|
|
1031
1041
|
}
|
|
1032
1042
|
}
|
|
1033
1043
|
catch (wireErr) {
|
|
1034
|
-
// Wire failure must NOT fail the install — the runtime is already up.
|
|
1044
|
+
// Wire failure must NOT fail the install — the runtime is already up. It IS reported
|
|
1045
|
+
// across the result boundary (payload.unwired) so a funding caller can refuse it.
|
|
1035
1046
|
unwiredRuntimes.set(resolvedId, "install_wire");
|
|
1047
|
+
unwiredPhase = "install_wire";
|
|
1036
1048
|
runtimeHandles.get(resolvedId)?.attachSupervisionSource(() => ({
|
|
1037
1049
|
unwired: true,
|
|
1038
1050
|
unwiredPhase: "install_wire",
|
|
@@ -1047,6 +1059,7 @@ const senpiRuntimePlugin = {
|
|
|
1047
1059
|
// external scanners but has NO live entry scanners. Record that fail-closed
|
|
1048
1060
|
// state (state only, no new event) so listRuntimes/health don't lie.
|
|
1049
1061
|
unwiredRuntimes.set(resolvedId, "no_intake");
|
|
1062
|
+
unwiredPhase = "no_intake";
|
|
1050
1063
|
runtimeHandles.get(resolvedId)?.attachSupervisionSource(() => ({
|
|
1051
1064
|
unwired: true,
|
|
1052
1065
|
unwiredPhase: "no_intake",
|
|
@@ -1054,14 +1067,31 @@ const senpiRuntimePlugin = {
|
|
|
1054
1067
|
}));
|
|
1055
1068
|
}
|
|
1056
1069
|
// ──────────────────────────────────────────────────────────────────────
|
|
1057
|
-
|
|
1070
|
+
return {
|
|
1071
|
+
ok: true,
|
|
1072
|
+
payload: {
|
|
1073
|
+
id: resolvedId,
|
|
1074
|
+
name: runtimeName,
|
|
1075
|
+
wallet,
|
|
1076
|
+
...(unwiredPhase ? { unwired: unwiredPhase } : {}),
|
|
1077
|
+
},
|
|
1078
|
+
};
|
|
1058
1079
|
}
|
|
1059
1080
|
catch (err) {
|
|
1060
1081
|
const next = registry.runtimes.filter((e) => e.id !== resolvedId);
|
|
1061
1082
|
await writeRegistry(baseStateDir, { ...registry, runtimes: next });
|
|
1062
1083
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1063
|
-
|
|
1084
|
+
return { ok: false, error: toUnavailable(`Could not start the runtime. This is usually due to invalid or incomplete YAML.\n\n${msg}`) };
|
|
1064
1085
|
}
|
|
1086
|
+
}
|
|
1087
|
+
/** Gateway respond accepts (ok, payload?, error?) with error as { code, message }; plugin SDK types may omit the third arg. */
|
|
1088
|
+
api.registerGatewayMethod("senpi.installRuntime", async (ctx) => {
|
|
1089
|
+
const respond = ctx.respond;
|
|
1090
|
+
const result = await installRuntimeCore(ctx.params);
|
|
1091
|
+
if (result.ok)
|
|
1092
|
+
respond(true, result.payload);
|
|
1093
|
+
else
|
|
1094
|
+
respond(false, undefined, result.error);
|
|
1065
1095
|
}, { scope: "operator.write" });
|
|
1066
1096
|
api.registerGatewayMethod("senpi.listRuntimes", async (ctx) => {
|
|
1067
1097
|
const respond = ctx.respond;
|
|
@@ -1088,9 +1118,12 @@ const senpiRuntimePlugin = {
|
|
|
1088
1118
|
});
|
|
1089
1119
|
respond(true, { runtimes });
|
|
1090
1120
|
}, { scope: "operator.read" });
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1121
|
+
/**
|
|
1122
|
+
* The whole `senpi.deleteRuntime` behaviour, callable without a gateway ctx. `senpi deploy`
|
|
1123
|
+
* reuses it to retire a runtime bound to an old wallet, so archival, launcher teardown and the
|
|
1124
|
+
* unwired-verdict cleanup all happen exactly once, here.
|
|
1125
|
+
*/
|
|
1126
|
+
async function deleteRuntimeCore(params) {
|
|
1094
1127
|
const { readRegistry, writeRegistry } = await import("./runtime/strategy-registry.js");
|
|
1095
1128
|
const registry = await readRegistry(baseStateDir);
|
|
1096
1129
|
let targetId = typeof params?.id === "string" ? params.id : undefined;
|
|
@@ -1099,29 +1132,26 @@ const senpiRuntimePlugin = {
|
|
|
1099
1132
|
if (!targetId && targetAddress) {
|
|
1100
1133
|
const entry = registry.runtimes.find((e) => e.wallet === targetAddress);
|
|
1101
1134
|
if (!entry) {
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
return;
|
|
1135
|
+
return { ok: false, error: {
|
|
1136
|
+
code: "NOT_FOUND",
|
|
1137
|
+
message: `No runtime found for wallet address "${targetAddress}".`,
|
|
1138
|
+
} };
|
|
1107
1139
|
}
|
|
1108
1140
|
targetId = entry.id;
|
|
1109
1141
|
}
|
|
1110
1142
|
if (!targetId) {
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
return;
|
|
1143
|
+
return { ok: false, error: {
|
|
1144
|
+
code: "INVALID_REQUEST",
|
|
1145
|
+
message: "Please provide the runtime id or wallet address. Example: openclaw senpi runtime delete --id my-runtime",
|
|
1146
|
+
} };
|
|
1116
1147
|
}
|
|
1117
1148
|
const deletedEntry = registry.runtimes.find((e) => e.id === targetId);
|
|
1118
1149
|
const handle = runtimeHandles.get(targetId);
|
|
1119
1150
|
if (!deletedEntry && !handle) {
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
return;
|
|
1151
|
+
return { ok: false, error: {
|
|
1152
|
+
code: "NOT_FOUND",
|
|
1153
|
+
message: `No runtime found with id "${targetId}".`,
|
|
1154
|
+
} };
|
|
1125
1155
|
}
|
|
1126
1156
|
if (handle) {
|
|
1127
1157
|
await handle.teardownDslForRuntimeDeletion();
|
|
@@ -1166,8 +1196,90 @@ const senpiRuntimePlugin = {
|
|
|
1166
1196
|
address: deletedEntry?.wallet,
|
|
1167
1197
|
hadLiveHandle: Boolean(handle),
|
|
1168
1198
|
}));
|
|
1169
|
-
|
|
1199
|
+
return { ok: true, payload: { id: targetId } };
|
|
1200
|
+
}
|
|
1201
|
+
api.registerGatewayMethod("senpi.deleteRuntime", async (ctx) => {
|
|
1202
|
+
const respond = ctx.respond;
|
|
1203
|
+
const result = await deleteRuntimeCore(ctx.params);
|
|
1204
|
+
if (result.ok)
|
|
1205
|
+
respond(true, result.payload);
|
|
1206
|
+
else
|
|
1207
|
+
respond(false, undefined, result.error);
|
|
1170
1208
|
}, { scope: "operator.write" });
|
|
1209
|
+
/** One connected SenpiClient per job; always disconnected, even on failure. */
|
|
1210
|
+
async function withSenpiClient(fn) {
|
|
1211
|
+
const client = mcpTransport ? new SenpiClient(mcpTransport) : new SenpiClient({ apiKey });
|
|
1212
|
+
if (!mcpTransport)
|
|
1213
|
+
await client.connect();
|
|
1214
|
+
try {
|
|
1215
|
+
return await fn(client);
|
|
1216
|
+
}
|
|
1217
|
+
finally {
|
|
1218
|
+
if (!mcpTransport)
|
|
1219
|
+
await client.disconnect().catch(() => { });
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
async function findRegistryEntryByWallet(wallet) {
|
|
1223
|
+
const { readRegistry, findByWallet } = await import("./runtime/strategy-registry.js");
|
|
1224
|
+
const entry = findByWallet(await readRegistry(baseStateDir), wallet);
|
|
1225
|
+
return entry ? { id: entry.id, wallet: entry.wallet } : undefined;
|
|
1226
|
+
}
|
|
1227
|
+
async function findRegistryEntryById(id) {
|
|
1228
|
+
const { readRegistry } = await import("./runtime/strategy-registry.js");
|
|
1229
|
+
const entry = (await readRegistry(baseStateDir)).runtimes.find((e) => e.id === id);
|
|
1230
|
+
return entry ? { id: entry.id, wallet: entry.wallet } : undefined;
|
|
1231
|
+
}
|
|
1232
|
+
/** Narrow a live runtime's system state to the scanner columns the deploy report quotes. */
|
|
1233
|
+
async function readScannerRows(runtimeId) {
|
|
1234
|
+
const handle = runtimeHandles.get(runtimeId);
|
|
1235
|
+
if (!handle)
|
|
1236
|
+
return null;
|
|
1237
|
+
try {
|
|
1238
|
+
const state = await handle.getSystemState();
|
|
1239
|
+
const rows = state?.components?.scanners?.state?.scanners;
|
|
1240
|
+
if (!Array.isArray(rows))
|
|
1241
|
+
return null;
|
|
1242
|
+
return rows.map((row) => ({
|
|
1243
|
+
scannerId: row.scannerId,
|
|
1244
|
+
health: row.health,
|
|
1245
|
+
enabled: row.enabled,
|
|
1246
|
+
scheduleMode: row.scheduleMode,
|
|
1247
|
+
lastRunStatus: row.lastRunStatus,
|
|
1248
|
+
lastRunFinishedAt: row.lastRunFinishedAt,
|
|
1249
|
+
lastAliveAt: row.lastAliveAt,
|
|
1250
|
+
nextRunAt: row.nextRunAt,
|
|
1251
|
+
runCount: row.runCount,
|
|
1252
|
+
errorCount: row.errorCount,
|
|
1253
|
+
signalsProduced: row.signalsProduced,
|
|
1254
|
+
lastError: row.lastError,
|
|
1255
|
+
}));
|
|
1256
|
+
}
|
|
1257
|
+
catch (err) {
|
|
1258
|
+
api.logger.warn("[senpi-runtime] deploy: could not read scanner rows", { runtimeId, err });
|
|
1259
|
+
return null;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
registerDeployGatewayMethods(api, {
|
|
1263
|
+
controller: deployController,
|
|
1264
|
+
withSenpiClient,
|
|
1265
|
+
installRuntime: (params) => installRuntimeCore(params),
|
|
1266
|
+
deleteRuntime: async (id) => {
|
|
1267
|
+
const result = await deleteRuntimeCore({ id });
|
|
1268
|
+
if (!result.ok)
|
|
1269
|
+
throw new Error(`${result.error.code}: ${result.error.message}`);
|
|
1270
|
+
},
|
|
1271
|
+
registryFindByWallet: findRegistryEntryByWallet,
|
|
1272
|
+
registryFindById: findRegistryEntryById,
|
|
1273
|
+
getScannerRows: readScannerRows,
|
|
1274
|
+
// H4: `runtimeHandles` holds the live `run()` handles. A registry row without one is a
|
|
1275
|
+
// previous install whose runtime died before start — deploy must reinstall, not report it
|
|
1276
|
+
// adopted. `unwiredRuntimes` is the same judgement one step further in: a runtime that is up
|
|
1277
|
+
// but whose entry scanners never wired is running BLIND (the health surface already renders
|
|
1278
|
+
// it "running — NO ENTRY SCANNERS"), and reporting `install: skipped` over it would be the
|
|
1279
|
+
// same clean line over a runtime that cannot tick.
|
|
1280
|
+
runtimeIsLive: async (id) => runtimeHandles.has(id) && !unwiredRuntimes.has(id),
|
|
1281
|
+
logger: { info: (line) => api.logger.info(`[senpi-runtime] ${line}`) },
|
|
1282
|
+
});
|
|
1171
1283
|
/**
|
|
1172
1284
|
* Gateway shim for `senpi.ingestExternalScannerData`.
|
|
1173
1285
|
*
|