#!/usr/bin/env python3 from __future__ import annotations import argparse import grp import json import os import pwd import subprocess from pathlib import Path from common import DeployError, managed_runtime_dir_removal_issues MANAGED_PREFIXES = ("downstream-", "nexuscrm-") def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Repair ownership on managed offline runtime worktree directories." ) parser.add_argument("--env-root", required=True, help="environment root, e.g. /home/ec2-user/.../envs/exp") parser.add_argument("--owner", default=pwd.getpwuid(os.geteuid()).pw_name) parser.add_argument("--group", default=grp.getgrgid(os.getegid()).gr_name) parser.add_argument("--apply", action="store_true", help="perform chown/chmod; default is dry-run") parser.add_argument("--summary-json", help="optional summary json output path") return parser.parse_args() def safe_resolve(path: Path) -> Path: return path.expanduser().resolve(strict=False) def ensure_within_root(path: Path, root: Path) -> None: try: safe_resolve(path).relative_to(safe_resolve(root)) except ValueError as exc: raise DeployError(f"refusing to operate outside managed root: {path}") from exc def managed_runtime_dirs(env_root: Path) -> list[Path]: runtime_root = env_root / ".runtime" if not runtime_root.is_dir(): return [] paths: list[Path] = [] for child in runtime_root.iterdir(): if child.is_symlink() or not child.is_dir(): continue if child.name.startswith(MANAGED_PREFIXES): ensure_within_root(child, runtime_root) paths.append(child) return sorted(paths) def run_repair(path: Path, owner: str, group: str) -> None: target = str(path) subprocess.run(["sudo", "chown", "-R", f"{owner}:{group}", target], check=True) subprocess.run(["sudo", "chmod", "-R", "u+rwX", target], check=True) def write_summary(path: str | None, payload: dict[str, object]) -> None: if not path: return out_path = Path(path).expanduser().resolve() out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") def main() -> int: args = parse_args() env_root = safe_resolve(Path(args.env_root)) runtime_root = env_root / ".runtime" ensure_within_root(runtime_root, env_root) target_uid = pwd.getpwnam(args.owner).pw_uid dirs = managed_runtime_dirs(env_root) before = [ { "path": str(path), "issues": managed_runtime_dir_removal_issues(path, expected_uid=target_uid), } for path in dirs ] repair_targets = [item for item in before if item["issues"]] if args.apply: for item in repair_targets: run_repair(Path(str(item["path"])), args.owner, args.group) after = [ { "path": str(path), "issues": managed_runtime_dir_removal_issues(path, expected_uid=target_uid), } for path in dirs ] payload = { "env_root": str(env_root), "runtime_root": str(runtime_root), "owner": args.owner, "group": args.group, "apply": bool(args.apply), "managed_dir_count": len(dirs), "repair_target_count": len(repair_targets), "before": before, "after": after, } write_summary(args.summary_json, payload) print(json.dumps(payload, indent=2, sort_keys=True)) remaining = [item for item in after if item["issues"]] return 1 if args.apply and remaining else 0 if __name__ == "__main__": try: raise SystemExit(main()) except (DeployError, KeyError) as exc: print(f"[repair] failed: {exc}") raise SystemExit(1)