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 ─────────────────────────────────────────────────
def list_users():
"""List system users with .codex/ or home dirs."""
users = []
try:
import pwd
for pw in pwd.getpwall():
home = pw.pw_dir
if not os.path.isdir(home):
def list_user_homes():
"""List home directories of real users. Returns list of (home_dir, uid, gid)."""
homes = []
if IS_MACOS:
# macOS: pwd.getpwall() is unreliable (Directory Services).
# Scan /Users/ directly + /var/root for root.
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
min_uid = 500 if IS_MACOS else 1000
if pw.pw_uid < min_uid and pw.pw_uid != 0:
if not os.path.isdir(base):
continue
if pw.pw_shell in ("/usr/sbin/nologin", "/bin/false"):
continue
users.append(pw)
except ImportError:
pass
return users
for name in os.listdir(base):
if name.startswith(".") or name == "Shared":
continue
udir = os.path.join(base, name)
if os.path.isdir(udir):
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):
@@ -748,14 +781,15 @@ def main():
# Patch other users if --all
if args.all:
for user in list_users():
if user.pw_dir == os.path.expanduser("~"):
my_home = os.path.expanduser("~")
for home_dir, uid, gid in list_user_homes():
if home_dir == my_home:
continue
try:
patch_user(user.pw_dir, config, uid=user.pw_uid, gid=user.pw_gid)
print(f" Patched {user.pw_name}: {user.pw_dir}/.codex/config.toml")
patch_user(home_dir, config, uid=uid, gid=gid)
print(f" Patched: {home_dir}/.codex/config.toml (uid={uid})")
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