Harden remote computer use readiness

Track computer-use CLI status, last error, and restore-token presence in connector metadata so stale Free Run settings are no longer treated as ready. Materialize CLI-provided screenshot artifacts through Agent Zero's file helpers, stop dispatching computer_use_remote actions when metadata already reports rearm required, and teach the skill to give backend-agnostic rearm guidance without screenshot or vision fallbacks.

Alessandro committed May 10, 2026 at 00:04 UTC c8e239d5a2257ddd3ae411aed6750929014fd499
4 files changed +117 -5
plugins/_a0_connector/api/ws_connector.py
+2
@@ -196,10 +196,12 @@ class WsConnector(WsHandler):
196 host_browser = host_browser_metadata_for_sid(sid) or {}
197 remote_files = remote_file_metadata_for_sid(sid) or {}
198 remote_exec = remote_exec_metadata_for_sid(sid) or {}
199 + computer_use_status = str(computer_use.get("status", "") or "").strip().lower()
200 return {
201 "contexts": sorted(subscribed_contexts_for_sid(sid)),
202 "computer_use": bool(
203 computer_use.get("supported") and computer_use.get("enabled")
204 + and computer_use_status != "rearm required"
205 ),
206 "host_browser": bool(
207 host_browser.get("supported") and host_browser.get("enabled")
plugins/_a0_connector/helpers/ws_runtime.py
+9
@@ -51,6 +51,9 @@ class ComputerUseMetadata:
51 supported: bool
52 enabled: bool
53 trust_mode: str
54 + status: str
55 + last_error: str
56 + restore_token_present: bool
57 artifact_root: str
58 backend_id: str
59 backend_family: str
@@ -338,6 +341,9 @@ def store_sid_computer_use_metadata(sid: str, payload: dict[str, Any]) -> Comput
341 supported=bool(payload.get("supported")),
342 enabled=bool(payload.get("supported")) and bool(payload.get("enabled")),
343 trust_mode=str(payload.get("trust_mode", "") or "").strip(),
344 + status=str(payload.get("status", "") or "").strip(),
345 + last_error=str(payload.get("last_error", "") or "").strip(),
346 + restore_token_present=bool(payload.get("restore_token_present")),
347 artifact_root=str(payload.get("artifact_root", "") or "").strip(),
348 backend_id=str(payload.get("backend_id", "") or "").strip(),
349 backend_family=str(payload.get("backend_family", "") or "").strip(),
@@ -364,6 +370,9 @@ def computer_use_metadata_for_sid(sid: str) -> dict[str, Any] | None:
370 "supported": metadata.supported,
371 "enabled": metadata.enabled,
372 "trust_mode": metadata.trust_mode,
373 + "status": metadata.status,
374 + "last_error": metadata.last_error,
375 + "restore_token_present": metadata.restore_token_present,
376 "artifact_root": metadata.artifact_root,
377 "backend_id": metadata.backend_id,
378 "backend_family": metadata.backend_family,
plugins/_a0_connector/tools/computer_use_remote.py
+104 -5
@@ -6,13 +6,14 @@ from pathlib import Path
6 import uuid
7 from typing import Any
8
9 -from helpers import history
9 +from helpers import files, history
10 from helpers.tool import Response, Tool
11 from helpers.ws import NAMESPACE
12 from helpers.ws_manager import ConnectionNotFoundError, get_shared_ws_manager
13
14 from plugins._a0_connector.helpers.ws_runtime import (
15 clear_pending_computer_use_op,
16 + computer_use_metadata_for_sid,
17 select_computer_use_target_sid,
18 store_pending_computer_use_op,
19 )
@@ -20,7 +21,12 @@ from plugins._a0_connector.helpers.ws_runtime import (
21
22 COMPUTER_USE_OP_TIMEOUT = 180.0
23 COMPUTER_USE_OP_EVENT = "connector_computer_use_op"
24 +COMPUTER_USE_CAPTURE_DIR = ("tmp", "_a0_connector", "computer_use", "captures")
25 CAPTURE_TOKENS_ESTIMATE = 1500
26 +MAX_CAPTURE_ARTIFACT_SIZE_BYTES = 25 * 1024 * 1024
27 +REARM_REQUIRED_DEFAULT_MESSAGE = (
28 + "Computer use is configured, but the installed desktop-control backend is not armed."
29 +)
30 _AUTO_CAPTURE_ACTIONS = {
31 "start_session",
32 "move",
@@ -75,6 +81,19 @@ class ComputerUseRemote(Tool):
81 break_loop=False,
82 )
83
84 + metadata = computer_use_metadata_for_sid(sid) or {}
85 + if str(metadata.get("status", "") or "").strip().lower() == "rearm required":
86 + return Response(
87 + message=self._format_error(
88 + {
89 + "code": "COMPUTER_USE_REARM_REQUIRED",
90 + "error": str(metadata.get("last_error", "") or "").strip()
91 + or REARM_REQUIRED_DEFAULT_MESSAGE,
92 + }
93 + ),
94 + break_loop=False,
95 + )
96 +
97 try:
98 payload = self._build_payload(op_id=str(uuid.uuid4()), context_id=context_id, action=action)
99 result = await self._dispatch_payload(sid=sid, payload=payload)
@@ -84,6 +103,7 @@ class ComputerUseRemote(Tool):
103 context_id=context_id,
104 result=result,
105 )
106 + message = self._extract_result(action, result)
107 except ValueError as exc:
108 return Response(
109 message=f"computer_use_remote: {exc}",
@@ -108,7 +128,6 @@ class ComputerUseRemote(Tool):
128 break_loop=False,
129 )
130
111 - message = self._extract_result(action, result)
131 if capture_note:
132 message = f"{message} {capture_note}".strip()
133
@@ -182,7 +201,10 @@ class ComputerUseRemote(Tool):
201 if not isinstance(capture_data, dict):
202 return "Automatic screen refresh failed: missing capture payload."
203
185 - summary = self._record_capture(capture_data)
204 + try:
205 + summary = self._record_capture(capture_data)
206 + except Exception as exc:
207 + return f"Automatic screen refresh failed: {exc}"
208 return f"Latest screen attached: {summary}"
209
210 def _auto_capture_settle_seconds(self, action: str) -> float:
@@ -296,6 +318,15 @@ class ComputerUseRemote(Tool):
318 def _format_error(self, result: dict[str, Any]) -> str:
319 error = str(result.get("error") or "Unknown error")
320 code = str(result.get("code") or "")
321 + if code == "COMPUTER_USE_REARM_REQUIRED" or error == "COMPUTER_USE_REARM_REQUIRED":
322 + detail = error if error and error != code else REARM_REQUIRED_DEFAULT_MESSAGE
323 + return (
324 + "COMPUTER_USE_REARM_REQUIRED: "
325 + f"{detail} Stop using computer_use_remote for now; ask the user to re-arm "
326 + "Computer Use in the A0 CLI with Confirm with User, approve the platform "
327 + "permission prompt if shown, then switch back to Free Run if desired. "
328 + "Do not retry or use screenshot fallbacks."
329 + )
330 if code:
331 return f"{code}: {error}"
332 return error
@@ -308,6 +339,19 @@ class ComputerUseRemote(Tool):
339 active_contexts = data.get("active_contexts") or []
340 active_text = ", ".join(str(item) for item in active_contexts) if active_contexts else "none"
341 backend_text = ""
342 + rearm_guidance = ""
343 + if status == "rearm required":
344 + detail = str(data.get("last_error") or "").strip()
345 + if detail and detail != "COMPUTER_USE_REARM_REQUIRED":
346 + rearm_guidance = (
347 + f" {detail} Stop using computer_use_remote until the user re-arms it."
348 + )
349 + else:
350 + rearm_guidance = (
351 + " Computer Use is configured but the installed desktop-control backend "
352 + "is not armed. "
353 + "Stop using computer_use_remote until the user re-arms it."
354 + )
355 if backend_id:
356 backend_text = backend_id
357 if backend_family:
@@ -315,11 +359,15 @@ class ComputerUseRemote(Tool):
359 if backend_text:
360 return (
361 f"Computer use status={status}, trust_mode={trust_mode or 'unknown'}, "
318 - f"backend={backend_text}, active_contexts={active_text}."
362 + f"backend={backend_text}, active_contexts={active_text}.{rearm_guidance}"
363 )
320 - return f"Computer use status={status}, trust_mode={trust_mode or 'unknown'}, active_contexts={active_text}."
364 + return (
365 + f"Computer use status={status}, trust_mode={trust_mode or 'unknown'}, "
366 + f"active_contexts={active_text}.{rearm_guidance}"
367 + )
368
369 def _record_capture(self, data: dict[str, Any]) -> str:
370 + data = self._materialize_capture_artifact(data)
371 _image_path, display_path = self._resolve_capture_path(data)
372 width = data.get("width", "?")
373 height = data.get("height", "?")
@@ -419,6 +467,7 @@ class ComputerUseRemote(Tool):
467
468 def _resolve_capture_path(self, data: dict[str, Any]) -> tuple[Path, str]:
469 candidates = [
470 + str(data.get("path", "") or "").strip(),
471 str(data.get("capture_path", "") or "").strip(),
472 str(data.get("container_path", "") or "").strip(),
473 str(data.get("host_path", "") or "").strip(),
@@ -430,6 +479,43 @@ class ComputerUseRemote(Tool):
479 f"Capture artifact was not found in any advertised path: {candidates!r}"
480 )
481
482 + def _materialize_capture_artifact(self, data: dict[str, Any]) -> dict[str, Any]:
483 + artifact = data.get("artifact")
484 + if not isinstance(artifact, dict):
485 + return data
486 + if str(artifact.get("encoding", "")).strip().lower() != "base64":
487 + return data
488 +
489 + encoded = str(artifact.get("data") or "")
490 + if not encoded:
491 + return data
492 +
493 + estimated_size = _estimated_base64_decoded_size(encoded)
494 + if estimated_size > MAX_CAPTURE_ARTIFACT_SIZE_BYTES:
495 + raise RuntimeError(
496 + "Computer-use capture artifact is too large to materialize safely "
497 + f"({estimated_size} bytes, limit {MAX_CAPTURE_ARTIFACT_SIZE_BYTES} bytes)."
498 + )
499 +
500 + filename = _safe_filename(str(artifact.get("filename") or "computer-use-capture.png"))
501 + context_id = str(getattr(getattr(self.agent, "context", None), "id", "") or "default")
502 + target_relative = str(Path(*COMPUTER_USE_CAPTURE_DIR, context_id, filename))
503 + target_path = Path(files.get_abs_path(target_relative))
504 + try:
505 + files.write_file_base64(target_relative, encoded)
506 + except Exception as exc:
507 + target_path.unlink(missing_ok=True)
508 + raise RuntimeError("Computer-use capture artifact could not be decoded.") from exc
509 +
510 + materialized = dict(data)
511 + materialized.pop("artifact", None)
512 + local_path = str(target_path)
513 + materialized["path"] = local_path
514 + materialized["a0_path"] = files.normalize_a0_path(local_path)
515 + materialized.setdefault("capture_path", local_path)
516 + materialized.setdefault("capture_id", target_path.stem)
517 + return materialized
518 +
519 def _coerce_int(self, value: object, *, name: str) -> int:
520 try:
521 return int(value or 0)
@@ -442,3 +528,16 @@ class ComputerUseRemote(Tool):
528 if isinstance(value, (int, float)):
529 return bool(value)
530 return str(value or "").strip().lower() in {"1", "true", "yes", "on"}
531 +
532 +
533 +def _safe_filename(value: str) -> str:
534 + cleaned = "".join(char if char.isalnum() or char in {"-", "_", "."} else "_" for char in value)
535 + cleaned = cleaned.strip("._") or f"computer-use-{uuid.uuid4().hex}.png"
536 + if "." not in cleaned:
537 + cleaned += ".png"
538 + return cleaned
539 +
540 +
541 +def _estimated_base64_decoded_size(data: str) -> int:
542 + compact_length = sum(1 for char in data if not char.isspace())
543 + return (compact_length * 3) // 4
skills/computer-use-remote/SKILL.md
+2
@@ -53,6 +53,8 @@ Arguments:
53
54 Availability, backend support, and trust mode are checked when the tool runs. If no CLI is connected or local computer use is disabled, tell the user what to enable instead of using the server environment.
55
56 +If any tool result contains `COMPUTER_USE_REARM_REQUIRED` or `status=rearm required`, stop the computer-use sequence immediately. Do not retry `start_session`, do not call `capture`, and do not use shell, vision, or screenshot fallbacks to bypass it. Tell the user that the A0 CLI has Computer Use configured but the installed desktop-control backend is not armed; they must re-arm it in the CLI with Confirm with User, approve the platform permission prompt if shown, and then switch back to Free Run if desired.
57 +
58 ## Core Loop
59
60 1. Call `start_session` first.