Clean desktop SSH agent state during self-update
Remove stale runtime entries from the desktop SSH agent directory when a self-update request is consumed. Keep the cleanup best-effort so missing paths, non-directory paths, and unexpected cleanup failures do not block update startup. Cover successful cleanup, missing-directory skips, and failure fallback with focused self-update manager tests.
Alessandro committed
May 27, 2026 at 16:25 UTC
8af14fcd93b62776a1b18a477ad56fa4a2637e78
2 files changed
+156
docker/run/fs/exe/self_update_manager.py
+62
@@ -46,6 +46,9 @@ DEFAULT_BACKUP_CONFLICT_POLICY = "rename"
46
BACKUP_CONFLICT_POLICIES = {"rename", "overwrite", "fail"}
47
MIN_SELECTOR_VERSION = (1, 0)
48
LATEST_SELECTOR_TAG = "latest"
49
+TRANSIENT_DESKTOP_SSH_AGENT_RELATIVE_DIR = Path(
50
+ "usr/plugins/_desktop/profiles/agent-zero-desktop/.ssh/agent"
51
+)
52
53
54
def now_iso() -> str:
@@ -448,6 +451,61 @@ def should_include_usr_backup_entry(source_file: Path, logger: AttemptLogger) ->
451
return True
452
453
454
+def clean_transient_desktop_ssh_agent_dir(
455
+ repo_dir: Path,
456
+ logger: AttemptLogger,
457
+) -> None:
458
+ agent_dir = repo_dir / TRANSIENT_DESKTOP_SSH_AGENT_RELATIVE_DIR
459
+ try:
460
+ agent_stat = agent_dir.lstat()
461
+ except FileNotFoundError:
462
+ logger.log(
463
+ f"Transient desktop SSH agent directory not found, skipping: {agent_dir}"
464
+ )
465
+ return
466
+ except OSError as exc:
467
+ logger.log(
468
+ f"Transient desktop SSH agent directory could not be inspected: {agent_dir}: {exc}"
469
+ )
470
+ return
471
+
472
+ if stat.S_ISLNK(agent_stat.st_mode) or not stat.S_ISDIR(agent_stat.st_mode):
473
+ logger.log(
474
+ f"Transient desktop SSH agent path is not a directory, skipping: {agent_dir}"
475
+ )
476
+ return
477
+
478
+ removed = 0
479
+ try:
480
+ entries = list(agent_dir.iterdir())
481
+ except OSError as exc:
482
+ logger.log(
483
+ f"Transient desktop SSH agent directory could not be listed: {agent_dir}: {exc}"
484
+ )
485
+ return
486
+
487
+ for entry in entries:
488
+ try:
489
+ if entry.is_symlink():
490
+ entry.unlink(missing_ok=True)
491
+ elif entry.is_dir():
492
+ shutil.rmtree(entry)
493
+ else:
494
+ entry.unlink(missing_ok=True)
495
+ removed += 1
496
+ except FileNotFoundError:
497
+ continue
498
+ except OSError as exc:
499
+ logger.log(
500
+ f"Skipping transient desktop SSH agent entry after error: {entry}: {exc}"
501
+ )
502
+
503
+ if removed:
504
+ logger.log(f"Removed {removed} transient desktop SSH agent entries from {agent_dir}")
505
+ else:
506
+ logger.log(f"Transient desktop SSH agent directory already empty: {agent_dir}")
507
+
508
+
509
def run_command(
510
command: list[str],
511
*,
@@ -1266,6 +1324,10 @@ def docker_run_ui() -> int:
1324
logger.log(f"Consumed update file at {TRIGGER_FILE}")
1325
logger.log_block("Trigger file content", raw_text)
1326
clean_uv_cache(logger)
1327
+ try:
1328
+ clean_transient_desktop_ssh_agent_dir(REPO_DIR, logger)
1329
+ except Exception as exc:
1330
+ logger.log(f"Transient desktop SSH agent cleanup skipped after error: {exc}")
1331
1332
try:
1333
current = get_repo_version_info(REPO_DIR)
tests/test_self_update_tag_filter.py
+94
@@ -876,6 +876,100 @@ def test_self_update_manager_usr_backup_skips_transient_desktop_ssh_agent_dir(tm
876
)
877
878
879
+def test_self_update_manager_cleans_transient_desktop_ssh_agent_dir():
880
+ manager = load_self_update_manager()
881
+ with tempfile.TemporaryDirectory(prefix="a0su-", dir="/tmp") as temp_root:
882
+ repo_dir = Path(temp_root) / "repo"
883
+ agent_dir = repo_dir / manager.TRANSIENT_DESKTOP_SSH_AGENT_RELATIVE_DIR
884
+ nested_dir = agent_dir / "nested"
885
+ agent_dir.mkdir(parents=True)
886
+ nested_dir.mkdir()
887
+ (agent_dir / "socket").write_text("ephemeral\n", encoding="utf-8")
888
+ (nested_dir / "token").write_text("ephemeral\n", encoding="utf-8")
889
+ (agent_dir / "broken-link").symlink_to("/missing/ssh-agent-socket")
890
+ socket_path = agent_dir / "runtime.sock"
891
+ messages = []
892
+
893
+ class ListLogger:
894
+ def log(self, message=""):
895
+ messages.append(message)
896
+
897
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as runtime_socket:
898
+ runtime_socket.bind(str(socket_path))
899
+
900
+ manager.clean_transient_desktop_ssh_agent_dir(repo_dir, ListLogger())
901
+
902
+ assert agent_dir.exists()
903
+ assert list(agent_dir.iterdir()) == []
904
+ assert any(
905
+ f"Removed 4 transient desktop SSH agent entries from {agent_dir}" in message
906
+ for message in messages
907
+ )
908
+
909
+
910
+def test_self_update_manager_cleans_transient_desktop_ssh_agent_dir_skips_missing(
911
+ tmp_path,
912
+):
913
+ manager = load_self_update_manager()
914
+ repo_dir = tmp_path / "repo"
915
+ messages = []
916
+
917
+ class ListLogger:
918
+ def log(self, message=""):
919
+ messages.append(message)
920
+
921
+ manager.clean_transient_desktop_ssh_agent_dir(repo_dir, ListLogger())
922
+
923
+ assert any(
924
+ "Transient desktop SSH agent directory not found, skipping:" in message
925
+ for message in messages
926
+ )
927
+
928
+
929
+def test_self_update_manager_desktop_ssh_cleanup_failure_does_not_block_startup(
930
+ monkeypatch,
931
+ tmp_path,
932
+):
933
+ manager = load_self_update_manager()
934
+ request_data = {"branch": "main", "tag": "v1.2", "requested_at": "now"}
935
+ current_info = {
936
+ "branch": "main",
937
+ "describe": "v1.2",
938
+ "short_tag": "v1.2",
939
+ "commit": "abc1234",
940
+ "short_commit": "abc1234",
941
+ }
942
+ launched = []
943
+ fake_process = object()
944
+
945
+ monkeypatch.setattr(manager, "LOG_FILE", tmp_path / "a0-self-update.log")
946
+ monkeypatch.setattr(
947
+ manager,
948
+ "load_request_file",
949
+ lambda: (request_data, "branch: main\ntag: v1.2\n"),
950
+ )
951
+ monkeypatch.setattr(manager, "clean_uv_cache", lambda logger: None)
952
+ monkeypatch.setattr(
953
+ manager,
954
+ "clean_transient_desktop_ssh_agent_dir",
955
+ lambda repo_dir, logger: (_ for _ in ()).throw(RuntimeError("cleanup boom")),
956
+ )
957
+ monkeypatch.setattr(manager, "get_repo_version_info", lambda repo_dir: current_info)
958
+ monkeypatch.setattr(manager, "record_result", lambda **kwargs: None)
959
+ monkeypatch.setattr(
960
+ manager,
961
+ "launch_ui_process",
962
+ lambda repo_dir, logger: launched.append(repo_dir) or fake_process,
963
+ )
964
+ monkeypatch.setattr(manager, "wait_for_process", lambda process: 0)
965
+
966
+ assert manager.docker_run_ui() == 0
967
+ assert launched == [manager.REPO_DIR]
968
+ assert "Transient desktop SSH agent cleanup skipped after error: cleanup boom" in (
969
+ tmp_path / "a0-self-update.log"
970
+ ).read_text(encoding="utf-8")
971
+
972
+
973
def test_self_update_manager_clean_uv_cache_uses_uv_when_available(monkeypatch):
974
manager = load_self_update_manager()
975
commands = []