@@ -7412,13 +7412,38 @@ def _retain_visual_resolution_evidence(
74127412
74137413 def _step_is_consequential(self, step: Step, workflow: Workflow) -> bool:
74147414 authorization = self.governed_authorization
7415+ return is_consequential(step, workflow) or (
7416+ authorization is not None
7417+ and authorization.requires_verified_identity(step.id)
7418+ )
7419+
7420+ def _step_requires_remote_identity_binding(
7421+ self, step: Step, workflow: Workflow
7422+ ) -> bool:
7423+ """Whether an external opaque-remote step needs an identity lease.
7424+
7425+ ``identity_armed`` says that the remote action must remain bound to the
7426+ identity that was verified on the exact live frame. It does not change
7427+ the business-risk classification of the action on browser or native
7428+ surfaces.
7429+ """
7430+
74157431 return (
7416- step.identity_armed
7417- or is_consequential(step, workflow)
7418- or (
7419- authorization is not None
7420- and authorization.requires_verified_identity(step.id)
7421- )
7432+ step.identity_armed is True
7433+ and workflow.surface in ("rdp", "citrix")
7434+ and workflow.execution_mode == "external"
7435+ and isinstance(self.backend, RemoteActuationBackend)
7436+ )
7437+
7438+ def _step_requires_remote_guarded_actuation(
7439+ self, step: Step, workflow: Workflow
7440+ ) -> bool:
7441+ """Whether a remote input edge must use the fresh-frame guard path."""
7442+
7443+ return isinstance(self.backend, RemoteActuationBackend) and (
7444+ self._step_is_consequential(step, workflow)
7445+ or self._step_requires_remote_identity_binding(step, workflow)
7446+ or self.qualification_fault_driver is not None
74227447 )
74237448
74247449 def _selection_contract_error(
@@ -7504,10 +7529,11 @@ def _step_needs_consequential_revalidation(
75047529 # fresh remote lease for those actions only during the fault run,
75057530 # so ordinary demo replay keeps its reversible fast path.
75067531 return True
7507- return self._step_is_consequential(step, workflow) and (
7508- isinstance(self.backend, RemoteActuationBackend)
7509- or self._step_has_identity_contract(step, workflow)
7510- )
7532+ if self._step_requires_remote_guarded_actuation(step, workflow):
7533+ return True
7534+ return self._step_is_consequential(
7535+ step, workflow
7536+ ) and self._step_has_identity_contract(step, workflow)
75117537
75127538 def _requires_atomic_identity_pointer(self, step: Step, workflow: Workflow) -> bool:
75137539 """Whether a pointer edge may not cross an unleased identity boundary.
@@ -7528,7 +7554,10 @@ def _requires_atomic_identity_pointer(self, step: Step, workflow: Workflow) -> b
75287554 ActionKind.TYPE,
75297555 ActionKind.SELECT_OPTION,
75307556 )
7531- and self._step_is_consequential(step, workflow)
7557+ and (
7558+ self._step_is_consequential(step, workflow)
7559+ or self._step_requires_remote_identity_binding(step, workflow)
7560+ )
75327561 and self._step_has_identity_contract(step, workflow)
75337562 )
75347563
@@ -7545,7 +7574,10 @@ def _requires_atomic_identity_keyboard(
75457574 ActionKind.TYPE,
75467575 ActionKind.SELECT_OPTION,
75477576 )
7548- and self._step_is_consequential(step, workflow)
7577+ and (
7578+ self._step_is_consequential(step, workflow)
7579+ or self._step_requires_remote_identity_binding(step, workflow)
7580+ )
75497581 and self._step_has_identity_contract(step, workflow)
75507582 )
75517583
@@ -7732,7 +7764,10 @@ def _revalidate_consequential_actuation(
77327764 )
77337765 focused_element_backend = (
77347766 arm_keyboard
7735- and self._step_is_consequential(step, workflow)
7767+ and (
7768+ self._step_is_consequential(step, workflow)
7769+ or self._step_requires_remote_identity_binding(step, workflow)
7770+ )
77367771 and isinstance(self.backend, RemoteActuationBackend)
77377772 and isinstance(self.backend, FocusedElementActuationLeaseBackend)
77387773 )
@@ -8615,11 +8650,8 @@ def _act(
86158650 result.delivery_receipt = delivery_receipt
86168651 result.actuation = "uia" if delivery_receipt.native else "dom"
86178652 else:
8618- remote_consequential = isinstance(
8619- self.backend, RemoteActuationBackend
8620- ) and (
8621- self._step_is_consequential(step, workflow)
8622- or self.qualification_fault_driver is not None
8653+ remote_consequential = self._step_requires_remote_guarded_actuation(
8654+ step, workflow
86238655 )
86248656 typed_remote = isinstance(
86258657 self.backend,
@@ -8740,11 +8772,8 @@ def _act(
87408772 requires_atomic_identity = self._requires_atomic_identity_pointer(
87418773 step, workflow
87428774 )
8743- remote_consequential = isinstance(
8744- self.backend, RemoteActuationBackend
8745- ) and (
8746- self._step_is_consequential(step, workflow)
8747- or self.qualification_fault_driver is not None
8775+ remote_consequential = self._step_requires_remote_guarded_actuation(
8776+ step, workflow
87488777 )
87498778 if remote_consequential and not isinstance(
87508779 self.backend,
@@ -8911,16 +8940,9 @@ def _act(
89118940 ),
89128941 )
89138942 result.actuation = "dom"
8914- elif (
8915- isinstance(self.backend, RemoteActuationBackend)
8916- and (
8917- self._step_is_consequential(step, workflow)
8918- or self.qualification_fault_driver is not None
8919- )
8920- and (
8921- isinstance(self.backend, GuardedRemotePointerActionBackend)
8922- or self._typed_remote_receipt_required()
8923- )
8943+ elif self._step_requires_remote_guarded_actuation(step, workflow) and (
8944+ isinstance(self.backend, GuardedRemotePointerActionBackend)
8945+ or self._typed_remote_receipt_required()
89248946 ):
89258947 if not isinstance(
89268948 self.backend,
0 commit comments