orchestrator/sm: drop out-of-chain ids at the dispatch boundary

Enforce component-id membership once in step() via Event::component_id(),
so no handler acts on an id outside the configured chain. Also: rename
RestoreGoldenImage -> RecoverComponent, add the commit-or-lock watchdog
(pending_commit/CommitTimeout), and drop #[non_exhaustive].
diff --git a/services/orchestrator/sm/src/lib.rs b/services/orchestrator/sm/src/lib.rs
index 7d31b67..8dd5f35 100644
--- a/services/orchestrator/sm/src/lib.rs
+++ b/services/orchestrator/sm/src/lib.rs
@@ -182,6 +182,21 @@
     /// resets it.
     statuses: heapless::Vec<ComponentStatus, N>,
     max_retry: u8,
+    /// Set when an update has been activated ([`Effect::ActivateUpdate`]) but
+    /// its anti-rollback floor has not yet been committed, i.e. the machine is
+    /// in the *activated-but-not-committed* window inside [`State::Ready`]. A
+    /// [`Event::BootConfirmed`] commits the floor and clears this; a
+    /// [`Event::CommitTimeout`] fired while this is set latches
+    /// [`State::Locked`] (commit-or-lock: the floor is never advanced for an
+    /// image that has not proven healthy, and the downgrade window is never left
+    /// open indefinitely). Cleared on [`Event::BootConfirmed`] (the window
+    /// closes normally) and on entry to the two states that end the window by
+    /// leaving `Ready` while still running — [`State::Updating`] (a superseding
+    /// update) and [`State::Recovering`] (corruption/timeout) — so any path that
+    /// leaves and later re-enters `Ready` resets the window without per-branch
+    /// bookkeeping. (It is *not* cleared on `Ready` entry, because activation
+    /// sets it while transitioning *into* `Ready`.)
+    pending_commit: bool,
     /// Ties the effect-buffer size `E` to this type (zero-sized).
     _effect_cap: PhantomData<[u8; E]>,
 }
@@ -209,6 +224,7 @@
             cursor: 0,
             statuses,
             max_retry,
+            pending_commit: false,
             _effect_cap: PhantomData,
         }
     }
@@ -228,6 +244,14 @@
         self.chain.iter().position(|(cid, _)| *cid == id)
     }
 
