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.