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.
Files changed (187) hide show
  1. androidctl/__init__.py +5 -0
  2. androidctl/__main__.py +4 -0
  3. androidctl/_version.py +1 -0
  4. androidctl/app.py +73 -0
  5. androidctl/cli_options.py +27 -0
  6. androidctl/command_payloads.py +264 -0
  7. androidctl/command_views.py +157 -0
  8. androidctl/commands/__init__.py +1 -0
  9. androidctl/commands/actions.py +236 -0
  10. androidctl/commands/adb_wireless.py +157 -0
  11. androidctl/commands/close.py +30 -0
  12. androidctl/commands/connect.py +69 -0
  13. androidctl/commands/execute.py +179 -0
  14. androidctl/commands/list_apps.py +26 -0
  15. androidctl/commands/observe.py +26 -0
  16. androidctl/commands/open.py +41 -0
  17. androidctl/commands/plumbing.py +58 -0
  18. androidctl/commands/run_pipeline.py +307 -0
  19. androidctl/commands/screenshot.py +29 -0
  20. androidctl/commands/setup.py +301 -0
  21. androidctl/commands/wait.py +60 -0
  22. androidctl/daemon/__init__.py +1 -0
  23. androidctl/daemon/client.py +348 -0
  24. androidctl/daemon/discovery.py +190 -0
  25. androidctl/daemon/launcher.py +26 -0
  26. androidctl/daemon/owner.py +349 -0
  27. androidctl/errors/__init__.py +1 -0
  28. androidctl/errors/mapping.py +149 -0
  29. androidctl/errors/models.py +16 -0
  30. androidctl/exit_codes.py +8 -0
  31. androidctl/output.py +147 -0
  32. androidctl/parsing/__init__.py +1 -0
  33. androidctl/parsing/duration.py +17 -0
  34. androidctl/parsing/open_target.py +51 -0
  35. androidctl/parsing/refs.py +12 -0
  36. androidctl/parsing/screen_id.py +10 -0
  37. androidctl/parsing/wait.py +70 -0
  38. androidctl/renderers/__init__.py +110 -0
  39. androidctl/renderers/_paths.py +109 -0
  40. androidctl/renderers/xml.py +234 -0
  41. androidctl/renderers/xml_projection.py +732 -0
  42. androidctl/resources/__init__.py +1 -0
  43. androidctl/resources/androidctl-agent-0.1.0-release.apk +0 -0
  44. androidctl/setup/__init__.py +1 -0
  45. androidctl/setup/accessibility.py +159 -0
  46. androidctl/setup/adb.py +586 -0
  47. androidctl/setup/apk_resource.py +29 -0
  48. androidctl/setup/pairing.py +70 -0
  49. androidctl/setup/verify.py +175 -0
  50. androidctl/workspace/__init__.py +3 -0
  51. androidctl/workspace/resolve.py +27 -0
  52. androidctl-0.1.0.dist-info/METADATA +217 -0
  53. androidctl-0.1.0.dist-info/RECORD +187 -0
  54. androidctl-0.1.0.dist-info/WHEEL +5 -0
  55. androidctl-0.1.0.dist-info/entry_points.txt +3 -0
  56. androidctl-0.1.0.dist-info/licenses/LICENSE +674 -0
  57. androidctl-0.1.0.dist-info/top_level.txt +3 -0
  58. androidctl_contracts/__init__.py +55 -0
  59. androidctl_contracts/_version.py +1 -0
  60. androidctl_contracts/_wire_helpers.py +31 -0
  61. androidctl_contracts/base.py +142 -0
  62. androidctl_contracts/command_catalog.py +414 -0
  63. androidctl_contracts/command_results.py +630 -0
  64. androidctl_contracts/daemon_api.py +335 -0
  65. androidctl_contracts/errors.py +44 -0
  66. androidctl_contracts/paths.py +5 -0
  67. androidctl_contracts/public_screen.py +579 -0
  68. androidctl_contracts/user_state.py +23 -0
  69. androidctl_contracts/vocabulary.py +82 -0
  70. androidctld/__init__.py +5 -0
  71. androidctld/__main__.py +63 -0
  72. androidctld/_version.py +1 -0
  73. androidctld/actions/__init__.py +1 -0
  74. androidctld/actions/action_target.py +142 -0
  75. androidctld/actions/capabilities.py +539 -0
  76. androidctld/actions/executor.py +894 -0
  77. androidctld/actions/focus_confirmation.py +177 -0
  78. androidctld/actions/focused_input_admissibility.py +120 -0
  79. androidctld/actions/fresh_current.py +176 -0
  80. androidctld/actions/postconditions.py +473 -0
  81. androidctld/actions/repair.py +101 -0
  82. androidctld/actions/request_builder.py +204 -0
  83. androidctld/actions/settle.py +146 -0
  84. androidctld/actions/submit_confirmation.py +211 -0
  85. androidctld/actions/submit_routing.py +311 -0
  86. androidctld/actions/type_confirmation.py +257 -0
  87. androidctld/app_targets.py +71 -0
  88. androidctld/artifacts/__init__.py +1 -0
  89. androidctld/artifacts/models.py +26 -0
  90. androidctld/artifacts/screen_lookup.py +241 -0
  91. androidctld/artifacts/screen_payloads.py +109 -0
  92. androidctld/artifacts/writer.py +286 -0
  93. androidctld/auth/__init__.py +1 -0
  94. androidctld/auth/active_registry.py +266 -0
  95. androidctld/auth/secret_files.py +52 -0
  96. androidctld/auth/token_store.py +59 -0
  97. androidctld/commands/__init__.py +1 -0
  98. androidctld/commands/assembly.py +231 -0
  99. androidctld/commands/command_models.py +254 -0
  100. androidctld/commands/dispatch.py +99 -0
  101. androidctld/commands/executor.py +31 -0
  102. androidctld/commands/from_boundary.py +175 -0
  103. androidctld/commands/handlers/__init__.py +15 -0
  104. androidctld/commands/handlers/action.py +439 -0
  105. androidctld/commands/handlers/connect.py +94 -0
  106. androidctld/commands/handlers/list_apps.py +215 -0
  107. androidctld/commands/handlers/observe.py +121 -0
  108. androidctld/commands/handlers/screenshot.py +105 -0
  109. androidctld/commands/handlers/wait.py +286 -0
  110. androidctld/commands/models.py +65 -0
  111. androidctld/commands/open_targets.py +56 -0
  112. androidctld/commands/orchestration.py +353 -0
  113. androidctld/commands/registry.py +116 -0
  114. androidctld/commands/result_builders.py +40 -0
  115. androidctld/commands/result_models.py +555 -0
  116. androidctld/commands/results.py +108 -0
  117. androidctld/commands/semantic_command_names.py +17 -0
  118. androidctld/commands/semantic_error_mapping.py +93 -0
  119. androidctld/commands/semantic_truth.py +135 -0
  120. androidctld/commands/service.py +67 -0
  121. androidctld/config.py +75 -0
  122. androidctld/daemon/__init__.py +1 -0
  123. androidctld/daemon/active_slot.py +326 -0
  124. androidctld/daemon/envelope.py +30 -0
  125. androidctld/daemon/http_host.py +123 -0
  126. androidctld/daemon/ingress.py +112 -0
  127. androidctld/daemon/ownership_probe.py +204 -0
  128. androidctld/daemon/server.py +286 -0
  129. androidctld/daemon/service.py +99 -0
  130. androidctld/device/__init__.py +1 -0
  131. androidctld/device/action_models.py +154 -0
  132. androidctld/device/action_serialization.py +121 -0
  133. androidctld/device/adapters.py +220 -0
  134. androidctld/device/bootstrap.py +153 -0
  135. androidctld/device/connectors.py +231 -0
  136. androidctld/device/errors.py +100 -0
  137. androidctld/device/interfaces.py +58 -0
  138. androidctld/device/parsing.py +320 -0
  139. androidctld/device/rpc.py +483 -0
  140. androidctld/device/schema.py +114 -0
  141. androidctld/device/types.py +161 -0
  142. androidctld/errors/__init__.py +94 -0
  143. androidctld/logging/__init__.py +22 -0
  144. androidctld/observation.py +98 -0
  145. androidctld/protocol.py +53 -0
  146. androidctld/refs/__init__.py +1 -0
  147. androidctld/refs/models.py +54 -0
  148. androidctld/refs/repair.py +284 -0
  149. androidctld/refs/service.py +422 -0
  150. androidctld/rendering/__init__.py +1 -0
  151. androidctld/rendering/screen_xml.py +256 -0
  152. androidctld/runtime/__init__.py +21 -0
  153. androidctld/runtime/kernel.py +548 -0
  154. androidctld/runtime/lifecycle.py +19 -0
  155. androidctld/runtime/models.py +48 -0
  156. androidctld/runtime/screen_state.py +117 -0
  157. androidctld/runtime/state_repo.py +70 -0
  158. androidctld/runtime/store.py +76 -0
  159. androidctld/runtime_policy.py +127 -0
  160. androidctld/schema/__init__.py +5 -0
  161. androidctld/schema/base.py +132 -0
  162. androidctld/schema/core.py +35 -0
  163. androidctld/schema/daemon_api.py +108 -0
  164. androidctld/schema/persistence.py +161 -0
  165. androidctld/schema/persistence_io.py +41 -0
  166. androidctld/schema/validation_errors.py +309 -0
  167. androidctld/semantics/__init__.py +1 -0
  168. androidctld/semantics/compiler.py +610 -0
  169. androidctld/semantics/continuity.py +107 -0
  170. androidctld/semantics/labels.py +252 -0
  171. androidctld/semantics/models.py +25 -0
  172. androidctld/semantics/policy.py +23 -0
  173. androidctld/semantics/public_models.py +123 -0
  174. androidctld/semantics/registries.py +13 -0
  175. androidctld/semantics/submit_refs.py +417 -0
  176. androidctld/semantics/surface.py +254 -0
  177. androidctld/semantics/targets.py +167 -0
  178. androidctld/snapshots/__init__.py +1 -0
  179. androidctld/snapshots/models.py +219 -0
  180. androidctld/snapshots/refresh.py +273 -0
  181. androidctld/snapshots/schema.py +74 -0
  182. androidctld/snapshots/service.py +138 -0
  183. androidctld/text_equivalence.py +67 -0
  184. androidctld/waits/__init__.py +1 -0
  185. androidctld/waits/evaluators.py +216 -0
  186. androidctld/waits/loop.py +305 -0
  187. androidctld/waits/matcher.py +41 -0