+    /// Whether `id` names a component in the configured chain. The core only
+    /// supervises chain components, so an event that names an id outside the
+    /// chain is dropped rather than acted on — it describes something the core
+    /// has no model of and never released.
+    fn in_chain(&self, id: ComponentId) -> bool {
+        self.status_index(id).is_some()
+    }
+
     fn is_gated(&self, id: ComponentId) -> bool {
         self.status_index(id)
             .is_some_and(|i| self.statuses[i].lifecycle == ComponentLifecycle::Isolated)
@@ -365,8 +389,8 @@
     /// so this path and the recovery-exhaustion path can never diverge:
     /// `Isolable`/`Cascading` → gate the component (single or cascade) and stay
     /// put, so a later re-walk skips it instead of silently re-releasing one we
-    /// already found corrupt; `Required`/unknown → recover first (the
-    /// halt-on-exhaustion decision happens later in `Recovering`).
+    /// already found corrupt; `Required` → recover first (the halt-on-exhaustion
+    /// decision happens later in `Recovering`).
     fn handle_corruption(&mut self, id: ComponentId, ctx: &mut Sink<E>) -> Outcome {
         match self.gate_by_policy(ctx, id) {
             Gating::Gated => Outcome::Handled,
@@ -552,18 +576,37 @@
                 Event::UpdateRequest => Outcome::Transition(State::Updating),
                 // Proven-boot checkpoint: the image authenticated at
                 // `ActivateUpdate`, but the SVN floor only advances now, once
-                // the shell reports it healthy. Handled in place — confirming a
-                // running image is not a state change.
+                // the driver reports it healthy. Handled in place — confirming a
+                // running image is not a state change. Closing the window clears
+                // `pending_commit` so a later `CommitTimeout` cannot lock a
+                // device that has already committed.
                 Event::BootConfirmed(id) => {
                     ctx.emit(Effect::CommitSvnFloor(*id));
+                    self.pending_commit = false;
                     Outcome::Handled
                 }
+                // Commit watchdog. If the activated-but-not-committed window is
+                // still open, fail closed: never commit an unproven image, and
+                // never leave the downgrade window open indefinitely. Outside
+                // the window this is a stale watchdog fire and is dropped.
+                Event::CommitTimeout => {
+                    if self.pending_commit {
+                        Outcome::Transition(State::Locked)
+                    } else {
+                        Outcome::Handled
+                    }
+                }
                 _ => Outcome::Super,
             },
 
             State::Updating => match event {
                 Event::UpdateVerified => {
                     ctx.emit(Effect::ActivateUpdate);
+                    // Open the activated-but-not-committed window: the floor is
+                    // NOT advanced here; it waits for `BootConfirmed`. The
+                    // driver arms its commit watchdog on `ActivateUpdate`, and
+                    // `CommitTimeout` bounds this window (commit-or-lock).
+                    self.pending_commit = true;
                     Outcome::Transition(State::Ready)
                 }
                 Event::UpdateRejected => {
@@ -715,15 +758,22 @@
                 let _ = self.advance_to_next_ungated(ctx, 0);
             }
             State::Updating => {
+                // A new update supersedes any activated-but-not-committed image;
+                // the prior commit window is void.
+                self.pending_commit = false;
                 ctx.emit(Effect::AuthenticateUpdate);
                 ctx.emit(Effect::StageUpdate);
             }
             State::Recovering(failed) => {
+                // Recovery voids any activated-but-not-committed image: the
+                // running image is now under suspicion, so its commit window
+                // ends here.
+                self.pending_commit = false;
                 // The component under recovery is being restored, not booting;
                 // drop any pending boot-progress watchdog so a late `Timeout`
                 // can't re-enter recovery for it.
                 self.clear_awaiting_boot(failed);
-                ctx.emit(Effect::RestoreGoldenImage(failed));
+                ctx.emit(Effect::RecoverComponent(failed));
             }
             State::Locked => {
                 ctx.emit(Effect::LatchLockdown);
@@ -818,6 +868,18 @@
     /// isolation reports, plus the destination `PreSupervision` entry's two
     /// effects share one `Sink`).
     fn step(&mut self, event: &Event, ctx: &mut Sink<E>) {
+        // 0. Single point of id-membership enforcement. The core supervises
+        //    only the components in the configured chain, so an event that names
+        //    an id the chain does not contain is dropped here, before any
+        //    handler runs — no handler needs its own membership check, and none
+        //    can act on a component the core never modeled. Events that name no
+        //    component (`component_id() == None`) always pass through.
+        if let Some(id) = event.component_id()
+            && !self.rot.in_chain(id)
+        {
+            return;
+        }
+
         // 1. Dispatch to the current (leaf) state.
         let mut outcome = self.rot.handle(self.state, event, ctx);
 
diff --git a/services/orchestrator/sm/src/model.rs b/services/orchestrator/sm/src/model.rs
index f9537e9..ae9cfd8 100644
--- a/services/orchestrator/sm/src/model.rs
+++ b/services/orchestrator/sm/src/model.rs
@@ -26,7 +26,6 @@
 /// Corresponds directly to the two-tier model in the CSA architecture document:
 /// `Active` = eRoT gate + iRoT gate; `Passive` = eRoT gate only.
 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
-#[non_exhaustive]
 pub enum ComponentKind {
     /// Has an integrated iRoT (e.g. Caliptra). Both eRoT-side (signature + SVN)
     /// and iRoT-side (local self-verification) checks apply. The machine waits in
@@ -54,7 +53,6 @@
 /// (The narrative design docs sometimes call the `Required` outcome "platform
 /// halt" — same behavior, this is the type-level name.)
 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
-#[non_exhaustive]
 pub enum FailurePolicy {
     /// Stop the boot sequence entirely: self-emits [`Event::RecoveryFailed`],
     /// which drives the machine to [`State::Locked`].
@@ -172,7 +170,6 @@
 
 /// The result of the board's power-on checks, delivered inside [`Event::PowerGood`].
 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
-#[non_exhaustive]
 pub enum PowerOnResult {
     /// Self-verified and provisioned.
     Provisioned,
@@ -184,7 +181,6 @@
 
 /// Everything the outside world can tell the state machine.
 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
-#[non_exhaustive]
 pub enum Event {
     /// Power-on, carrying the shell's self-verification and provisioning result.
     PowerGood(PowerOnResult),
@@ -215,7 +211,7 @@
     BootConfirmed(ComponentId),
     /// This component was found corrupt at runtime.
     CorruptionDetected(ComponentId),
-    /// This component's golden image has been restored.
+    /// This component has been restored from its configured recovery source.
     Restored(ComponentId),
     /// A required component's recovery was exhausted.
     RecoveryFailed,
@@ -228,6 +224,18 @@
     /// is stale/spurious and dropped. The watchdog is per component and
     /// device-agnostic, matching CSA boot-progress checkpointing.
     Timeout(ComponentId),
+    /// The driver's *commit* watchdog fired: an update was activated
+    /// ([`Effect::ActivateUpdate`]) but did not report [`Event::BootConfirmed`]
+    /// within the policy window. Distinct from [`Event::Timeout`], which is the
+    /// per-component boot-progress watchdog; this one bounds the single
+    /// activated-but-not-committed window in [`State::Ready`]. Handled only while
+    /// that window is open: it latches [`State::Locked`] (commit-or-lock — the
+    /// anti-rollback floor is never advanced for an unproven image, and the
+    /// downgrade window may not stay open indefinitely). Outside the window
+    /// (nothing pending) it is stale and dropped. The driver arms this watchdog
+    /// when it executes [`Effect::ActivateUpdate`] and cancels it on
+    /// [`Effect::CommitSvnFloor`].
+    CommitTimeout,
     /// The shell could not carry out an emitted [`Effect`]; fail-closed, it
     /// latches to [`State::Locked`] from any state. Injected by the driver when
     /// a [`Platform::execute`](crate::Platform::execute) call fails; never
@@ -235,13 +243,43 @@
     EffectFailed,
 }
 
+impl Event {
+    /// The component this event is about, or `None` for events that name no
+    /// component (`PowerGood`, `UpdateRequest`, `CommitTimeout`, …).
+    ///
+    /// This is the single enumeration of the id-carrying events, consulted once
+    /// at the dispatch boundary ([`Orchestrator::step`](crate::Orchestrator::step))
+    /// to drop any event that names a component outside the configured chain
+    /// before a handler ever sees it. A new id-carrying variant must be listed
+    /// here, or it will bypass that membership check.
+    pub(crate) fn component_id(&self) -> Option<ComponentId> {
+        match self {
+            Event::VerificationPassed(id)
+            | Event::VerificationFailed(id)
+            | Event::ComponentReady(id)
+            | Event::Booted(id)
+            | Event::BootConfirmed(id)
+            | Event::CorruptionDetected(id)
+            | Event::Restored(id)
+            | Event::Timeout(id) => Some(*id),
+            Event::PowerGood(_)
+            | Event::AttestationChallenge
+            | Event::UpdateRequest
+            | Event::UpdateVerified
+            | Event::UpdateRejected
+            | Event::RecoveryFailed
+            | Event::CommitTimeout
+            | Event::EffectFailed => None,
+        }
+    }
+}
+
 /// Everything the state machine can ask the outside world to do.
 ///
 /// [`Effect::Emit`] is the sole internal effect: the orchestrator catches it and
 /// queues the carried event for immediate handling, making follow-up events
 /// visible in the effect trace instead of hidden state changes.
 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
-#[non_exhaustive]
 pub enum Effect {
     ReadFirmware(ComponentId),
     VerifyFirmware(ComponentId),
@@ -263,7 +301,12 @@
     /// floor earlier would burn anti-rollback on an image that authenticated
     /// but has not yet demonstrated it can boot and run.
     CommitSvnFloor(ComponentId),
-    RestoreGoldenImage(ComponentId),
+    /// Recover `id` from its configured recovery source. The mechanism —
+    /// golden image, A/B slot, streamed image, or vendor-specific scheme — is
+    /// deferred to the [`Platform`](crate::Platform) driver, which resolves it
+    /// per configuration policy. The reducer only names the component to
+    /// recover; it does not encode how recovery is performed.
+    RecoverComponent(ComponentId),
     /// Report that a component has been isolated (held in reset and removed
     /// from the trust chain) so management software is aware the platform is
     /// running degraded. Emitted once per component, at the moment it is
@@ -307,7 +350,6 @@
 /// cursor, the gate set, retry counts) lives in [`Rot`](crate::Rot) shared
 /// storage.
 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
-#[non_exhaustive]
 pub enum State {
     PowerOnReset,
     PreSupervision,
@@ -326,7 +368,8 @@
     Ready,
     Updating,
     /// A component failed verification (or was found corrupt under a
-    /// non-gating policy) and its golden image is being restored. The payload is
+    /// non-gating policy) and is being restored from its configured recovery
+    /// source. The payload is
     /// that component — always present, since the machine only enters this state
     /// with a recovery target in hand.
     Recovering(ComponentId),
@@ -360,7 +403,6 @@
 
 /// Why a `heapless::Vec` of components is not a valid [`Chain`].
 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
-#[non_exhaustive]
 pub enum ChainError {
     /// The chain has no components.
     Empty,
diff --git a/services/orchestrator/sm/src/tests.rs b/services/orchestrator/sm/src/tests.rs
index bc4636d..b479509 100644
--- a/services/orchestrator/sm/src/tests.rs
+++ b/services/orchestrator/sm/src/tests.rs
@@ -211,7 +211,7 @@
         ],
     );
     assert_eq!(state, State::Recovering(C0));
-    assert!(effects.contains(&Effect::RestoreGoldenImage(C0)));
+    assert!(effects.contains(&Effect::RecoverComponent(C0)));
 }
 
 /// INV7 (feedback-as-data): after MAX_RETRY restores the core self-emits
@@ -437,7 +437,7 @@
         &[BOOT, Event::VerificationPassed(C0), Event::Timeout(C0)],
     );
     assert_eq!(state, State::Recovering(C0));
-    assert!(effects.contains(&Effect::RestoreGoldenImage(C0)));
+    assert!(effects.contains(&Effect::RecoverComponent(C0)));
 }
 
 /// D2: a timeout for a component that is not awaiting boot-progress is
@@ -458,7 +458,39 @@
         ],
     );
     assert_eq!(state, State::AwaitingReady(Some(C0)));
-    assert!(!effects.contains(&Effect::RestoreGoldenImage(C1)));
+    assert!(!effects.contains(&Effect::RecoverComponent(C1)));
+}
+
+/// An out-of-chain id in a `VerificationFailed` report is dropped: the core
+/// supervises only chain components, so a verdict for an id the chain does not
+/// contain neither enters `Recovering` nor emits `RecoverComponent`.
+#[test]
+fn verification_failed_out_of_chain_id_is_dropped() {
+    let (effects, state) = drive(
+        passive_required(&[C0, C1]),
+        &[BOOT, Event::VerificationFailed(C3)],
+    );
+    assert!(!effects.contains(&Effect::RecoverComponent(C3)));
+    // Untouched: still walking the chain from the top with C0 under verification.
+    assert_eq!(state, State::PreSupervision);
+}
+
+/// An out-of-chain id in a `CorruptionDetected` report is likewise dropped: a
+/// malformed report from the platform cannot drive a spurious recovery or move
+/// the machine out of `Ready`.
+#[test]
+fn corruption_out_of_chain_id_is_dropped() {
+    let (effects, state) = drive(
+        passive_required(&[C0, C1]),
+        &[
+            BOOT,
+            Event::VerificationPassed(C0),
+            Event::VerificationPassed(C1),
+            Event::CorruptionDetected(C3),
+        ],
+    );
+    assert!(!effects.contains(&Effect::RecoverComponent(C3)));
+    assert_eq!(state, State::Ready);
 }
 
 /// Device-agnostic boot-progress: a *passive* component that is released but
@@ -476,7 +508,7 @@
     );
     assert_eq!(state, State::Recovering(C0));
     assert!(effects.contains(&Effect::ReleaseReset(C0)));
-    assert!(effects.contains(&Effect::RestoreGoldenImage(C0)));
+    assert!(effects.contains(&Effect::RecoverComponent(C0)));
 }
 
 /// A passive boot timeout is caught even after the chain walk has completed and
@@ -492,7 +524,7 @@
         &[BOOT, Event::VerificationPassed(C0), Event::Timeout(C0)],
     );
     assert_eq!(state, State::Recovering(C0));
