Agent Zero AI framework
Harden desktop session reliability
Target LibreOffice saves to the requested document window and report whether the file content changed. Fail fast on dead XFCE startup, probe the live X display, replace session manifests atomically, and decode XWD screenshots through Pillow's raw decoder. Add focused regressions for each path.
Alessandro committed
Jul 11, 2026 at 23:25 UTC
07e7e1e07554341d1e9815ceb815f3e691313a0a
4 files changed
+269
-164
plugins/_desktop/helpers/desktop_session.py
+108
-76
@@ -5,7 +5,6 @@ import fcntl
5
import hashlib
6
import json
7
import os
8
-import re
8
import shutil
9
import socket
10
import subprocess
@@ -223,39 +222,69 @@ class DesktopSessionManager:
222
return {"ok": True, "refreshed": refreshed, "desktop": session.public(doc)}
223
224
def save(self, session_id: str, file_id: str = "") -> dict[str, Any]:
226
- session = self.require(session_id)
227
- doc = self._document_for_save(session, file_id)
228
- xdotool = shutil.which("xdotool")
229
- if not xdotool:
230
- updated = document_store.register_document(doc["path"]) if doc else None
231
- return {
232
- "ok": False,
233
- "error": "xdotool is not installed; use LibreOffice's Save control inside the canvas.",
234
- "document": _public_doc(updated) if updated else None,
235
- }
225
+ with self._lock:
226
+ session = self.require(session_id)
227
+ doc = self._document_for_save(session, file_id)
228
+ if not doc:
229
+ return {
230
+ "ok": True,
231
+ "session_id": session.session_id,
232
+ "document": None,
233
+ "changed": False,
234
+ }
235
237
- result = subprocess.run(
238
- [xdotool, "key", "--clearmodifiers", "ctrl+s"],
239
- check=False,
240
- capture_output=True,
241
- text=True,
242
- timeout=8,
243
- env=self._display_env(session),
244
- )
245
- time.sleep(0.8)
246
- updated = document_store.register_document(doc["path"]) if doc else None
247
- if result.returncode != 0:
248
- detail = (result.stderr or result.stdout or "").strip()
236
+ xdotool = shutil.which("xdotool")
237
+ if not xdotool:
238
+ updated = document_store.register_document(doc["path"])
239
+ return {
240
+ "ok": False,
241
+ "error": "xdotool is not installed; use LibreOffice's Save control inside the canvas.",
242
+ "document": _public_doc(updated),
243
+ }
244
+
245
+ window_id = self._office_window_id_locked(
246
+ session,
247
+ title=str(doc.get("basename") or Path(doc["path"]).name),
248
+ fallback=False,
249
+ )
250
+ if not window_id:
251
+ return {
252
+ "ok": False,
253
+ "error": f"LibreOffice window not found for {doc['basename']}.",
254
+ "document": _public_doc(doc),
255
+ }
256
+
257
+ result = subprocess.run(
258
+ [
259
+ xdotool,
260
+ "windowactivate",
261
+ "--sync",
262
+ window_id,
263
+ "key",
264
+ "--clearmodifiers",
265
+ "ctrl+s",
266
+ ],
267
+ check=False,
268
+ capture_output=True,
269
+ text=True,
270
+ timeout=8,
271
+ env=self._display_env(session),
272
+ )
273
+ time.sleep(0.8)
274
+ updated = document_store.register_document(doc["path"])
275
+ if result.returncode != 0:
276
+ detail = (result.stderr or result.stdout or "").strip()
277
+ return {
278
+ "ok": False,
279
+ "error": detail or "LibreOffice desktop save shortcut failed.",
280
+ "document": _public_doc(updated),
281
+ }
282
return {
250
- "ok": False,
251
- "error": detail or "LibreOffice desktop save shortcut failed.",
252
- "document": _public_doc(updated) if updated else None,
283
+ "ok": True,
284
+ "session_id": session.session_id,
285
+ "document": _public_doc(updated),
286
+ "changed": updated.get("sha256") != doc.get("sha256"),
287
}
254
- return {
255
- "ok": True,
256
- "session_id": session.session_id,
257
- "document": _public_doc(updated) if updated else None,
258
- }
288
289
def sync(self, session_id: str = "", file_id: str = "") -> dict[str, Any]:
290
session = self.get(session_id) if session_id else self._find_by_file_id(file_id)
@@ -820,57 +849,48 @@ class DesktopSessionManager:
849
return ""
850
env = self._display_env(session)
851
title = str(title or "").strip()
823
- searches: list[list[str]] = []
852
+ try:
853
+ result = subprocess.run(
854
+ [xdotool, "search", "--onlyvisible", "--class", "libreoffice"],
855
+ check=False,
856
+ capture_output=True,
857
+ text=True,
858
+ timeout=2,
859
+ env=env,
860
+ )
861
+ window_ids = [line.strip() for line in result.stdout.splitlines() if line.strip()]
862
+ except (OSError, subprocess.TimeoutExpired):
863
+ window_ids = []
864
if title:
825
- escaped_title = re.escape(title)
826
- searches.append([
827
- xdotool,
828
- "search",
829
- "--onlyvisible",
830
- "--name",
831
- escaped_title,
832
- ])
833
- for window_class in (
834
- "libreoffice",
835
- "libreoffice-writer",
836
- "libreoffice-calc",
837
- "libreoffice-impress",
838
- ):
839
- searches.append([
840
- xdotool,
841
- "search",
842
- "--onlyvisible",
843
- "--class",
844
- window_class,
845
- "--name",
846
- escaped_title,
847
- ])
865
+ for window_id in reversed(window_ids):
866
+ try:
867
+ result = subprocess.run(
868
+ [xdotool, "getwindowname", window_id],
869
+ check=False,
870
+ capture_output=True,
871
+ text=True,
872
+ timeout=2,
873
+ env=env,
874
+ )
875
+ except (OSError, subprocess.TimeoutExpired):
876
+ continue
877
+ if title.casefold() in result.stdout.strip().casefold():
878
+ return window_id
879
+ if fallback and window_ids:
880
+ return window_ids[-1]
881
if fallback:
849
- for window_class in (
850
- "libreoffice",
851
- "libreoffice-writer",
852
- "libreoffice-calc",
853
- "libreoffice-impress",
854
- ):
855
- searches.append([xdotool, "search", "--onlyvisible", "--class", window_class])
856
- searches.append([xdotool, "search", "--onlyvisible", "--name", "LibreOffice"])
857
- for command in searches:
882
try:
883
result = subprocess.run(
860
- command,
884
+ [xdotool, "search", "--onlyvisible", "--name", "LibreOffice"],
885
check=False,
886
capture_output=True,
887
text=True,
888
timeout=2,
889
env=env,
890
)
891
+ window_ids = [line.strip() for line in result.stdout.splitlines() if line.strip()]
892
except (OSError, subprocess.TimeoutExpired):
868
- continue
869
- window_ids = [
870
- line.strip()
871
- for line in result.stdout.splitlines()
872
- if line.strip()
873
- ]
893
+ window_ids = []
894
if window_ids:
895
return window_ids[-1]
896
return ""
@@ -1573,14 +1593,20 @@ fi
1593
self._remove_manifest(session_id)
1594
1595
def _wait_for_display(self, session: DesktopSession) -> None:
1576
- marker = Path(f"/tmp/.X11-unix/X{session.display}")
1596
deadline = time.time() + DISPLAY_START_TIMEOUT_SECONDS
1597
while time.time() < deadline:
1598
process = session.processes.get("xvfb") or session.processes.get("xpra")
1599
if process and process.poll() is not None:
1600
raise RuntimeError("The LibreOffice X display exited before it was ready.")
1582
- if marker.exists():
1583
- return
1601
+ try:
1602
+ if virtual_desktop.current_display_size(
1603
+ session.display,
1604
+ xauthority=self._xauthority(session),
1605
+ home=str(session.profile_dir),
1606
+ ):
1607
+ return
1608
+ except (OSError, subprocess.TimeoutExpired):
1609
+ pass
1610
time.sleep(0.1)
1611
raise TimeoutError("Timed out waiting for the LibreOffice X display.")
1612
@@ -1589,7 +1615,7 @@ fi
1615
while time.time() < deadline:
1616
process = session.processes.get("xfce")
1617
if process and process.poll() is not None:
1592
- return
1618
+ raise RuntimeError("The XFCE desktop session exited before it was ready.")
1619
if virtual_desktop.has_window(
1620
display=session.display,
1621
name="xfce4-panel",
@@ -1619,7 +1645,13 @@ fi
1645
"owner_pid": os.getpid(),
1646
"pids": pids,
1647
}
1622
- (SESSION_DIR / f"{session.session_id}.json").write_text(json.dumps(payload), encoding="utf-8")
1648
+ manifest = SESSION_DIR / f"{session.session_id}.json"
1649
+ tmp = manifest.with_name(f".{manifest.name}.{uuid.uuid4().hex}.tmp")
1650
+ try:
1651
+ tmp.write_text(json.dumps(payload), encoding="utf-8")
1652
+ os.replace(tmp, manifest)
1653
+ finally:
1654
+ tmp.unlink(missing_ok=True)
1655
1656
def _remove_manifest(self, session_id: str) -> None:
1657
(SESSION_DIR / f"{session_id}.json").unlink(missing_ok=True)
plugins/_desktop/helpers/desktop_state.py
+26
-55
@@ -262,50 +262,43 @@ def convert_xwd_to_image(raw_path: Path, target: Path) -> dict[str, int]:
262
from PIL import Image
263
264
data = raw_path.read_bytes()
265
- header, endian = parse_xwd_header(data)
265
+ header, _ = parse_xwd_header(data)
266
width = header["pixmap_width"]
267
height = header["pixmap_height"]
268
bytes_per_line = header["bytes_per_line"]
269
bits_per_pixel = header["bits_per_pixel"]
270
- image_byte_order = "little" if header["byte_order"] == 0 else "big"
270
color_table_size = header["ncolors"] * 12
271
pixel_offset = header["header_size"] + color_table_size
273
- bytes_per_pixel = max((bits_per_pixel + 7) // 8, 1)
274
- if width > 0 and bytes_per_line % width == 0:
275
- bytes_per_pixel = max(bytes_per_pixel, bytes_per_line // width)
272
if width <= 0 or height <= 0 or bytes_per_line <= 0:
273
raise ValueError("invalid XWD dimensions")
278
- if pixel_offset + (height * bytes_per_line) > len(data):
274
+ pixel_size = height * bytes_per_line
275
+ if pixel_offset + pixel_size > len(data):
276
raise ValueError("truncated XWD pixel data")
277
281
- red_mask = header["red_mask"]
282
- green_mask = header["green_mask"]
283
- blue_mask = header["blue_mask"]
284
- red_shift, red_bits = mask_shift_and_bits(red_mask)
285
- green_shift, green_bits = mask_shift_and_bits(green_mask)
286
- blue_shift, blue_bits = mask_shift_and_bits(blue_mask)
287
- if min(red_bits, green_bits, blue_bits) <= 0:
278
+ if (header["red_mask"], header["green_mask"], header["blue_mask"]) != (
279
+ 0x00FF0000,
280
+ 0x0000FF00,
281
+ 0x000000FF,
282
+ ):
283
raise ValueError("unsupported XWD visual masks")
289
-
290
- pixels: list[tuple[int, int, int]] = []
291
- for row in range(height):
292
- row_start = pixel_offset + (row * bytes_per_line)
293
- for column in range(width):
294
- start = row_start + (column * bytes_per_pixel)
295
- pixel_bytes = data[start : start + bytes_per_pixel]
296
- if len(pixel_bytes) < bytes_per_pixel:
297
- raise ValueError("truncated XWD pixel")
298
- pixel = int.from_bytes(pixel_bytes, image_byte_order, signed=False)
299
- pixels.append(
300
- (
301
- scale_channel((pixel & red_mask) >> red_shift, red_bits),
302
- scale_channel((pixel & green_mask) >> green_shift, green_bits),
303
- scale_channel((pixel & blue_mask) >> blue_shift, blue_bits),
304
- ),
305
- )
306
-
307
- image = Image.new("RGB", (width, height))
308
- image.putdata(pixels)
284
+ raw_mode = {
285
+ (24, 0): "BGR",
286
+ (24, 1): "RGB",
287
+ (32, 0): "BGRX",
288
+ (32, 1): "XRGB",
289
+ }.get((bits_per_pixel, header["byte_order"]))
290
+ if not raw_mode:
291
+ raise ValueError(f"unsupported XWD pixel layout: {bits_per_pixel} bpp")
292
+
293
+ image = Image.frombytes(
294
+ "RGB",
295
+ (width, height),
296
+ data[pixel_offset : pixel_offset + pixel_size],
297
+ "raw",
298
+ raw_mode,
299
+ bytes_per_line,
300
+ 1,
301
+ )
302
image.save(target)
303
return {"width": width, "height": height}
304
@@ -348,28 +341,6 @@ def parse_xwd_header(data: bytes) -> tuple[dict[str, int], str]:
341
raise ValueError("unsupported XWD header")
342
343
351
-def mask_shift_and_bits(mask: int) -> tuple[int, int]:
352
- if mask <= 0:
353
- return 0, 0
354
- shift = 0
355
- value = mask
356
- while value and value & 1 == 0:
357
- shift += 1
358
- value >>= 1
359
- bits = 0
360
- while value & 1:
361
- bits += 1
362
- value >>= 1
363
- return shift, bits
364
-
365
-
366
-def scale_channel(value: int, bits: int) -> int:
367
- if bits >= 8:
368
- return max(0, min(255, value >> (bits - 8)))
369
- max_value = (1 << bits) - 1
370
- return 0 if max_value <= 0 else round((value / max_value) * 255)
371
-
372
-
344
def resolve_environment(*, errors: list[str] | None = None, session_id: str = SESSION_ID) -> dict[str, str]:
345
local_errors = errors if errors is not None else []
346
manifest = session_manifest_path(session_id)
tests/test_office_desktop_state.py
+18
-33
@@ -6,6 +6,8 @@ import sys
6
import types
7
from pathlib import Path
8
9
+import pytest
10
+
11
PROJECT_ROOT = Path(__file__).resolve().parents[1]
12
if str(PROJECT_ROOT) not in sys.path:
13
sys.path.insert(0, str(PROJECT_ROOT))
@@ -324,7 +326,16 @@ def test_virtual_desktop_system_display_normalization_rejects_portrait_viewports
326
assert virtual_desktop.normalize_desktop_display_size(1600, 900) == (1600, 900)
327
328
327
-def test_xwd_fallback_parser_handles_truecolor_pixels(tmp_path, monkeypatch):
329
+@pytest.mark.parametrize(
330
+ ("byte_order", "pixel_bytes"),
331
+ (
332
+ (0, bytes.fromhex("0000ff00") + bytes.fromhex("00ff0000")),
333
+ (1, bytes.fromhex("00ff0000") + bytes.fromhex("0000ff00")),
334
+ ),
335
+)
336
+def test_xwd_fallback_parser_handles_truecolor_pixels(tmp_path, byte_order, pixel_bytes):
337
+ from PIL import Image
338
+
339
raw_path = tmp_path / "shot.xwd"
340
target = tmp_path / "shot.png"
341
header_values = [
@@ -335,7 +346,7 @@ def test_xwd_fallback_parser_handles_truecolor_pixels(tmp_path, monkeypatch):
346
2, # pixmap_width
347
1, # pixmap_height
348
0, # xoffset
338
- 1, # byte_order: MSBFirst for pixel bytes
349
+ byte_order,
350
32, # bitmap_unit
351
1, # bitmap_bit_order
352
32, # bitmap_pad
@@ -354,38 +365,12 @@ def test_xwd_fallback_parser_handles_truecolor_pixels(tmp_path, monkeypatch):
365
0, # window_y
366
0, # window_bdrwidth
367
]
357
- raw_path.write_bytes(
358
- struct.pack(">25I", *header_values)
359
- + bytes.fromhex("00ff0000")
360
- + bytes.fromhex("0000ff00")
361
- )
362
-
363
- captured: dict[str, object] = {}
364
- image_module = types.ModuleType("PIL.Image")
365
-
366
- class FakeOutputImage:
367
- def putdata(self, pixels):
368
- captured["pixels"] = list(pixels)
369
-
370
- def save(self, path):
371
- Path(path).write_bytes(b"fallback-png")
372
-
373
- def fake_new(mode, size):
374
- captured["mode"] = mode
375
- captured["size"] = size
376
- return FakeOutputImage()
377
-
378
- image_module.new = fake_new
379
- pil_module = types.ModuleType("PIL")
380
- pil_module.Image = image_module
381
-
382
- monkeypatch.setitem(sys.modules, "PIL", pil_module)
383
- monkeypatch.setitem(sys.modules, "PIL.Image", image_module)
368
+ raw_path.write_bytes(struct.pack(">25I", *header_values) + pixel_bytes)
369
370
converted = desktop_state.convert_xwd_to_image(raw_path, target)
371
372
assert converted == {"width": 2, "height": 1}
388
- assert captured["mode"] == "RGB"
389
- assert captured["size"] == (2, 1)
390
- assert captured["pixels"] == [(255, 0, 0), (0, 255, 0)]
391
- assert target.read_bytes() == b"fallback-png"
373
+ with Image.open(target) as image:
374
+ assert image.mode == "RGB"
375
+ assert image.size == (2, 1)
376
+ assert list(image.getdata()) == [(255, 0, 0), (0, 255, 0)]
tests/test_office_document_store.py
+117
@@ -1037,6 +1037,123 @@ def test_official_desktop_session_manager_opens_binary_session(office_state, tmp
1037
assert manager.close(payload["session_id"], save_first=False)["persistent"] is True
1038
1039
1040
+def test_desktop_save_targets_requested_libreoffice_window(office_state, tmp_path, monkeypatch):
1041
+ doc = document_store.create_document("spreadsheet", "Targeted Save", "ods", "Name,Value\nA,1")
1042
+ session = desktop_session.DesktopSession(
1043
+ session_id=desktop_session.SYSTEM_SESSION_ID,
1044
+ file_id=doc["file_id"],
1045
+ extension=doc["extension"],
1046
+ path=doc["path"],
1047
+ title=doc["basename"],
1048
+ display=desktop_session.DISPLAY_BASE,
1049
+ xpra_port=desktop_session.XPRA_PORT_BASE,
1050
+ token=desktop_session.SYSTEM_SESSION_ID,
1051
+ url="/desktop/session/agent-zero-desktop/index.html",
1052
+ profile_dir=tmp_path / "profile",
1053
+ processes={"xpra": types.SimpleNamespace(poll=lambda: None)},
1054
+ )
1055
+ manager = desktop_session.DesktopSessionManager()
1056
+ manager._sessions[session.session_id] = session
1057
+ commands = []
1058
+
1059
+ monkeypatch.setattr(desktop_session.shutil, "which", lambda name: f"/usr/bin/{name}")
1060
+ monkeypatch.setattr(desktop_session.time, "sleep", lambda _seconds: None)
1061
+
1062
+ def fake_run(command, **_kwargs):
1063
+ commands.append(command)
1064
+ if command[1] == "search":
1065
+ assert command == ["/usr/bin/xdotool", "search", "--onlyvisible", "--class", "libreoffice"]
1066
+ return subprocess.CompletedProcess(command, 0, "222\n", "")
1067
+ if command[1] == "getwindowname":
1068
+ return subprocess.CompletedProcess(command, 0, f"{doc['basename']} — LibreOffice Calc\n", "")
1069
+ Path(doc["path"]).write_bytes(Path(doc["path"]).read_bytes() + b"changed")
1070
+ return subprocess.CompletedProcess(command, 0, "", "")
1071
+
1072
+ monkeypatch.setattr(desktop_session.subprocess, "run", fake_run)
1073
+
1074
+ result = manager.save(session.session_id, doc["file_id"])
1075
+
1076
+ assert result["ok"] is True
1077
+ assert result["changed"] is True
1078
+ assert commands[-1] == [
1079
+ "/usr/bin/xdotool",
1080
+ "windowactivate",
1081
+ "--sync",
1082
+ "222",
1083
+ "key",
1084
+ "--clearmodifiers",
1085
+ "ctrl+s",
1086
+ ]
1087
+
1088
+ session.file_id = desktop_session.SYSTEM_FILE_ID
1089
+ command_count = len(commands)
1090
+ assert manager.save(session.session_id)["changed"] is False
1091
+ assert len(commands) == command_count
1092
+
1093
+
1094
+def test_desktop_startup_waiters_probe_display_and_reject_dead_xfce(tmp_path, monkeypatch):
1095
+ session = desktop_session.DesktopSession(
1096
+ session_id=desktop_session.SYSTEM_SESSION_ID,
1097
+ file_id=desktop_session.SYSTEM_FILE_ID,
1098
+ extension="desktop",
1099
+ path=str(tmp_path),
1100
+ title=desktop_session.SYSTEM_TITLE,
1101
+ display=desktop_session.DISPLAY_BASE,
1102
+ xpra_port=desktop_session.XPRA_PORT_BASE,
1103
+ token=desktop_session.SYSTEM_SESSION_ID,
1104
+ url="/desktop/session/agent-zero-desktop/index.html",
1105
+ profile_dir=tmp_path / "profile",
1106
+ processes={"xvfb": types.SimpleNamespace(poll=lambda: None)},
1107
+ )
1108
+ manager = desktop_session.DesktopSessionManager()
1109
+ probes = iter((None, (1920, 1080)))
1110
+ monkeypatch.setattr(
1111
+ desktop_session.virtual_desktop,
1112
+ "current_display_size",
1113
+ lambda *_args, **_kwargs: next(probes),
1114
+ )
1115
+ monkeypatch.setattr(desktop_session.time, "sleep", lambda _seconds: None)
1116
+
1117
+ manager._wait_for_display(session)
1118
+
1119
+ session.processes = {"xfce": types.SimpleNamespace(poll=lambda: 1)}
1120
+ with pytest.raises(RuntimeError, match="XFCE desktop session exited"):
1121
+ manager._wait_for_xfce(session)
1122
+
1123
+
1124
+def test_desktop_manifest_is_replaced_atomically(tmp_path, monkeypatch):
1125
+ session_dir = tmp_path / "sessions"
1126
+ session_dir.mkdir()
1127
+ manifest = session_dir / f"{desktop_session.SYSTEM_SESSION_ID}.json"
1128
+ manifest.write_text('{"old": true}', encoding="utf-8")
1129
+ session = desktop_session.DesktopSession(
1130
+ session_id=desktop_session.SYSTEM_SESSION_ID,
1131
+ file_id=desktop_session.SYSTEM_FILE_ID,
1132
+ extension="desktop",
1133
+ path=str(tmp_path),
1134
+ title=desktop_session.SYSTEM_TITLE,
1135
+ display=desktop_session.DISPLAY_BASE,
1136
+ xpra_port=desktop_session.XPRA_PORT_BASE,
1137
+ token=desktop_session.SYSTEM_SESSION_ID,
1138
+ url="/desktop/session/agent-zero-desktop/index.html",
1139
+ profile_dir=tmp_path / "profile",
1140
+ )
1141
+ real_replace = os.replace
1142
+
1143
+ def assert_atomic_replace(source, destination):
1144
+ assert json.loads(Path(destination).read_text(encoding="utf-8")) == {"old": True}
1145
+ assert json.loads(Path(source).read_text(encoding="utf-8"))["display"] == session.display
1146
+ real_replace(source, destination)
1147
+
1148
+ monkeypatch.setattr(desktop_session, "SESSION_DIR", session_dir)
1149
+ monkeypatch.setattr(desktop_session.os, "replace", assert_atomic_replace)
1150
+
1151
+ desktop_session.DesktopSessionManager()._write_manifest(session)
1152
+
1153
+ assert json.loads(manifest.read_text(encoding="utf-8"))["session_id"] == session.session_id
1154
+ assert list(session_dir.glob(".*.tmp")) == []
1155
+
1156
+
1157
def test_shutdown_panel_launcher_requires_second_click(tmp_path):
1158
profile_dir = tmp_path / "desktop" / "profiles" / desktop_session.SYSTEM_SESSION_ID
1159
profile_dir.mkdir(parents=True)