Skip to content

Commit 4e07e4f

Browse files
committed
fix: scope identity actuation to remote surfaces
1 parent f2a49a0 commit 4e07e4f

3 files changed

Lines changed: 74 additions & 37 deletions

File tree

benchmark/rdp_multiapp/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ docker run --rm --name oaflow-rdp-multiapp \
3535
oaflow-rdp-multiapp:latest
3636
```
3737

38-
Run the implemented subset after the container is ready:
38+
Run the complete campaign after the container is ready:
3939

4040
```bash
4141
python benchmark/rdp_multiapp/run_qualification.py \
@@ -44,6 +44,6 @@ python benchmark/rdp_multiapp/run_qualification.py \
4444
```
4545

4646
The runner uses the same `DockerX11RdpTransport` and `FreeRDPBackend` contract
47-
as `benchmark/rdp_ladder`. The first subset runs healthy, row-reordered,
48-
wrong-record, and focus-theft conditions. The result cannot describe the full
49-
campaign as complete until every condition in `campaign.json` has run.
47+
as `benchmark/rdp_ladder`. It runs all nine conditions in `campaign.json` three
48+
times. The result cannot describe the campaign as complete unless all 27 trials
49+
run and the aggregate acceptance contract passes.

openadapt_flow/runtime/replayer.py

Lines changed: 55 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -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,

tests/test_replayer.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,21 @@ def test_identity_armed_remote_click_retains_closed_guarded_actuation(
772772
assert backend.actions == [("click", 110, 105, False)]
773773

774774

775+
def test_identity_armed_browser_step_does_not_change_business_risk():
776+
"""Identity metadata alone must not make a reversible browser step a write."""
777+
778+
step = click_step(risk="reversible", ocr_text="Details")
779+
step.identity_armed = True
780+
assert step.anchor is not None
781+
step.anchor.context_text = "Expected record"
782+
workflow = Workflow(name="wf", surface="web", steps=[step])
783+
replayer = Replayer(FakeBackend(), vision=FakeVision())
784+
785+
assert replayer._step_is_consequential(step, workflow) is False
786+
assert replayer._step_needs_consequential_revalidation(step, workflow) is False
787+
assert replayer._requires_atomic_identity_pointer(step, workflow) is False
788+
789+
775790
def test_consequential_lease_click_still_refuses_a_changed_frame(bundle, run_dir):
776791
"""The lease is the safety property: a changed frame must stop delivery."""
777792
backend = PixelOnlyRemoteBackend()

0 commit comments

Comments
 (0)