-    assert!(effects.contains(&Effect::RestoreGoldenImage(C0)));
+    assert!(effects.contains(&Effect::RecoverComponent(C0)));
 }
 
 /// A passive component that reports [`Event::Booted`] retires its watchdog, so a
@@ -511,7 +543,7 @@
         ],
     );
     assert_eq!(state, State::Ready);
-    assert!(!effects.contains(&Effect::RestoreGoldenImage(C0)));
+    assert!(!effects.contains(&Effect::RecoverComponent(C0)));
 }
 
 /// D2: full path — timeout drives recovery, restore rewalks from the top, and
@@ -534,7 +566,7 @@
         ],
     );
     assert_eq!(state, State::Ready);
-    assert!(effects.contains(&Effect::RestoreGoldenImage(C0)));
+    assert!(effects.contains(&Effect::RecoverComponent(C0)));
     assert!(effects.contains(&Effect::ReleaseReset(C1)));
 }
 
@@ -560,7 +592,7 @@
     // C1 must never be released.
     assert!(!effects.contains(&Effect::ReleaseReset(C1)));
     // Recovery IS attempted before C1 is classified and held.
-    assert!(effects.contains(&Effect::RestoreGoldenImage(C1)));
+    assert!(effects.contains(&Effect::RecoverComponent(C1)));
     assert!(effects.contains(&Effect::AssertReset(C1)));
     assert!(!effects.contains(&Effect::LatchLockdown));
 }
