/** * `senpi update --dry-run` — resolve the target, read both recipes, and produce the plan. * * The I/O lives in injected deps so the interesting parts — which runtime, which refusals, what the * report ends up saying — are testable without a gateway, a registry on disk, or a running runtime. * The gateway registration is then a thin adapter, which is the same shape `senpi deploy` uses. * * ## Dry run only * * Nothing here tears anything down. That is not merely "not implemented yet": the verb's one safety * property is that a refusal changes nothing, so the decision half is worth having — and shipping — * before the half that can leave a strategy with no scanners and an open book. * * ## What it reports rather than refuses * * A missing or stale proof will refuse the APPLY, exactly as it refuses a deploy. On a dry run it is * reported instead: the run changes nothing, so refusing costs the reader the information they came * for, and they would have to fix the proof before they could even see whether the change was worth * applying. * * Guardrail state is deliberately NOT gathered. Every gate is derived from live venue history — the * per-asset cooldown alone pages through trader history until it finds a close — so collecting them * turns a plan into a network round trip per gate. The report says so rather than staying silent, * because silence there reads as "all clear". */ import type { RuntimeConfig } from "./runtime-schema.js"; import type { DslState } from "../types/dsl/index.js"; import type { Finding } from "../validate/types.js"; import { type UpdatePlan } from "./update-plan.js"; /** Where a recipe is being read from — mirrors `senpi.installRuntime`'s parameter shape. */ export interface RecipeSource { path?: string; content?: string; /** Directory relative scanner paths resolve against, when reading content. */ dir?: string; } /** One installed runtime, as the registry knows it. */ export interface RegisteredRuntime { id: string; wallet: string; /** Where the recipe it is CURRENTLY running came from. */ source: RecipeSource; } export interface UpdateHandlerDeps { /** Every installed runtime. Resolution and the ambiguity refusal are decided here, not by the caller. */ listRuntimes(): Promise; parseConfig(source: RecipeSource): Promise; /** Active DSL states for a runtime; empty when it has no DSL or is not currently running. */ activePositions(runtimeId: string): Promise; /** Static recipe findings, for the install gate. */ recipeFindings(source: RecipeSource): Promise; /** * Blocking subset of those findings. Async only because `index.ts` loads the validate modules * lazily to keep the boot path light — the gate itself is pure. */ installGate(findings: Finding[]): Promise<{ ok: boolean; blocking: Finding[]; }>; /** * Pure observation; never enforced here. * * `appliedRecipe` is the recipe text as the CALLER supplied it, before the wallet placeholder is * bound — the proof describes bytes on disk, and binding happens after, so the bound form would * never match a package that uses `${WALLET}`. Passing it lets the check answer the question that * matters: does the proof cover the recipe about to be applied, or merely some recipe that lives * in the same directory? */ verifyProof(packageDir: string, appliedRecipe?: string): Promise<{ ok: boolean; finding?: Finding; }>; } export interface UpdateRequest { /** Explicit target. Omit both when the box runs exactly one runtime. */ id?: string; address?: string; recipe: RecipeSource; /** * "I changed only scanner code, not the recipe." * * A guard rather than a shortcut: an apply reloads scanner code either way, because the scanner * module is rebuilt and the apply restarts every external scanner, which re-reads its entrypoint * at spawn. What this adds is * the refusal when the claim is false — an edit that also touched the recipe is exactly the one a * user did not mean to ship, and without the flag it would apply silently alongside the code. */ codeOnly?: boolean; } export type UpdateOutcome = { ok: true; plan: Extract; } | { ok: false; refusals: Finding[]; }; export declare const UPDATE_HANDLER_CODE: { readonly noSuchRuntime: "E_UPDATE_NO_SUCH_RUNTIME"; readonly ambiguousTarget: "E_UPDATE_AMBIGUOUS_TARGET"; readonly noRuntimes: "E_UPDATE_NO_RUNTIMES"; readonly unreadableRecipe: "E_UPDATE_UNREADABLE_RECIPE"; /** Apply refused: the package carries no proof of a passing validation. */ readonly proofRequired: "E_UPDATE_PROOF_REQUIRED"; /** * Apply refused: a proof exists, but it covers different bytes than the ones being applied. * * Distinct from {@link proofRequired} because the fix differs — there IS a passing validation, it * just does not describe this recipe, so the caller has to reconcile the two rather than run * validate for the first time. */ readonly proofCoversOtherBytes: "E_UPDATE_PROOF_MISMATCH"; /** `--code-only` was passed, but the recipe changed too. */ readonly recipeChangedUnderCodeOnly: "E_UPDATE_RECIPE_CHANGED"; /** The proposed recipe still carries a `${VAR}` that nothing in the environment resolves. */ readonly unresolvedPlaceholder: "E_UPDATE_UNRESOLVED_PLACEHOLDER"; /** The swap failed and the runtime was restored to the recipe it was already running. */ readonly rebuildFailed: "E_UPDATE_REBUILD_FAILED"; /** The swap failed AND the restore failed. Nothing is managing the book. */ readonly unmanaged: "E_UPDATE_ROLLBACK_FAILED"; /** Applied to the live runtime, but not persisted — a restart would revert it. */ readonly registryDiverged: "E_UPDATE_REGISTRY_DIVERGED"; }; export declare function runUpdateDryRun(deps: UpdateHandlerDeps, req: UpdateRequest): Promise; /** What an apply needs on top of the plan: something to swap, and somewhere to record it. */ export interface UpdateApplyDeps extends UpdateHandlerDeps { /** * Swap the running runtime onto the new recipe. * * Throws on failure. Recovery is the runtime's job, not this handler's: `rebuildComponents` does * its fallible work before tearing anything down and rolls back to the previous recipe if the * swap itself fails, so a throw here means the runtime is either back on its old recipe or — for * a `RuntimeRebuildRollbackFailedError` — down with an unmanaged book. */ /** * Swap the runtime onto the new recipe and restart its external scanners. * * Resolves with `scannersUnwired` set when the recipe DID apply but the scanner children could * not be restarted. That is not a failed apply — the new configuration is live in-process — but * it is not a clean one either: the outgoing children were already stopped, so a strategy whose * entries come from an external scanner has silently stopped taking them. Reporting it as success * with no qualification is how a trading halt gets mistaken for a normal day. */ applyToRuntime(runtimeId: string, next: RuntimeConfig, recipe: RecipeSource): Promise; /** * Persist the recipe a restart should replay. * * Called ONLY after the swap succeeded. The two writes cannot be atomic, and this order is the * deliberate one: if this fails, a restart reverts to a recipe known to work, where the reverse * order would have a restart apply a recipe that had just failed to build. */ writeRegistry(runtimeId: string, recipe: RecipeSource): Promise; } /** * The outcome of an apply. * * Three arms rather than two, because "nothing happened" and "something happened and then broke" * need different responses from whoever is reading. `refused` means the runtime is untouched and * the caller should fix their recipe; `failed` means an apply was attempted, and the finding says * what state the runtime is in now. */ export type UpdateApplyOutcome = { ok: true; plan: Extract; /** * Exactly what was written to the registry. * * The audit event hashes THIS rather than the request: `runtime.updated`'s * `senpi.config.hash` is meant to be comparable with the one `runtime.started` stamps on the * next boot, and the next boot reads the stored bytes. Hashing the unbound request would * make an update look like a different recipe from the one it installed. */ stored?: RecipeSource; } | { ok: false; stage: "refused"; refusals: Finding[]; } | { ok: false; stage: "failed"; refusals: Finding[]; }; /** * `senpi update --apply` — plan it, then actually do it. * * Every refusal the dry run can produce applies here unchanged and still changes nothing. Beyond * those, one refusal exists only on this path: a missing or stale proof. A dry run reports it, * because refusing would cost the reader the information they came for; an apply enforces it, * because otherwise the blocker the dry run printed was decorative. */ export declare function runUpdateApply(deps: UpdateApplyDeps, req: UpdateRequest): Promise; //# sourceMappingURL=update-handler.d.ts.map