mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-07-09 17:08:29 +00:00
Sync stale self-update manager at startup
Add a core startup migration that refreshes /exe/self_update_manager.py from the repository copy when the installed runtime updater is missing the socket-safe backup and Desktop cleanup markers. Validate the source updater before replacing anything, preserve the previous runtime script as a backup, and leave current or non-regular targets untouched. Add regression coverage for stale runtime replacement, safe no-op/refusal paths, and the real repository updater source.
This commit is contained in:
parent
c576f67469
commit
f9031b7575
3 changed files with 267 additions and 0 deletions
|
|
@ -13,6 +13,7 @@
|
|||
- Migrations must be safe to run repeatedly.
|
||||
- Preserve user data and create backups or reversible paths when changing durable state.
|
||||
- Keep long-running work bounded and observable.
|
||||
- `_10_self_update_manager.py` may replace `/exe/self_update_manager.py` from the repository copy when the installed runtime updater is stale; it must validate required safety markers and keep a backup before replacement.
|
||||
|
||||
## Work Guidance
|
||||
|
||||
|
|
|
|||
125
extensions/python/startup_migration/_10_self_update_manager.py
Normal file
125
extensions/python/startup_migration/_10_self_update_manager.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from helpers.extension import Extension
|
||||
from helpers.print_style import PrintStyle
|
||||
|
||||
|
||||
SELF_UPDATE_MANAGER_PATH = Path(
|
||||
os.environ.get("A0_SELF_UPDATE_MANAGER_PATH", "/exe/self_update_manager.py")
|
||||
)
|
||||
SELF_UPDATE_MANAGER_SOURCE_PATH = Path(
|
||||
os.environ.get(
|
||||
"A0_SELF_UPDATE_MANAGER_SOURCE_PATH",
|
||||
"/a0/docker/run/fs/exe/self_update_manager.py",
|
||||
)
|
||||
)
|
||||
BACKUP_SUFFIX = ".startup-migration-backup"
|
||||
REQUIRED_RUNTIME_MARKERS = (
|
||||
"def should_include_usr_backup_entry(",
|
||||
"Skipping non-regular usr backup entry",
|
||||
"def clean_transient_desktop_agent_state(",
|
||||
"clean_transient_desktop_agent_state(REPO_DIR, logger)",
|
||||
)
|
||||
|
||||
|
||||
class SelfUpdateManagerRuntimeSync(Extension):
|
||||
def execute(self, **kwargs):
|
||||
result = ensure_self_update_manager_runtime_current()
|
||||
if result.get("updated"):
|
||||
PrintStyle.info("Self-update manager runtime synchronized:", result["target"])
|
||||
elif result.get("warning"):
|
||||
PrintStyle.warning("Self-update manager runtime sync skipped:", result["warning"])
|
||||
|
||||
|
||||
def ensure_self_update_manager_runtime_current(
|
||||
*,
|
||||
target_path: Path | str | None = None,
|
||||
source_path: Path | str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
target = Path(target_path) if target_path is not None else SELF_UPDATE_MANAGER_PATH
|
||||
source = Path(source_path) if source_path is not None else SELF_UPDATE_MANAGER_SOURCE_PATH
|
||||
|
||||
target_text, target_warning = _read_regular_text(target, role="runtime self-update manager")
|
||||
if target_text is None:
|
||||
return {"ok": True, "updated": False, "reason": target_warning}
|
||||
|
||||
source_text, source_warning = _read_regular_text(source, role="source self-update manager")
|
||||
if source_text is None:
|
||||
return {"ok": False, "updated": False, "warning": source_warning}
|
||||
|
||||
missing_source_markers = _missing_required_markers(source_text)
|
||||
if missing_source_markers:
|
||||
return {
|
||||
"ok": False,
|
||||
"updated": False,
|
||||
"warning": (
|
||||
"source self-update manager is missing required safety markers: "
|
||||
+ ", ".join(missing_source_markers)
|
||||
),
|
||||
}
|
||||
|
||||
if not _missing_required_markers(target_text):
|
||||
return {"ok": True, "updated": False, "reason": "already-current"}
|
||||
|
||||
try:
|
||||
backup = _replace_runtime_manager(target, source_text)
|
||||
except OSError as exc:
|
||||
return {
|
||||
"ok": False,
|
||||
"updated": False,
|
||||
"warning": f"could not update {target}: {exc}",
|
||||
}
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"updated": True,
|
||||
"target": str(target),
|
||||
"backup": str(backup),
|
||||
}
|
||||
|
||||
|
||||
def _missing_required_markers(text: str) -> list[str]:
|
||||
return [marker for marker in REQUIRED_RUNTIME_MARKERS if marker not in text]
|
||||
|
||||
|
||||
def _read_regular_text(path: Path, *, role: str) -> tuple[str | None, str]:
|
||||
try:
|
||||
path_stat = path.lstat()
|
||||
except FileNotFoundError:
|
||||
return None, f"{role} not found: {path}"
|
||||
except OSError as exc:
|
||||
return None, f"{role} could not be inspected: {path}: {exc}"
|
||||
|
||||
if not stat.S_ISREG(path_stat.st_mode):
|
||||
return None, f"{role} is not a regular file: {path}"
|
||||
|
||||
try:
|
||||
return path.read_text(encoding="utf-8"), ""
|
||||
except OSError as exc:
|
||||
return None, f"{role} could not be read: {path}: {exc}"
|
||||
|
||||
|
||||
def _replace_runtime_manager(target: Path, source_text: str) -> Path:
|
||||
target_stat = target.stat()
|
||||
backup = _ensure_backup(target)
|
||||
temp_path = target.with_name(f".{target.name}.{os.getpid()}.tmp")
|
||||
try:
|
||||
temp_path.write_text(source_text, encoding="utf-8")
|
||||
os.chmod(temp_path, stat.S_IMODE(target_stat.st_mode))
|
||||
os.replace(temp_path, target)
|
||||
finally:
|
||||
temp_path.unlink(missing_ok=True)
|
||||
return backup
|
||||
|
||||
|
||||
def _ensure_backup(target: Path) -> Path:
|
||||
backup = target.with_name(f"{target.name}{BACKUP_SUFFIX}")
|
||||
if not backup.exists():
|
||||
shutil.copy2(target, backup)
|
||||
return backup
|
||||
Loading…
Add table
Add a link
Reference in a new issue