@@ -586,7 +618,7 @@
     );
     assert_eq!(state, State::Ready);
     assert!(!effects.contains(&Effect::ReleaseReset(C1)));
-    assert!(effects.contains(&Effect::RestoreGoldenImage(C1)));
+    assert!(effects.contains(&Effect::RecoverComponent(C1)));
     assert!(effects.contains(&Effect::AssertReset(C1)));
 }
 
@@ -608,7 +640,7 @@
     );
     assert_eq!(state, State::Ready);
     assert!(effects.contains(&Effect::AssertReset(C1)));
-    assert!(!effects.contains(&Effect::RestoreGoldenImage(C1)));
+    assert!(!effects.contains(&Effect::RecoverComponent(C1)));
     assert!(!effects.contains(&Effect::LatchLockdown));
 }
 
@@ -708,7 +740,7 @@
         ],
     );
     assert_eq!(state, State::Recovering(C0));
-    assert!(effects.contains(&Effect::RestoreGoldenImage(C0)));
+    assert!(effects.contains(&Effect::RecoverComponent(C0)));
 }
 
 /// Runtime corruption of a `Cascading` component must gate the whole
@@ -737,7 +769,7 @@
     assert!(effects.contains(&Effect::AssertReset(C1)));
     assert!(effects.contains(&Effect::AssertReset(C2)));
     // No recovery is started for a non-required corruption.
