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 = []