fix(codex): replace pwd.getpwall() with directory scan for macOS

macOS Directory Services makes pwd.getpwall() unreliable — regular users
may not be returned. Now scans /Users/* directly on macOS, /home/* on Linux.
Also fixes chown to use actual directory ownership.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
delta-cloud-208e
2026-03-10 18:28:10 +00:00
parent b27c0cd65e
commit 1f827cc9d8

View File

@@ -659,24 +659,57 @@ def rollback(home_dir=None):
# ─── Multi-User Support ───────────────────────────────────────────────── # ─── Multi-User Support ─────────────────────────────────────────────────
def list_users(): def list_user_homes():
"""List system users with .codex/ or home dirs.""" """List home directories of real users. Returns list of (home_dir, uid, gid)."""
users = [] homes = []
try:
import pwd if IS_MACOS:
for pw in pwd.getpwall(): # macOS: pwd.getpwall() is unreliable (Directory Services).
home = pw.pw_dir # Scan /Users/ directly + /var/root for root.
if not os.path.isdir(home): import stat as st
for base in ["/Users", "/var/root"]:
if base == "/var/root":
if os.path.isdir(base):
info = os.stat(base)
homes.append((base, info.st_uid, info.st_gid))
continue continue
min_uid = 500 if IS_MACOS else 1000 if not os.path.isdir(base):
if pw.pw_uid < min_uid and pw.pw_uid != 0:
continue continue
if pw.pw_shell in ("/usr/sbin/nologin", "/bin/false"): for name in os.listdir(base):
continue if name.startswith(".") or name == "Shared":
users.append(pw) continue
except ImportError: udir = os.path.join(base, name)
pass if os.path.isdir(udir):
return users info = os.stat(udir)
homes.append((udir, info.st_uid, info.st_gid))
else:
# Linux: use pwd
try:
import pwd
for pw in pwd.getpwall():
if not os.path.isdir(pw.pw_dir):
continue
if pw.pw_uid < 1000 and pw.pw_uid != 0:
continue
if pw.pw_shell in ("/usr/sbin/nologin", "/bin/false"):
continue
homes.append((pw.pw_dir, pw.pw_uid, pw.pw_gid))
except ImportError:
# Fallback: scan /home + /root
for base in ["/home", "/root"]:
if base == "/root":
if os.path.isdir(base):
homes.append((base, 0, 0))
continue
if not os.path.isdir(base):
continue
for name in os.listdir(base):
udir = os.path.join(base, name)
if os.path.isdir(udir):
info = os.stat(udir)
homes.append((udir, info.st_uid, info.st_gid))
return homes
def patch_user(user_home, config, uid=None, gid=None): def patch_user(user_home, config, uid=None, gid=None):
@@ -748,14 +781,15 @@ def main():
# Patch other users if --all # Patch other users if --all
if args.all: if args.all:
for user in list_users(): my_home = os.path.expanduser("~")
if user.pw_dir == os.path.expanduser("~"): for home_dir, uid, gid in list_user_homes():
if home_dir == my_home:
continue continue
try: try:
patch_user(user.pw_dir, config, uid=user.pw_uid, gid=user.pw_gid) patch_user(home_dir, config, uid=uid, gid=gid)
print(f" Patched {user.pw_name}: {user.pw_dir}/.codex/config.toml") print(f" Patched: {home_dir}/.codex/config.toml (uid={uid})")
except Exception as e: except Exception as e:
print(f" {RED}Failed {user.pw_name}: {e}{RESET}") print(f" {RED}Failed {home_dir}: {e}{RESET}")
return 0 if ok else 1 return 0 if ok else 1