-    assert!(!effects.contains(&Effect::RestoreGoldenImage(C1)));
+    assert!(!effects.contains(&Effect::RecoverComponent(C1)));
     assert!(!effects.contains(&Effect::LatchLockdown));
 }
 
@@ -829,7 +861,7 @@
     assert!(effects.contains(&Effect::ReportIsolated(C2)));
     assert!(effects.contains(&Effect::ReportIsolated(C3)));
     // A non-required cascade never enters recovery or lockdown.
-    assert!(!effects.contains(&Effect::RestoreGoldenImage(C1)));
+    assert!(!effects.contains(&Effect::RecoverComponent(C1)));
     assert!(!effects.contains(&Effect::LatchLockdown));
 }
 
@@ -852,7 +884,7 @@
     );
     assert_eq!(state, State::Ready);
     assert!(effects.contains(&Effect::ReportIsolated(C1)));
-    assert!(!effects.contains(&Effect::RestoreGoldenImage(C1)));
+    assert!(!effects.contains(&Effect::RecoverComponent(C1)));
 }
 
 /// A `Required` component whose recovery is exhausted is named in a
@@ -900,7 +932,7 @@
         ],
     );
     assert_eq!(state, State::Ready);
-    assert!(effects.contains(&Effect::RestoreGoldenImage(C0)));
+    assert!(effects.contains(&Effect::RecoverComponent(C0)));
     assert!(
         !effects.iter().any(|e| matches!(
             e,
@@ -955,7 +987,7 @@
         &[BOOT, Event::VerificationFailed(C0)],
     );
     assert_eq!(state, State::Recovering(C0));
-    assert!(effects.contains(&Effect::RestoreGoldenImage(C0)));
+    assert!(effects.contains(&Effect::RecoverComponent(C0)));
     // Component must never be released when its eRoT check failed.
     assert!(!effects.contains(&Effect::ReleaseReset(C0)));
 }
@@ -974,7 +1006,7 @@
         ],
     );
     assert_eq!(state, State::Ready);
