@senpi/runtime 3.0.72 → 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/senpi-commands.d.ts.map +1 -1
- package/dist/cli/senpi-commands.js +6 -5
- 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 +33 -0
- package/dist/deploy/package.d.ts.map +1 -1
- package/dist/deploy/package.js +142 -3
- 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 +230 -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/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,6 +17,7 @@ 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";
|
|
22
23
|
import { describeRuntimeEntry } from "./runtime/runtime-descriptor.js";
|
|
@@ -137,6 +138,13 @@ const PLUGIN_PACKAGE_NAME = resolvePackageName();
|
|
|
137
138
|
* nothing about the decision a caller has to make.
|
|
138
139
|
*/
|
|
139
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}]`;
|
|
140
148
|
/**
|
|
141
149
|
* The id-collision refusal's code — the third gate on the same `senpi runtime create` path, and
|
|
142
150
|
* plausibly the one hit most often, since re-running create with the same id lands on it.
|
|
@@ -342,6 +350,23 @@ const senpiRuntimePlugin = {
|
|
|
342
350
|
scanners: config.scanners ?? [],
|
|
343
351
|
});
|
|
344
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();
|
|
345
370
|
// Dedicated logger for the runtime-api subtree — required because the
|
|
346
371
|
// PluginLogger surface (info/warn/error) lacks `debug`, while the
|
|
347
372
|
// Logger contract used by `runtime-api/` includes it.
|
|
@@ -1015,6 +1040,7 @@ const senpiRuntimePlugin = {
|
|
|
1015
1040
|
const resolvedId = id && id.length > 0
|
|
1016
1041
|
? (sanitizeId(id) || "runtime")
|
|
1017
1042
|
: deriveRuntimeId(runtimeName, wallet);
|
|
1043
|
+
const walletKey = (wallet ?? "").trim().toLowerCase();
|
|
1018
1044
|
if (hasId(registry, resolvedId)) {
|
|
1019
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}`) };
|
|
1020
1046
|
}
|
|
@@ -1039,9 +1065,28 @@ const senpiRuntimePlugin = {
|
|
|
1039
1065
|
* both, because the registry stores `strategy.wallet` verbatim from the YAML and a
|
|
1040
1066
|
* checksummed address is the same margin account as its lowercase spelling.
|
|
1041
1067
|
*/
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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];
|
|
1045
1090
|
// Which runtime holds this wallet, from either read. The live half matches on
|
|
1046
1091
|
// `handle.wallet`, the registry half on `entry.wallet`, so the id is what they have in
|
|
1047
1092
|
// common — and it is the id, not which read matched, that the two messages differ on.
|
|
@@ -1072,197 +1117,205 @@ const senpiRuntimePlugin = {
|
|
|
1072
1117
|
`openclaw senpi runtime delete --id ${holderId}`) };
|
|
1073
1118
|
}
|
|
1074
1119
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
: new SenpiClient({ apiKey });
|
|
1084
|
-
if (!mcpTransport)
|
|
1085
|
-
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) {
|
|
1086
1128
|
try {
|
|
1087
|
-
const
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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 });
|
|
1096
1156
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
// H7/D-10: the status vocabulary lives in one module, shared with deploy's
|
|
1101
|
-
// reconcile. This gate's private copy was already missing TERMINATED and
|
|
1102
|
-
// CLOSING_DONE, so a gone strategy fell to the generic "only ACTIVE" hint.
|
|
1103
|
-
`Strategy wallet "${wallet}" (YAML field: strategy.wallet) has status "${match.status}" — a runtime requires ACTIVE status. ` +
|
|
1104
|
-
buildWalletStatusHint(match.status) +
|
|
1105
|
-
` To check the latest status, call the MCP tool: strategy_list.`) };
|
|
1157
|
+
finally {
|
|
1158
|
+
if (!mcpTransport)
|
|
1159
|
+
await client.disconnect().catch(() => { });
|
|
1106
1160
|
}
|
|
1107
|
-
pluginLogger.info("[senpi-runtime] Wallet validation passed", { wallet, strategyId: match.id, status: match.status });
|
|
1108
1161
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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 });
|
|
1112
1167
|
}
|
|
1113
1168
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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) };
|
|
1119
1183
|
}
|
|
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
|
-
|
|
1206
|
-
runtimeHandles.get(resolvedId)?.attachSupervisionSource(() => ({
|
|
1207
|
-
unwired: false,
|
|
1208
|
-
rows: hotLauncher.describeSupervision(),
|
|
1209
|
-
}));
|
|
1210
|
-
}
|
|
1211
|
-
catch (startErr) {
|
|
1212
|
-
await hotLauncher.stop().catch(() => { });
|
|
1213
|
-
unwiredRuntimes.set(resolvedId, "install_launch");
|
|
1214
|
-
unwiredPhase = "install_launch";
|
|
1215
|
-
runtimeHandles.get(resolvedId)?.attachSupervisionSource(() => ({
|
|
1216
|
-
unwired: true,
|
|
1217
|
-
unwiredPhase: "install_launch",
|
|
1218
|
-
rows: [],
|
|
1219
|
-
}));
|
|
1220
|
-
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
|
+
}
|
|
1221
1270
|
}
|
|
1222
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
|
+
}
|
|
1223
1284
|
}
|
|
1224
|
-
|
|
1225
|
-
//
|
|
1226
|
-
//
|
|
1227
|
-
|
|
1228
|
-
|
|
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";
|
|
1229
1292
|
runtimeHandles.get(resolvedId)?.attachSupervisionSource(() => ({
|
|
1230
1293
|
unwired: true,
|
|
1231
|
-
unwiredPhase: "
|
|
1294
|
+
unwiredPhase: "no_intake",
|
|
1232
1295
|
rows: [],
|
|
1233
1296
|
}));
|
|
1234
|
-
eventScannersUnwired("install_wire", resolvedId, wallet, appendCause("[senpi-runtime] hot-install wiring failed — runtime running without supervised scanners", wireErr), wireErr);
|
|
1235
1297
|
}
|
|
1298
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
1299
|
+
return {
|
|
1300
|
+
ok: true,
|
|
1301
|
+
payload: {
|
|
1302
|
+
id: resolvedId,
|
|
1303
|
+
name: runtimeName,
|
|
1304
|
+
wallet,
|
|
1305
|
+
...(unwiredPhase ? { unwired: unwiredPhase } : {}),
|
|
1306
|
+
},
|
|
1307
|
+
};
|
|
1236
1308
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
unwiredRuntimes.set(resolvedId, "no_intake");
|
|
1243
|
-
unwiredPhase = "no_intake";
|
|
1244
|
-
runtimeHandles.get(resolvedId)?.attachSupervisionSource(() => ({
|
|
1245
|
-
unwired: true,
|
|
1246
|
-
unwiredPhase: "no_intake",
|
|
1247
|
-
rows: [],
|
|
1248
|
-
}));
|
|
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}`) };
|
|
1249
1314
|
}
|
|
1250
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
1251
|
-
return {
|
|
1252
|
-
ok: true,
|
|
1253
|
-
payload: {
|
|
1254
|
-
id: resolvedId,
|
|
1255
|
-
name: runtimeName,
|
|
1256
|
-
wallet,
|
|
1257
|
-
...(unwiredPhase ? { unwired: unwiredPhase } : {}),
|
|
1258
|
-
},
|
|
1259
|
-
};
|
|
1260
1315
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1265
|
-
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);
|
|
1266
1319
|
}
|
|
1267
1320
|
}
|
|
1268
1321
|
/** Gateway respond accepts (ok, payload?, error?) with error as { code, message }; plugin SDK types may omit the third arg. */
|
|
@@ -1466,6 +1519,7 @@ const senpiRuntimePlugin = {
|
|
|
1466
1519
|
// it "running — NO ENTRY SCANNERS"), and reporting `install: skipped` over it would be the
|
|
1467
1520
|
// same clean line over a runtime that cannot tick.
|
|
1468
1521
|
runtimeIsLive: async (id) => runtimeHandles.has(id) && !unwiredRuntimes.has(id),
|
|
1522
|
+
installPendingOnWallet: async (wallet) => installsInFlight.has(wallet.trim().toLowerCase()),
|
|
1469
1523
|
logger: { info: (line) => api.logger.info(`[senpi-runtime] ${line}`) },
|
|
1470
1524
|
});
|
|
1471
1525
|
/**
|