@@ -0,0 +1,335 @@
1
+ """Typed shared daemon API transport and runtime-route models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated, Any, Generic, Literal, TypeAlias, TypeVar
6
+
7
+ from pydantic import (
8
+ Field,
9
+ StringConstraints,
10
+ field_validator,
11
+ model_serializer,
12
+ model_validator,
13
+ )
14
+
15
+ from ._wire_helpers import _drop_unset_keys, _validate_absolute_path
16
+ from .base import DaemonWireModel
17
+ from .errors import DaemonError
18
+ from .vocabulary import RuntimeStatus
19
+
20
+ TOKEN_HEADER_NAME = "X-Androidctld-Token"
21
+ OWNER_HEADER_NAME = "X-Androidctld-Owner"
22
+
23
+ ResultT = TypeVar("ResultT")
24
+ TrimmedString = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
25
+ PositiveInt = Annotated[int, Field(ge=1)]
26
+ NonNegativeInt = Annotated[int, Field(ge=0)]
27
+
28
+
29
+ def _strip_optional_string(value: object) -> object:
30
+ if value is None:
31
+ return None
32
+ if isinstance(value, str):
33
+ normalized = value.strip()
34
+ if not normalized:
35
+ return None
36
+ return normalized
37
+ return value
38
+
39
+
40
+ def _normalize_discriminator_payload(
41
+ value: object,
42
+ *,
43
+ nested_field: str | None = None,
44
+ ) -> object:
45
+ if not isinstance(value, dict):
46
+ return value
47
+ payload = dict(value)
48
+ kind = payload.get("kind")
49
+ if isinstance(kind, str):
50
+ payload["kind"] = kind.strip()
51
+ if nested_field is None:
52
+ return payload
53
+ nested_value = payload.get(nested_field)
54
+ if isinstance(nested_value, dict):
55
+ payload[nested_field] = _normalize_discriminator_payload(nested_value)
56
+ return payload
57
+
58
+
59
+ class HealthResult(DaemonWireModel):
60
+ """Typed health payload exposed by ``POST /health``."""
61
+
62
+ service: str
63
+ version: str
64
+ workspace_root: str
65
+ owner_id: str
66
+
67
+ _validate_workspace_root = field_validator("workspace_root")(
68
+ _validate_absolute_path
69
+ )
70
+
71
+
72
+ class RuntimePayload(DaemonWireModel):
73
+ """Stable runtime projection used by runtime endpoints."""
74
+
75
+ workspace_root: str
76
+ artifact_root: str
77
+ status: RuntimeStatus
78
+ current_screen_id: str | None = None
79
+
80
+ _validate_workspace_root = field_validator("workspace_root")(
81
+ _validate_absolute_path
82
+ )
83
+ _validate_artifact_root = field_validator("artifact_root")(_validate_absolute_path)
84
+
85
+ @model_serializer(mode="wrap")
86
+ def serialize_model(self, handler: Any) -> dict[str, Any]:
87
+ return _drop_unset_keys(
88
+ handler(self),
89
+ fields_set=self.model_fields_set,
90
+ optional_fields={"current_screen_id"},
91
+ )
92
+
93
+
94
+ class DaemonSuccessEnvelope(DaemonWireModel, Generic[ResultT]):
95
+ """Shared success envelope for daemon endpoints."""
96
+
97
+ ok: Literal[True] = True
98
+ result: ResultT
99
+
100
+
101
+ class DaemonErrorEnvelope(DaemonWireModel):
102
+ """Shared error envelope for daemon endpoints."""
103
+
104
+ ok: Literal[False] = False
105
+ error: DaemonError
106
+
107
+
108
+ class RuntimeGetResult(DaemonWireModel):
109
+ """Typed runtime/get result payload."""
110
+
111
+ runtime: RuntimePayload
112
+
113
+
114
+ class ConnectionPayload(DaemonWireModel):
115
+ """Connection options for the retained ``connect`` daemon command."""
116
+
117
+ mode: Literal["adb", "lan"]
118
+ token: TrimmedString
119
+ serial: str | None = None
120
+ host: str | None = None
121
+ port: PositiveInt | None = None
122
+
123
+ @field_validator("serial", "host", mode="before")
124
+ @classmethod
125
+ def normalize_optional_strings(cls, value: object) -> object:
126
+ return _strip_optional_string(value)
127
+
128
+ @model_validator(mode="after")
129
+ def validate_shape(self) -> ConnectionPayload:
130
+ if self.mode == "adb":
131
+ if self.host is not None:
132
+ raise ValueError("host is only allowed for lan connect mode")
133
+ if self.port is not None:
134
+ raise ValueError("port is only allowed for lan connect mode")
135
+ return self
136
+
137
+ if self.host is None:
138
+ raise ValueError("host is required for lan connect mode")
139
+ if self.port is None:
140
+ raise ValueError("port is required for lan connect mode")
141
+ if self.serial is not None:
142
+ raise ValueError("serial is only allowed for adb connect mode")
143
+ return self
144
+
145
+
146
+ class ConnectCommandPayload(DaemonWireModel):
147
+ kind: Literal["connect"]
148
+ connection: ConnectionPayload
149
+
150
+
151
+ class ObserveCommandPayload(DaemonWireModel):
152
+ kind: Literal["observe"]
153
+
154
+
155
+ class OpenAppTargetPayload(DaemonWireModel):
156
+ kind: Literal["app"]
157
+ value: TrimmedString
158
+
159
+
160
+ class OpenUrlTargetPayload(DaemonWireModel):
161
+ kind: Literal["url"]
162
+ value: TrimmedString
163
+
164
+
165
+ OpenTargetPayload: TypeAlias = Annotated[
166
+ OpenAppTargetPayload | OpenUrlTargetPayload,
167
+ Field(discriminator="kind"),
168
+ ]
169
+
170
+
171
+ class OpenCommandPayload(DaemonWireModel):
172
+ kind: Literal["open"]
173
+ target: OpenTargetPayload
174
+
175
+ @model_validator(mode="before")
176
+ @classmethod
177
+ def normalize_target_discriminator(cls, value: object) -> object:
178
+ return _normalize_discriminator_payload(value, nested_field="target")
179
+
180
+
181
+ class RefActionCommandPayload(DaemonWireModel):
182
+ kind: Literal["tap", "longTap", "focus", "submit"]
183
+ ref: TrimmedString
184
+ source_screen_id: TrimmedString
185
+
186
+
187
+ class TypeCommandPayload(DaemonWireModel):
188
+ kind: Literal["type"]
189
+ ref: TrimmedString
190
+ source_screen_id: TrimmedString
191
+ text: str
192
+
193
+
194
+ class ScrollCommandPayload(DaemonWireModel):
195
+ kind: Literal["scroll"]
196
+ ref: TrimmedString
197
+ source_screen_id: TrimmedString
198
+ direction: Literal["up", "down", "left", "right", "backward"]
199
+
200
+
201
+ class GlobalActionCommandPayload(DaemonWireModel):
202
+ kind: Literal["back", "home", "recents", "notifications"]
203
+ source_screen_id: TrimmedString | None = None
204
+
205
+
206
+ class ScreenChangePredicatePayload(DaemonWireModel):
207
+ kind: Literal["screen-change"]
208
+ source_screen_id: TrimmedString
209
+
210
+
211
+ class TextPresentPredicatePayload(DaemonWireModel):
212
+ kind: Literal["text-present"]
213
+ text: TrimmedString
214
+
215
+
216
+ class GonePredicatePayload(DaemonWireModel):
217
+ kind: Literal["gone"]
218
+ source_screen_id: TrimmedString
219
+ ref: TrimmedString
220
+
221
+
222
+ class AppPredicatePayload(DaemonWireModel):
223
+ kind: Literal["app"]
224
+ package_name: TrimmedString
225
+
226
+
227
+ class IdlePredicatePayload(DaemonWireModel):
228
+ kind: Literal["idle"]
229
+
230
+
231
+ LiveScreenBoundCommandPayload: TypeAlias = (
232
+ RefActionCommandPayload
233
+ | TypeCommandPayload
234
+ | ScrollCommandPayload
235
+ | GlobalActionCommandPayload
236
+ )
237
+
238
+
239
+ ScreenRelativeWaitPredicatePayload: TypeAlias = (
240
+ ScreenChangePredicatePayload | GonePredicatePayload
241
+ )
242
+
243
+
244
+ WaitPredicatePayload: TypeAlias = Annotated[
245
+ (
246
+ ScreenChangePredicatePayload
247
+ | TextPresentPredicatePayload
248
+ | GonePredicatePayload
249
+ | AppPredicatePayload
250
+ | IdlePredicatePayload
251
+ ),
252
+ Field(discriminator="kind"),
253
+ ]
254
+
255
+
256
+ class WaitCommandPayload(DaemonWireModel):
257
+ kind: Literal["wait"]
258
+ predicate: WaitPredicatePayload
259
+ timeout_ms: NonNegativeInt | None = None
260
+
261
+ @model_validator(mode="before")
262
+ @classmethod
263
+ def normalize_predicate_discriminator(cls, value: object) -> object:
264
+ return _normalize_discriminator_payload(value, nested_field="predicate")
265
+
266
+
267
+ class ScreenshotCommandPayload(DaemonWireModel):
268
+ kind: Literal["screenshot"]
269
+
270
+
271
+ class ListAppsCommandPayload(DaemonWireModel):
272
+ kind: Literal["listApps"]
273
+
274
+
275
+ DaemonCommandPayload: TypeAlias = Annotated[
276
+ (
277
+ ConnectCommandPayload
278
+ | ObserveCommandPayload
279
+ | OpenCommandPayload
280
+ | RefActionCommandPayload
281
+ | TypeCommandPayload
282
+ | ScrollCommandPayload
283
+ | GlobalActionCommandPayload
284
+ | WaitCommandPayload
285
+ | ListAppsCommandPayload
286
+ | ScreenshotCommandPayload
287
+ ),
288
+ Field(discriminator="kind"),
289
+ ]
290
+
291
+
292
+ class CommandRunRequest(DaemonWireModel):
293
+ """Typed ``POST /commands/run`` request."""
294
+
295
+ command: DaemonCommandPayload
296
+
297
+ @model_validator(mode="before")
298
+ @classmethod
299
+ def normalize_command_discriminator(cls, value: object) -> object:
300
+ return _normalize_discriminator_payload(value, nested_field="command")
301
+
302
+
303
+ __all__ = [
304
+ "OWNER_HEADER_NAME",
305
+ "TOKEN_HEADER_NAME",
306
+ "AppPredicatePayload",
307
+ "CommandRunRequest",
308
+ "ConnectCommandPayload",
309
+ "ConnectionPayload",
310
+ "DaemonCommandPayload",
311
+ "DaemonErrorEnvelope",
312
+ "DaemonSuccessEnvelope",
313
+ "GlobalActionCommandPayload",
314
+ "GonePredicatePayload",
315
+ "HealthResult",
316
+ "IdlePredicatePayload",
317
+ "ListAppsCommandPayload",
318
+ "LiveScreenBoundCommandPayload",
319
+ "ObserveCommandPayload",
320
+ "OpenAppTargetPayload",
321
+ "OpenCommandPayload",
322
+ "OpenTargetPayload",
323
+ "OpenUrlTargetPayload",
324
+ "RefActionCommandPayload",
325
+ "RuntimeGetResult",
326
+ "RuntimePayload",
327
+ "ScreenChangePredicatePayload",
328
+ "ScreenRelativeWaitPredicatePayload",
329
+ "ScreenshotCommandPayload",
330
+ "ScrollCommandPayload",
331
+ "TextPresentPredicatePayload",
332
+ "TypeCommandPayload",
333
+ "WaitCommandPayload",
334
+ "WaitPredicatePayload",
335
+ ]
@@ -0,0 +1,44 @@
1
+ """Stable daemon error contract models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+
7
+ from pydantic import Field
8
+
9
+ from .base import DaemonWireModel
10
+
11
+
12
+ class DaemonErrorCode(str, Enum):
13
+ """Stable daemon error codes that cross the CLI and daemon boundary."""
14
+
15
+ DAEMON_BAD_REQUEST = "DAEMON_BAD_REQUEST"
16
+ DAEMON_UNAUTHORIZED = "DAEMON_UNAUTHORIZED"
17
+ WORKSPACE_BUSY = "WORKSPACE_BUSY"
18
+ RUNTIME_BUSY = "RUNTIME_BUSY"
19
+ RUNTIME_NOT_CONNECTED = "RUNTIME_NOT_CONNECTED"
20
+ SCREEN_NOT_READY = "SCREEN_NOT_READY"
21
+ COMMAND_NOT_FOUND = "COMMAND_NOT_FOUND"
22
+ COMMAND_CANCELLED = "COMMAND_CANCELLED"
23
+ REF_RESOLUTION_FAILED = "REF_RESOLUTION_FAILED"
24
+ DEVICE_RPC_FAILED = "DEVICE_RPC_FAILED"
25
+ DEVICE_DISCONNECTED = "DEVICE_DISCONNECTED"
26
+ DEVICE_AGENT_UNAVAILABLE = "DEVICE_AGENT_UNAVAILABLE"
27
+ DEVICE_AGENT_UNAUTHORIZED = "DEVICE_AGENT_UNAUTHORIZED"
28
+ DEVICE_AGENT_VERSION_MISMATCH = "DEVICE_AGENT_VERSION_MISMATCH"
29
+ DEVICE_AGENT_CAPABILITY_MISMATCH = "DEVICE_AGENT_CAPABILITY_MISMATCH"
30
+ ACCESSIBILITY_NOT_READY = "ACCESSIBILITY_NOT_READY"
31
+ DEVICE_RPC_TRANSPORT_RESET = "DEVICE_RPC_TRANSPORT_RESET"
32
+ INTERNAL_COMMAND_FAILURE = "INTERNAL_COMMAND_FAILURE"
33
+ WORKSPACE_UNAVAILABLE = "WORKSPACE_UNAVAILABLE"
34
+ ARTIFACT_ROOT_UNWRITABLE = "ARTIFACT_ROOT_UNWRITABLE"
35
+ ARTIFACT_WRITE_FAILED = "ARTIFACT_WRITE_FAILED"
36
+
37
+
38
+ class DaemonError(DaemonWireModel):
39
+ """Typed daemon error payload for shared transport envelopes."""
40
+
41
+ code: DaemonErrorCode
42
+ message: str
43
+ retryable: bool
44
+ details: dict[str, object] = Field(default_factory=dict)
@@ -0,0 +1,5 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def daemon_state_root(workspace_root: Path) -> Path:
5
+ return workspace_root / ".androidctl" / "daemon"