-    assert!(effects.contains(&Effect::RestoreGoldenImage(C0)));
+    assert!(effects.contains(&Effect::RecoverComponent(C0)));
     // ReleaseReset only after the recovery re-walk passes.
     assert!(effects.contains(&Effect::ReleaseReset(C0)));
 }
@@ -995,7 +1027,7 @@
         ],
     );
     assert_eq!(state, State::Recovering(C1));
-    assert!(effects.contains(&Effect::RestoreGoldenImage(C1)));
+    assert!(effects.contains(&Effect::RecoverComponent(C1)));
     assert!(!effects.contains(&Effect::ReleaseReset(C1)));
 }
 
@@ -1015,7 +1047,7 @@
         ],
     );
     assert_eq!(state, State::Recovering(C0));
-    assert!(effects.contains(&Effect::RestoreGoldenImage(C0)));
+    assert!(effects.contains(&Effect::RecoverComponent(C0)));
 }
 
 /// CorruptionDetected while in Updating (required component) → Recovering
@@ -1032,7 +1064,7 @@
         ],
     );
     assert_eq!(state, State::Recovering(C0));
-    assert!(effects.contains(&Effect::RestoreGoldenImage(C0)));
+    assert!(effects.contains(&Effect::RecoverComponent(C0)));
 }
 
 /// Concurrent faults: corruption of a *different* component arriving while the
@@ -1055,9 +1087,9 @@
         ],
     );
     assert_eq!(state, State::Recovering(C2));
-    // Both recovery episodes kicked off a golden-image restore.
-    assert!(effects.contains(&Effect::RestoreGoldenImage(C1)));
-    assert!(effects.contains(&Effect::RestoreGoldenImage(C2)));
+    // Both recovery episodes kicked off a recovery.
+    assert!(effects.contains(&Effect::RecoverComponent(C1)));
+    assert!(effects.contains(&Effect::RecoverComponent(C2)));
     assert!(!effects.contains(&Effect::LatchLockdown));
 }
 
@@ -1145,7 +1177,7 @@
     assert_eq!(state, State::Ready);
     assert!(effects.contains(&Effect::ActivateUpdate));
     assert!(!effects.contains(&Effect::DiscardStaged));
-    assert!(!effects.contains(&Effect::RestoreGoldenImage(C0)));
+    assert!(!effects.contains(&Effect::RecoverComponent(C0)));
 }
 
 /// The anti-rollback floor is committed only on a proven-healthy boot, never
@@ -1184,6 +1216,91 @@
     assert!(confirmed.contains(&Effect::CommitSvnFloor(C0)));
 }
 
