@senpi/runtime 3.0.71 → 3.0.73
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 +12 -1
- 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 +23 -20
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/deploy/deadlines.d.ts +61 -1
- package/dist/deploy/deadlines.d.ts.map +1 -1
- package/dist/deploy/deadlines.js +85 -2
- package/dist/deploy/deadlines.js.map +1 -1
- package/dist/deploy/funding.d.ts +38 -0
- package/dist/deploy/funding.d.ts.map +1 -1
- package/dist/deploy/funding.js +39 -0
- package/dist/deploy/funding.js.map +1 -1
- package/dist/deploy/job.d.ts.map +1 -1
- package/dist/deploy/job.js +7 -1
- package/dist/deploy/job.js.map +1 -1
- package/dist/deploy/messages.d.ts +603 -0
- package/dist/deploy/messages.d.ts.map +1 -0
- package/dist/deploy/messages.js +1388 -0
- package/dist/deploy/messages.js.map +1 -0
- package/dist/deploy/orchestrator.d.ts +8 -0
- package/dist/deploy/orchestrator.d.ts.map +1 -1
- package/dist/deploy/orchestrator.js +146 -1211
- package/dist/deploy/orchestrator.js.map +1 -1
- package/dist/deploy/package.d.ts +42 -0
- package/dist/deploy/package.d.ts.map +1 -1
- package/dist/deploy/package.js +146 -4
- package/dist/deploy/package.js.map +1 -1
- package/dist/deploy/recheck-commands.d.ts.map +1 -1
- package/dist/deploy/recheck-commands.js +21 -3
- package/dist/deploy/recheck-commands.js.map +1 -1
- package/dist/deploy/register.d.ts +2 -0
- package/dist/deploy/register.d.ts.map +1 -1
- package/dist/deploy/register.js +23 -6
- package/dist/deploy/register.js.map +1 -1
- package/dist/deploy/taxonomy.d.ts +19 -0
- package/dist/deploy/taxonomy.d.ts.map +1 -1
- package/dist/deploy/taxonomy.js +28 -0
- package/dist/deploy/taxonomy.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +235 -176
- package/dist/index.js.map +1 -1
- package/dist/runtime/env-resolve.d.ts +13 -0
- package/dist/runtime/env-resolve.d.ts.map +1 -1
- package/dist/runtime/env-resolve.js +14 -1
- package/dist/runtime/env-resolve.js.map +1 -1
- package/dist/runtime/runtime-descriptor.d.ts +37 -0
- package/dist/runtime/runtime-descriptor.d.ts.map +1 -0
- package/dist/runtime/runtime-descriptor.js +163 -0
- package/dist/runtime/runtime-descriptor.js.map +1 -0
- package/dist/telemetry/metrics.d.ts +7 -0
- package/dist/telemetry/metrics.d.ts.map +1 -1
- package/dist/telemetry/metrics.js +7 -0
- package/dist/telemetry/metrics.js.map +1 -1
- package/dist/validate/codes.d.ts +2 -0
- package/dist/validate/codes.d.ts.map +1 -1
- package/dist/validate/codes.js +7 -0
- package/dist/validate/codes.js.map +1 -1
- package/dist/validate/run.d.ts.map +1 -1
- package/dist/validate/run.js +6 -1
- package/dist/validate/run.js.map +1 -1
- package/dist/validate/wallet-binding-stage.d.ts +44 -0
- package/dist/validate/wallet-binding-stage.d.ts.map +1 -0
- package/dist/validate/wallet-binding-stage.js +103 -0
- package/dist/validate/wallet-binding-stage.js.map +1 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -17,8 +17,10 @@ import { SenpiClient } from "./senpi/client.js";
|
|
|
17
17
|
import { buildWalletStatusHint } from "./senpi/strategy-status.js";
|
|
18
18
|
import { DeployJobController } from "./deploy/job.js";
|
|
19
19
|
import { registerDeployGatewayMethods } from "./deploy/register.js";
|
|
20
|
+
import { INSTALL_DEADLINE_MS, WALLET_INSTALL_IN_FLIGHT_CODE } from "./deploy/deadlines.js";
|
|
20
21
|
import { AuditQueryError } from "./runtime/audit-query.types.js";
|
|
21
22
|
import { buildRuntimeStateDir, archiveStateDir } from "./runtime/state-layout.js";
|
|
23
|
+
import { describeRuntimeEntry } from "./runtime/runtime-descriptor.js";
|
|
22
24
|
import { registerSenpiCli } from "./cli/senpi-commands.js";
|
|
23
25
|
import { readSenpiCliStore, resolveSenpiApiKeyFromStore, resolveSenpiBaseStateDir, } from "./cli/senpi-cli-config.js";
|
|
24
26
|
import { getDefaultStateDir } from "./runtime/default-state-dir.js";
|
|
@@ -136,6 +138,13 @@ const PLUGIN_PACKAGE_NAME = resolvePackageName();
|
|
|
136
138
|
* nothing about the decision a caller has to make.
|
|
137
139
|
*/
|
|
138
140
|
const WALLET_HAS_RUNTIME = "[E_WALLET_HAS_RUNTIME]";
|
|
141
|
+
/**
|
|
142
|
+
* The fence refusal's code, IMPORTED rather than spelled here — `deploy/messages.ts` renders the
|
|
143
|
+
* same code for the same condition on the deploy path, and two module-private literals were one
|
|
144
|
+
* edit away from answering two different codes for one condition. See its doc in `deadlines.ts`
|
|
145
|
+
* for why it lives beside the install bound both copies quote.
|
|
146
|
+
*/
|
|
147
|
+
const WALLET_INSTALL_IN_FLIGHT = `[${WALLET_INSTALL_IN_FLIGHT_CODE}]`;
|
|
139
148
|
/**
|
|
140
149
|
* The id-collision refusal's code — the third gate on the same `senpi runtime create` path, and
|
|
141
150
|
* plausibly the one hit most often, since re-running create with the same id lands on it.
|
|
@@ -341,6 +350,23 @@ const senpiRuntimePlugin = {
|
|
|
341
350
|
scanners: config.scanners ?? [],
|
|
342
351
|
});
|
|
343
352
|
};
|
|
353
|
+
/**
|
|
354
|
+
* Wallets with an install IN FLIGHT in this process, keyed by lowercased address.
|
|
355
|
+
*
|
|
356
|
+
* IN MEMORY, never persisted — the same reasoning `deploy/job.ts` gives for the single-flight
|
|
357
|
+
* slot. `installRuntimeCore` is in-process work, so a pending install cannot outlive this
|
|
358
|
+
* process: if the gateway dies mid-install, the install dies with it and the row it left
|
|
359
|
+
* behind is the ordinary "registry row with no live handle" case that deploy already handles by
|
|
360
|
+
* deleting and reinstalling. A persisted marker would need a liveness heuristic to tell those
|
|
361
|
+
* apart, and would wedge every install on the box after one crash.
|
|
362
|
+
*
|
|
363
|
+
* What it prevents: a deploy whose wait on an install expired (or whose job was force-
|
|
364
|
+
* terminated) frees its slot while the install runs on. Without this, the next deploy finds the
|
|
365
|
+
* row, sees no live handle, deletes it and reinstalls — and the first install then lands on top,
|
|
366
|
+
* clobbering `runtimeHandles` under a runtime id both installs derive identically. Two exit
|
|
367
|
+
* engines, one margin account: the exact state `[E_WALLET_HAS_RUNTIME]` exists to prevent.
|
|
368
|
+
*/
|
|
369
|
+
const installsInFlight = new Map();
|
|
344
370
|
// Dedicated logger for the runtime-api subtree — required because the
|
|
345
371
|
// PluginLogger surface (info/warn/error) lacks `debug`, while the
|
|
346
372
|
// Logger contract used by `runtime-api/` includes it.
|
|
@@ -1014,6 +1040,7 @@ const senpiRuntimePlugin = {
|
|
|
1014
1040
|
const resolvedId = id && id.length > 0
|
|
1015
1041
|
? (sanitizeId(id) || "runtime")
|
|
1016
1042
|
: deriveRuntimeId(runtimeName, wallet);
|
|
1043
|
+
const walletKey = (wallet ?? "").trim().toLowerCase();
|
|
1017
1044
|
if (hasId(registry, resolvedId)) {
|
|
1018
1045
|
return { ok: false, error: toError(`${RUNTIME_ID_TAKEN} 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}`) };
|
|
1019
1046
|
}
|
|
@@ -1038,9 +1065,28 @@ const senpiRuntimePlugin = {
|
|
|
1038
1065
|
* both, because the registry stores `strategy.wallet` verbatim from the YAML and a
|
|
1039
1066
|
* checksummed address is the same margin account as its lowercase spelling.
|
|
1040
1067
|
*/
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1068
|
+
// Gated on the FOLDED key, not on the raw field. `strategy.wallet` only has to be
|
|
1069
|
+
// `z.string().min(1)`, so a whitespace-only value reaches here with `walletKey === ""` — and
|
|
1070
|
+
// an empty key is not an identity: the live-handle compare below folds the same way, so `""`
|
|
1071
|
+
// matched every handle carrying no wallet at all and refused the install with
|
|
1072
|
+
// `[E_WALLET_HAS_RUNTIME]` naming an unrelated runtime. (`findByWallet` already refuses to
|
|
1073
|
+
// match on an empty address for exactly this reason, and the fence is keyed on `walletKey`
|
|
1074
|
+
// too, so a blank one is never set.) Nothing is skipped that could have matched.
|
|
1075
|
+
if (walletKey) {
|
|
1076
|
+
const inFlight = installsInFlight.get(walletKey);
|
|
1077
|
+
if (inFlight) {
|
|
1078
|
+
const heldSec = Math.round((Date.now() - inFlight.startedAtMs) / 1000);
|
|
1079
|
+
const boundSec = Math.round(INSTALL_DEADLINE_MS / 1000);
|
|
1080
|
+
return { ok: false, error: toError(`${WALLET_INSTALL_IN_FLIGHT} An install is already running on wallet "${wallet}" ` +
|
|
1081
|
+
`(runtime "${inFlight.runtimeId}", started ${heldSec}s ago) in this gateway. Installing a ` +
|
|
1082
|
+
`second runtime on the same wallet would put two exit engines on one margin account. ` +
|
|
1083
|
+
`Nothing was installed and nothing was changed. This normally clears on its own once that ` +
|
|
1084
|
+
`install finishes. A deploy's own install bound is ${boundSec}s — well past that, this install ` +
|
|
1085
|
+
`is more likely wedged than merely slow, and if so the fence on this wallet will not clear ` +
|
|
1086
|
+
`until this gateway process restarts. Check what it produced before installing anything here: ` +
|
|
1087
|
+
`openclaw senpi deploy status, then openclaw senpi runtime list.`) };
|
|
1088
|
+
}
|
|
1089
|
+
const liveId = [...runtimeHandles].find(([, handle]) => (handle.wallet ?? "").trim().toLowerCase() === walletKey)?.[0];
|
|
1044
1090
|
// Which runtime holds this wallet, from either read. The live half matches on
|
|
1045
1091
|
// `handle.wallet`, the registry half on `entry.wallet`, so the id is what they have in
|
|
1046
1092
|
// common — and it is the id, not which read matched, that the two messages differ on.
|
|
@@ -1071,197 +1117,205 @@ const senpiRuntimePlugin = {
|
|
|
1071
1117
|
`openclaw senpi runtime delete --id ${holderId}`) };
|
|
1072
1118
|
}
|
|
1073
1119
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
: new SenpiClient({ apiKey });
|
|
1083
|
-
if (!mcpTransport)
|
|
1084
|
-
await client.connect();
|
|
1120
|
+
if (walletKey)
|
|
1121
|
+
installsInFlight.set(walletKey, { runtimeId: resolvedId, startedAtMs: Date.now() });
|
|
1122
|
+
try {
|
|
1123
|
+
// Best-effort wallet ownership validation: calls the MCP server to check
|
|
1124
|
+
// whether the wallet belongs to the authenticated user's strategies.
|
|
1125
|
+
// If the MCP call fails (network issue, bad apiKey, server down), we warn
|
|
1126
|
+
// and allow the install to proceed rather than blocking the user.
|
|
1127
|
+
if (wallet) {
|
|
1085
1128
|
try {
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1129
|
+
const client = mcpTransport
|
|
1130
|
+
? new SenpiClient(mcpTransport)
|
|
1131
|
+
: new SenpiClient({ apiKey });
|
|
1132
|
+
if (!mcpTransport)
|
|
1133
|
+
await client.connect();
|
|
1134
|
+
try {
|
|
1135
|
+
const strategies = await client.listStrategies({ strategyAddresses: [wallet] });
|
|
1136
|
+
const match = strategies.find((s) => s.strategyWalletAddress.toLowerCase() === wallet.toLowerCase());
|
|
1137
|
+
if (!match) {
|
|
1138
|
+
pluginLogger.info("[senpi-runtime] Wallet validation failed: not owned", { wallet });
|
|
1139
|
+
// `return` triggers the inner `finally` (disconnect) before exiting.
|
|
1140
|
+
return { ok: false, error: toError(`Strategy wallet "${wallet}" (YAML field: strategy.wallet) was not found in your Senpi account's strategies. ` +
|
|
1141
|
+
`Check the address for typos. ` +
|
|
1142
|
+
`To list all your strategies and their wallet addresses, call the MCP tool: strategy_list (no arguments needed). ` +
|
|
1143
|
+
`Each result includes "strategyWalletAddress" — use that value as strategy.wallet in your YAML.`) };
|
|
1144
|
+
}
|
|
1145
|
+
if (match.status !== "ACTIVE") {
|
|
1146
|
+
pluginLogger.info("[senpi-runtime] Wallet validation failed: strategy not active", { wallet, status: match.status, strategyId: match.id });
|
|
1147
|
+
return { ok: false, error: toError(
|
|
1148
|
+
// H7/D-10: the status vocabulary lives in one module, shared with deploy's
|
|
1149
|
+
// reconcile. This gate's private copy was already missing TERMINATED and
|
|
1150
|
+
// CLOSING_DONE, so a gone strategy fell to the generic "only ACTIVE" hint.
|
|
1151
|
+
`Strategy wallet "${wallet}" (YAML field: strategy.wallet) has status "${match.status}" — a runtime requires ACTIVE status. ` +
|
|
1152
|
+
buildWalletStatusHint(match.status) +
|
|
1153
|
+
` To check the latest status, call the MCP tool: strategy_list.`) };
|
|
1154
|
+
}
|
|
1155
|
+
pluginLogger.info("[senpi-runtime] Wallet validation passed", { wallet, strategyId: match.id, status: match.status });
|
|
1095
1156
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
// H7/D-10: the status vocabulary lives in one module, shared with deploy's
|
|
1100
|
-
// reconcile. This gate's private copy was already missing TERMINATED and
|
|
1101
|
-
// CLOSING_DONE, so a gone strategy fell to the generic "only ACTIVE" hint.
|
|
1102
|
-
`Strategy wallet "${wallet}" (YAML field: strategy.wallet) has status "${match.status}" — a runtime requires ACTIVE status. ` +
|
|
1103
|
-
buildWalletStatusHint(match.status) +
|
|
1104
|
-
` To check the latest status, call the MCP tool: strategy_list.`) };
|
|
1157
|
+
finally {
|
|
1158
|
+
if (!mcpTransport)
|
|
1159
|
+
await client.disconnect().catch(() => { });
|
|
1105
1160
|
}
|
|
1106
|
-
pluginLogger.info("[senpi-runtime] Wallet validation passed", { wallet, strategyId: match.id, status: match.status });
|
|
1107
1161
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1162
|
+
catch (err) {
|
|
1163
|
+
// Soft-fail: validation is best-effort. If connect() or listStrategies()
|
|
1164
|
+
// throws, log a warning and let the install proceed.
|
|
1165
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1166
|
+
pluginLogger.warn("[senpi-runtime] Could not validate strategy wallet ownership", { wallet, err: msg });
|
|
1111
1167
|
}
|
|
1112
1168
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1169
|
+
const entry = hasPath
|
|
1170
|
+
? { id: resolvedId, runtimeYamlPath, schemaVersion: parsedSchemaVersion, wallet }
|
|
1171
|
+
: {
|
|
1172
|
+
id: resolvedId,
|
|
1173
|
+
runtimeYamlContent: runtimeYamlContent,
|
|
1174
|
+
schemaVersion: parsedSchemaVersion,
|
|
1175
|
+
wallet,
|
|
1176
|
+
// Persist the recipe dir so a gateway restart can resolve an
|
|
1177
|
+
// external scanner's relative `path: ./` (content installs drop it).
|
|
1178
|
+
...(runtimeYamlDir ? { runtimeYamlDir } : {}),
|
|
1179
|
+
};
|
|
1180
|
+
const valid = validateEntry(entry);
|
|
1181
|
+
if (!valid.ok) {
|
|
1182
|
+
return { ok: false, error: toError(valid.error) };
|
|
1118
1183
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
runtimeHandles.get(resolvedId)?.attachSupervisionSource(() => ({
|
|
1206
|
-
unwired: false,
|
|
1207
|
-
rows: hotLauncher.describeSupervision(),
|
|
1208
|
-
}));
|
|
1209
|
-
}
|
|
1210
|
-
catch (startErr) {
|
|
1211
|
-
await hotLauncher.stop().catch(() => { });
|
|
1212
|
-
unwiredRuntimes.set(resolvedId, "install_launch");
|
|
1213
|
-
unwiredPhase = "install_launch";
|
|
1214
|
-
runtimeHandles.get(resolvedId)?.attachSupervisionSource(() => ({
|
|
1215
|
-
unwired: true,
|
|
1216
|
-
unwiredPhase: "install_launch",
|
|
1217
|
-
rows: [],
|
|
1218
|
-
}));
|
|
1219
|
-
eventScannersUnwired("install_launch", resolvedId, wallet, appendCause("[senpi-runtime] hot-install supervised scanner launch failed — runtime running without it", startErr), startErr);
|
|
1184
|
+
registry.runtimes.push(entry);
|
|
1185
|
+
await writeRegistry(baseStateDir, registry);
|
|
1186
|
+
/**
|
|
1187
|
+
* The unwired verdict for THIS install, tracked locally rather than read back out of
|
|
1188
|
+
* `unwiredRuntimes`, so the result reports what this call did and never a leftover verdict
|
|
1189
|
+
* from an earlier runtime that happened to share the id. Set at each of the three sites that
|
|
1190
|
+
* leave the runtime without live entry scanners; cleared by the wire-success path.
|
|
1191
|
+
*/
|
|
1192
|
+
let unwiredPhase;
|
|
1193
|
+
try {
|
|
1194
|
+
const scannerProviders = await createScannerProvidersForRun({
|
|
1195
|
+
mcpTransport,
|
|
1196
|
+
apiKey,
|
|
1197
|
+
});
|
|
1198
|
+
const entryStateDir = buildRuntimeStateDir(baseStateDir, resolvedId, wallet);
|
|
1199
|
+
const handle = await run(pathOrContent, {
|
|
1200
|
+
fromPath: hasPath,
|
|
1201
|
+
stateDir: entryStateDir,
|
|
1202
|
+
mcpTransport,
|
|
1203
|
+
apiKey,
|
|
1204
|
+
scannerProviders,
|
|
1205
|
+
runtimeId: resolvedId,
|
|
1206
|
+
});
|
|
1207
|
+
runtimeHandles.set(resolvedId, handle);
|
|
1208
|
+
if (parsedConfig)
|
|
1209
|
+
noteTelemetryRuntimeConfig(resolvedId, parsedConfig);
|
|
1210
|
+
else
|
|
1211
|
+
noteTelemetryRuntime(resolvedId, wallet);
|
|
1212
|
+
// Registry create fact — distinct from the runtime.started boot transition (which also fires
|
|
1213
|
+
// on a transient restart). Emitted inside emitEvent's guard so telemetry never fails the install.
|
|
1214
|
+
emitEvent(apiLogger, () => buildRuntimeCreatedEvent({ runtimeId: resolvedId, address: wallet, recipeName: runtimeName }));
|
|
1215
|
+
// ── hot-install supervised scanner wire ───────────────────────────────
|
|
1216
|
+
// Live-install path: used when a runtime is registered after boot via
|
|
1217
|
+
// senpi.installRuntime, while the API is already listening — the
|
|
1218
|
+
// counterpart to the boot-time `launcher` above. A runtime takes exactly
|
|
1219
|
+
// one path: boot-present → `launcher` (deferred start); installed live →
|
|
1220
|
+
// `hotLauncher` (immediate start). The two are mutually exclusive because
|
|
1221
|
+
// installRuntime rejects any id/wallet already in the registry before
|
|
1222
|
+
// wiring (the hasId guard), so a boot-present runtime never re-enters here.
|
|
1223
|
+
// The API is already listening at this point, so we wire and start
|
|
1224
|
+
// immediately (no deferred start needed). Any failure is swallowed so
|
|
1225
|
+
// the install itself is not rolled back — the runtime is already up.
|
|
1226
|
+
// Gate on the same API-disabled flag as the boot path: when the HTTP API
|
|
1227
|
+
// is off (a test-only seam), the /signals route is never mounted, so
|
|
1228
|
+
// starting scanners would only spawn children that POST to a closed port.
|
|
1229
|
+
if (parsedConfig &&
|
|
1230
|
+
sharedIntakeContext &&
|
|
1231
|
+
process.env.SENPI_RUNTIME_API_DISABLED !== "true") {
|
|
1232
|
+
try {
|
|
1233
|
+
const apiConfig = loadApiConfig(api.pluginConfig);
|
|
1234
|
+
const scaffoldPackageDir = fileURLToPath(new URL("./scanners/external", import.meta.url));
|
|
1235
|
+
const entryStateDir = buildRuntimeStateDir(baseStateDir, resolvedId, wallet);
|
|
1236
|
+
const hotLauncher = wireRuntime({
|
|
1237
|
+
runtimeConfig: parsedConfig,
|
|
1238
|
+
recipeDir: hasPath ? dirname(pathOrContent) : runtimeYamlDir,
|
|
1239
|
+
runtimeId: resolvedId,
|
|
1240
|
+
wallet,
|
|
1241
|
+
sharedCtx: sharedIntakeContext,
|
|
1242
|
+
runtimeHandles,
|
|
1243
|
+
intakeUrl: intakeUrlFor(apiConfig.host, apiConfig.port),
|
|
1244
|
+
scaffoldPackageDir,
|
|
1245
|
+
runtimeStateDir: entryStateDir,
|
|
1246
|
+
logger: apiLogger,
|
|
1247
|
+
});
|
|
1248
|
+
if (hotLauncher) {
|
|
1249
|
+
try {
|
|
1250
|
+
await hotLauncher.start();
|
|
1251
|
+
launchers.set(resolvedId, hotLauncher);
|
|
1252
|
+
unwiredRuntimes.delete(resolvedId);
|
|
1253
|
+
unwiredPhase = undefined;
|
|
1254
|
+
runtimeHandles.get(resolvedId)?.attachSupervisionSource(() => ({
|
|
1255
|
+
unwired: false,
|
|
1256
|
+
rows: hotLauncher.describeSupervision(),
|
|
1257
|
+
}));
|
|
1258
|
+
}
|
|
1259
|
+
catch (startErr) {
|
|
1260
|
+
await hotLauncher.stop().catch(() => { });
|
|
1261
|
+
unwiredRuntimes.set(resolvedId, "install_launch");
|
|
1262
|
+
unwiredPhase = "install_launch";
|
|
1263
|
+
runtimeHandles.get(resolvedId)?.attachSupervisionSource(() => ({
|
|
1264
|
+
unwired: true,
|
|
1265
|
+
unwiredPhase: "install_launch",
|
|
1266
|
+
rows: [],
|
|
1267
|
+
}));
|
|
1268
|
+
eventScannersUnwired("install_launch", resolvedId, wallet, appendCause("[senpi-runtime] hot-install supervised scanner launch failed — runtime running without it", startErr), startErr);
|
|
1269
|
+
}
|
|
1220
1270
|
}
|
|
1221
1271
|
}
|
|
1272
|
+
catch (wireErr) {
|
|
1273
|
+
// Wire failure must NOT fail the install — the runtime is already up. It IS reported
|
|
1274
|
+
// across the result boundary (payload.unwired) so a funding caller can refuse it.
|
|
1275
|
+
unwiredRuntimes.set(resolvedId, "install_wire");
|
|
1276
|
+
unwiredPhase = "install_wire";
|
|
1277
|
+
runtimeHandles.get(resolvedId)?.attachSupervisionSource(() => ({
|
|
1278
|
+
unwired: true,
|
|
1279
|
+
unwiredPhase: "install_wire",
|
|
1280
|
+
rows: [],
|
|
1281
|
+
}));
|
|
1282
|
+
eventScannersUnwired("install_wire", resolvedId, wallet, appendCause("[senpi-runtime] hot-install wiring failed — runtime running without supervised scanners", wireErr), wireErr);
|
|
1283
|
+
}
|
|
1222
1284
|
}
|
|
1223
|
-
|
|
1224
|
-
//
|
|
1225
|
-
//
|
|
1226
|
-
|
|
1227
|
-
|
|
1285
|
+
else if (parsedConfig && !sharedIntakeContext && selectScanners(parsedConfig).length > 0) {
|
|
1286
|
+
// Supervised intake is DISABLED for the whole box (corrupt acceptance record).
|
|
1287
|
+
// The wire block above is skipped, so this hot-installed runtime declares
|
|
1288
|
+
// external scanners but has NO live entry scanners. Record that fail-closed
|
|
1289
|
+
// state (state only, no new event) so listRuntimes/health don't lie.
|
|
1290
|
+
unwiredRuntimes.set(resolvedId, "no_intake");
|
|
1291
|
+
unwiredPhase = "no_intake";
|
|
1228
1292
|
runtimeHandles.get(resolvedId)?.attachSupervisionSource(() => ({
|
|
1229
1293
|
unwired: true,
|
|
1230
|
-
unwiredPhase: "
|
|
1294
|
+
unwiredPhase: "no_intake",
|
|
1231
1295
|
rows: [],
|
|
1232
1296
|
}));
|
|
1233
|
-
eventScannersUnwired("install_wire", resolvedId, wallet, appendCause("[senpi-runtime] hot-install wiring failed — runtime running without supervised scanners", wireErr), wireErr);
|
|
1234
1297
|
}
|
|
1298
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1299
|
+
return {
|
|
1300
|
+
ok: true,
|
|
1301
|
+
payload: {
|
|
1302
|
+
id: resolvedId,
|
|
1303
|
+
name: runtimeName,
|
|
1304
|
+
wallet,
|
|
1305
|
+
...(unwiredPhase ? { unwired: unwiredPhase } : {}),
|
|
1306
|
+
},
|
|
1307
|
+
};
|
|
1235
1308
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
unwiredRuntimes.set(resolvedId, "no_intake");
|
|
1242
|
-
unwiredPhase = "no_intake";
|
|
1243
|
-
runtimeHandles.get(resolvedId)?.attachSupervisionSource(() => ({
|
|
1244
|
-
unwired: true,
|
|
1245
|
-
unwiredPhase: "no_intake",
|
|
1246
|
-
rows: [],
|
|
1247
|
-
}));
|
|
1309
|
+
catch (err) {
|
|
1310
|
+
const next = registry.runtimes.filter((e) => e.id !== resolvedId);
|
|
1311
|
+
await writeRegistry(baseStateDir, { ...registry, runtimes: next });
|
|
1312
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1313
|
+
return { ok: false, error: toUnavailable(`Could not start the runtime. This is usually due to invalid or incomplete YAML.\n\n${msg}`) };
|
|
1248
1314
|
}
|
|
1249
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
1250
|
-
return {
|
|
1251
|
-
ok: true,
|
|
1252
|
-
payload: {
|
|
1253
|
-
id: resolvedId,
|
|
1254
|
-
name: runtimeName,
|
|
1255
|
-
wallet,
|
|
1256
|
-
...(unwiredPhase ? { unwired: unwiredPhase } : {}),
|
|
1257
|
-
},
|
|
1258
|
-
};
|
|
1259
1315
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1264
|
-
return { ok: false, error: toUnavailable(`Could not start the runtime. This is usually due to invalid or incomplete YAML.\n\n${msg}`) };
|
|
1316
|
+
finally {
|
|
1317
|
+
if (walletKey)
|
|
1318
|
+
installsInFlight.delete(walletKey);
|
|
1265
1319
|
}
|
|
1266
1320
|
}
|
|
1267
1321
|
/** Gateway respond accepts (ok, payload?, error?) with error as { code, message }; plugin SDK types may omit the third arg. */
|
|
@@ -1294,6 +1348,10 @@ const senpiRuntimePlugin = {
|
|
|
1294
1348
|
? e.runtimeYamlPath
|
|
1295
1349
|
: "(inline)",
|
|
1296
1350
|
status,
|
|
1351
|
+
// The deploy path installs by CONTENT, not path (orchestrator.ts installRuntime), so
|
|
1352
|
+
// `source` is "(inline)" for every deployed strategy and the YAML is unreachable through
|
|
1353
|
+
// it. The engine renders the facts instead of shipping a blob to re-interpret.
|
|
1354
|
+
descriptor: describeRuntimeEntry(e),
|
|
1297
1355
|
};
|
|
1298
1356
|
});
|
|
1299
1357
|
respond(true, { runtimes });
|
|
@@ -1461,6 +1519,7 @@ const senpiRuntimePlugin = {
|
|
|
1461
1519
|
// it "running — NO ENTRY SCANNERS"), and reporting `install: skipped` over it would be the
|
|
1462
1520
|
// same clean line over a runtime that cannot tick.
|
|
1463
1521
|
runtimeIsLive: async (id) => runtimeHandles.has(id) && !unwiredRuntimes.has(id),
|
|
1522
|
+
installPendingOnWallet: async (wallet) => installsInFlight.has(wallet.trim().toLowerCase()),
|
|
1464
1523
|
logger: { info: (line) => api.logger.info(`[senpi-runtime] ${line}`) },
|
|
1465
1524
|
});
|
|
1466
1525
|
/**
|