Skip transient Desktop SSH agent state during self-update backup

Exclude the Desktop profile .ssh/agent runtime directory from usr backups during self-update so live SSH agent sockets do not abort upgrades. Keep the rule in the self-update manager, where the usr backup actually runs, and cover it with a regression test alongside the existing runtime-socket backup cases.

Alessandro committed May 26, 2026 at 14:54 UTC 369e0df17f60463d7619963a2fd49420b896afda
2 files changed +75 -1
docker/run/fs/exe/self_update_manager.py
+28 -1
@@ -363,8 +363,17 @@ def create_usr_backup(
363 compression=zipfile.ZIP_DEFLATED,
364 compresslevel=6,
365 ) as archive:
366 - for root, _, files in os.walk(usr_dir):
366 + for root, dirs, files in os.walk(usr_dir):
367 root_path = Path(root)
368 + root_relative = root_path.relative_to(usr_dir)
369 + dirs[:] = [
370 + dirname
371 + for dirname in dirs
372 + if not should_exclude_from_usr_backup(
373 + root_relative / dirname,
374 + logger,
375 + )
376 + ]
377 for filename in files:
378 source_file = root_path / filename
379 if not should_include_usr_backup_entry(source_file, logger):
@@ -386,6 +395,24 @@ def create_usr_backup(
395 temporary_backup.unlink(missing_ok=True)
396
397
398 +def should_exclude_from_usr_backup(
399 + relative_dir: Path,
400 + logger: AttemptLogger,
401 +) -> bool:
402 + parts = relative_dir.parts
403 + if (
404 + len(parts) >= 6
405 + and parts[0] == "plugins"
406 + and parts[1] == "_desktop"
407 + and parts[2] == "profiles"
408 + and parts[-2] == ".ssh"
409 + and parts[-1] == "agent"
410 + ):
411 + logger.log(f"Skipping transient usr backup directory: {Path('usr') / relative_dir}")
412 + return True
413 + return False
414 +
415 +
416 def should_include_usr_backup_entry(source_file: Path, logger: AttemptLogger) -> bool:
417 try:
418 source_stat = source_file.lstat()
tests/test_self_update_tag_filter.py
+47
@@ -829,6 +829,53 @@ def test_self_update_manager_usr_backup_skips_runtime_sockets():
829 )
830
831
832 +def test_self_update_manager_usr_backup_skips_transient_desktop_ssh_agent_dir(tmp_path):
833 + manager = load_self_update_manager()
834 + repo_dir = tmp_path / "repo"
835 + usr_dir = repo_dir / "usr"
836 + ssh_dir = (
837 + usr_dir
838 + / "plugins"
839 + / "_desktop"
840 + / "profiles"
841 + / "agent-zero-desktop"
842 + / ".ssh"
843 + )
844 + transient_agent_dir = ssh_dir / "agent"
845 + transient_agent_dir.mkdir(parents=True)
846 + (transient_agent_dir / "socket").write_text("ephemeral\n", encoding="utf-8")
847 + (ssh_dir / "config").write_text("Host github.com\n", encoding="utf-8")
848 + (usr_dir / "settings.json").write_text('{"ok": true}\n', encoding="utf-8")
849 + messages = []
850 +
851 + class ListLogger:
852 + def log(self, message=""):
853 + messages.append(message)
854 +
855 + backup_path = manager.create_usr_backup(
856 + repo_dir=repo_dir,
857 + backup_path=str(tmp_path / "backups"),
858 + backup_name="usr-backup.zip",
859 + conflict_policy="rename",
860 + logger=ListLogger(),
861 + )
862 +
863 + with zipfile.ZipFile(backup_path) as archive:
864 + names = set(archive.namelist())
865 +
866 + assert "usr/settings.json" in names
867 + assert "usr/plugins/_desktop/profiles/agent-zero-desktop/.ssh/config" in names
868 + assert (
869 + "usr/plugins/_desktop/profiles/agent-zero-desktop/.ssh/agent/socket"
870 + not in names
871 + )
872 + assert any(
873 + "Skipping transient usr backup directory: "
874 + "usr/plugins/_desktop/profiles/agent-zero-desktop/.ssh/agent" in message
875 + for message in messages
876 + )
877 +
878 +
879 def test_self_update_manager_clean_uv_cache_uses_uv_when_available(monkeypatch):
880 manager = load_self_update_manager()
881 commands = []