+/// Commit-or-lock watchdog: while the activated-but-not-committed window is
+/// open (update activated, `BootConfirmed` not yet seen), a `CommitTimeout`
+/// fails closed — the machine latches `Locked` rather than leaving the
+/// downgrade window open indefinitely, and never commits the unproven image.
+#[test]
+fn commit_timeout_while_pending_latches_locked() {
+    let (effects, state) = drive(
+        passive_required(&[C0]),
+        &[
+            BOOT,
+            Event::VerificationPassed(C0),
+            Event::UpdateRequest,
+            Event::UpdateVerified,
+            // Window open: activated, awaiting BootConfirmed. Watchdog fires.
+            Event::CommitTimeout,
+        ],
+    );
+    assert_eq!(state, State::Locked);
+    assert!(effects.contains(&Effect::ActivateUpdate));
+    assert!(effects.contains(&Effect::LatchLockdown));
+    // The floor was never advanced for the unproven image.
+    assert!(!effects.contains(&Effect::CommitSvnFloor(C0)));
+}
+
+/// Once `BootConfirmed` has committed the floor the window is closed, so a
+/// later (stale) `CommitTimeout` is a no-op: the machine stays `Ready` and does
+/// not lock.
+#[test]
+fn commit_timeout_after_confirm_is_stale_noop() {
+    let (effects, state) = drive(
+        passive_required(&[C0]),
+        &[
+            BOOT,
+            Event::VerificationPassed(C0),
+            Event::UpdateRequest,
+            Event::UpdateVerified,
+            Event::BootConfirmed(C0),
+            // Window already closed by the commit above.
+            Event::CommitTimeout,
+        ],
+    );
+    assert_eq!(state, State::Ready);
+    assert!(effects.contains(&Effect::CommitSvnFloor(C0)));
+    assert!(!effects.contains(&Effect::LatchLockdown));
+}
+
+/// A `CommitTimeout` in steady-state `Ready` with no update in flight (no
+/// window open) is stale and ignored — a spurious watchdog fire must not lock a
+/// healthy device.
+#[test]
+fn commit_timeout_without_pending_is_ignored() {
+    let (effects, state) = drive(
+        passive_required(&[C0]),
+        &[BOOT, Event::VerificationPassed(C0), Event::CommitTimeout],
+    );
+    assert_eq!(state, State::Ready);
+    assert!(!effects.contains(&Effect::LatchLockdown));
+}
+
+/// A recovery that intervenes during the commit window voids it: after the
+/// machine recovers and walks back to `Ready`, the window is closed, so a
+/// `CommitTimeout` no longer locks. This pins that entering `Recovering` clears
+/// `pending_commit`, so the flag cannot go stale across a recovery round-trip.
+#[test]
+fn recovery_clears_commit_window() {
+    let (effects, state) = drive(
+        passive_required(&[C0]),
+        &[
+            BOOT,
+            Event::VerificationPassed(C0),
+            Event::UpdateRequest,
+            Event::UpdateVerified,
+            // Window open, then a Required corruption preempts to recovery.
+            Event::CorruptionDetected(C0),
+            // Restore succeeds (retry < MAX_RETRY) and re-walk re-verifies.
+            Event::Restored(C0),
+            Event::VerificationPassed(C0),
+            // Back in Ready with the window voided; the watchdog is now stale.
+            Event::CommitTimeout,
+        ],
+    );
+    assert_eq!(state, State::Ready);
+    assert!(!effects.contains(&Effect::LatchLockdown));
+}
+
 /// Locked is a terminal state: no effects are produced in response to any
 /// event after the machine latches.
 #[test]
@@ -1244,7 +1361,7 @@
     assert!(!effects.contains(&Effect::ReleaseReset(C0)));
     assert!(effects.contains(&Effect::ReleaseReset(C1)));
     // Recovery IS attempted before C0 is classified and held.
-    assert!(effects.contains(&Effect::RestoreGoldenImage(C0)));
+    assert!(effects.contains(&Effect::RecoverComponent(C0)));
     assert!(effects.contains(&Effect::AssertReset(C0)));
 }
 
@@ -1452,7 +1569,7 @@
 }
 
 /// A failed recovery actuation is fail-closed too: if the shell cannot even
-/// restore a required component's golden image, the platform latches rather
+/// recover a required component, the platform latches rather
 /// than continuing with an unrecovered component.
 #[test]
 fn failed_restore_actuation_latches_lockdown() {
@@ -1460,10 +1577,10 @@
     c.push((C0, ComponentAttrs::passive_required())).unwrap();
     let mut orch =
         Orchestrator::<CAPACITY, ECAP>::new(c.try_into().expect("valid chain"), MAX_RETRY);
-    let mut plat = FailOn::new(Effect::RestoreGoldenImage(C0));
+    let mut plat = FailOn::new(Effect::RecoverComponent(C0));
 
     orch.dispatch(&mut plat, BOOT);
-    orch.dispatch(&mut plat, Event::VerificationFailed(C0)); // → Recovering → RestoreGoldenImage(C0) fails
+    orch.dispatch(&mut plat, Event::VerificationFailed(C0)); // → Recovering → RecoverComponent(C0) fails
 
     assert!(plat.failed);
     assert_eq!(orch.state(), State::Locked);