crabcode 0.1.6__py3-none-win_amd64.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 (205) hide show
  1. crabcode-0.1.6.dist-info/METADATA +63 -0
  2. crabcode-0.1.6.dist-info/RECORD +205 -0
  3. crabcode-0.1.6.dist-info/WHEEL +4 -0
  4. crabcode-0.1.6.dist-info/entry_points.txt +2 -0
  5. crabcode_cli/__init__.py +7 -0
  6. crabcode_cli/__main__.py +4 -0
  7. crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
  8. crabcode_cli/_native/runtime.json +6 -0
  9. crabcode_cli/app.py +776 -0
  10. crabcode_cli/banner.py +39 -0
  11. crabcode_cli/computer_use.py +286 -0
  12. crabcode_cli/model_manager.py +649 -0
  13. crabcode_cli/model_picker.py +168 -0
  14. crabcode_cli/pipe.py +148 -0
  15. crabcode_cli/render/__init__.py +0 -0
  16. crabcode_cli/render/messages.py +19 -0
  17. crabcode_cli/repl.py +4841 -0
  18. crabcode_cli/session_picker.py +239 -0
  19. crabcode_core/__init__.py +7 -0
  20. crabcode_core/_version.py +3 -0
  21. crabcode_core/_windows_process.py +68 -0
  22. crabcode_core/agent_manager.py +1731 -0
  23. crabcode_core/api/__init__.py +20 -0
  24. crabcode_core/api/anthropic_adapter.py +648 -0
  25. crabcode_core/api/azure_adapter.py +69 -0
  26. crabcode_core/api/base.py +233 -0
  27. crabcode_core/api/codex_adapter.py +1327 -0
  28. crabcode_core/api/error_logging.py +132 -0
  29. crabcode_core/api/gemini_adapter.py +26 -0
  30. crabcode_core/api/model_info.py +257 -0
  31. crabcode_core/api/network.py +62 -0
  32. crabcode_core/api/ollama_adapter.py +27 -0
  33. crabcode_core/api/openai_adapter.py +323 -0
  34. crabcode_core/api/registry.py +58 -0
  35. crabcode_core/command_line.py +18 -0
  36. crabcode_core/compact/__init__.py +17 -0
  37. crabcode_core/compact/compact.py +514 -0
  38. crabcode_core/compact/context_tokens.py +165 -0
  39. crabcode_core/computer_use_mode.py +38 -0
  40. crabcode_core/computer_use_observation.py +144 -0
  41. crabcode_core/config/__init__.py +3 -0
  42. crabcode_core/config/claudemd.py +61 -0
  43. crabcode_core/config/manager.py +237 -0
  44. crabcode_core/config/model_catalog.py +256 -0
  45. crabcode_core/events.py +4896 -0
  46. crabcode_core/file_lock.py +81 -0
  47. crabcode_core/filesystem.py +22 -0
  48. crabcode_core/goal.py +149 -0
  49. crabcode_core/hooks/__init__.py +5 -0
  50. crabcode_core/hooks/manager.py +290 -0
  51. crabcode_core/image_io.py +111 -0
  52. crabcode_core/io_worker.py +211 -0
  53. crabcode_core/logging_utils.py +168 -0
  54. crabcode_core/lsp/__init__.py +6 -0
  55. crabcode_core/lsp/client.py +786 -0
  56. crabcode_core/lsp/diagnostics.py +150 -0
  57. crabcode_core/lsp/manager.py +583 -0
  58. crabcode_core/mcp/__init__.py +4 -0
  59. crabcode_core/mcp/client.py +169 -0
  60. crabcode_core/mcp/config.py +57 -0
  61. crabcode_core/path_validation.py +34 -0
  62. crabcode_core/paths.py +24 -0
  63. crabcode_core/peer/__init__.py +10 -0
  64. crabcode_core/peer/runtime.py +518 -0
  65. crabcode_core/permissions/__init__.py +7 -0
  66. crabcode_core/permissions/ai_reviewer.py +184 -0
  67. crabcode_core/permissions/manager.py +187 -0
  68. crabcode_core/plan/__init__.py +5 -0
  69. crabcode_core/plan/executor.py +270 -0
  70. crabcode_core/plan/types.py +169 -0
  71. crabcode_core/prompts/__init__.py +0 -0
  72. crabcode_core/prompts/blocks.py +10 -0
  73. crabcode_core/prompts/context.py +229 -0
  74. crabcode_core/prompts/library.py +92 -0
  75. crabcode_core/prompts/profile.py +79 -0
  76. crabcode_core/prompts/system.py +356 -0
  77. crabcode_core/prompts/templates.py +104 -0
  78. crabcode_core/query/__init__.py +0 -0
  79. crabcode_core/query/computer_use_history.py +83 -0
  80. crabcode_core/query/loop.py +2446 -0
  81. crabcode_core/query/retry.py +116 -0
  82. crabcode_core/schedule/__init__.py +13 -0
  83. crabcode_core/schedule/manager.py +631 -0
  84. crabcode_core/schedule/models.py +204 -0
  85. crabcode_core/schedule/store.py +558 -0
  86. crabcode_core/schedule/timing.py +287 -0
  87. crabcode_core/session/__init__.py +12 -0
  88. crabcode_core/session/export.py +141 -0
  89. crabcode_core/session/meta_db.py +684 -0
  90. crabcode_core/session/storage.py +2032 -0
  91. crabcode_core/session/title_gen.py +52 -0
  92. crabcode_core/skills/__init__.py +6 -0
  93. crabcode_core/skills/context.py +23 -0
  94. crabcode_core/skills/loader.py +214 -0
  95. crabcode_core/skills/matcher.py +203 -0
  96. crabcode_core/snapshot/__init__.py +21 -0
  97. crabcode_core/snapshot/snapshot.py +436 -0
  98. crabcode_core/snapshot/tracker.py +359 -0
  99. crabcode_core/subprocess_utils.py +260 -0
  100. crabcode_core/team/__init__.py +27 -0
  101. crabcode_core/team/bridge.py +66 -0
  102. crabcode_core/team/inbox.py +237 -0
  103. crabcode_core/team/manager.py +1660 -0
  104. crabcode_core/team/message_bus.py +821 -0
  105. crabcode_core/team/models.py +191 -0
  106. crabcode_core/team/recovery.py +192 -0
  107. crabcode_core/text_io.py +52 -0
  108. crabcode_core/tools/__init__.py +112 -0
  109. crabcode_core/tools/_input_helpers.py +39 -0
  110. crabcode_core/tools/_input_schema.py +130 -0
  111. crabcode_core/tools/agent.py +413 -0
  112. crabcode_core/tools/apply_patch.py +587 -0
  113. crabcode_core/tools/ask_user.py +122 -0
  114. crabcode_core/tools/bash.py +154 -0
  115. crabcode_core/tools/browser.py +678 -0
  116. crabcode_core/tools/checklist.py +241 -0
  117. crabcode_core/tools/checkpoint.py +78 -0
  118. crabcode_core/tools/computer_use.py +599 -0
  119. crabcode_core/tools/diff_utils.py +123 -0
  120. crabcode_core/tools/file_edit.py +240 -0
  121. crabcode_core/tools/file_read.py +187 -0
  122. crabcode_core/tools/file_write.py +154 -0
  123. crabcode_core/tools/glob.py +123 -0
  124. crabcode_core/tools/goal.py +134 -0
  125. crabcode_core/tools/grep.py +247 -0
  126. crabcode_core/tools/image.py +173 -0
  127. crabcode_core/tools/image_generate.py +115 -0
  128. crabcode_core/tools/lint.py +388 -0
  129. crabcode_core/tools/loading.py +187 -0
  130. crabcode_core/tools/memory.py +456 -0
  131. crabcode_core/tools/monitor.py +677 -0
  132. crabcode_core/tools/peer.py +103 -0
  133. crabcode_core/tools/revert.py +90 -0
  134. crabcode_core/tools/schedule.py +683 -0
  135. crabcode_core/tools/skill.py +108 -0
  136. crabcode_core/tools/switch_mode.py +213 -0
  137. crabcode_core/tools/team.py +329 -0
  138. crabcode_core/tools/tool_search.py +64 -0
  139. crabcode_core/tools/web_search.py +444 -0
  140. crabcode_core/turn_details.py +64 -0
  141. crabcode_core/types/__init__.py +95 -0
  142. crabcode_core/types/config.py +434 -0
  143. crabcode_core/types/event.py +334 -0
  144. crabcode_core/types/message.py +263 -0
  145. crabcode_core/types/tool.py +208 -0
  146. crabcode_core/usage.py +289 -0
  147. crabcode_core/utf8_sanitize.py +29 -0
  148. crabcode_debugger/__init__.py +5 -0
  149. crabcode_debugger/adapters.py +332 -0
  150. crabcode_debugger/dap.py +304 -0
  151. crabcode_debugger/memory.py +1806 -0
  152. crabcode_debugger/process.py +707 -0
  153. crabcode_debugger/runtime.py +58 -0
  154. crabcode_debugger/sessions.py +387 -0
  155. crabcode_debugger/tool.py +731 -0
  156. crabcode_gateway/__init__.py +18 -0
  157. crabcode_gateway/acp/__init__.py +13 -0
  158. crabcode_gateway/acp/agent.py +1581 -0
  159. crabcode_gateway/acp/session.py +114 -0
  160. crabcode_gateway/acp/transport.py +39 -0
  161. crabcode_gateway/acp/types.py +90 -0
  162. crabcode_gateway/adapter.py +30 -0
  163. crabcode_gateway/auth.py +139 -0
  164. crabcode_gateway/computer_use.py +364 -0
  165. crabcode_gateway/document_engine.py +637 -0
  166. crabcode_gateway/document_worker.py +301 -0
  167. crabcode_gateway/event_bus.py +655 -0
  168. crabcode_gateway/grpc/__init__.py +0 -0
  169. crabcode_gateway/grpc/crabcode_pb2.py +70 -0
  170. crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
  171. crabcode_gateway/grpc/proto/crabcode.proto +160 -0
  172. crabcode_gateway/grpc/server.py +765 -0
  173. crabcode_gateway/middleware.py +344 -0
  174. crabcode_gateway/protocol.py +17 -0
  175. crabcode_gateway/routes/__init__.py +0 -0
  176. crabcode_gateway/routes/agent.py +392 -0
  177. crabcode_gateway/routes/auth.py +150 -0
  178. crabcode_gateway/routes/computer_use.py +101 -0
  179. crabcode_gateway/routes/config.py +1698 -0
  180. crabcode_gateway/routes/document.py +1810 -0
  181. crabcode_gateway/routes/event.py +3801 -0
  182. crabcode_gateway/routes/git_review.py +232 -0
  183. crabcode_gateway/routes/health.py +14 -0
  184. crabcode_gateway/routes/peer.py +78 -0
  185. crabcode_gateway/routes/permission.py +121 -0
  186. crabcode_gateway/routes/schedule.py +300 -0
  187. crabcode_gateway/routes/session.py +1496 -0
  188. crabcode_gateway/routes/snapshot.py +211 -0
  189. crabcode_gateway/routes/tasks.py +318 -0
  190. crabcode_gateway/routes/team.py +392 -0
  191. crabcode_gateway/routes/usage.py +49 -0
  192. crabcode_gateway/routes/workspace.py +299 -0
  193. crabcode_gateway/safe_file.py +63 -0
  194. crabcode_gateway/schemas.py +1820 -0
  195. crabcode_gateway/server.py +514 -0
  196. crabcode_gateway/session_registry.py +36 -0
  197. crabcode_gateway/task_registry.py +708 -0
  198. crabcode_gateway/turn_details.py +60 -0
  199. crabcode_search/__init__.py +11 -0
  200. crabcode_search/background.py +368 -0
  201. crabcode_search/chunker.py +517 -0
  202. crabcode_search/embedder.py +497 -0
  203. crabcode_search/indexer.py +215 -0
  204. crabcode_search/store.py +222 -0
  205. crabcode_search/tool.py +268 -0
