/** * The unconditional destruction gate — runs at the read→mutate boundary of * every application deploy, independent of the approval gate. No flag can * disengage it: `autoApprove`, `skipConfirmation`, `force`, and approval * tokens never satisfy it (drift-remediation plan § 1.2 / § 4). * * Ticketing is stateful/pinned-only (the § 9 ceremony-fatigue defence): * - a replacement that loses data → replacement-required * - a replacement of a custom-named * resource (CloudFormation will REFUSE) → pinned-name-replacement * - a delete of a stateful resource → data-loss-delete * Stateless unpinned replacements and stateless deletes never ticket — the * plan renderer and approval gate already surface them. * * Consent = exact, case-sensitive `{physicalName, verb}` pairs covering EVERY * ticket resource. Surplus, misspelt, or wrong-verb consents fail closed with * a per-name verdict table [G6]. `recreate` on unpinned findings executes as * CloudFormation's native replacement / the consented delete; `recreate` on a * pinned-name row runs the orchestrated snapshot-flip surgery * (`runRecreateSurgery` — the 3f pre-bootstrap block); `pin` proceeds into * the pre-bootstrap pin arm (live-value probe + codemod offer — the deploy * stops for config write-back, nothing is applied). Verbs without an * execution arm (`forget` deploy-side, `repair`) withhold with a clear * message rather than burning a doomed run. */ import { type Result } from "@fjall/generator"; import type { DeployCallbacks } from "../../types/callbacks.js"; import type { DeployOptions } from "../../types/params.js"; import type { ConsentVerdictRow, DestructionConsent, DestructionConsentGate, DestructionConsentReport, DestructionFinding, DestructionOutcome, DestructionTicket, DestructionTicketResource, RemediationVerb } from "../../types/destruction.js"; import type { DeployPlanLoader } from "./plan/loadDeployPlan.js"; import type { DeployPlan } from "./plan/types.js"; export declare function legalRemediationVerbs(finding: DestructionFinding): readonly RemediationVerb[]; export declare function executableRemediationVerbs(finding: DestructionFinding): readonly RemediationVerb[]; /** * [G11]/[H7] The verbs a surface may ADVERTISE for one ticket row — the * row's legal vocabulary reduced to exactly what the gates would let * execute. Advertising anything wider sells a consent the execution check * (`collectExecutionHalts` here, the mirrored check in `runDriftRepairGate`) * is guaranteed to halt; the operator types the ceremony, then the gate * withholds — a dead-end loop the menu itself caused. The filter mirrors the * halt quantifiers exactly: * - `abort` always advertises — a matched abort is an operator decline, * never an execution halt; * - every other verb must be in `executableRemediationVerbs(finding)`; * - `pin` additionally needs `pinProbeCoverage` over the offending * properties (the v1 probe table). * The gate halts stay as the backstop for hand-typed consents; the * executable set and probe coverage live here (deploy-core) so no surface * re-derives them. Consumers: the CLI's `--remediate` re-run hint, the * interactive ceremony's verb menu, the agent ticket blocks and the shared * ticket-row renderer below. */ export declare function advertisableRemediationVerbs(resource: Pick): RemediationVerb[]; /** * `hash(assemblyDigest + canonical resource set)` — the TOCTOU binding for * consents minted before apply time. Rows are keyed and sorted so the digest * is independent of plan ordering; verbs are deliberately excluded (a verb * vocabulary change must not invalidate a consent to the same resource set). */ export declare function computeTicketDigest(assemblyDigest: string, resources: ReadonlyArray>): string; /** * Build the destruction ticket from an overlay-corrected plan. Returns * undefined when the plan contains no ticketable rows (the common case). * `physicalName` is the template-pinned literal when one exists; otherwise * the `remediationPlaceholderPhysicalId` address — still an exact, * unambiguous string the operator can copy from the rendered ticket, and * shared-source with the drift-repair ticket's fallback so the same resource * reads identically on both tickets in the blocked-deploy → repair flow. */ export declare function buildDestructionTicket(plan: DeployPlan, verifiedAbsentByNecessity?: ReadonlyArray<{ stackName: string; logicalId: string; }>): DestructionTicket | undefined; export interface EvaluateDestructionConsentsParams { ticket: DestructionTicket; consents: readonly DestructionConsent[]; /** * The ticket digest the consents were minted against (webapp pre-auth / * resumed runs). A mismatch with the freshly recomputed digest fail-closes * every consent — the plan changed between mint and apply. */ consentDigest?: string; } /** * Exact-match consent evaluation [G6]. Case-sensitive; never normalised. * Every supplied consent gets a verdict row; every uncovered ticket resource * fails the gate closed. */ export declare function evaluateDestructionConsents(params: EvaluateDestructionConsentsParams): DestructionConsentReport; /** * Renderer for a ticket — every surface renders these same lines. [H6] * The verbs line advertises through `advertisableRemediationVerbs` at render * time (the ticket itself — and its digest — keeps the full legal * vocabulary): a raw `resource.verbs` here would contradict the G11-filtered * re-run hint on the same card. * * The digest closes the block because it is the ticket's IDENTITY, not one of * its rows, and `--destruction-consent-digest` is the flag every one of those * surfaces tells the operator to pass next. It lives here rather than at the * five call sites for the reason the rows do: rendered here it cannot be * omitted. It previously was, on three of them — a withheld `deploy`, a * withheld `drift repair` and the domain consent ceremony each demanded a * digest they never printed, and the two plan surfaces that did print it * buried it inside a prose instruction on the conversation stream, where the * plan's own rows do not go. * * An empty ticket still renders nothing: a digest with no rows under it names * a decision nobody has to make. */ export declare function renderDestructionTicketLines(ticket: DestructionTicket): string[]; /** Renderer for the per-name verdict table [G6]. */ export declare function renderConsentVerdictLines(report: DestructionConsentReport): string[]; export interface RunDestructionGateParams { /** The memoised plan supplier shared with the approval gate. */ loadPlan: DeployPlanLoader; options: DeployOptions; target: string; callbacks: DeployCallbacks; /** * Interactive ceremony resolver (from `DeployParams.destructionConsentGate`). * Consulted ONLY when the ticket is unsatisfied and NO flag consents were * supplied — explicit `--remediate` consents that fail keep failing loudly * with the verdict table [G6], never silently re-prompt. The resolution is * re-evaluated through `evaluateDestructionConsents` + the digest TOCTOU * check, so evaluation never moves surface-side. */ consentGate?: DestructionConsentGate; /** Flipless-by-necessity targets the drift pre-flight gate re-proved * absent THIS deploy — their delete rows are suppressed from the ticket * (see `buildDestructionTicket`). */ verifiedAbsentByNecessity?: ReadonlyArray<{ stackName: string; logicalId: string; }>; } export type DestructionGateOutcome = /** No ticketable rows — the deploy proceeds untouched. */ { kind: "clear"; } /** * Every resource consented with an executable verb — proceed. The deploy * branches on `verbsByPhysicalName`: any `pin` row diverts into the * pre-bootstrap pin arm instead of applying the destructive change. */ | { kind: "proceed"; ticket: DestructionTicket; /** The consented verb per exact physical name (matched rows only). */ verbsByPhysicalName: ReadonlyMap; } /** * Consent withheld / incomplete / stale — nothing may be applied. * `outcome` says WHY (surfaces must render each variant truthfully — a * consented-but-not-executable user is never told to consent again); * `reasons` carries the halt/decline lines. Both are identifier-only * (physical names, verbs) — pre-masked by construction. */ | { kind: "withheld"; outcome: DestructionOutcome; ticket: DestructionTicket; report: DestructionConsentReport; reasons: string[]; }; /** * Post-coverage execution check: a matched consent whose verb has no * execution arm in this release still withholds the deploy (fail closed, * never a doomed CloudFormation run). Returns halt reasons, empty = proceed. */ export declare function collectExecutionHalts(ticket: DestructionTicket, rows: readonly ConsentVerdictRow[]): string[]; /** * Run the unconditional destruction gate. Loads the (memoised) plan, builds * the ticket, evaluates consents, and either clears the boundary or withholds * the deploy. * * A withhold DECIDES and RETURNS — it renders nothing. The ticket, the * outcome, the per-name verdict report and the halt reasons all travel on the * outcome (and onward on `DeployResult`), because the withhold IS the run's * result and a result belongs on the result channel: the agent lane needs the * verdict table structurally, and prose over `onLog` reaches stderr, where a * caller reading only stdout never sees why its consents were refused. The * exported `renderDestructionTicketLines` / `renderConsentVerdictLines` are * how a text surface draws the same lines from that data. Deliberately * carries NO ready-to-run command [G5]; the CLI's human path appends its own * re-run hint. * * Failure messages are pre-masked at construction — the sole failure path * routes the plan error through `maskSensitiveOutput`, so callers may surface * `error` to callback/log sinks directly. */ export declare function runDestructionGate(params: RunDestructionGateParams): Promise>;