Baniloo Baniloo

July 29, 2026

The safety model lives in one place, not in five actuators

postmortem's v2.0 actuator framework enforces severity, dry-run, and confirmation gates once in the registry — then Telegram, webhook, and GitHub actuators plug into a seam that can't be config'd into firing something dangerous

Session 16 turns src/actuators/{base,index}.ts from a scaffold into the real thing, with zero concrete actuators registered yet, on purpose — the framework had to be provably safe before anything used it. Session 17 picks up the same day and registers the first three: Telegram, a generic webhook, and GitHub issues. All three ship disabled by default, so installing this version changes nothing on its own.

Where the safety model actually lives

The tempting design is to make every actuator responsible for its own caution — check severity, respect dry-run, ask before acting. That’s also the design where the fifth actuator someone writes forgets one of the three. ActuatorRegistry.runAll() enforces the whole chain once, before any actuator-specific code runs at all: severity gate first (below the configured floor, not even describe() runs), then dry-run (the registry logs what the action would be and writes an audit row, execute() is never called), then a confirmation gate, then — only then — an isolated, try/caught execute().

The confirmation gate is the part I didn’t want to leave configurable. requiresConfirmation is a property declared on the actuator class itself, not a [actuators.x] config key. If it were a TOML flag, the highest-stakes actuator in the system could be armed by editing a text file. Making it a hardcoded class property means the only way a confirmation-gated actuator fires is a code change — which is exactly the amount of friction acting on production should require.

A gap the first real actuators exposed

Sensors have always run their output through a central redactor before anything touches disk — BaseSensor.emit() guarantees it. The actuator registry’s audit trail didn’t, because in Session 16 there was nothing flowing through it worth redacting. The moment Session 17 registered actuators that carry live external data — API error bodies, the configured webhook URL, whatever a failed request echoes back — that gap became a real risk instead of a theoretical one. audit() now redacts result.message and result.detail the same way sensors do, so an actuator author never has to remember to do it themselves.

Built by reusing what already worked

WebhookActuator signs its POST body the same HMAC-SHA256 scheme the inbound webhook receiver already verifies, and runs the user-supplied URL through the exact SSRF guard written for the health-check sensor — verified live by pointing it at a loopback address and watching the registry block it before any request left the process. TelegramActuator is the v1.1 alert plumbing, just called from a different place now. GitHubActuator opens an issue through the same apiClient convention the GitHub Actions sensor already used. None of it is new infrastructure; it’s the same trust boundaries, reused instead of rebuilt.

RollbackActuator and PagerDuty — the two actuators classified high-stakes enough to need the confirmation gate for real — are what Session 17 finishes next.