@senpi/runtime 3.0.82 → 3.0.83
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 +33 -0
- 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 +12 -0
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/cli/update-command.d.ts +70 -0
- package/dist/cli/update-command.d.ts.map +1 -0
- package/dist/cli/update-command.js +132 -0
- package/dist/cli/update-command.js.map +1 -0
- package/dist/dsl/plugin/index.d.ts +35 -1
- package/dist/dsl/plugin/index.d.ts.map +1 -1
- package/dist/dsl/plugin/index.js +62 -7
- package/dist/dsl/plugin/index.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +86 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/effective-config.d.ts +67 -0
- package/dist/runtime/effective-config.d.ts.map +1 -0
- package/dist/runtime/effective-config.js +324 -0
- package/dist/runtime/effective-config.js.map +1 -0
- package/dist/runtime/lifecycle-events.d.ts +19 -0
- package/dist/runtime/lifecycle-events.d.ts.map +1 -1
- package/dist/runtime/lifecycle-events.js +27 -0
- package/dist/runtime/lifecycle-events.js.map +1 -1
- package/dist/runtime/rebuild-errors.d.ts +26 -0
- package/dist/runtime/rebuild-errors.d.ts.map +1 -0
- package/dist/runtime/rebuild-errors.js +36 -0
- package/dist/runtime/rebuild-errors.js.map +1 -0
- package/dist/runtime/run.d.ts +10 -0
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js +13 -1
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/runtime.d.ts +99 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +321 -127
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/update-apply-deps.d.ts +43 -0
- package/dist/runtime/update-apply-deps.d.ts.map +1 -0
- package/dist/runtime/update-apply-deps.js +62 -0
- package/dist/runtime/update-apply-deps.js.map +1 -0
- package/dist/runtime/update-handler.d.ts +158 -0
- package/dist/runtime/update-handler.d.ts.map +1 -0
- package/dist/runtime/update-handler.js +252 -0
- package/dist/runtime/update-handler.js.map +1 -0
- package/dist/runtime/update-plan.d.ts +60 -0
- package/dist/runtime/update-plan.d.ts.map +1 -0
- package/dist/runtime/update-plan.js +164 -0
- package/dist/runtime/update-plan.js.map +1 -0
- package/dist/runtime/update-register.d.ts +72 -0
- package/dist/runtime/update-register.d.ts.map +1 -0
- package/dist/runtime/update-register.js +146 -0
- package/dist/runtime/update-register.js.map +1 -0
- package/dist/runtime/update-report.d.ts +93 -0
- package/dist/runtime/update-report.d.ts.map +1 -0
- package/dist/runtime/update-report.js +133 -0
- package/dist/runtime/update-report.js.map +1 -0
- package/dist/utils/event-catalog.d.ts +26 -0
- package/dist/utils/event-catalog.d.ts.map +1 -1
- package/dist/utils/event-catalog.js +1 -0
- package/dist/utils/event-catalog.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `senpi update --dry-run` — resolve the target, read both recipes, and produce the plan.
|
|
3
|
+
*
|
|
4
|
+
* The I/O lives in injected deps so the interesting parts — which runtime, which refusals, what the
|
|
5
|
+
* report ends up saying — are testable without a gateway, a registry on disk, or a running runtime.
|
|
6
|
+
* The gateway registration is then a thin adapter, which is the same shape `senpi deploy` uses.
|
|
7
|
+
*
|
|
8
|
+
* ## Dry run only
|
|
9
|
+
*
|
|
10
|
+
* Nothing here tears anything down. That is not merely "not implemented yet": the verb's one safety
|
|
11
|
+
* property is that a refusal changes nothing, so the decision half is worth having — and shipping —
|
|
12
|
+
* before the half that can leave a strategy with no scanners and an open book.
|
|
13
|
+
*
|
|
14
|
+
* ## What it reports rather than refuses
|
|
15
|
+
*
|
|
16
|
+
* A missing or stale proof will refuse the APPLY, exactly as it refuses a deploy. On a dry run it is
|
|
17
|
+
* reported instead: the run changes nothing, so refusing costs the reader the information they came
|
|
18
|
+
* for, and they would have to fix the proof before they could even see whether the change was worth
|
|
19
|
+
* applying.
|
|
20
|
+
*
|
|
21
|
+
* Guardrail state is deliberately NOT gathered. Every gate is derived from live venue history — the
|
|
22
|
+
* per-asset cooldown alone pages through trader history until it finds a close — so collecting them
|
|
23
|
+
* turns a plan into a network round trip per gate. The report says so rather than staying silent,
|
|
24
|
+
* because silence there reads as "all clear".
|
|
25
|
+
*/
|
|
26
|
+
import { planUpdate } from "./update-plan.js";
|
|
27
|
+
import { isRollbackFailure } from "./rebuild-errors.js";
|
|
28
|
+
export const UPDATE_HANDLER_CODE = {
|
|
29
|
+
noSuchRuntime: "E_UPDATE_NO_SUCH_RUNTIME",
|
|
30
|
+
ambiguousTarget: "E_UPDATE_AMBIGUOUS_TARGET",
|
|
31
|
+
noRuntimes: "E_UPDATE_NO_RUNTIMES",
|
|
32
|
+
unreadableRecipe: "E_UPDATE_UNREADABLE_RECIPE",
|
|
33
|
+
/** Apply refused: the package carries no proof of a passing validation. */
|
|
34
|
+
proofRequired: "E_UPDATE_PROOF_REQUIRED",
|
|
35
|
+
/** `--code-only` was passed, but the recipe changed too. */
|
|
36
|
+
recipeChangedUnderCodeOnly: "E_UPDATE_RECIPE_CHANGED",
|
|
37
|
+
/** The swap failed and the runtime was restored to the recipe it was already running. */
|
|
38
|
+
rebuildFailed: "E_UPDATE_REBUILD_FAILED",
|
|
39
|
+
/** The swap failed AND the restore failed. Nothing is managing the book. */
|
|
40
|
+
unmanaged: "E_UPDATE_ROLLBACK_FAILED",
|
|
41
|
+
/** Applied to the live runtime, but not persisted — a restart would revert it. */
|
|
42
|
+
registryDiverged: "E_UPDATE_REGISTRY_DIVERGED",
|
|
43
|
+
};
|
|
44
|
+
function refusal(code, what, why, fix) {
|
|
45
|
+
return { code, severity: "error", stage: "invocation", actor: "agent", fix_kind: "command", what, why, fix };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Which runtime is meant.
|
|
49
|
+
*
|
|
50
|
+
* Inference is allowed only when there is exactly one candidate. An update rebuilds a live
|
|
51
|
+
* strategy's components, so guessing at the target is not a convenience — it is picking someone's
|
|
52
|
+
* position book at random.
|
|
53
|
+
*/
|
|
54
|
+
function resolveTarget(runtimes, req) {
|
|
55
|
+
if (runtimes.length === 0) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
refusal: refusal(UPDATE_HANDLER_CODE.noRuntimes, "No runtimes are installed on this box", "There is nothing to update. A strategy has to be deployed before its recipe can be changed.", "Deploy it first: senpi deploy -p <package-dir> --budget <usd>"),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const wanted = req.id ?? req.address;
|
|
62
|
+
if (wanted !== undefined) {
|
|
63
|
+
const match = runtimes.find((r) => r.id === req.id || (req.address !== undefined && r.wallet.toLowerCase() === req.address.toLowerCase()));
|
|
64
|
+
if (!match) {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
refusal: refusal(UPDATE_HANDLER_CODE.noSuchRuntime, `No installed runtime matches '${wanted}'`, "The update would have nothing to act on.", `Installed runtimes: ${runtimes.map((r) => r.id).join(", ")}. Check the id with: openclaw senpi runtime list`),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return { ok: true, runtime: match };
|
|
71
|
+
}
|
|
72
|
+
if (runtimes.length > 1) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
refusal: refusal(UPDATE_HANDLER_CODE.ambiguousTarget, `This box runs ${runtimes.length} runtimes and none was named`, "An update rebuilds a live strategy's components. Choosing one by guessing is choosing someone's open position book by guessing.", `Name one with --id: ${runtimes.map((r) => r.id).join(", ")}`),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return { ok: true, runtime: runtimes[0] };
|
|
79
|
+
}
|
|
80
|
+
/** The report needs a stop price and who owns it; the rest of DslState is noise here. */
|
|
81
|
+
function toReportedPosition(state) {
|
|
82
|
+
return {
|
|
83
|
+
asset: state.asset,
|
|
84
|
+
direction: state.direction,
|
|
85
|
+
entryPrice: state.entryPrice,
|
|
86
|
+
floorPrice: state.floorPrice,
|
|
87
|
+
phase: state.phase,
|
|
88
|
+
// Once handed off, the backend ratchet owns the venue stop and the runtime cannot re-configure
|
|
89
|
+
// it — which is exactly what the reader needs to know before believing a preset change reaches
|
|
90
|
+
// this position.
|
|
91
|
+
backendManaged: state.handoffStatus === "ok",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Everything both the plan and the apply must agree on: which runtime, both recipes, the gate, the
|
|
96
|
+
* report, and whatever would block an apply.
|
|
97
|
+
*
|
|
98
|
+
* Shared rather than duplicated so a plan cannot describe one thing and an apply do another — the
|
|
99
|
+
* property a test already pins by asserting the two reports differ only in `applied`.
|
|
100
|
+
*/
|
|
101
|
+
async function prepare(deps, req, applied) {
|
|
102
|
+
const target = resolveTarget(await deps.listRuntimes(), req);
|
|
103
|
+
if (!target.ok)
|
|
104
|
+
return { ok: false, refusals: [target.refusal] };
|
|
105
|
+
const runtime = target.runtime;
|
|
106
|
+
let current;
|
|
107
|
+
let next;
|
|
108
|
+
try {
|
|
109
|
+
current = await deps.parseConfig(runtime.source);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
// The recipe the runtime is RUNNING failed to parse. That is not the user's edit being wrong —
|
|
113
|
+
// it is the stored copy being unreadable, which they cannot fix by editing their package.
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
refusals: [
|
|
117
|
+
refusal(UPDATE_HANDLER_CODE.unreadableRecipe, `The recipe '${runtime.id}' is currently running could not be parsed`, "Without it there is nothing to compare the change against, so the update cannot say what it would alter.", `This is a stored-state problem rather than a problem with your edit. Reinstall the runtime: ${String(err)}`),
|
|
118
|
+
],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
next = await deps.parseConfig(req.recipe);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
refusals: [
|
|
128
|
+
refusal(UPDATE_HANDLER_CODE.unreadableRecipe, "The proposed recipe could not be parsed", "Nothing is changed; the runtime keeps running its current recipe.", `Fix the YAML and re-run: ${String(err)}`),
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// The same gate `senpi.installRuntime` applies, run BEFORE anything is torn down — which is the
|
|
133
|
+
// whole reason an update can refuse safely where a boot loop cannot.
|
|
134
|
+
const gate = await deps.installGate(await deps.recipeFindings(req.recipe));
|
|
135
|
+
if (!gate.ok)
|
|
136
|
+
return { ok: false, refusals: gate.blocking };
|
|
137
|
+
// Compared as PARSED configs rather than raw text: a reformatted comment or a changed line
|
|
138
|
+
// ending is not a change to what the strategy does, and refusing over one would teach users to
|
|
139
|
+
// stop passing the flag.
|
|
140
|
+
const recipeUnchanged = JSON.stringify(current) === JSON.stringify(next);
|
|
141
|
+
if (req.codeOnly === true && !recipeUnchanged) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
refusals: [
|
|
145
|
+
refusal(UPDATE_HANDLER_CODE.recipeChangedUnderCodeOnly, "--code-only was passed, but the recipe changed as well", "The flag asserts that only scanner code differs. Applying anyway would ship a recipe " +
|
|
146
|
+
"change the caller did not mean to make, alongside the code they did.", "Re-run without --code-only to see what the recipe change would do, or revert the recipe " +
|
|
147
|
+
"and re-run with it."),
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const plan = planUpdate({
|
|
152
|
+
runtimeId: runtime.id,
|
|
153
|
+
current,
|
|
154
|
+
next,
|
|
155
|
+
// Read BEFORE anything is swapped: these are the positions the change is being judged against.
|
|
156
|
+
openPositions: (await deps.activePositions(runtime.id)).map(toReportedPosition),
|
|
157
|
+
closedGates: [],
|
|
158
|
+
applied,
|
|
159
|
+
});
|
|
160
|
+
if (!plan.ok)
|
|
161
|
+
return { ok: false, refusals: plan.refusals };
|
|
162
|
+
const packageDir = req.recipe.dir ?? (req.recipe.path !== undefined ? req.recipe.path : undefined);
|
|
163
|
+
const applyBlockers = [];
|
|
164
|
+
if (packageDir !== undefined) {
|
|
165
|
+
const proof = await deps.verifyProof(packageDir);
|
|
166
|
+
if (!proof.ok && proof.finding)
|
|
167
|
+
applyBlockers.push(proof.finding);
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
runtime,
|
|
172
|
+
next,
|
|
173
|
+
plan: { ok: true, report: { ...plan.report, applyBlockers, recipeUnchanged } },
|
|
174
|
+
applyBlockers,
|
|
175
|
+
recipeUnchanged,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export async function runUpdateDryRun(deps, req) {
|
|
179
|
+
const prepared = await prepare(deps, req, false);
|
|
180
|
+
if (!prepared.ok)
|
|
181
|
+
return { ok: false, refusals: prepared.refusals };
|
|
182
|
+
return { ok: true, plan: prepared.plan };
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* `senpi update --apply` — plan it, then actually do it.
|
|
186
|
+
*
|
|
187
|
+
* Every refusal the dry run can produce applies here unchanged and still changes nothing. Beyond
|
|
188
|
+
* those, one refusal exists only on this path: a missing or stale proof. A dry run reports it,
|
|
189
|
+
* because refusing would cost the reader the information they came for; an apply enforces it,
|
|
190
|
+
* because otherwise the blocker the dry run printed was decorative.
|
|
191
|
+
*/
|
|
192
|
+
export async function runUpdateApply(deps, req) {
|
|
193
|
+
const prepared = await prepare(deps, req, true);
|
|
194
|
+
if (!prepared.ok)
|
|
195
|
+
return { ok: false, stage: "refused", refusals: prepared.refusals };
|
|
196
|
+
// The proof gate. `senpi validate` writes the proof, and an update is a change to the same bytes
|
|
197
|
+
// a deploy would have had to validate, so applying without one would let an update install
|
|
198
|
+
// something `senpi deploy` would have refused.
|
|
199
|
+
if (prepared.applyBlockers.length > 0) {
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
stage: "refused",
|
|
203
|
+
refusals: prepared.applyBlockers.map((blocker) => ({
|
|
204
|
+
...blocker,
|
|
205
|
+
code: blocker.code,
|
|
206
|
+
what: blocker.what,
|
|
207
|
+
why: `${blocker.why} Nothing has been changed — the runtime keeps running its current recipe.`,
|
|
208
|
+
})),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
await deps.applyToRuntime(prepared.runtime.id, prepared.next, req.recipe);
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
// The runtime distinguishes the two failure shapes, and they need different reactions: one is a
|
|
216
|
+
// failed change on a healthy runtime, the other is an open book with nothing watching it.
|
|
217
|
+
if (isRollbackFailure(err)) {
|
|
218
|
+
return {
|
|
219
|
+
ok: false,
|
|
220
|
+
stage: "failed",
|
|
221
|
+
refusals: [
|
|
222
|
+
refusal(UPDATE_HANDLER_CODE.unmanaged, `Runtime '${prepared.runtime.id}' is DOWN and its positions are unmanaged`, "The updated recipe failed to build and the previous one could not be restored, so no " +
|
|
223
|
+
"exit engine is running. Open positions are not being watched and no stop is being maintained.", `Restart the runtime, then check this wallet's positions at the venue: ${err.address}`),
|
|
224
|
+
],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
stage: "failed",
|
|
230
|
+
refusals: [
|
|
231
|
+
refusal(UPDATE_HANDLER_CODE.rebuildFailed, `The update to '${prepared.runtime.id}' could not be applied`, `The runtime was restored to the recipe it was already running and is still trading on it. ${String(err)}`, "Fix the recipe and re-run. Nothing about the running strategy has changed."),
|
|
232
|
+
],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
await deps.writeRegistry(prepared.runtime.id, req.recipe);
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
// The change IS live. What failed is the record of it, which is what a restart replays — so
|
|
240
|
+
// this cannot be reported as success, and it cannot be reported as "nothing happened" either.
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
stage: "failed",
|
|
244
|
+
refusals: [
|
|
245
|
+
refusal(UPDATE_HANDLER_CODE.registryDiverged, `Runtime '${prepared.runtime.id}' is running the new recipe, but it was not recorded`, `The swap succeeded and the strategy is trading the updated recipe now. Saving it as the ` +
|
|
246
|
+
`recipe to replay failed, so a restart would silently revert to the previous one. ${String(err)}`, "Re-run the update to record it. Do not restart the runtime until it succeeds."),
|
|
247
|
+
],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return { ok: true, plan: prepared.plan };
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=update-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-handler.js","sourceRoot":"","sources":["../../src/runtime/update-handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether a recipe change can be applied in place, and what it would do.
|
|
3
|
+
*
|
|
4
|
+
* Pure: the caller supplies the two recipes and the runtime facts, and gets back either a set of
|
|
5
|
+
* refusals or the report an update would produce. Nothing here reads disk, the venue, or the
|
|
6
|
+
* registry, so the whole decision is testable without standing a runtime up — and the same function
|
|
7
|
+
* backs both `--dry-run` and the real apply, which is what makes their output identical by
|
|
8
|
+
* construction rather than by discipline.
|
|
9
|
+
*
|
|
10
|
+
* ## Why refusals are computed BEFORE anything is torn down
|
|
11
|
+
*
|
|
12
|
+
* The update verb's one safety property is that a refusal changes nothing. The install gate exists
|
|
13
|
+
* but runs at install only, deliberately — the boot loop starts every runtime inside one `try`, so
|
|
14
|
+
* refusing there would stop every other strategy on the box over one bad recipe. An update has no
|
|
15
|
+
* such constraint and every reason to refuse loudly, because the alternative is a strategy left
|
|
16
|
+
* with no scanners and no actions but an open book.
|
|
17
|
+
*
|
|
18
|
+
* ## What counts as "not an update"
|
|
19
|
+
*
|
|
20
|
+
* Three changes are structural rather than configural: they would silently orphan state that the
|
|
21
|
+
* runtime keys on. They refuse with the remedy named, rather than proceeding and reporting the
|
|
22
|
+
* damage afterwards.
|
|
23
|
+
*/
|
|
24
|
+
import type { RuntimeConfig } from "./runtime-schema.js";
|
|
25
|
+
import type { Finding } from "../validate/types.js";
|
|
26
|
+
import { type ReportedGate, type ReportedPosition, type UpdateReport } from "./update-report.js";
|
|
27
|
+
/** Codes this module raises. Distinct from validate's set: these are update-time decisions. */
|
|
28
|
+
export declare const UPDATE_CODE: {
|
|
29
|
+
readonly walletChanged: "E_UPDATE_WALLET_CHANGED";
|
|
30
|
+
readonly scannerRenamed: "E_UPDATE_SCANNER_RENAMED";
|
|
31
|
+
readonly scannerReordered: "E_UPDATE_SCANNER_REORDERED";
|
|
32
|
+
readonly actionTypeChanged: "E_UPDATE_ACTION_TYPE_CHANGED";
|
|
33
|
+
};
|
|
34
|
+
export interface UpdatePlanInputs {
|
|
35
|
+
runtimeId: string;
|
|
36
|
+
/** The recipe the runtime is running now, as stored in the registry. */
|
|
37
|
+
current: RuntimeConfig;
|
|
38
|
+
/** The recipe being proposed. */
|
|
39
|
+
next: RuntimeConfig;
|
|
40
|
+
openPositions: readonly ReportedPosition[];
|
|
41
|
+
closedGates: readonly ReportedGate[];
|
|
42
|
+
/** False produces the identical report minus the claim that anything happened. */
|
|
43
|
+
applied: boolean;
|
|
44
|
+
}
|
|
45
|
+
export type UpdatePlan = {
|
|
46
|
+
ok: true;
|
|
47
|
+
report: UpdateReport;
|
|
48
|
+
} | {
|
|
49
|
+
ok: false;
|
|
50
|
+
refusals: Finding[];
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* The whole decision, in one call.
|
|
54
|
+
*
|
|
55
|
+
* Refusals are returned rather than thrown: a caller must be able to render several at once, and an
|
|
56
|
+
* exception would stop at the first — leaving an agent to fix one problem, re-run, and discover the
|
|
57
|
+
* next, which is the loop this codebase's finding model exists to avoid.
|
|
58
|
+
*/
|
|
59
|
+
export declare function planUpdate(inputs: UpdatePlanInputs): UpdatePlan;
|
|
60
|
+
//# sourceMappingURL=update-plan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-plan.d.ts","sourceRoot":"","sources":["../../src/runtime/update-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAEpD,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAClB,MAAM,oBAAoB,CAAC;AAE5B,+FAA+F;AAC/F,eAAO,MAAM,WAAW;;;;;CAKd,CAAC;AAEX,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,OAAO,EAAE,aAAa,CAAC;IACvB,iCAAiC;IACjC,IAAI,EAAE,aAAa,CAAC;IACpB,aAAa,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC3C,WAAW,EAAE,SAAS,YAAY,EAAE,CAAC;IACrC,kFAAkF;IAClF,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,GAClC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;CAAE,CAAC;AAoIvC;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,gBAAgB,GAAG,UAAU,CAiB/D"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether a recipe change can be applied in place, and what it would do.
|
|
3
|
+
*
|
|
4
|
+
* Pure: the caller supplies the two recipes and the runtime facts, and gets back either a set of
|
|
5
|
+
* refusals or the report an update would produce. Nothing here reads disk, the venue, or the
|
|
6
|
+
* registry, so the whole decision is testable without standing a runtime up — and the same function
|
|
7
|
+
* backs both `--dry-run` and the real apply, which is what makes their output identical by
|
|
8
|
+
* construction rather than by discipline.
|
|
9
|
+
*
|
|
10
|
+
* ## Why refusals are computed BEFORE anything is torn down
|
|
11
|
+
*
|
|
12
|
+
* The update verb's one safety property is that a refusal changes nothing. The install gate exists
|
|
13
|
+
* but runs at install only, deliberately — the boot loop starts every runtime inside one `try`, so
|
|
14
|
+
* refusing there would stop every other strategy on the box over one bad recipe. An update has no
|
|
15
|
+
* such constraint and every reason to refuse loudly, because the alternative is a strategy left
|
|
16
|
+
* with no scanners and no actions but an open book.
|
|
17
|
+
*
|
|
18
|
+
* ## What counts as "not an update"
|
|
19
|
+
*
|
|
20
|
+
* Three changes are structural rather than configural: they would silently orphan state that the
|
|
21
|
+
* runtime keys on. They refuse with the remedy named, rather than proceeding and reporting the
|
|
22
|
+
* damage afterwards.
|
|
23
|
+
*/
|
|
24
|
+
import { resolveEffectiveConfig } from "./effective-config.js";
|
|
25
|
+
import { buildUpdateReport, } from "./update-report.js";
|
|
26
|
+
/** Codes this module raises. Distinct from validate's set: these are update-time decisions. */
|
|
27
|
+
export const UPDATE_CODE = {
|
|
28
|
+
walletChanged: "E_UPDATE_WALLET_CHANGED",
|
|
29
|
+
scannerRenamed: "E_UPDATE_SCANNER_RENAMED",
|
|
30
|
+
scannerReordered: "E_UPDATE_SCANNER_REORDERED",
|
|
31
|
+
actionTypeChanged: "E_UPDATE_ACTION_TYPE_CHANGED",
|
|
32
|
+
};
|
|
33
|
+
function refusal(code, parts) {
|
|
34
|
+
return {
|
|
35
|
+
code,
|
|
36
|
+
severity: "error",
|
|
37
|
+
stage: "static",
|
|
38
|
+
actor: "agent",
|
|
39
|
+
fix_kind: "edit",
|
|
40
|
+
...(parts.yamlPath ? { where: { yaml_path: parts.yamlPath } } : {}),
|
|
41
|
+
what: parts.what,
|
|
42
|
+
why: parts.why,
|
|
43
|
+
fix: parts.fix,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function scannerNames(config) {
|
|
47
|
+
return (config.scanners ?? []).map((s) => String(s.name));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Structural changes: the ones that orphan state the runtime keys on, rather than merely changing
|
|
51
|
+
* behaviour. Each names what would be orphaned, because "refused" without that is a dead end.
|
|
52
|
+
*/
|
|
53
|
+
function structuralRefusals(current, next) {
|
|
54
|
+
const out = [];
|
|
55
|
+
const currentWallet = String(current.strategy.wallet ?? "");
|
|
56
|
+
const nextWallet = String(next.strategy.wallet ?? "");
|
|
57
|
+
if (currentWallet !== nextWallet) {
|
|
58
|
+
out.push(refusal(UPDATE_CODE.walletChanged, {
|
|
59
|
+
yamlPath: "strategy.wallet",
|
|
60
|
+
what: `The recipe changes strategy.wallet from ${currentWallet} to ${nextWallet}`,
|
|
61
|
+
why: "The wallet is the deployment's identity: the state directory is named for it, the backend " +
|
|
62
|
+
"strategy is it, and the venue book is it. Nothing on disk or at the venue carries across, " +
|
|
63
|
+
"so this is a new deployment wearing an old name — and the old wallet's positions would be " +
|
|
64
|
+
"left running with nothing managing them.",
|
|
65
|
+
fix: `Deploy this as a new strategy, and close the book on ${currentWallet} explicitly.`,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
// Scanner state lives at `scanner-state/{name}-{index}` for external scanners, so BOTH the name
|
|
69
|
+
// and the position in the array are load-bearing. A rename or a reorder silently repoints a
|
|
70
|
+
// scanner at an empty directory — losing cached cohorts, first-seen ledgers and dedup rings with
|
|
71
|
+
// no error anywhere.
|
|
72
|
+
const before = scannerNames(current);
|
|
73
|
+
const after = scannerNames(next);
|
|
74
|
+
const beforeSet = new Set(before);
|
|
75
|
+
const afterSet = new Set(after);
|
|
76
|
+
const removed = before.filter((n) => !afterSet.has(n));
|
|
77
|
+
const added = after.filter((n) => !beforeSet.has(n));
|
|
78
|
+
if (removed.length > 0 && added.length > 0) {
|
|
79
|
+
out.push(refusal(UPDATE_CODE.scannerRenamed, {
|
|
80
|
+
yamlPath: "scanners",
|
|
81
|
+
what: `Scanner ${removed.join(", ")} is gone and ${added.join(", ")} is new`,
|
|
82
|
+
why: "Scanner state is keyed on the scanner's name, so a rename starts it from empty — cached " +
|
|
83
|
+
"cohorts, first-seen ledgers and rotation state are all lost, silently. If this really is " +
|
|
84
|
+
"a rename rather than a replacement, the state should move with it.",
|
|
85
|
+
fix: `Keep the name \`${removed[0]}\`, or accept the reset explicitly by deleting its state ` +
|
|
86
|
+
`directory first.`,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
// Same set, different order: only reordering can produce this, and it moves the index half of
|
|
91
|
+
// the key while leaving the name half intact — the subtler of the two failures.
|
|
92
|
+
const common = before.filter((n) => afterSet.has(n));
|
|
93
|
+
const commonAfter = after.filter((n) => beforeSet.has(n));
|
|
94
|
+
if (common.length === commonAfter.length && common.some((n, i) => n !== commonAfter[i])) {
|
|
95
|
+
out.push(refusal(UPDATE_CODE.scannerReordered, {
|
|
96
|
+
yamlPath: "scanners",
|
|
97
|
+
what: `The scanners block is reordered: ${common.join(", ")} → ${commonAfter.join(", ")}`,
|
|
98
|
+
why: "External scanner state is keyed on the array index as well as the name, so reordering " +
|
|
99
|
+
"repoints each moved scanner at another's directory or at an empty one. It also changes " +
|
|
100
|
+
"the staggered start order.",
|
|
101
|
+
fix: "Restore the original order. Adding a scanner at the END of the block is safe.",
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Action state lives at `strategies/{address}/actions/{name}/`, so a type change under a stable
|
|
106
|
+
// name hands a different action class the previous one's persisted state.
|
|
107
|
+
const currentByName = new Map((current.actions ?? []).map((a) => [String(a.name), a]));
|
|
108
|
+
for (const action of next.actions ?? []) {
|
|
109
|
+
const name = String(action.name);
|
|
110
|
+
const prior = currentByName.get(name);
|
|
111
|
+
if (!prior)
|
|
112
|
+
continue;
|
|
113
|
+
const before_ = String(prior.action_type);
|
|
114
|
+
const after_ = String(action.action_type);
|
|
115
|
+
if (before_ === after_)
|
|
116
|
+
continue;
|
|
117
|
+
out.push(refusal(UPDATE_CODE.actionTypeChanged, {
|
|
118
|
+
yamlPath: `actions.${name}.action_type`,
|
|
119
|
+
what: `Action '${name}' changes type from ${before_} to ${after_}`,
|
|
120
|
+
why: "Action state is keyed on the action's name, so the new class would inherit the previous " +
|
|
121
|
+
"one's latest.json and execution history — records written by different code about a " +
|
|
122
|
+
"different thing.",
|
|
123
|
+
fix: `Give the ${after_} action a new name, or reset '${name}' state explicitly.`,
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
/** Did the exit ladder change? Drives the report's forward-only wording. */
|
|
129
|
+
function exitPresetChanged(current, next) {
|
|
130
|
+
const a = current.exit?.dsl_preset;
|
|
131
|
+
const b = next.exit?.dsl_preset;
|
|
132
|
+
return JSON.stringify(a ?? null) !== JSON.stringify(b ?? null);
|
|
133
|
+
}
|
|
134
|
+
/** `slots` as the recipe declares it; the report needs the authored value, not the effective one. */
|
|
135
|
+
function slotsOf(config) {
|
|
136
|
+
const raw = config.strategy.slots;
|
|
137
|
+
return typeof raw === "number" && Number.isFinite(raw) ? raw : undefined;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The whole decision, in one call.
|
|
141
|
+
*
|
|
142
|
+
* Refusals are returned rather than thrown: a caller must be able to render several at once, and an
|
|
143
|
+
* exception would stop at the first — leaving an agent to fix one problem, re-run, and discover the
|
|
144
|
+
* next, which is the loop this codebase's finding model exists to avoid.
|
|
145
|
+
*/
|
|
146
|
+
export function planUpdate(inputs) {
|
|
147
|
+
const refusals = structuralRefusals(inputs.current, inputs.next);
|
|
148
|
+
if (refusals.length > 0)
|
|
149
|
+
return { ok: false, refusals };
|
|
150
|
+
return {
|
|
151
|
+
ok: true,
|
|
152
|
+
report: buildUpdateReport({
|
|
153
|
+
runtimeId: inputs.runtimeId,
|
|
154
|
+
applied: inputs.applied,
|
|
155
|
+
before: resolveEffectiveConfig(inputs.current),
|
|
156
|
+
after: resolveEffectiveConfig(inputs.next),
|
|
157
|
+
openPositions: inputs.openPositions,
|
|
158
|
+
closedGates: inputs.closedGates,
|
|
159
|
+
exitPresetChanged: exitPresetChanged(inputs.current, inputs.next),
|
|
160
|
+
...(slotsOf(inputs.next) !== undefined ? { slots: slotsOf(inputs.next) } : {}),
|
|
161
|
+
}),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=update-plan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-plan.js","sourceRoot":"","sources":["../../src/runtime/update-plan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway wiring for `senpi update`.
|
|
3
|
+
*
|
|
4
|
+
* `senpi.update` is **internal transport** between the CLI verb and the plugin, deliberately absent
|
|
5
|
+
* from the cheatsheet, the README's agent-facing sections and the skills — the same treatment
|
|
6
|
+
* `senpi.deploy.*` gets, and for the same reason: one teaching surface wins routing, and naming the
|
|
7
|
+
* RPC too would invite raw calls that bypass the CLI-side validation.
|
|
8
|
+
*
|
|
9
|
+
* Named `senpi.update` rather than `senpi.update.start`: `.start` belongs to a verb with a matching
|
|
10
|
+
* `.status`, because deploy has to detach around multi-minute wallet funding. An update moves no
|
|
11
|
+
* money and has nothing to wait on, so it answers in one call.
|
|
12
|
+
*
|
|
13
|
+
* The module owns no logic beyond adapting a gateway context into the handler's dependencies, so
|
|
14
|
+
* everything worth testing lives in `update-handler.ts` and tests never drive a gateway.
|
|
15
|
+
*/
|
|
16
|
+
import type { Finding } from "../validate/types.js";
|
|
17
|
+
import type { DslState } from "../types/dsl/index.js";
|
|
18
|
+
import type { RuntimeConfig } from "./runtime-schema.js";
|
|
19
|
+
import { type RecipeSource, type RegisteredRuntime } from "./update-handler.js";
|
|
20
|
+
/** The plugin surface this needs. Explicit, so the module never reaches into the boot closure. */
|
|
21
|
+
export interface UpdateGatewayContext {
|
|
22
|
+
listRuntimes(): Promise<RegisteredRuntime[]>;
|
|
23
|
+
parseConfig(source: RecipeSource): Promise<RuntimeConfig>;
|
|
24
|
+
activePositions(runtimeId: string): Promise<DslState[]>;
|
|
25
|
+
recipeFindings(source: RecipeSource): Promise<Finding[]>;
|
|
26
|
+
installGate(findings: Finding[]): Promise<{
|
|
27
|
+
ok: boolean;
|
|
28
|
+
blocking: Finding[];
|
|
29
|
+
}>;
|
|
30
|
+
verifyProof(packageDir: string): Promise<{
|
|
31
|
+
ok: boolean;
|
|
32
|
+
finding?: Finding;
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* Swap a running runtime onto the new recipe, and record it.
|
|
36
|
+
*
|
|
37
|
+
* Optional: a gateway with no live runtimes to mutate (or a deployment that has not enabled the
|
|
38
|
+
* apply) simply omits them, and `dryRun: false` is refused as unsupported rather than silently
|
|
39
|
+
* planning. Present together or not at all — recording without swapping, or the reverse, is not a
|
|
40
|
+
* coherent half-state to offer.
|
|
41
|
+
*/
|
|
42
|
+
applyToRuntime?(runtimeId: string, next: RuntimeConfig, recipe: RecipeSource): Promise<void>;
|
|
43
|
+
writeRegistry?(runtimeId: string, recipe: RecipeSource): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Called once an apply has FULLY succeeded — swapped and recorded.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately not called for a swap whose registry write failed: that outcome is reported as a
|
|
48
|
+
* failure, and an audit trail saying a strategy was updated when a restart would revert it is
|
|
49
|
+
* worse than no entry at all. Telemetry only; it must never fail the apply, so the implementation
|
|
50
|
+
* is expected to guard itself.
|
|
51
|
+
*/
|
|
52
|
+
onApplied?(info: {
|
|
53
|
+
runtimeId: string;
|
|
54
|
+
address?: string;
|
|
55
|
+
recipe: RecipeSource;
|
|
56
|
+
previousRecipe: RecipeSource;
|
|
57
|
+
exitPresetChanged: boolean;
|
|
58
|
+
openPositions: number;
|
|
59
|
+
slots?: number;
|
|
60
|
+
}): void;
|
|
61
|
+
}
|
|
62
|
+
interface GatewayApi {
|
|
63
|
+
registerGatewayMethod(name: string, handler: (ctx: {
|
|
64
|
+
params?: Record<string, unknown>;
|
|
65
|
+
respond: (ok: boolean, payload?: unknown, error?: unknown) => void;
|
|
66
|
+
}) => Promise<void>, opts?: {
|
|
67
|
+
scope?: string;
|
|
68
|
+
}): void;
|
|
69
|
+
}
|
|
70
|
+
export declare function registerUpdateGatewayMethod(api: GatewayApi, ctxDeps: UpdateGatewayContext): void;
|
|
71
|
+
export {};
|
|
72
|
+
//# sourceMappingURL=update-register.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-register.d.ts","sourceRoot":"","sources":["../../src/runtime/update-register.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACvB,MAAM,qBAAqB,CAAC;AAG7B,kGAAkG;AAClG,MAAM,WAAW,oBAAoB;IACnC,YAAY,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;IAC7C,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IAC1D,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IACxD,cAAc,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IACzD,WAAW,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC,CAAC;IAChF,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC7E;;;;;;;OAOG;IACH,cAAc,CAAC,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7F,aAAa,CAAC,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE;;;;;;;OAOG;IACH,SAAS,CAAC,CAAC,IAAI,EAAE;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,YAAY,CAAC;QACrB,cAAc,EAAE,YAAY,CAAC;QAC7B,iBAAiB,EAAE,OAAO,CAAC;QAC3B,aAAa,EAAE,MAAM,CAAC;QACtB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,GAAG,IAAI,CAAC;CACV;AAED,UAAU,UAAU;IAClB,qBAAqB,CACnB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,CAAC,GAAG,EAAE;QACb,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACjC,OAAO,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;KACpE,KAAK,OAAO,CAAC,IAAI,CAAC,EACnB,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GACxB,IAAI,CAAC;CACT;AAiCD,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAuHhG"}
|