@@ -0,0 +1,364 @@
1
+ """Broker Computer Use requests between Core sessions and Desktop hosts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ import uuid
8
+ from dataclasses import dataclass, field
9
+ from typing import Any
10
+
11
+ from fastapi import WebSocket
12
+
13
+ COMPUTER_USE_RELEASE_RETENTION_MS = 15_000
14
+
15
+
16
+ LeaseKey = tuple[str, str, str | None]
17
+
18
+
19
+ @dataclass
20
+ class PendingComputerUseRequest:
21
+ host_id: str
22
+ future: asyncio.Future[dict[str, Any]]
23
+ lease: LeaseKey
24
+
25
+
26
+ @dataclass
27
+ class ComputerUseHost:
28
+ host_id: str
29
+ websocket: WebSocket
30
+ enabled: bool
31
+ gui_available: bool
32
+ capabilities: dict[str, Any]
33
+ action_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
34
+ send_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
35
+
36
+
37
+ class ComputerUseBroker:
38
+ """Tracks connected GUI hosts and dispatches one action at a time per host."""
39
+
40
+ def __init__(self, timeout_seconds: float = 90.0) -> None:
41
+ self.timeout_seconds = timeout_seconds
42
+ self._hosts: dict[str, ComputerUseHost] = {}
43
+ self._pending: dict[str, PendingComputerUseRequest] = {}
44
+ self._active_leases: set[LeaseKey] = set()
45
+ # Keep the latest preview for each lease in the Gateway so a Desktop
46
+ # webview reload can reconstruct its UI. Released previews remain
47
+ # here only through the same 15-second retention window as Desktop.
48
+ self._lease_previews: dict[LeaseKey, dict[str, Any]] = {}
49
+
50
+ @staticmethod
51
+ def _now_ms() -> int:
52
+ return int(time.time() * 1000)
53
+
54
+ def _purge_expired_previews(self, now_ms: int | None = None) -> None:
55
+ now = self._now_ms() if now_ms is None else now_ms
56
+ for lease, preview in list(self._lease_previews.items()):
57
+ deadline = preview.get("release_deadline_ms")
58
+ if (
59
+ lease not in self._active_leases
60
+ and isinstance(deadline, (int, float))
61
+ and deadline <= now
62
+ ):
63
+ self._lease_previews.pop(lease, None)
64
+
65
+ def restorable_previews(self, host_id: str) -> list[dict[str, Any]]:
66
+ """Return active or retained previews for a reconnecting Desktop host."""
67
+ self._purge_expired_previews()
68
+ return [
69
+ dict(preview)
70
+ for lease, preview in self._lease_previews.items()
71
+ if lease[0] == host_id
72
+ ]
73
+
74
+ def register(
75
+ self,
76
+ host_id: str,
77
+ websocket: WebSocket,
78
+ *,
79
+ enabled: bool,
80
+ gui_available: bool,
81
+ capabilities: dict[str, Any] | None = None,
82
+ ) -> ComputerUseHost:
83
+ previous = self._hosts.get(host_id)
84
+ if previous is not None and previous.websocket is not websocket:
85
+ for request_id, pending in list(self._pending.items()):
86
+ if pending.host_id != host_id:
87
+ continue
88
+ self._pending.pop(request_id, None)
89
+ if not pending.future.done():
90
+ pending.future.set_exception(RuntimeError("Computer Use host reconnected"))
91
+ host = ComputerUseHost(
92
+ host_id=host_id,
93
+ websocket=websocket,
94
+ enabled=enabled,
95
+ gui_available=gui_available,
96
+ capabilities=dict(capabilities or {}),
97
+ )
98
+ self._hosts[host_id] = host
99
+ return host
100
+
101
+ def update_state(
102
+ self,
103
+ host_id: str,
104
+ websocket: WebSocket,
105
+ *,
106
+ enabled: bool,
107
+ gui_available: bool,
108
+ capabilities: dict[str, Any] | None = None,
109
+ ) -> bool:
110
+ host = self._hosts.get(host_id)
111
+ if host is None or host.websocket is not websocket:
112
+ return False
113
+ host.enabled = enabled
114
+ host.gui_available = gui_available
115
+ if capabilities is not None:
116
+ host.capabilities = dict(capabilities)
117
+ return True
118
+
119
+ def unregister(self, host_id: str, websocket: WebSocket) -> None:
120
+ host = self._hosts.get(host_id)
121
+ if host is None or host.websocket is not websocket:
122
+ return
123
+ self._hosts.pop(host_id, None)
124
+ for request_id, pending in list(self._pending.items()):
125
+ if pending.host_id != host_id:
126
+ continue
127
+ self._pending.pop(request_id, None)
128
+ if not pending.future.done():
129
+ pending.future.set_exception(RuntimeError("Computer Use host disconnected"))
130
+
131
+ def is_available(self, host_id: str | None, mode: str | None = None) -> bool:
132
+ if not host_id:
133
+ return False
134
+ host = self._hosts.get(host_id)
135
+ if not (host and host.enabled and host.gui_available):
136
+ return False
137
+ if mode is None:
138
+ return True
139
+ supported_modes = host.capabilities.get("supported_modes")
140
+ if not isinstance(supported_modes, list):
141
+ # Hosts predating mode negotiation only implement the legacy
142
+ # foreground desktop behavior.
143
+ return mode == "foreground_desktop"
144
+ return mode in supported_modes
145
+
146
+ def environment(self, host_id: str | None) -> dict[str, Any]:
147
+ """Transport-owned identity; model actions cannot select an environment."""
148
+ host = self._hosts.get(host_id or "")
149
+ if host is None or host.capabilities.get("environment") != "local_vm":
150
+ return {}
151
+ return {
152
+ key: host.capabilities.get(key)
153
+ for key in ("environment", "environment_id", "environment_name", "instance_id",
154
+ "shared_directory", "shared_read_only", "forwarded_ports")
155
+ }
156
+
157
+ def capabilities(self, host_id: str | None) -> dict[str, Any]:
158
+ host = self._hosts.get(host_id or "")
159
+ return dict(host.capabilities) if host and host.enabled else {}
160
+
161
+ def resolve(
162
+ self, host_id: str, request_id: str, result: dict[str, Any],
163
+ *, preview_screenshot: dict[str, Any] | None = None,
164
+ ) -> bool:
165
+ pending = self._pending.get(request_id)
166
+ if pending is None or pending.host_id != host_id:
167
+ return False
168
+ self._pending.pop(request_id, None)
169
+ previous = self._lease_previews.get(pending.lease, {})
170
+ # This frame is exclusively for the human monitor and reconnect replay.
171
+ # Remove it before returning the tool result to Core/model history.
172
+ result = dict(result)
173
+ preview_frame = result.pop("preview_screenshot", None)
174
+ result.pop("preview_screenshot_error", None)
175
+ frame = result.get("screenshot") or preview_screenshot or preview_frame
176
+ accessibility = result.get("accessibility")
177
+ ax = accessibility if isinstance(accessibility, dict) else {}
178
+ elements = ax.get("elements")
179
+ element_count = ax.get("element_count", len(elements) if isinstance(elements, list) else 0)
180
+ has_ax = isinstance(accessibility, dict)
181
+ has_frame = bool(result.get("screenshot"))
182
+ keep_observation = not has_ax and not has_frame
183
+ self._lease_previews[pending.lease] = {
184
+ **previous,
185
+ "session_id": pending.lease[1],
186
+ "agent_id": pending.lease[2],
187
+ "mode": previous.get("mode", "background_app"),
188
+ "target_scope": previous.get("target_scope", "app_window"),
189
+ "delivery_policy": previous.get("delivery_policy", "allow_foreground"),
190
+ "status": "error" if result.get("ok") is False else "ready",
191
+ "action": str(result.get("action") or previous.get("action") or "unknown"),
192
+ "summary": str(
193
+ result.get("summary")
194
+ or result.get("error")
195
+ or previous.get("summary")
196
+ or "Computer Use action completed"
197
+ ),
198
+ "frame": frame or previous.get("frame"),
199
+ "frame_updated_at_ms": self._now_ms() if frame else previous.get("frame_updated_at_ms"),
200
+ "observation_kind": result.get("observation_kind") or (
201
+ previous.get("observation_kind") if keep_observation else
202
+ "ax_and_screenshot" if has_ax and has_frame else "ax" if has_ax else "screenshot"
203
+ ),
204
+ "ax_element_count": element_count if has_ax else previous.get("ax_element_count") if keep_observation else None,
205
+ "cursor": result.get("cursor") or previous.get("cursor"),
206
+ "updated_at_ms": self._now_ms(),
207
+ "release_deadline_ms": previous.get("release_deadline_ms"),
208
+ }
209
+ if not pending.future.done():
210
+ pending.future.set_result(result)
211
+ return True
212
+
213
+ async def release(
214
+ self,
215
+ host_id: str,
216
+ *,
217
+ session_id: str,
218
+ agent_id: str | None = None,
219
+ all_agents: bool = False,
220
+ ) -> bool:
221
+ """Tell a host that an agent/session no longer owns its preview.
222
+
223
+ Action completion is deliberately separate from release: a model may
224
+ spend an arbitrary amount of time thinking between Computer Use calls.
225
+ The Desktop starts its retention timer only after this message.
226
+ """
227
+ if all_agents:
228
+ leases = {
229
+ lease for lease in self._active_leases
230
+ if lease[0] == host_id and lease[1] == session_id
231
+ }
232
+ else:
233
+ lease = (host_id, session_id, agent_id)
234
+ leases = {lease} if lease in self._active_leases else set()
235
+ if not leases:
236
+ return False
237
+
238
+ deadline_ms = self._now_ms() + COMPUTER_USE_RELEASE_RETENTION_MS
239
+ self._active_leases.difference_update(leases)
240
+ for lease in leases:
241
+ preview = self._lease_previews.get(lease)
242
+ if preview is not None:
243
+ preview["release_deadline_ms"] = deadline_ms
244
+
245
+ message: dict[str, Any] = {
246
+ "type": "computer_use_release",
247
+ "session_id": session_id,
248
+ "release_deadline_ms": deadline_ms,
249
+ }
250
+ if all_agents:
251
+ message["all_agents"] = True
252
+ elif agent_id is not None:
253
+ message["agent_id"] = agent_id
254
+
255
+ # The lifecycle state is released even if Desktop is briefly absent
256
+ # during a reload. A reconnect receives the retained preview and the
257
+ # original absolute deadline from restorable_previews().
258
+ host = self._hosts.get(host_id)
259
+ if host is not None:
260
+ async with host.send_lock:
261
+ if self._hosts.get(host_id) is host:
262
+ await host.websocket.send_json(message)
263
+ return True
264
+
265
+ async def execute(
266
+ self,
267
+ host_id: str,
268
+ *,
269
+ session_id: str,
270
+ agent_id: str | None,
271
+ action: dict[str, Any],
272
+ mode: str | None = None,
273
+ target_scope: str | None = None,
274
+ delivery_policy: str = "allow_foreground",
275
+ ) -> dict[str, Any]:
276
+ host = self._hosts.get(host_id)
277
+ if host is None or not host.enabled or not host.gui_available:
278
+ raise RuntimeError("Computer Use is unavailable or disabled")
279
+ if target_scope is None:
280
+ target_scope = "desktop" if mode == "foreground_desktop" else "app_window"
281
+ if target_scope not in ("app_window", "desktop") or delivery_policy not in (
282
+ "strict_background", "allow_foreground"
283
+ ):
284
+ raise ValueError("Invalid Computer Use target scope or delivery policy")
285
+ expected_mode = "foreground_desktop" if target_scope == "desktop" else "background_app"
286
+ if mode is None:
287
+ mode = expected_mode
288
+ if mode != expected_mode:
289
+ raise ValueError("Conflicting Computer Use mode and target scope")
290
+ read_only = action.get("action") in ("observe", "list_windows", "list_displays", "wait")
291
+
292
+ async with host.action_lock:
293
+ request_id = str(uuid.uuid4())
294
+ try:
295
+ async with host.send_lock:
296
+ # Recheck immediately before sending: either lock can wait
297
+ # while the host disconnects or changes its capabilities.
298
+ if self._hosts.get(host_id) is not host or not host.enabled or not host.gui_available:
299
+ raise RuntimeError("Computer Use became unavailable")
300
+ supported_modes = host.capabilities.get("supported_modes")
301
+ if not isinstance(supported_modes, list):
302
+ supported_modes = ["foreground_desktop"]
303
+ if mode not in supported_modes:
304
+ raise RuntimeError(f"Computer Use mode '{mode}' is unavailable on this host")
305
+ error = None
306
+ if delivery_policy == "strict_background" and target_scope == "desktop":
307
+ error = "Desktop scope requires allow_foreground"
308
+ elif not read_only and host.capabilities.get("delivery_policy_version") != 1:
309
+ # Old hosts can silently ignore unknown policy fields.
310
+ error = "This host must be upgraded before it can enforce delivery policy"
311
+ elif (action.get("action") in ("press", "set_value", "perform_action")
312
+ or any(key in action for key in ("observation", "snapshot_id", "element_id", "ax_action"))):
313
+ if (target_scope != "app_window" or host.capabilities.get("environment") == "local_vm"
314
+ or host.capabilities.get("ax_protocol_version") != 1
315
+ or host.capabilities.get("ax_available") is not True):
316
+ error = "AX actions require an AX-capable macOS window host"
317
+ if error:
318
+ return {
319
+ "ok": False, "action": action.get("action"), "error": error, "summary": error,
320
+ "error_code": "background_delivery_unsupported", "action_dispatched": False,
321
+ "dispatch_succeeded": False, "effect_verified": False, "retry_safe": True,
322
+ "target_scope": target_scope, "delivery_policy": delivery_policy,
323
+ "focus_isolation": "unavailable",
324
+ }
325
+ future: asyncio.Future[dict[str, Any]] = asyncio.get_running_loop().create_future()
326
+ lease = (host_id, session_id, agent_id)
327
+ self._pending[request_id] = PendingComputerUseRequest(
328
+ host_id=host_id,
329
+ future=future,
330
+ lease=lease,
331
+ )
332
+ self._purge_expired_previews()
333
+ self._active_leases.add(lease)
334
+ previous = self._lease_previews.get(lease, {})
335
+ self._lease_previews[lease] = {
336
+ **previous,
337
+ "session_id": session_id,
338
+ "agent_id": agent_id,
339
+ "mode": mode,
340
+ "target_scope": target_scope,
341
+ "delivery_policy": delivery_policy,
342
+ "status": "busy",
343
+ "action": str(action.get("action") or "unknown"),
344
+ "summary": "Executing…",
345
+ "updated_at_ms": self._now_ms(),
346
+ "release_deadline_ms": None,
347
+ }
348
+ await host.websocket.send_json({
349
+ "type": "computer_use_request",
350
+ "request_id": request_id,
351
+ "session_id": session_id,
352
+ "agent_id": agent_id,
353
+ "mode": mode,
354
+ "target_scope": target_scope,
355
+ "delivery_policy": delivery_policy,
356
+ "environment_id": host.capabilities.get("environment_id"),
357
+ "instance_id": host.capabilities.get("instance_id"),
358
+ "action": action,
359
+ })
360
+ return await asyncio.wait_for(future, timeout=self.timeout_seconds)
361
+ except asyncio.TimeoutError as exc:
362
+ raise RuntimeError("Computer Use host timed out; input may have arrived. Observe before retrying.") from exc
363
+ finally:
364
+ self._pending.pop(request_id, None)