androidctl 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- androidctl/__init__.py +5 -0
- androidctl/__main__.py +4 -0
- androidctl/_version.py +1 -0
- androidctl/app.py +73 -0
- androidctl/cli_options.py +27 -0
- androidctl/command_payloads.py +264 -0
- androidctl/command_views.py +157 -0
- androidctl/commands/__init__.py +1 -0
- androidctl/commands/actions.py +236 -0
- androidctl/commands/adb_wireless.py +157 -0
- androidctl/commands/close.py +30 -0
- androidctl/commands/connect.py +69 -0
- androidctl/commands/execute.py +179 -0
- androidctl/commands/list_apps.py +26 -0
- androidctl/commands/observe.py +26 -0
- androidctl/commands/open.py +41 -0
- androidctl/commands/plumbing.py +58 -0
- androidctl/commands/run_pipeline.py +307 -0
- androidctl/commands/screenshot.py +29 -0
- androidctl/commands/setup.py +301 -0
- androidctl/commands/wait.py +60 -0
- androidctl/daemon/__init__.py +1 -0
- androidctl/daemon/client.py +348 -0
- androidctl/daemon/discovery.py +190 -0
- androidctl/daemon/launcher.py +26 -0
- androidctl/daemon/owner.py +349 -0
- androidctl/errors/__init__.py +1 -0
- androidctl/errors/mapping.py +149 -0
- androidctl/errors/models.py +16 -0
- androidctl/exit_codes.py +8 -0
- androidctl/output.py +147 -0
- androidctl/parsing/__init__.py +1 -0
- androidctl/parsing/duration.py +17 -0
- androidctl/parsing/open_target.py +51 -0
- androidctl/parsing/refs.py +12 -0
- androidctl/parsing/screen_id.py +10 -0
- androidctl/parsing/wait.py +70 -0
- androidctl/renderers/__init__.py +110 -0
- androidctl/renderers/_paths.py +109 -0
- androidctl/renderers/xml.py +234 -0
- androidctl/renderers/xml_projection.py +732 -0
- androidctl/resources/__init__.py +1 -0
- androidctl/resources/androidctl-agent-0.1.0-release.apk +0 -0
- androidctl/setup/__init__.py +1 -0
- androidctl/setup/accessibility.py +159 -0
- androidctl/setup/adb.py +586 -0
- androidctl/setup/apk_resource.py +29 -0
- androidctl/setup/pairing.py +70 -0
- androidctl/setup/verify.py +175 -0
- androidctl/workspace/__init__.py +3 -0
- androidctl/workspace/resolve.py +27 -0
- androidctl-0.1.0.dist-info/METADATA +217 -0
- androidctl-0.1.0.dist-info/RECORD +187 -0
- androidctl-0.1.0.dist-info/WHEEL +5 -0
- androidctl-0.1.0.dist-info/entry_points.txt +3 -0
- androidctl-0.1.0.dist-info/licenses/LICENSE +674 -0
- androidctl-0.1.0.dist-info/top_level.txt +3 -0
- androidctl_contracts/__init__.py +55 -0
- androidctl_contracts/_version.py +1 -0
- androidctl_contracts/_wire_helpers.py +31 -0
- androidctl_contracts/base.py +142 -0
- androidctl_contracts/command_catalog.py +414 -0
- androidctl_contracts/command_results.py +630 -0
- androidctl_contracts/daemon_api.py +335 -0
- androidctl_contracts/errors.py +44 -0
- androidctl_contracts/paths.py +5 -0
- androidctl_contracts/public_screen.py +579 -0
- androidctl_contracts/user_state.py +23 -0
- androidctl_contracts/vocabulary.py +82 -0
- androidctld/__init__.py +5 -0
- androidctld/__main__.py +63 -0
- androidctld/_version.py +1 -0
- androidctld/actions/__init__.py +1 -0
- androidctld/actions/action_target.py +142 -0
- androidctld/actions/capabilities.py +539 -0
- androidctld/actions/executor.py +894 -0
- androidctld/actions/focus_confirmation.py +177 -0
- androidctld/actions/focused_input_admissibility.py +120 -0
- androidctld/actions/fresh_current.py +176 -0
- androidctld/actions/postconditions.py +473 -0
- androidctld/actions/repair.py +101 -0
- androidctld/actions/request_builder.py +204 -0
- androidctld/actions/settle.py +146 -0
- androidctld/actions/submit_confirmation.py +211 -0
- androidctld/actions/submit_routing.py +311 -0
- androidctld/actions/type_confirmation.py +257 -0
- androidctld/app_targets.py +71 -0
- androidctld/artifacts/__init__.py +1 -0
- androidctld/artifacts/models.py +26 -0
- androidctld/artifacts/screen_lookup.py +241 -0
- androidctld/artifacts/screen_payloads.py +109 -0
- androidctld/artifacts/writer.py +286 -0
- androidctld/auth/__init__.py +1 -0
- androidctld/auth/active_registry.py +266 -0
- androidctld/auth/secret_files.py +52 -0
- androidctld/auth/token_store.py +59 -0
- androidctld/commands/__init__.py +1 -0
- androidctld/commands/assembly.py +231 -0
- androidctld/commands/command_models.py +254 -0
- androidctld/commands/dispatch.py +99 -0
- androidctld/commands/executor.py +31 -0
- androidctld/commands/from_boundary.py +175 -0
- androidctld/commands/handlers/__init__.py +15 -0
- androidctld/commands/handlers/action.py +439 -0
- androidctld/commands/handlers/connect.py +94 -0
- androidctld/commands/handlers/list_apps.py +215 -0
- androidctld/commands/handlers/observe.py +121 -0
- androidctld/commands/handlers/screenshot.py +105 -0
- androidctld/commands/handlers/wait.py +286 -0
- androidctld/commands/models.py +65 -0
- androidctld/commands/open_targets.py +56 -0
- androidctld/commands/orchestration.py +353 -0
- androidctld/commands/registry.py +116 -0
- androidctld/commands/result_builders.py +40 -0
- androidctld/commands/result_models.py +555 -0
- androidctld/commands/results.py +108 -0
- androidctld/commands/semantic_command_names.py +17 -0
- androidctld/commands/semantic_error_mapping.py +93 -0
- androidctld/commands/semantic_truth.py +135 -0
- androidctld/commands/service.py +67 -0
- androidctld/config.py +75 -0
- androidctld/daemon/__init__.py +1 -0
- androidctld/daemon/active_slot.py +326 -0
- androidctld/daemon/envelope.py +30 -0
- androidctld/daemon/http_host.py +123 -0
- androidctld/daemon/ingress.py +112 -0
- androidctld/daemon/ownership_probe.py +204 -0
- androidctld/daemon/server.py +286 -0
- androidctld/daemon/service.py +99 -0
- androidctld/device/__init__.py +1 -0
- androidctld/device/action_models.py +154 -0
- androidctld/device/action_serialization.py +121 -0
- androidctld/device/adapters.py +220 -0
- androidctld/device/bootstrap.py +153 -0
- androidctld/device/connectors.py +231 -0
- androidctld/device/errors.py +100 -0
- androidctld/device/interfaces.py +58 -0
- androidctld/device/parsing.py +320 -0
- androidctld/device/rpc.py +483 -0
- androidctld/device/schema.py +114 -0
- androidctld/device/types.py +161 -0
- androidctld/errors/__init__.py +94 -0
- androidctld/logging/__init__.py +22 -0
- androidctld/observation.py +98 -0
- androidctld/protocol.py +53 -0
- androidctld/refs/__init__.py +1 -0
- androidctld/refs/models.py +54 -0
- androidctld/refs/repair.py +284 -0
- androidctld/refs/service.py +422 -0
- androidctld/rendering/__init__.py +1 -0
- androidctld/rendering/screen_xml.py +256 -0
- androidctld/runtime/__init__.py +21 -0
- androidctld/runtime/kernel.py +548 -0
- androidctld/runtime/lifecycle.py +19 -0
- androidctld/runtime/models.py +48 -0
- androidctld/runtime/screen_state.py +117 -0
- androidctld/runtime/state_repo.py +70 -0
- androidctld/runtime/store.py +76 -0
- androidctld/runtime_policy.py +127 -0
- androidctld/schema/__init__.py +5 -0
- androidctld/schema/base.py +132 -0
- androidctld/schema/core.py +35 -0
- androidctld/schema/daemon_api.py +108 -0
- androidctld/schema/persistence.py +161 -0
- androidctld/schema/persistence_io.py +41 -0
- androidctld/schema/validation_errors.py +309 -0
- androidctld/semantics/__init__.py +1 -0
- androidctld/semantics/compiler.py +610 -0
- androidctld/semantics/continuity.py +107 -0
- androidctld/semantics/labels.py +252 -0
- androidctld/semantics/models.py +25 -0
- androidctld/semantics/policy.py +23 -0
- androidctld/semantics/public_models.py +123 -0
- androidctld/semantics/registries.py +13 -0
- androidctld/semantics/submit_refs.py +417 -0
- androidctld/semantics/surface.py +254 -0
- androidctld/semantics/targets.py +167 -0
- androidctld/snapshots/__init__.py +1 -0
- androidctld/snapshots/models.py +219 -0
- androidctld/snapshots/refresh.py +273 -0
- androidctld/snapshots/schema.py +74 -0
- androidctld/snapshots/service.py +138 -0
- androidctld/text_equivalence.py +67 -0
- androidctld/waits/__init__.py +1 -0
- androidctld/waits/evaluators.py +216 -0
- androidctld/waits/loop.py +305 -0
- androidctld/waits/matcher.py +41 -0
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
"""Mutating action runtime orchestration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Protocol, cast
|
|
7
|
+
|
|
8
|
+
from androidctl_contracts.command_results import (
|
|
9
|
+
ActionTargetEvidence,
|
|
10
|
+
ActionTargetPayload,
|
|
11
|
+
)
|
|
12
|
+
from androidctld.actions.action_target import (
|
|
13
|
+
build_action_target_payload,
|
|
14
|
+
build_same_or_successor_action_target,
|
|
15
|
+
public_ref_for_handle,
|
|
16
|
+
public_ref_for_raw_node,
|
|
17
|
+
)
|
|
18
|
+
from androidctld.actions.capabilities import (
|
|
19
|
+
ensure_action_request_supported,
|
|
20
|
+
ensure_command_supported,
|
|
21
|
+
validate_action_semantics,
|
|
22
|
+
validate_resolved_ref_action,
|
|
23
|
+
)
|
|
24
|
+
from androidctld.actions.focus_confirmation import (
|
|
25
|
+
FocusConfirmationContext,
|
|
26
|
+
FocusConfirmationOutcome,
|
|
27
|
+
build_focus_confirmation_context,
|
|
28
|
+
)
|
|
29
|
+
from androidctld.actions.fresh_current import (
|
|
30
|
+
capture_global_fresh_current_baseline,
|
|
31
|
+
validate_global_fresh_current_evidence,
|
|
32
|
+
)
|
|
33
|
+
from androidctld.actions.postconditions import (
|
|
34
|
+
RefActionPostconditionContext,
|
|
35
|
+
validate_postcondition,
|
|
36
|
+
)
|
|
37
|
+
from androidctld.actions.request_builder import (
|
|
38
|
+
build_action_request,
|
|
39
|
+
build_submit_action_request_for_route,
|
|
40
|
+
resolve_ref_target,
|
|
41
|
+
)
|
|
42
|
+
from androidctld.actions.settle import ActionSettler
|
|
43
|
+
from androidctld.actions.submit_confirmation import (
|
|
44
|
+
SubmitConfirmationContext,
|
|
45
|
+
SubmitConfirmationOutcome,
|
|
46
|
+
build_submit_confirmation_context,
|
|
47
|
+
validate_submit_confirmation,
|
|
48
|
+
)
|
|
49
|
+
from androidctld.actions.submit_routing import (
|
|
50
|
+
SubmitRouteOutcome,
|
|
51
|
+
resolve_submit_route,
|
|
52
|
+
)
|
|
53
|
+
from androidctld.actions.type_confirmation import (
|
|
54
|
+
TypeConfirmationCandidate,
|
|
55
|
+
TypeConfirmationContext,
|
|
56
|
+
build_type_confirmation_context,
|
|
57
|
+
validate_type_confirmation,
|
|
58
|
+
)
|
|
59
|
+
from androidctld.commands.command_models import (
|
|
60
|
+
ActionCommand,
|
|
61
|
+
FocusCommand,
|
|
62
|
+
GlobalCommand,
|
|
63
|
+
LongTapCommand,
|
|
64
|
+
OpenCommand,
|
|
65
|
+
RefBoundActionCommand,
|
|
66
|
+
ScrollCommand,
|
|
67
|
+
SubmitCommand,
|
|
68
|
+
TypeCommand,
|
|
69
|
+
is_ref_bound_action_command,
|
|
70
|
+
)
|
|
71
|
+
from androidctld.commands.models import CommandRecord
|
|
72
|
+
from androidctld.commands.result_builders import app_payload
|
|
73
|
+
from androidctld.commands.result_models import SemanticResultAssemblyInput
|
|
74
|
+
from androidctld.device.action_models import BuiltDeviceActionRequest
|
|
75
|
+
from androidctld.device.interfaces import DeviceClientFactory
|
|
76
|
+
from androidctld.errors import DaemonError, DaemonErrorCode
|
|
77
|
+
from androidctld.protocol import DeviceRpcErrorCode
|
|
78
|
+
from androidctld.refs.models import NodeHandle
|
|
79
|
+
from androidctld.runtime import RuntimeKernel, RuntimeLifecycleLease
|
|
80
|
+
from androidctld.runtime.models import WorkspaceRuntime
|
|
81
|
+
from androidctld.runtime.screen_state import (
|
|
82
|
+
current_compiled_screen,
|
|
83
|
+
current_public_screen,
|
|
84
|
+
)
|
|
85
|
+
from androidctld.runtime_policy import (
|
|
86
|
+
DEVICE_RPC_REQUEST_ID_ACTION,
|
|
87
|
+
)
|
|
88
|
+
from androidctld.semantics.compiler import CompiledScreen
|
|
89
|
+
from androidctld.semantics.public_models import (
|
|
90
|
+
PublicNode,
|
|
91
|
+
PublicScreen,
|
|
92
|
+
iter_public_nodes,
|
|
93
|
+
)
|
|
94
|
+
from androidctld.snapshots.models import RawSnapshot
|
|
95
|
+
from androidctld.snapshots.refresh import ScreenRefreshService, settle_screen_signature
|
|
96
|
+
|
|
97
|
+
_POST_DISPATCH_SETTLE_TIMEOUT_WARNING = (
|
|
98
|
+
"post-dispatch observation timed out before stability was confirmed"
|
|
99
|
+
)
|
|
100
|
+
_ACTION_AVAILABILITY_ERROR_CODES = {
|
|
101
|
+
DaemonErrorCode.RUNTIME_NOT_CONNECTED,
|
|
102
|
+
DaemonErrorCode.SCREEN_NOT_READY,
|
|
103
|
+
DaemonErrorCode.DEVICE_DISCONNECTED,
|
|
104
|
+
DaemonErrorCode.DEVICE_AGENT_UNAVAILABLE,
|
|
105
|
+
DaemonErrorCode.DEVICE_AGENT_UNAUTHORIZED,
|
|
106
|
+
DaemonErrorCode.DEVICE_RPC_FAILED,
|
|
107
|
+
DaemonErrorCode.DEVICE_RPC_TRANSPORT_RESET,
|
|
108
|
+
}
|
|
109
|
+
_POST_DISPATCH_TRANSPORT_INVALIDATING_ERROR_CODES = {
|
|
110
|
+
DaemonErrorCode.DEVICE_DISCONNECTED,
|
|
111
|
+
DaemonErrorCode.DEVICE_RPC_TRANSPORT_RESET,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class ActionExecutionFailure(Exception):
|
|
117
|
+
"""Failure after the device action RPC returned and a later phase failed."""
|
|
118
|
+
|
|
119
|
+
original_error: DaemonError
|
|
120
|
+
normalized_error: DaemonError
|
|
121
|
+
dispatch_attempted: bool
|
|
122
|
+
truth_lost_after_dispatch: bool = False
|
|
123
|
+
|
|
124
|
+
def __post_init__(self) -> None:
|
|
125
|
+
Exception.__init__(self, self.normalized_error.message)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _try_focused_input_noop_success(
|
|
129
|
+
session: WorkspaceRuntime,
|
|
130
|
+
command: ActionCommand,
|
|
131
|
+
) -> SemanticResultAssemblyInput | None:
|
|
132
|
+
if not isinstance(command, FocusCommand):
|
|
133
|
+
return None
|
|
134
|
+
if command.source_screen_id != session.current_screen_id:
|
|
135
|
+
return None
|
|
136
|
+
snapshot = session.latest_snapshot
|
|
137
|
+
public_screen = current_public_screen(session)
|
|
138
|
+
compiled_screen = current_compiled_screen(session)
|
|
139
|
+
if snapshot is None or public_screen is None or compiled_screen is None:
|
|
140
|
+
return None
|
|
141
|
+
if public_screen.surface.focus.input_ref != command.ref:
|
|
142
|
+
return None
|
|
143
|
+
public_node = _unique_public_node(public_screen, command.ref)
|
|
144
|
+
if public_node is None or public_node.role != "input":
|
|
145
|
+
return None
|
|
146
|
+
focused_node = compiled_screen.focused_input_node()
|
|
147
|
+
if (
|
|
148
|
+
focused_node is None
|
|
149
|
+
or focused_node.ref != command.ref
|
|
150
|
+
or compiled_screen.focused_input_ref() != command.ref
|
|
151
|
+
):
|
|
152
|
+
return None
|
|
153
|
+
return SemanticResultAssemblyInput(
|
|
154
|
+
app_payload=app_payload(snapshot),
|
|
155
|
+
execution_outcome="notAttempted",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _unique_public_node(screen: PublicScreen, ref: str) -> PublicNode | None:
|
|
160
|
+
nodes = [
|
|
161
|
+
node
|
|
162
|
+
for group in screen.groups
|
|
163
|
+
for node in iter_public_nodes(group.nodes)
|
|
164
|
+
if node.ref == ref
|
|
165
|
+
]
|
|
166
|
+
return nodes[0] if len(nodes) == 1 else None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class ActionCommandRepairPort(Protocol):
|
|
170
|
+
def repair_action_command(
|
|
171
|
+
self,
|
|
172
|
+
session: WorkspaceRuntime,
|
|
173
|
+
record: CommandRecord,
|
|
174
|
+
command: RefBoundActionCommand,
|
|
175
|
+
*,
|
|
176
|
+
lifecycle_lease: RuntimeLifecycleLease,
|
|
177
|
+
) -> BuiltDeviceActionRequest: ...
|
|
178
|
+
|
|
179
|
+
def repair_action_binding(
|
|
180
|
+
self,
|
|
181
|
+
session: WorkspaceRuntime,
|
|
182
|
+
record: CommandRecord,
|
|
183
|
+
command: RefBoundActionCommand,
|
|
184
|
+
*,
|
|
185
|
+
lifecycle_lease: RuntimeLifecycleLease,
|
|
186
|
+
) -> NodeHandle | None: ...
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class ActionExecutor:
|
|
190
|
+
def __init__(
|
|
191
|
+
self,
|
|
192
|
+
*,
|
|
193
|
+
device_client_factory: DeviceClientFactory,
|
|
194
|
+
screen_refresh: ScreenRefreshService,
|
|
195
|
+
settler: ActionSettler,
|
|
196
|
+
repairer: ActionCommandRepairPort,
|
|
197
|
+
runtime_kernel: RuntimeKernel | None = None,
|
|
198
|
+
) -> None:
|
|
199
|
+
self._device_client_factory = device_client_factory
|
|
200
|
+
self._screen_refresh = screen_refresh
|
|
201
|
+
self._settler = settler
|
|
202
|
+
self._repairer = repairer
|
|
203
|
+
self._runtime_kernel = runtime_kernel or getattr(
|
|
204
|
+
screen_refresh,
|
|
205
|
+
"runtime_kernel",
|
|
206
|
+
None,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def execute(
|
|
210
|
+
self,
|
|
211
|
+
session: WorkspaceRuntime,
|
|
212
|
+
record: CommandRecord,
|
|
213
|
+
command: ActionCommand,
|
|
214
|
+
lifecycle_lease: RuntimeLifecycleLease,
|
|
215
|
+
) -> SemanticResultAssemblyInput:
|
|
216
|
+
kind = record.kind
|
|
217
|
+
if session.connection is None or session.device_token is None:
|
|
218
|
+
raise DaemonError(
|
|
219
|
+
code=DaemonErrorCode.RUNTIME_NOT_CONNECTED,
|
|
220
|
+
message="runtime is not connected to a device",
|
|
221
|
+
retryable=False,
|
|
222
|
+
details={"workspaceRoot": session.workspace_root.as_posix()},
|
|
223
|
+
http_status=200,
|
|
224
|
+
)
|
|
225
|
+
if is_ref_bound_action_command(command) and (
|
|
226
|
+
session.latest_snapshot is None or session.screen_state is None
|
|
227
|
+
):
|
|
228
|
+
raise DaemonError(
|
|
229
|
+
code=DaemonErrorCode.SCREEN_NOT_READY,
|
|
230
|
+
message="screen is not ready yet",
|
|
231
|
+
retryable=False,
|
|
232
|
+
details={"workspaceRoot": session.workspace_root.as_posix()},
|
|
233
|
+
http_status=200,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
focused_noop_success = _try_focused_input_noop_success(session, command)
|
|
237
|
+
if focused_noop_success is not None:
|
|
238
|
+
return focused_noop_success
|
|
239
|
+
|
|
240
|
+
client = None
|
|
241
|
+
if session.transport is None:
|
|
242
|
+
client = self._device_client_factory(
|
|
243
|
+
session,
|
|
244
|
+
lifecycle_lease=lifecycle_lease,
|
|
245
|
+
)
|
|
246
|
+
ensure_command_supported(session, command)
|
|
247
|
+
validate_action_semantics(session, command)
|
|
248
|
+
previous_screen = current_public_screen(session)
|
|
249
|
+
previous_snapshot = session.latest_snapshot
|
|
250
|
+
previous_compiled = current_compiled_screen(session)
|
|
251
|
+
settle_baseline_signature = _settle_baseline_signature(
|
|
252
|
+
previous_compiled,
|
|
253
|
+
previous_snapshot,
|
|
254
|
+
)
|
|
255
|
+
fresh_current_baseline = (
|
|
256
|
+
capture_global_fresh_current_baseline(
|
|
257
|
+
action=command.action,
|
|
258
|
+
snapshot=previous_snapshot,
|
|
259
|
+
compiled_screen=previous_compiled,
|
|
260
|
+
)
|
|
261
|
+
if isinstance(command, GlobalCommand)
|
|
262
|
+
else None
|
|
263
|
+
)
|
|
264
|
+
if client is None:
|
|
265
|
+
client = self._device_client_factory(
|
|
266
|
+
session,
|
|
267
|
+
lifecycle_lease=lifecycle_lease,
|
|
268
|
+
)
|
|
269
|
+
repaired_once = False
|
|
270
|
+
submit_route: SubmitRouteOutcome | None = None
|
|
271
|
+
request, submit_route, repaired_once = self._build_dispatch_request(
|
|
272
|
+
session,
|
|
273
|
+
record,
|
|
274
|
+
command,
|
|
275
|
+
lifecycle_lease=lifecycle_lease,
|
|
276
|
+
repaired_once=repaired_once,
|
|
277
|
+
)
|
|
278
|
+
if is_ref_bound_action_command(command) and not isinstance(
|
|
279
|
+
command, SubmitCommand
|
|
280
|
+
):
|
|
281
|
+
validate_resolved_ref_action(session, command, request.request_handle)
|
|
282
|
+
subject_ref, dispatched_ref = _action_target_request_refs(
|
|
283
|
+
session=session,
|
|
284
|
+
command=command,
|
|
285
|
+
request=request,
|
|
286
|
+
submit_route=submit_route,
|
|
287
|
+
)
|
|
288
|
+
focus_context, type_confirmation_context, submit_context = (
|
|
289
|
+
_confirmation_contexts_for_request(
|
|
290
|
+
session=session,
|
|
291
|
+
command=command,
|
|
292
|
+
request=request,
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
ref_postcondition_context = _ref_postcondition_context_for_request(
|
|
296
|
+
session=session,
|
|
297
|
+
command=command,
|
|
298
|
+
request=request,
|
|
299
|
+
)
|
|
300
|
+
type_command = command if isinstance(command, TypeCommand) else None
|
|
301
|
+
submit_command = command if isinstance(command, SubmitCommand) else None
|
|
302
|
+
try:
|
|
303
|
+
action_result = client.action_perform(
|
|
304
|
+
request.payload,
|
|
305
|
+
request_id=DEVICE_RPC_REQUEST_ID_ACTION,
|
|
306
|
+
)
|
|
307
|
+
except DaemonError as error:
|
|
308
|
+
if (
|
|
309
|
+
is_ref_bound_action_command(command)
|
|
310
|
+
and self._is_device_rpc_error(error, DeviceRpcErrorCode.STALE_TARGET)
|
|
311
|
+
and not repaired_once
|
|
312
|
+
):
|
|
313
|
+
request, submit_route, repaired_once = self._build_dispatch_request(
|
|
314
|
+
session,
|
|
315
|
+
record,
|
|
316
|
+
command,
|
|
317
|
+
lifecycle_lease=lifecycle_lease,
|
|
318
|
+
repaired_once=True,
|
|
319
|
+
required_submit_route=(
|
|
320
|
+
submit_route.route if submit_route is not None else None
|
|
321
|
+
),
|
|
322
|
+
)
|
|
323
|
+
if is_ref_bound_action_command(command) and not isinstance(
|
|
324
|
+
command, SubmitCommand
|
|
325
|
+
):
|
|
326
|
+
validate_resolved_ref_action(
|
|
327
|
+
session,
|
|
328
|
+
command,
|
|
329
|
+
request.request_handle,
|
|
330
|
+
)
|
|
331
|
+
subject_ref, dispatched_ref = _action_target_request_refs(
|
|
332
|
+
session=session,
|
|
333
|
+
command=command,
|
|
334
|
+
request=request,
|
|
335
|
+
submit_route=submit_route,
|
|
336
|
+
)
|
|
337
|
+
focus_context, type_confirmation_context, submit_context = (
|
|
338
|
+
_confirmation_contexts_for_request(
|
|
339
|
+
session=session,
|
|
340
|
+
command=command,
|
|
341
|
+
request=request,
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
ref_postcondition_context = _ref_postcondition_context_for_request(
|
|
345
|
+
session=session,
|
|
346
|
+
command=command,
|
|
347
|
+
request=request,
|
|
348
|
+
)
|
|
349
|
+
try:
|
|
350
|
+
action_result = client.action_perform(
|
|
351
|
+
request.payload,
|
|
352
|
+
request_id=DEVICE_RPC_REQUEST_ID_ACTION,
|
|
353
|
+
)
|
|
354
|
+
except DaemonError as retry_error:
|
|
355
|
+
raise self._map_action_error(
|
|
356
|
+
retry_error,
|
|
357
|
+
command=command,
|
|
358
|
+
) from retry_error
|
|
359
|
+
else:
|
|
360
|
+
raise self._map_action_error(error, command=command) from error
|
|
361
|
+
|
|
362
|
+
if isinstance(command, GlobalCommand) and self._runtime_kernel is not None:
|
|
363
|
+
self._runtime_kernel.drop_current_screen_authority(
|
|
364
|
+
session,
|
|
365
|
+
lifecycle_lease,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
candidate_validator = None
|
|
370
|
+
if fresh_current_baseline is not None:
|
|
371
|
+
|
|
372
|
+
def candidate_validator(
|
|
373
|
+
candidate_snapshot: RawSnapshot,
|
|
374
|
+
_public_screen: PublicScreen,
|
|
375
|
+
candidate_compiled_screen: CompiledScreen,
|
|
376
|
+
) -> None:
|
|
377
|
+
validate_global_fresh_current_evidence(
|
|
378
|
+
fresh_current_baseline,
|
|
379
|
+
snapshot=candidate_snapshot,
|
|
380
|
+
compiled_screen=candidate_compiled_screen,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
settle_result = self._settler.settle(
|
|
384
|
+
session,
|
|
385
|
+
client,
|
|
386
|
+
kind,
|
|
387
|
+
baseline_signature=settle_baseline_signature,
|
|
388
|
+
lifecycle_lease=lifecycle_lease,
|
|
389
|
+
)
|
|
390
|
+
snapshot = settle_result.snapshot
|
|
391
|
+
snapshot, public_screen, _artifacts = self._screen_refresh.refresh(
|
|
392
|
+
session,
|
|
393
|
+
snapshot,
|
|
394
|
+
lifecycle_lease=lifecycle_lease,
|
|
395
|
+
command_kind=kind,
|
|
396
|
+
record=record,
|
|
397
|
+
candidate_validator=candidate_validator,
|
|
398
|
+
)
|
|
399
|
+
type_confirmation: TypeConfirmationCandidate | None = None
|
|
400
|
+
submit_confirmation: SubmitConfirmationOutcome | None = None
|
|
401
|
+
command_target_handle = request.request_handle
|
|
402
|
+
|
|
403
|
+
postcondition = validate_postcondition(
|
|
404
|
+
command,
|
|
405
|
+
previous_snapshot,
|
|
406
|
+
snapshot,
|
|
407
|
+
previous_screen,
|
|
408
|
+
public_screen,
|
|
409
|
+
session=session,
|
|
410
|
+
focus_context=focus_context,
|
|
411
|
+
action_result=action_result,
|
|
412
|
+
ref_context=ref_postcondition_context,
|
|
413
|
+
)
|
|
414
|
+
if type_command is not None:
|
|
415
|
+
if type_confirmation_context is None:
|
|
416
|
+
raise RuntimeError("type confirmation context was not prepared")
|
|
417
|
+
confirmed_candidate = validate_type_confirmation(
|
|
418
|
+
session=session,
|
|
419
|
+
command=type_command,
|
|
420
|
+
snapshot=snapshot,
|
|
421
|
+
context=type_confirmation_context,
|
|
422
|
+
action_result=action_result,
|
|
423
|
+
)
|
|
424
|
+
type_confirmation = confirmed_candidate
|
|
425
|
+
command_target_handle = (
|
|
426
|
+
type_confirmation.target_handle or command_target_handle
|
|
427
|
+
)
|
|
428
|
+
if submit_command is not None:
|
|
429
|
+
if submit_route is None:
|
|
430
|
+
raise RuntimeError("submit route was not resolved")
|
|
431
|
+
submit_confirmation = validate_submit_confirmation(
|
|
432
|
+
session=session,
|
|
433
|
+
route_kind=submit_route.route,
|
|
434
|
+
action_result=action_result,
|
|
435
|
+
previous_snapshot=previous_snapshot,
|
|
436
|
+
snapshot=snapshot,
|
|
437
|
+
previous_screen=previous_screen,
|
|
438
|
+
public_screen=public_screen,
|
|
439
|
+
context=submit_context,
|
|
440
|
+
command_target_handle=command_target_handle,
|
|
441
|
+
)
|
|
442
|
+
if (
|
|
443
|
+
submit_confirmation is not None
|
|
444
|
+
and submit_route is not None
|
|
445
|
+
and submit_route.route == "direct"
|
|
446
|
+
and submit_confirmation.status == "unconfirmed"
|
|
447
|
+
):
|
|
448
|
+
raise DaemonError(
|
|
449
|
+
code=DaemonErrorCode.SUBMIT_NOT_CONFIRMED,
|
|
450
|
+
message="submit effect could not be confirmed",
|
|
451
|
+
retryable=True,
|
|
452
|
+
details={"reason": "direct_submit_not_confirmed"},
|
|
453
|
+
http_status=200,
|
|
454
|
+
)
|
|
455
|
+
action_target = _build_success_action_target(
|
|
456
|
+
command=command,
|
|
457
|
+
source_ref=getattr(command, "ref", None),
|
|
458
|
+
source_screen_id=getattr(command, "source_screen_id", None),
|
|
459
|
+
subject_ref=subject_ref,
|
|
460
|
+
dispatched_ref=dispatched_ref,
|
|
461
|
+
repaired_once=repaired_once,
|
|
462
|
+
public_screen=public_screen,
|
|
463
|
+
compiled_screen=current_compiled_screen(session),
|
|
464
|
+
focus_confirmation=postcondition.focus_confirmation,
|
|
465
|
+
type_confirmation=type_confirmation,
|
|
466
|
+
submit_confirmation=submit_confirmation,
|
|
467
|
+
submit_route=submit_route,
|
|
468
|
+
)
|
|
469
|
+
except DaemonError as error:
|
|
470
|
+
normalized_error = self._map_action_error(error, command=command)
|
|
471
|
+
credential_invalidated = (
|
|
472
|
+
normalized_error.code == DaemonErrorCode.DEVICE_AGENT_UNAUTHORIZED
|
|
473
|
+
)
|
|
474
|
+
if credential_invalidated:
|
|
475
|
+
if self._runtime_kernel is not None:
|
|
476
|
+
self._runtime_kernel.invalidate_device_credentials(
|
|
477
|
+
session,
|
|
478
|
+
lifecycle_lease,
|
|
479
|
+
)
|
|
480
|
+
truth_lost_after_dispatch = False
|
|
481
|
+
else:
|
|
482
|
+
truth_lost_after_dispatch = _is_action_availability_error(
|
|
483
|
+
normalized_error
|
|
484
|
+
)
|
|
485
|
+
if truth_lost_after_dispatch and self._runtime_kernel is not None:
|
|
486
|
+
self._runtime_kernel.drop_current_screen_authority(
|
|
487
|
+
session,
|
|
488
|
+
lifecycle_lease,
|
|
489
|
+
discard_transport=(
|
|
490
|
+
_should_discard_transport_after_dispatch(normalized_error)
|
|
491
|
+
),
|
|
492
|
+
)
|
|
493
|
+
raise ActionExecutionFailure(
|
|
494
|
+
original_error=error,
|
|
495
|
+
normalized_error=normalized_error,
|
|
496
|
+
# This means the device action RPC returned successfully; a
|
|
497
|
+
# later settle/refresh/confirmation phase failed.
|
|
498
|
+
dispatch_attempted=True,
|
|
499
|
+
truth_lost_after_dispatch=truth_lost_after_dispatch,
|
|
500
|
+
) from error
|
|
501
|
+
|
|
502
|
+
return SemanticResultAssemblyInput(
|
|
503
|
+
app_payload=app_payload(
|
|
504
|
+
snapshot,
|
|
505
|
+
app_match=postcondition.app_match,
|
|
506
|
+
),
|
|
507
|
+
action_target=action_target,
|
|
508
|
+
warnings=(
|
|
509
|
+
(_POST_DISPATCH_SETTLE_TIMEOUT_WARNING,)
|
|
510
|
+
if settle_result.timed_out
|
|
511
|
+
else ()
|
|
512
|
+
),
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
def _build_dispatch_request(
|
|
516
|
+
self,
|
|
517
|
+
session: WorkspaceRuntime,
|
|
518
|
+
record: CommandRecord,
|
|
519
|
+
command: ActionCommand,
|
|
520
|
+
*,
|
|
521
|
+
lifecycle_lease: RuntimeLifecycleLease,
|
|
522
|
+
repaired_once: bool,
|
|
523
|
+
required_submit_route: str | None = None,
|
|
524
|
+
) -> tuple[BuiltDeviceActionRequest, SubmitRouteOutcome | None, bool]:
|
|
525
|
+
if isinstance(command, SubmitCommand):
|
|
526
|
+
if repaired_once or command.source_screen_id != session.current_screen_id:
|
|
527
|
+
subject_handle = self._repair_action_binding(
|
|
528
|
+
session,
|
|
529
|
+
record,
|
|
530
|
+
command,
|
|
531
|
+
lifecycle_lease=lifecycle_lease,
|
|
532
|
+
)
|
|
533
|
+
source_evidence: ActionTargetEvidence = "refRepair"
|
|
534
|
+
repaired_once = True
|
|
535
|
+
else:
|
|
536
|
+
subject_handle = resolve_ref_target(
|
|
537
|
+
session,
|
|
538
|
+
command.ref,
|
|
539
|
+
command.source_screen_id,
|
|
540
|
+
)
|
|
541
|
+
source_evidence = "liveRef"
|
|
542
|
+
route = resolve_submit_route(
|
|
543
|
+
session,
|
|
544
|
+
command,
|
|
545
|
+
subject_handle=subject_handle,
|
|
546
|
+
source_evidence=source_evidence,
|
|
547
|
+
)
|
|
548
|
+
if (
|
|
549
|
+
required_submit_route is not None
|
|
550
|
+
and route.route != required_submit_route
|
|
551
|
+
):
|
|
552
|
+
raise DaemonError(
|
|
553
|
+
code=DaemonErrorCode.TARGET_NOT_ACTIONABLE,
|
|
554
|
+
message="submit is not available for the requested target",
|
|
555
|
+
retryable=False,
|
|
556
|
+
details={
|
|
557
|
+
"reason": "submit_route_changed_after_repair",
|
|
558
|
+
"ref": command.ref,
|
|
559
|
+
"requiredRoute": required_submit_route,
|
|
560
|
+
"resolvedRoute": route.route,
|
|
561
|
+
},
|
|
562
|
+
http_status=200,
|
|
563
|
+
)
|
|
564
|
+
request = build_submit_action_request_for_route(route)
|
|
565
|
+
ensure_action_request_supported(
|
|
566
|
+
session,
|
|
567
|
+
command=command.kind,
|
|
568
|
+
request=request.payload,
|
|
569
|
+
)
|
|
570
|
+
return request, route, repaired_once
|
|
571
|
+
|
|
572
|
+
if is_ref_bound_action_command(command) and (
|
|
573
|
+
repaired_once or command.source_screen_id != session.current_screen_id
|
|
574
|
+
):
|
|
575
|
+
request = self._repairer.repair_action_command(
|
|
576
|
+
session,
|
|
577
|
+
record,
|
|
578
|
+
command,
|
|
579
|
+
lifecycle_lease=lifecycle_lease,
|
|
580
|
+
)
|
|
581
|
+
return request, None, True
|
|
582
|
+
return build_action_request(session, command), None, repaired_once
|
|
583
|
+
|
|
584
|
+
def _repair_action_binding(
|
|
585
|
+
self,
|
|
586
|
+
session: WorkspaceRuntime,
|
|
587
|
+
record: CommandRecord,
|
|
588
|
+
command: RefBoundActionCommand,
|
|
589
|
+
*,
|
|
590
|
+
lifecycle_lease: RuntimeLifecycleLease,
|
|
591
|
+
) -> NodeHandle | None:
|
|
592
|
+
return self._repairer.repair_action_binding(
|
|
593
|
+
session,
|
|
594
|
+
record,
|
|
595
|
+
command,
|
|
596
|
+
lifecycle_lease=lifecycle_lease,
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
def _map_action_error(
|
|
600
|
+
self, error: DaemonError, *, command: ActionCommand
|
|
601
|
+
) -> DaemonError:
|
|
602
|
+
if isinstance(command, OpenCommand) and self._is_device_rpc_error(
|
|
603
|
+
error, DeviceRpcErrorCode.ACTION_FAILED
|
|
604
|
+
):
|
|
605
|
+
return DaemonError(
|
|
606
|
+
code=DaemonErrorCode.OPEN_FAILED,
|
|
607
|
+
message=error.message,
|
|
608
|
+
retryable=error.retryable,
|
|
609
|
+
details=dict(error.details),
|
|
610
|
+
http_status=error.http_status,
|
|
611
|
+
)
|
|
612
|
+
if not self._is_device_rpc_error(
|
|
613
|
+
error, DeviceRpcErrorCode.TARGET_NOT_ACTIONABLE
|
|
614
|
+
):
|
|
615
|
+
return error
|
|
616
|
+
return DaemonError(
|
|
617
|
+
code=DaemonErrorCode.TARGET_NOT_ACTIONABLE,
|
|
618
|
+
message=error.message,
|
|
619
|
+
retryable=error.retryable,
|
|
620
|
+
details=dict(error.details),
|
|
621
|
+
http_status=error.http_status,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
def _is_device_rpc_error(
|
|
625
|
+
self, error: DaemonError, code: DeviceRpcErrorCode
|
|
626
|
+
) -> bool:
|
|
627
|
+
return (
|
|
628
|
+
error.code == DaemonErrorCode.DEVICE_RPC_FAILED
|
|
629
|
+
and error.details.get("deviceCode") == code.value
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _settle_baseline_signature(
|
|
634
|
+
compiled_screen: CompiledScreen | None,
|
|
635
|
+
previous_snapshot: RawSnapshot | None,
|
|
636
|
+
) -> tuple[object, ...]:
|
|
637
|
+
if previous_snapshot is not None:
|
|
638
|
+
return settle_screen_signature(compiled_screen, previous_snapshot)
|
|
639
|
+
return ("connected-without-screen",)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _action_target_request_refs(
|
|
643
|
+
*,
|
|
644
|
+
session: WorkspaceRuntime,
|
|
645
|
+
command: ActionCommand,
|
|
646
|
+
request: BuiltDeviceActionRequest,
|
|
647
|
+
submit_route: SubmitRouteOutcome | None,
|
|
648
|
+
) -> tuple[str | None, str | None]:
|
|
649
|
+
if not isinstance(command, (FocusCommand, TypeCommand, SubmitCommand)):
|
|
650
|
+
return None, None
|
|
651
|
+
if submit_route is not None:
|
|
652
|
+
return submit_route.subject_ref, submit_route.dispatched_ref
|
|
653
|
+
subject_ref = public_ref_for_handle(
|
|
654
|
+
compiled_screen=current_compiled_screen(session),
|
|
655
|
+
public_screen=current_public_screen(session),
|
|
656
|
+
handle=request.request_handle,
|
|
657
|
+
)
|
|
658
|
+
dispatched_handle = request.dispatched_handle or request.request_handle
|
|
659
|
+
dispatched_ref = public_ref_for_handle(
|
|
660
|
+
compiled_screen=current_compiled_screen(session),
|
|
661
|
+
public_screen=current_public_screen(session),
|
|
662
|
+
handle=dispatched_handle,
|
|
663
|
+
)
|
|
664
|
+
return subject_ref, dispatched_ref
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def _confirmation_contexts_for_request(
|
|
668
|
+
*,
|
|
669
|
+
session: WorkspaceRuntime,
|
|
670
|
+
command: ActionCommand,
|
|
671
|
+
request: BuiltDeviceActionRequest,
|
|
672
|
+
) -> tuple[
|
|
673
|
+
FocusConfirmationContext | None,
|
|
674
|
+
TypeConfirmationContext | None,
|
|
675
|
+
SubmitConfirmationContext | None,
|
|
676
|
+
]:
|
|
677
|
+
focus_command = command if isinstance(command, FocusCommand) else None
|
|
678
|
+
submit_command = command if isinstance(command, SubmitCommand) else None
|
|
679
|
+
type_command = command if isinstance(command, TypeCommand) else None
|
|
680
|
+
focus_context = (
|
|
681
|
+
build_focus_confirmation_context(session, focus_command, request.request_handle)
|
|
682
|
+
if focus_command is not None
|
|
683
|
+
else None
|
|
684
|
+
)
|
|
685
|
+
type_confirmation_context = (
|
|
686
|
+
build_type_confirmation_context(session, type_command, request.request_handle)
|
|
687
|
+
if type_command is not None
|
|
688
|
+
else None
|
|
689
|
+
)
|
|
690
|
+
submit_context = (
|
|
691
|
+
build_submit_confirmation_context(
|
|
692
|
+
session,
|
|
693
|
+
submit_command,
|
|
694
|
+
request.request_handle,
|
|
695
|
+
)
|
|
696
|
+
if submit_command is not None
|
|
697
|
+
else None
|
|
698
|
+
)
|
|
699
|
+
return focus_context, type_confirmation_context, submit_context
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _ref_postcondition_context_for_request(
|
|
703
|
+
*,
|
|
704
|
+
session: WorkspaceRuntime,
|
|
705
|
+
command: ActionCommand,
|
|
706
|
+
request: BuiltDeviceActionRequest,
|
|
707
|
+
) -> RefActionPostconditionContext | None:
|
|
708
|
+
if not isinstance(command, (LongTapCommand, ScrollCommand)):
|
|
709
|
+
return None
|
|
710
|
+
baseline_screen = current_public_screen(session)
|
|
711
|
+
target_ref = public_ref_for_handle(
|
|
712
|
+
compiled_screen=current_compiled_screen(session),
|
|
713
|
+
public_screen=baseline_screen,
|
|
714
|
+
handle=request.request_handle,
|
|
715
|
+
)
|
|
716
|
+
baseline_target = (
|
|
717
|
+
None
|
|
718
|
+
if baseline_screen is None or target_ref is None
|
|
719
|
+
else _unique_public_node(baseline_screen, target_ref)
|
|
720
|
+
)
|
|
721
|
+
return RefActionPostconditionContext(
|
|
722
|
+
target_ref=target_ref,
|
|
723
|
+
baseline_screen=baseline_screen,
|
|
724
|
+
baseline_target=baseline_target,
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _is_action_availability_error(error: DaemonError) -> bool:
|
|
729
|
+
return error.code in _ACTION_AVAILABILITY_ERROR_CODES
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _should_discard_transport_after_dispatch(error: DaemonError) -> bool:
|
|
733
|
+
return error.code in _POST_DISPATCH_TRANSPORT_INVALIDATING_ERROR_CODES
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def _build_success_action_target(
|
|
737
|
+
*,
|
|
738
|
+
command: ActionCommand,
|
|
739
|
+
source_ref: object,
|
|
740
|
+
source_screen_id: object,
|
|
741
|
+
subject_ref: str | None,
|
|
742
|
+
dispatched_ref: str | None,
|
|
743
|
+
repaired_once: bool,
|
|
744
|
+
public_screen: PublicScreen,
|
|
745
|
+
compiled_screen: CompiledScreen | None,
|
|
746
|
+
focus_confirmation: FocusConfirmationOutcome | None,
|
|
747
|
+
type_confirmation: TypeConfirmationCandidate | None,
|
|
748
|
+
submit_confirmation: SubmitConfirmationOutcome | None,
|
|
749
|
+
submit_route: SubmitRouteOutcome | None,
|
|
750
|
+
) -> ActionTargetPayload | None:
|
|
751
|
+
if not isinstance(command, (FocusCommand, TypeCommand, SubmitCommand)):
|
|
752
|
+
return None
|
|
753
|
+
if not isinstance(source_ref, str) or not isinstance(source_screen_id, str):
|
|
754
|
+
return None
|
|
755
|
+
source_evidence: ActionTargetEvidence = (
|
|
756
|
+
submit_route.source_evidence
|
|
757
|
+
if submit_route is not None
|
|
758
|
+
else "refRepair" if repaired_once else "liveRef"
|
|
759
|
+
)
|
|
760
|
+
next_screen_id = public_screen.screen_id
|
|
761
|
+
if isinstance(command, FocusCommand):
|
|
762
|
+
next_ref = public_ref_for_handle(
|
|
763
|
+
compiled_screen=compiled_screen,
|
|
764
|
+
public_screen=public_screen,
|
|
765
|
+
handle=(
|
|
766
|
+
None if focus_confirmation is None else focus_confirmation.target_handle
|
|
767
|
+
),
|
|
768
|
+
)
|
|
769
|
+
strategy = None if focus_confirmation is None else focus_confirmation.strategy
|
|
770
|
+
return build_same_or_successor_action_target(
|
|
771
|
+
source_ref=source_ref,
|
|
772
|
+
source_screen_id=source_screen_id,
|
|
773
|
+
subject_ref=subject_ref,
|
|
774
|
+
dispatched_ref=dispatched_ref,
|
|
775
|
+
next_screen_id=next_screen_id,
|
|
776
|
+
next_ref=next_ref,
|
|
777
|
+
evidence=_confirmation_evidence(
|
|
778
|
+
source_evidence,
|
|
779
|
+
strategy,
|
|
780
|
+
"focusConfirmation",
|
|
781
|
+
),
|
|
782
|
+
)
|
|
783
|
+
if isinstance(command, TypeCommand):
|
|
784
|
+
next_ref = public_ref_for_handle(
|
|
785
|
+
compiled_screen=compiled_screen,
|
|
786
|
+
public_screen=public_screen,
|
|
787
|
+
handle=(
|
|
788
|
+
None if type_confirmation is None else type_confirmation.target_handle
|
|
789
|
+
),
|
|
790
|
+
)
|
|
791
|
+
if next_ref is None and type_confirmation is not None:
|
|
792
|
+
next_ref = public_ref_for_raw_node(
|
|
793
|
+
compiled_screen=compiled_screen,
|
|
794
|
+
public_screen=public_screen,
|
|
795
|
+
node=type_confirmation.node,
|
|
796
|
+
)
|
|
797
|
+
strategy = None if type_confirmation is None else type_confirmation.strategy
|
|
798
|
+
return build_same_or_successor_action_target(
|
|
799
|
+
source_ref=source_ref,
|
|
800
|
+
source_screen_id=source_screen_id,
|
|
801
|
+
subject_ref=subject_ref,
|
|
802
|
+
dispatched_ref=dispatched_ref,
|
|
803
|
+
next_screen_id=next_screen_id,
|
|
804
|
+
next_ref=next_ref,
|
|
805
|
+
evidence=_confirmation_evidence(
|
|
806
|
+
source_evidence,
|
|
807
|
+
strategy,
|
|
808
|
+
"typeConfirmation",
|
|
809
|
+
),
|
|
810
|
+
)
|
|
811
|
+
if submit_confirmation is None:
|
|
812
|
+
return None
|
|
813
|
+
submit_evidence = _submit_confirmation_evidence(source_evidence, submit_route)
|
|
814
|
+
if submit_confirmation.status == "targetGone":
|
|
815
|
+
return build_action_target_payload(
|
|
816
|
+
source_ref=source_ref,
|
|
817
|
+
source_screen_id=source_screen_id,
|
|
818
|
+
subject_ref=subject_ref,
|
|
819
|
+
dispatched_ref=dispatched_ref,
|
|
820
|
+
next_screen_id=next_screen_id,
|
|
821
|
+
identity_status="gone",
|
|
822
|
+
evidence=(*submit_evidence, "targetGone"),
|
|
823
|
+
)
|
|
824
|
+
if submit_confirmation.status == "sameTarget":
|
|
825
|
+
next_ref = public_ref_for_handle(
|
|
826
|
+
compiled_screen=compiled_screen,
|
|
827
|
+
public_screen=public_screen,
|
|
828
|
+
handle=submit_confirmation.target_handle,
|
|
829
|
+
)
|
|
830
|
+
if next_ref is None:
|
|
831
|
+
next_ref = public_ref_for_raw_node(
|
|
832
|
+
compiled_screen=compiled_screen,
|
|
833
|
+
public_screen=public_screen,
|
|
834
|
+
node=submit_confirmation.node,
|
|
835
|
+
)
|
|
836
|
+
return build_same_or_successor_action_target(
|
|
837
|
+
source_ref=source_ref,
|
|
838
|
+
source_screen_id=source_screen_id,
|
|
839
|
+
subject_ref=subject_ref,
|
|
840
|
+
dispatched_ref=dispatched_ref,
|
|
841
|
+
next_screen_id=next_screen_id,
|
|
842
|
+
next_ref=next_ref,
|
|
843
|
+
evidence=submit_evidence,
|
|
844
|
+
)
|
|
845
|
+
if submit_confirmation.status == "publicChange":
|
|
846
|
+
return build_action_target_payload(
|
|
847
|
+
source_ref=source_ref,
|
|
848
|
+
source_screen_id=source_screen_id,
|
|
849
|
+
subject_ref=subject_ref,
|
|
850
|
+
dispatched_ref=dispatched_ref,
|
|
851
|
+
next_screen_id=next_screen_id,
|
|
852
|
+
identity_status="unconfirmed",
|
|
853
|
+
evidence=(*submit_evidence, "publicChange"),
|
|
854
|
+
)
|
|
855
|
+
if submit_confirmation.status == "unconfirmed":
|
|
856
|
+
if submit_route is not None and submit_route.route == "direct":
|
|
857
|
+
return None
|
|
858
|
+
return build_action_target_payload(
|
|
859
|
+
source_ref=source_ref,
|
|
860
|
+
source_screen_id=source_screen_id,
|
|
861
|
+
subject_ref=subject_ref,
|
|
862
|
+
dispatched_ref=dispatched_ref,
|
|
863
|
+
next_screen_id=next_screen_id,
|
|
864
|
+
identity_status="unconfirmed",
|
|
865
|
+
evidence=(*submit_evidence, "ambiguousSuccessor"),
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def _confirmation_evidence(
|
|
870
|
+
source_evidence: ActionTargetEvidence,
|
|
871
|
+
strategy: object,
|
|
872
|
+
command_evidence: ActionTargetEvidence,
|
|
873
|
+
) -> tuple[ActionTargetEvidence, ...]:
|
|
874
|
+
evidence: list[ActionTargetEvidence] = [source_evidence]
|
|
875
|
+
if strategy in {
|
|
876
|
+
"requestTarget",
|
|
877
|
+
"resolvedTarget",
|
|
878
|
+
"reusedRef",
|
|
879
|
+
"fingerprintRematch",
|
|
880
|
+
}:
|
|
881
|
+
evidence.append(cast(ActionTargetEvidence, strategy))
|
|
882
|
+
evidence.append(command_evidence)
|
|
883
|
+
return tuple(evidence)
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def _submit_confirmation_evidence(
|
|
887
|
+
source_evidence: ActionTargetEvidence,
|
|
888
|
+
submit_route: SubmitRouteOutcome | None,
|
|
889
|
+
) -> tuple[ActionTargetEvidence, ...]:
|
|
890
|
+
evidence: list[ActionTargetEvidence] = [source_evidence]
|
|
891
|
+
if submit_route is not None and submit_route.route == "attributed":
|
|
892
|
+
evidence.append("attributedRoute")
|
|
893
|
+
evidence.append("submitConfirmation")
|
|
894
|
+
return tuple(evidence)
|