diff --git a/augment/hooks/mdm/setup.py b/augment/hooks/mdm/setup.py index 40f49230..07df1da1 100644 --- a/augment/hooks/mdm/setup.py +++ b/augment/hooks/mdm/setup.py @@ -1184,7 +1184,7 @@ def _is_unbound(cmd: str) -> bool: return "failed" -def clear_setup(): +def clear_setup() -> bool: print("=" * 60) print("Augment Code Hooks - Clearing MDM Setup") print("=" * 60) @@ -1192,8 +1192,9 @@ def clear_setup(): if not check_admin_privileges(): print("This script requires administrator/root privileges") print(" Please re-run with sudo.") - return + return False + teardown_failed = False print("\nClearing environment variables...") # Windows `reg delete HKLM\...` is machine-wide; fall through with a # placeholder so the removal runs even if C:\Users has no profiles. @@ -1220,6 +1221,7 @@ def clear_setup(): elif not_found: print(f"API_KEY not set, nothing to clear for {not_found} user(s)") if failed: + teardown_failed = True print(f"Failed to clear API_KEY for {failed} user(s)") print("\nClearing managed hooks...") @@ -1230,11 +1232,13 @@ def clear_setup(): elif status == "not_found": print(f"Managed hooks not found in {managed_dir}") else: + teardown_failed = True print(f"Failed to clear managed hooks in {managed_dir}") print("\n" + "=" * 60) print("Clear Complete!") print("=" * 60) + return not teardown_failed def detect_install_state() -> Optional[str]: @@ -1291,8 +1295,7 @@ def main(): print("[backfill] Augment backfill is not supported.") if clear_mode: - clear_setup() - return + return clear_setup() print("=" * 60) print("Augment Code Hooks - MDM Setup") @@ -1306,7 +1309,7 @@ def main(): ) print("This script requires administrator/root privileges") print(" Please re-run with sudo.") - return + return False base_url = "https://backend.getunbound.ai" gateway_url = DEFAULT_GATEWAY_URL @@ -1341,27 +1344,27 @@ def main(): print("\nMissing required argument: --api-key") print("Usage: sudo python3 setup.py --api-key [--backend-url ] [--app_name ] [--debug]") print(" Or: sudo python3 setup.py --clear [--debug]") - return + return False print("\nGetting device identifier...") device_id = get_device_identifier() if not device_id: print("Failed to get device identifier") - return + return False debug_print(f"Device identifier: {device_id}") print("Device identifier retrieved") print("\nFetching API key from MDM...") api_key = fetch_api_key_from_mdm(base_url, app_name, auth_api_key, device_id) if not api_key: - return + return False print("API key received") print("\nSetting environment variables system-wide...") success, _ = set_env_var_system_wide("UNBOUND_AUGMENT_API_KEY", api_key) if not success: print("Failed to set UNBOUND_AUGMENT_API_KEY") - return + return False debug_print("UNBOUND_AUGMENT_API_KEY set successfully") # Write the per-user unbound config now (needed by the managed hook; harmless @@ -1376,7 +1379,7 @@ def main(): print("\nConfiguring Augment managed hooks...") if not setup_managed_hooks(gateway_url=gateway_url): print("Failed to configure managed hooks") - return + return False managed_dir = get_managed_settings_dir() print(f"Created managed hooks in {managed_dir}") @@ -1397,12 +1400,16 @@ def main(): notify_setup_complete(api_key, "augment_code", backend_url=base_url, install_state=state, serial_number=device_id) + return True + if __name__ == "__main__": try: - main() + ok = main() except KeyboardInterrupt: print("\n\nSetup cancelled.") + sys.exit(1) except Exception as e: print(f"\nError: {e}") - exit(1) + sys.exit(1) + sys.exit(0 if ok else 1) diff --git a/claude-code/gateway/mdm/setup.py b/claude-code/gateway/mdm/setup.py index d33a4dd9..f4f3f9f9 100644 --- a/claude-code/gateway/mdm/setup.py +++ b/claude-code/gateway/mdm/setup.py @@ -807,7 +807,7 @@ def _clear_env_var_across_users(var_name: str, user_homes, label: str = None) -> return cleared, not_found, failed -def clear_setup(): +def clear_setup() -> bool: print("=" * 60) print("Claude Code - Clearing MDM Setup") print("=" * 60) @@ -815,8 +815,9 @@ def clear_setup(): if not check_admin_privileges(): print("This script requires administrator/root privileges") print(" Please re-run with sudo.") - return + return False + teardown_failed = False print("\nClearing environment variables...") user_homes = get_all_user_homes() or ([(None, None)] if platform.system().lower() == "windows" else []) @@ -830,6 +831,7 @@ def clear_setup(): elif not (f1 or f2): print(f"API_KEY not set, nothing to clear for {n1} user(s)") if f1 or f2: + teardown_failed = True print(f"Failed to clear for {max(f1, f2)} user(s)") print("\nClearing managed settings...") @@ -840,11 +842,13 @@ def clear_setup(): elif status == "not_found": print(f"Managed settings not found in {managed_dir}") else: + teardown_failed = True print(f"Failed to clear managed settings in {managed_dir}") print("\n" + "=" * 60) print("Clear Complete!") print("=" * 60) + return not teardown_failed def detect_install_state() -> Optional[str]: @@ -904,8 +908,7 @@ def main(): debug_print("Debug mode enabled") if clear_mode: - clear_setup() - return + return clear_setup() print("=" * 60) print("Claude Code - MDM Setup") @@ -919,7 +922,7 @@ def main(): ) print("āŒ This script requires administrator/root privileges") print(" Please re-run with sudo.") - return + return False base_url = "https://backend.getunbound.ai" gateway_url = DEFAULT_GATEWAY_URL @@ -950,13 +953,13 @@ def main(): print("\nāŒ Missing required argument: --api-key") print("Usage: sudo python3 setup.py --api-key [--backend-url ] [--app_name ] [--debug]") print(" Or: sudo python3 setup.py --clear [--debug]") - return + return False print("\nšŸ” Getting device identifier...") device_id = get_device_identifier() if not device_id: print("āŒ Failed to get device identifier") - return + return False debug_print(f"Device identifier: {device_id}") print("āœ… Device identifier retrieved") @@ -966,7 +969,7 @@ def main(): print("\nšŸ”‘ Fetching API key from MDM...") claude_api_key = fetch_api_key_from_mdm(base_url, app_name, auth_api_key, device_id) if not claude_api_key: - return + return False print("āœ… API key received") print("\nšŸ“ Setting environment variables system-wide...") @@ -977,13 +980,13 @@ def main(): success, env_changed = set_env_var_system_wide("UNBOUND_API_KEY", claude_api_key) if not success: print(f"āŒ Failed to set UNBOUND_API_KEY") - return + return False debug_print("UNBOUND_API_KEY set successfully") success, url_changed = set_env_var_system_wide("ANTHROPIC_BASE_URL", gateway_url) if not success: print(f"āŒ Failed to set ANTHROPIC_BASE_URL") - return + return False debug_print("ANTHROPIC_BASE_URL set successfully") # Remove leftover hooks scripts, strip leftover user-level Unbound @@ -1000,7 +1003,7 @@ def main(): print(f"āœ… Created managed settings in {managed_dir}") else: print("āŒ Failed to configure managed settings") - return + return False print("\n" + "=" * 60) print("Setup Complete!") @@ -1008,12 +1011,16 @@ def main(): notify_setup_complete(claude_api_key, "unbound-claude-code", backend_url=base_url, install_state=install_state, serial_number=device_id) + return True + if __name__ == "__main__": try: - main() + ok = main() except KeyboardInterrupt: print("\n\nāš ļø Setup cancelled.") + sys.exit(1) except Exception as e: print(f"\nāŒ Error: {e}") - exit(1) + sys.exit(1) + sys.exit(0 if ok else 1) diff --git a/claude-code/hooks/mdm/setup.py b/claude-code/hooks/mdm/setup.py index f81fe603..8d6515bc 100644 --- a/claude-code/hooks/mdm/setup.py +++ b/claude-code/hooks/mdm/setup.py @@ -1117,7 +1117,7 @@ def clear_managed_hooks() -> str: return "failed" -def clear_setup(): +def clear_setup() -> bool: print("=" * 60) print("Claude Code Hooks - Clearing MDM Setup") print("=" * 60) @@ -1125,8 +1125,9 @@ def clear_setup(): if not check_admin_privileges(): print("This script requires administrator/root privileges") print(" Please re-run with sudo.") - return + return False + teardown_failed = False print("\nClearing environment variables...") # Windows `reg delete HKLM\...` is machine-wide; fall through with a # placeholder so the removal runs even if C:\Users has no profiles. @@ -1153,6 +1154,7 @@ def clear_setup(): elif not_found: print(f"API_KEY not set, nothing to clear for {not_found} user(s)") if failed: + teardown_failed = True print(f"Failed to clear API_KEY for {failed} user(s)") print("\nClearing managed hooks...") @@ -1163,11 +1165,13 @@ def clear_setup(): elif status == "not_found": print(f"Managed hooks not found in {managed_dir}") else: + teardown_failed = True print(f"Failed to clear managed hooks in {managed_dir}") print("\n" + "=" * 60) print("Clear Complete!") print("=" * 60) + return not teardown_failed def _backfill_collect_session(transcript_path: Path) -> Optional[Dict]: @@ -1611,8 +1615,7 @@ def main(): DEBUG = True if clear_mode: - clear_setup() - return + return clear_setup() print("=" * 60) print("Claude Code Hooks - MDM Setup") @@ -1626,7 +1629,7 @@ def main(): ) print("This script requires administrator/root privileges") print(" Please re-run with sudo.") - return + return False base_url = "https://backend.getunbound.ai" gateway_url = DEFAULT_GATEWAY_URL @@ -1665,20 +1668,20 @@ def main(): print("\nMissing required argument: --api-key") print("Usage: sudo python3 setup.py --api-key [--backend-url ] [--app_name ] [--debug] [--backfill]") print(" Or: sudo python3 setup.py --clear [--debug]") - return + return False print("\nGetting device identifier...") device_id = get_device_identifier() if not device_id: print("Failed to get device identifier") - return + return False debug_print(f"Device identifier: {device_id}") print("Device identifier retrieved") print("\nFetching API key from MDM...") api_key = fetch_api_key_from_mdm(base_url, app_name, auth_api_key, device_id) if not api_key: - return + return False print("API key received") print("\nSetting environment variables system-wide...") @@ -1690,7 +1693,7 @@ def main(): success, _ = set_env_var_system_wide("UNBOUND_CLAUDE_API_KEY", api_key) if not success: print("Failed to set UNBOUND_CLAUDE_API_KEY") - return + return False debug_print("UNBOUND_CLAUDE_API_KEY set successfully") # Remove gateway artifacts, strip leftover user-level Unbound hooks @@ -1708,7 +1711,7 @@ def main(): print(f"Created managed hooks in {managed_dir}") else: print("Failed to configure managed hooks") - return + return False print("\n" + "=" * 60) print("Setup Complete!") @@ -1719,12 +1722,16 @@ def main(): if backfill_mode: run_backfill(api_key, base_url, get_all_user_homes()) + return True + if __name__ == "__main__": try: - main() + ok = main() except KeyboardInterrupt: print("\n\nSetup cancelled.") + sys.exit(1) except Exception as e: print(f"\nError: {e}") - exit(1) + sys.exit(1) + sys.exit(0 if ok else 1) diff --git a/codex/gateway/mdm/setup.py b/codex/gateway/mdm/setup.py index 4ab8c981..a2a36e8e 100644 --- a/codex/gateway/mdm/setup.py +++ b/codex/gateway/mdm/setup.py @@ -671,7 +671,7 @@ def _clear_env_var_across_users(var_name: str, user_homes, label: str = None) -> return cleared, not_found, failed -def clear_setup(): +def clear_setup() -> bool: print("=" * 60) print("Codex - Clearing MDM Setup") print("=" * 60) @@ -679,8 +679,9 @@ def clear_setup(): if not check_admin_privileges(): print("This script requires administrator/root privileges") print(" Please re-run with sudo.") - return + return False + teardown_failed = False print("\nClearing environment variables...") user_homes = get_all_user_homes() or ([(None, None)] if platform.system().lower() == "windows" else []) @@ -694,15 +695,18 @@ def clear_setup(): elif not (f1 or f2): print(f"API_KEY not set, nothing to clear for {n1} user(s)") if f1 or f2: + teardown_failed = True print(f"Failed to clear for {max(f1, f2)} user(s)") for username, home_dir in user_homes: if home_dir is not None: if not remove_codex_config_base_url_for_user(username, home_dir): + teardown_failed = True debug_print(f"Could not remove openai_base_url from codex config for {username}") print("\n" + "=" * 60) print("Clear Complete!") print("=" * 60) + return not teardown_failed def remove_hooks_unbound_script_for_user(username: str, home_dir: Path) -> None: @@ -867,8 +871,7 @@ def main(): debug_print("Debug mode enabled") if clear_mode: - clear_setup() - return + return clear_setup() print("=" * 60) print("Codex - MDM Setup") @@ -882,7 +885,7 @@ def main(): ) print("āŒ This script requires administrator/root privileges") print(" Please re-run with sudo.") - return + return False base_url = "https://backend.getunbound.ai" gateway_url = DEFAULT_GATEWAY_URL @@ -913,13 +916,13 @@ def main(): print("\nāŒ Missing required argument: --api-key") print("Usage: sudo python3 setup.py --api-key [--backend-url ] [--app_name ] [--debug]") print(" Or: sudo python3 setup.py --clear [--debug]") - return + return False print("\nšŸ” Getting device identifier...") device_id = get_device_identifier() if not device_id: print("āŒ Failed to get device identifier") - return + return False debug_print(f"Device identifier: {device_id}") print("āœ… Device identifier retrieved") @@ -929,7 +932,7 @@ def main(): print("\nšŸ”‘ Fetching API key from MDM...") codex_api_key = fetch_api_key_from_mdm(base_url, app_name, auth_api_key, device_id) if not codex_api_key: - return + return False print("āœ… API key received") # Remove leftover hooks setup env var @@ -940,14 +943,14 @@ def main(): success, env_changed = set_env_var_system_wide("OPENAI_API_KEY", codex_api_key) if not success: print(f"āŒ Failed to set OPENAI_API_KEY") - return + return False debug_print("OPENAI_API_KEY set successfully") print("\nšŸ“ Configuring all users...") user_homes = get_all_user_homes() if not user_homes: print("āŒ No user home directories found") - return + return False config_count = 0 for username, home_dir in user_homes: if write_codex_config_for_user(username, home_dir, f"{gateway_url.rstrip('/')}/v1"): @@ -958,7 +961,7 @@ def main(): if config_count == 0: print("āŒ Failed to configure codex for any users") - return + return False print(f"āœ… Configured {config_count} user(s)") @@ -971,12 +974,16 @@ def main(): notify_setup_complete(codex_api_key, "unbound-codex", backend_url=base_url, install_state=install_state, serial_number=device_id) + return True + if __name__ == "__main__": try: - main() + ok = main() except KeyboardInterrupt: print("\n\nāš ļø Setup cancelled by user.") + sys.exit(1) except Exception as e: print(f"\nāŒ An error occurred: {e}") - exit(1) + sys.exit(1) + sys.exit(0 if ok else 1) diff --git a/codex/hooks/mdm/setup.py b/codex/hooks/mdm/setup.py index 0e8811e8..99e628ed 100644 --- a/codex/hooks/mdm/setup.py +++ b/codex/hooks/mdm/setup.py @@ -1248,7 +1248,7 @@ def _disable(): debug_print(f"Removed codex_hooks feature for {username}") -def clear_setup(): +def clear_setup() -> bool: print("=" * 60) print("Codex Hooks - Clearing MDM Setup") print("=" * 60) @@ -1256,7 +1256,9 @@ def clear_setup(): if not check_admin_privileges(): print("This script requires administrator/root privileges") print(" Please re-run with sudo.") - return + return False + + teardown_failed = False print("\nClearing environment variables...") # Windows `reg delete HKLM\...` is machine-wide; fall through with a @@ -1287,6 +1289,7 @@ def clear_setup(): elif not_found: print(f"API_KEY not set, nothing to clear for {not_found} user(s)") if failed: + teardown_failed = True print(f"Failed to clear API_KEY for {failed} user(s)") print("\nClearing managed hooks...") @@ -1297,12 +1300,15 @@ def clear_setup(): elif status == "not_found": print(f"Managed hooks not found in {managed_dir}") else: + teardown_failed = True print(f"Failed to clear managed hooks in {managed_dir}") print("\n" + "=" * 60) print("Clear Complete!") print("=" * 60) + return not teardown_failed + def _backfill_session_id_from_filename(transcript_path: Path) -> Optional[str]: # Only `rollout--.jsonl` — never a generic stem fallback. @@ -1758,8 +1764,7 @@ def main(): DEBUG = True if clear_mode: - clear_setup() - return + return clear_setup() print("=" * 60) print("Codex Hooks - MDM Setup") @@ -1773,7 +1778,7 @@ def main(): ) print("This script requires administrator/root privileges") print(" Please re-run with sudo.") - return + return False base_url = "https://backend.getunbound.ai" gateway_url = DEFAULT_GATEWAY_URL @@ -1812,20 +1817,20 @@ def main(): print("\nMissing required argument: --api-key") print("Usage: sudo python3 setup.py --api-key [--backend-url ] [--app_name ] [--debug] [--backfill]") print(" Or: sudo python3 setup.py --clear [--debug]") - return + return False print("\nGetting device identifier...") device_id = get_device_identifier() if not device_id: print("Failed to get device identifier") - return + return False debug_print(f"Device identifier: {device_id}") print("Device identifier retrieved") print("\nFetching API key from MDM...") api_key = fetch_api_key_from_mdm(base_url, app_name, auth_api_key, device_id) if not api_key: - return + return False print("API key received") print("\nSetting environment variables system-wide...") @@ -1836,7 +1841,7 @@ def main(): success, _ = set_env_var_system_wide("UNBOUND_CODEX_API_KEY", api_key) if not success: print("Failed to set UNBOUND_CODEX_API_KEY") - return + return False debug_print("UNBOUND_CODEX_API_KEY set successfully") # codex 0.125 discovers hooks from ~/.codex/hooks.json (user layer), not the @@ -1856,25 +1861,34 @@ def main(): print(f"Registered codex hooks for {username}") installed += 1 - if user_homes and installed == 0: - print("Failed to configure codex hooks for any user") - return + success = bool(user_homes) and installed == len(user_homes) + if not user_homes: + print("No user home directories found") + elif success: + print(f"Registered codex hooks for {installed} user(s)") + else: + print(f"Registered codex hooks for {installed} of {len(user_homes)} user(s) — {len(user_homes) - installed} failed") print("\n" + "=" * 60) - print("Setup Complete!") + print("Setup Complete!" if success else "Setup Failed") print("=" * 60) - notify_setup_complete(api_key, "codex", backend_url=base_url, install_state=state, serial_number=device_id) + if success: + notify_setup_complete(api_key, "codex", backend_url=base_url, install_state=state, serial_number=device_id) - if backfill_mode: + if success and backfill_mode: run_backfill(api_key, base_url, get_all_user_homes()) + return success + if __name__ == "__main__": try: - main() + ok = main() except KeyboardInterrupt: print("\n\nSetup cancelled.") + sys.exit(1) except Exception as e: print(f"\nError: {e}") - exit(1) + sys.exit(1) + sys.exit(0 if ok else 1) diff --git a/copilot/hooks/mdm/setup.py b/copilot/hooks/mdm/setup.py index 260b6a82..7ff2f969 100644 --- a/copilot/hooks/mdm/setup.py +++ b/copilot/hooks/mdm/setup.py @@ -644,17 +644,38 @@ def download_file(url: str, dest_path: Path) -> bool: return False -def rewrite_gateway_url_in_file(path: Path, gateway_url: str) -> None: - """Replace the hardcoded default gateway URL inside a downloaded unbound.py.""" +def _apply_gateway_url(text: str, gateway_url: str) -> str: if not gateway_url or gateway_url == DEFAULT_GATEWAY_URL: - return + return text + return text.replace(f'"{DEFAULT_GATEWAY_URL}"', f'"{gateway_url}"') + + +def _fetch_hook_script(gateway_url: str) -> Optional[str]: + staging_dir = Path(tempfile.mkdtemp(prefix="unbound-copilot-")) + source_script = staging_dir / "unbound.py" try: - text = path.read_text(encoding="utf-8") - new_text = text.replace(f'"{DEFAULT_GATEWAY_URL}"', f'"{gateway_url}"') - if new_text != text: - path.write_text(new_text, encoding="utf-8") + if not download_file(SCRIPT_URL, source_script): + return None + return _apply_gateway_url(source_script.read_text(encoding="utf-8"), gateway_url) except Exception as e: - debug_print(f"Could not rewrite gateway URL in {path}: {e}") + debug_print(f"Could not read downloaded unbound.py: {e}") + return None + finally: + shutil.rmtree(staging_dir, ignore_errors=True) + + +def _write_file(path: Path, data: str, mode: int) -> None: + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | getattr(os, 'O_NOFOLLOW', 0) + fd = os.open(str(path), flags, mode) + try: + f = os.fdopen(fd, 'w', encoding='utf-8') + except Exception: + os.close(fd) + raise + with f: + if os.name == "posix": + os.fchmod(f.fileno(), mode) + f.write(data) def _copilot_hooks_config(script_path: Path) -> Dict: @@ -688,28 +709,23 @@ def _copilot_hooks_config(script_path: Path) -> Dict: return {"version": 1, "hooks": hooks} -def install_hooks_for_user(username: str, home_dir: Path, gateway_url: str, source_script: Path) -> bool: - """Install unbound.py and unbound.json into a user's ~/.copilot/hooks. - Privilege-drops to the target user before any FS op — curl already ran as - root to fetch source_script; the per-user write happens post-drop.""" +def install_hooks_for_user(username: str, home_dir: Path, script_text: str) -> bool: + """Install unbound.py and unbound.json into a user's ~/.copilot/hooks, writing + as the target user so a symlink in their home cannot redirect a root write.""" hooks_dir = home_dir / ".copilot" / "hooks" script_path = hooks_dir / "unbound.py" hooks_json = hooks_dir / "unbound.json" - system = platform.system().lower() + script_mode = 0o755 if platform.system().lower() in ("darwin", "linux") else 0o644 def _install(): - hooks_dir.mkdir(parents=True, exist_ok=True) - shutil.copyfile(str(source_script), str(script_path)) - if system in ["darwin", "linux"]: - os.chmod(script_path, 0o755) - rewrite_gateway_url_in_file(script_path, gateway_url) - - config = _copilot_hooks_config(script_path) - flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | getattr(os, 'O_NOFOLLOW', 0) - fd = os.open(str(hooks_json), flags, 0o644) - with os.fdopen(fd, 'w', encoding='utf-8') as f: - json.dump(config, f, indent=2) - return True + try: + hooks_dir.mkdir(parents=True, exist_ok=True) + _write_file(script_path, script_text, script_mode) + _write_file(hooks_json, json.dumps(_copilot_hooks_config(script_path), indent=2), 0o644) + return True + except Exception as e: + debug_print(f"install for {username} failed: {e}") + raise ok = bool(_run_as_user(username, _install)) debug_print(f"{'Installed' if ok else 'Failed to install'} Copilot hooks for {username}") @@ -1223,7 +1239,7 @@ def notify_setup_complete(api_key: str, tool_type: str, backend_url: str = "http debug_print(f"Could not notify backend: {e}") -def clear_setup(): +def clear_setup() -> bool: print("=" * 60) print("Copilot Hooks - Clearing MDM Setup") print("=" * 60) @@ -1231,7 +1247,9 @@ def clear_setup(): if not check_admin_privileges(): print("This script requires administrator/root privileges") print(" Please re-run with sudo.") - return + return False + + teardown_failed = False print("\nClearing environment variables...") # Windows `reg delete HKLM\...` is machine-wide; fall through with a @@ -1270,16 +1288,19 @@ def clear_setup(): elif env_not_found: print(f"API_KEY not set, nothing to clear for {env_not_found} user(s)") if env_failed: + teardown_failed = True print(f"Failed to clear API_KEY for {env_failed} user(s)") if hooks_cleared: print(f"Cleared Copilot hooks for {hooks_cleared} user(s)") if hooks_failed: + teardown_failed = True print(f"Failed to clear Copilot hooks for {hooks_failed} user(s)") print("\n" + "=" * 60) print("Clear Complete!") print("=" * 60) + return not teardown_failed def main(): @@ -1291,8 +1312,7 @@ def main(): DEBUG = True if clear_mode: - clear_setup() - return + return clear_setup() print("=" * 60) print("Copilot Hooks - MDM Setup") @@ -1306,7 +1326,7 @@ def main(): ) print("This script requires administrator/root privileges") print(" Please re-run with sudo.") - return + return False base_url = "https://backend.getunbound.ai" gateway_url = DEFAULT_GATEWAY_URL @@ -1345,46 +1365,34 @@ def main(): print("\nMissing required argument: --api-key") print("Usage: sudo python3 setup.py --api-key [--backend-url ] [--app_name ] [--debug] [--backfill]") print(" Or: sudo python3 setup.py --clear [--debug]") - return + return False print("\nGetting device identifier...") device_id = get_device_identifier() if not device_id: print("Failed to get device identifier") - return + return False debug_print(f"Device identifier: {device_id}") print("Device identifier retrieved") print("\nFetching API key from MDM...") api_key = fetch_api_key_from_mdm(base_url, app_name, auth_api_key, device_id) if not api_key: - return + return False print("API key received") print("\nSetting environment variables system-wide...") - success, _ = set_env_var_system_wide("UNBOUND_COPILOT_API_KEY", api_key) - if not success: + env_ok, _ = set_env_var_system_wide("UNBOUND_COPILOT_API_KEY", api_key) + if not env_ok: print("Failed to set UNBOUND_COPILOT_API_KEY") - return + return False debug_print("UNBOUND_COPILOT_API_KEY set successfully") - # Download unbound.py once as root into a private root-owned temp dir. - # mkdtemp gives an unpredictable name so a local user cannot pre-create or - # symlink the path the root curl writes to. The dir is then widened to - # 0o711 (traverse, still no listing) and the file to 0o644 so the per-user - # installs — which run after privilege-drop — can read the script. print("\nDownloading hook script...") - staging_dir = Path(tempfile.mkdtemp(prefix="unbound-copilot-")) - source_script = staging_dir / "unbound.py" - if not download_file(SCRIPT_URL, source_script): + script_text = _fetch_hook_script(gateway_url) + if script_text is None: print("Failed to download unbound.py") - shutil.rmtree(staging_dir, ignore_errors=True) - return - try: - os.chmod(staging_dir, 0o711) - os.chmod(source_script, 0o644) - except OSError: - pass + return False state = detect_install_state() @@ -1393,33 +1401,37 @@ def main(): installed_count = 0 for username, home_dir in user_homes: write_unbound_config_for_user(username, home_dir, api_key, urls={"base_url": base_url, "gateway_url": gateway_url, "frontend_url": frontend_url}) - if install_hooks_for_user(username, home_dir, gateway_url, source_script): + if install_hooks_for_user(username, home_dir, script_text): installed_count += 1 - shutil.rmtree(staging_dir, ignore_errors=True) - + success = bool(user_homes) and installed_count == len(user_homes) if not user_homes: print("No user home directories found") - elif installed_count == len(user_homes): + elif success: print(f"Installed Copilot hooks for {installed_count} user(s)") else: print(f"Installed Copilot hooks for {installed_count} of {len(user_homes)} user(s) — {len(user_homes) - installed_count} failed") print("\n" + "=" * 60) - print("Setup Complete!") + print("Setup Complete!" if success else "Setup Failed") print("=" * 60) - notify_setup_complete(api_key, "copilot", backend_url=base_url, install_state=state, serial_number=device_id) + if success: + notify_setup_complete(api_key, "copilot", backend_url=base_url, install_state=state, serial_number=device_id) - if backfill_mode: + if success and backfill_mode: run_backfill(api_key, base_url, user_homes) + return success + if __name__ == "__main__": try: - main() + ok = main() except KeyboardInterrupt: print("\n\nSetup cancelled.") + sys.exit(1) except Exception as e: print(f"\nError: {e}") - exit(1) + sys.exit(1) + sys.exit(0 if ok else 1) diff --git a/cursor/mdm/setup.py b/cursor/mdm/setup.py index 98aec7e9..9a89bf38 100644 --- a/cursor/mdm/setup.py +++ b/cursor/mdm/setup.py @@ -867,7 +867,7 @@ def restart_cursor() -> bool: return False -def clear_setup(): +def clear_setup() -> bool: """Remove hooks and environment variables set by the setup script.""" print("=" * 60) print("Unbound Cursor Hooks - Clearing Setup") @@ -877,8 +877,9 @@ def clear_setup(): if not check_admin_privileges(): print("āŒ This script requires administrator/root privileges") print(" Please re-run with sudo.") - return + return False + teardown_failed = False # Remove enterprise hooks files (NOT the entire Cursor directory) print("\nClearing enterprise hooks...") enterprise_dir = get_enterprise_hooks_dir() @@ -891,6 +892,7 @@ def clear_setup(): hooks_json.unlink() print("Cleared enterprise hooks.json") except Exception as e: + teardown_failed = True print(f"Failed to clear hooks.json: {e}") # Remove hooks directory @@ -900,6 +902,7 @@ def clear_setup(): shutil.rmtree(hooks_dir) print("Cleared enterprise hooks directory") except Exception as e: + teardown_failed = True print(f"Failed to clear hooks directory: {e}") # Remove environment variable from all users @@ -912,6 +915,7 @@ def clear_setup(): elif status == "not_found": print("API_KEY not set, nothing to clear") else: + teardown_failed = True print("Failed to clear API_KEY") else: user_homes = get_all_user_homes() @@ -936,6 +940,7 @@ def clear_setup(): elif not_found: print(f"API_KEY not set, nothing to clear for {not_found} user(s)") if failed: + teardown_failed = True print(f"Failed to clear API_KEY for {failed} user(s)") # Per-user hook logs live in ~/.cursor/hooks on every platform; remove them @@ -954,6 +959,7 @@ def clear_setup(): print("=" * 60) print("\nNote: Restart your terminal or log out/in for env var changes to take effect") print("=" * 60) + return not teardown_failed def fetch_api_key_from_mdm(base_url: str, app_name: str, auth_api_key: str, serial_number: str) -> str: @@ -1071,8 +1077,7 @@ def main(): # If clear mode, run cleanup and exit if clear_mode: - clear_setup() - return + return clear_setup() print("=" * 60) print("Unbound Cursor Hooks - MDM Setup") @@ -1088,7 +1093,7 @@ def main(): ) print("āŒ This script requires administrator/root privileges") print(" Please re-run with sudo.") - return + return False # Parse arguments base_url = "https://backend.getunbound.ai" @@ -1124,14 +1129,14 @@ def main(): print("\nāŒ Missing required argument: --api-key") print("Usage: sudo python3 setup.py --api-key [--backend-url ] [--app_name ] [--debug]") print(" Or: sudo python3 setup.py --clear [--debug]") - return + return False # Get device identifier print("\nšŸ” Getting device serial number...") serial_number = get_device_identifier() if not serial_number: print("āŒ Failed to get device serial number") - return + return False debug_print(f"Serial number: {serial_number}") print("āœ… Serial number retrieved") @@ -1139,7 +1144,7 @@ def main(): print("\nšŸ”‘ Fetching API key from MDM...") cursor_api_key = fetch_api_key_from_mdm(base_url, app_name, auth_api_key, serial_number) if not cursor_api_key: - return + return False print("āœ… API key received") # Set environment variable @@ -1147,7 +1152,7 @@ def main(): success, env_changed, message = set_env_var("UNBOUND_CURSOR_API_KEY", cursor_api_key) if not success: print(f"āŒ Failed to set environment variable: {message}") - return + return False print(f"āœ… Environment variable set ({message})") # Write API key to ~/.unbound/config.json for each user and remove user-level hooks @@ -1170,7 +1175,7 @@ def main(): hooks_success, hooks_changed = setup_hooks(gateway_url=gateway_url) if not hooks_success: print("\nāŒ Failed to setup hooks") - return + return False debug_print("Hooks setup complete") print("\n" + "=" * 60) @@ -1188,12 +1193,16 @@ def main(): print("=" * 60) print("\n") + return True + if __name__ == "__main__": try: - main() + ok = main() except KeyboardInterrupt: print("\n\nāš ļø Setup cancelled.") + sys.exit(1) except Exception as e: print(f"\nāŒ Error: {e}") - exit(1) + sys.exit(1) + sys.exit(0 if ok else 1) diff --git a/gemini-cli/gateway/mdm/setup.py b/gemini-cli/gateway/mdm/setup.py index 65ad03c2..53e483a5 100644 --- a/gemini-cli/gateway/mdm/setup.py +++ b/gemini-cli/gateway/mdm/setup.py @@ -573,7 +573,7 @@ def _clear_env_var_across_users(var_name: str, user_homes, label: str = None) -> return cleared, not_found, failed -def clear_setup(): +def clear_setup() -> bool: print("=" * 60) print("Gemini CLI - Clearing MDM Setup") print("=" * 60) @@ -581,8 +581,9 @@ def clear_setup(): if not check_admin_privileges(): print("This script requires administrator/root privileges") print(" Please re-run with sudo.") - return + return False + teardown_failed = False print("\nClearing environment variables...") # Windows `reg delete HKLM\...` is machine-wide; fall through with a # placeholder so the removal runs even if C:\Users has no profiles. @@ -598,6 +599,7 @@ def clear_setup(): elif not (f1 or f2): print(f"API_KEY not set, nothing to clear for {n1} user(s)") if f1 or f2: + teardown_failed = True print(f"Failed to clear for {max(f1, f2)} user(s)") if platform.system().lower() == "windows": @@ -610,6 +612,7 @@ def clear_setup(): settings_path.unlink() debug_print(f"Removed {settings_path}") except Exception as e: + teardown_failed = True print(f"Failed to clear managed settings file: {e}") if settings_dir.exists(): try: @@ -624,11 +627,13 @@ def clear_setup(): if status == "cleared": print("Cleared managed settings path") elif status == "failed": + teardown_failed = True print("Failed to clear managed settings path") print("\n" + "=" * 60) print("Clear Complete!") print("=" * 60) + return not teardown_failed def detect_install_state() -> Optional[str]: @@ -681,8 +686,7 @@ def main(): debug_print("Debug mode enabled") if clear_mode: - clear_setup() - return + return clear_setup() print("=" * 60) print("Gemini CLI - MDM Setup") @@ -696,7 +700,7 @@ def main(): ) print("āŒ This script requires administrator/root privileges") print(" Please re-run with sudo.") - return + return False base_url = "https://backend.getunbound.ai" gateway_url = DEFAULT_GATEWAY_URL @@ -727,13 +731,13 @@ def main(): print("\nāŒ Missing required argument: --api-key") print("Usage: sudo python3 setup.py --api-key [--backend-url ] [--app_name ] [--debug]") print(" Or: sudo python3 setup.py --clear [--debug]") - return + return False print("\nšŸ” Getting device identifier...") device_id = get_device_identifier() if not device_id: print("āŒ Failed to get device identifier") - return + return False debug_print(f"Device identifier: {device_id}") print("āœ… Device identifier retrieved") @@ -743,20 +747,20 @@ def main(): print("\nšŸ”‘ Fetching API key from MDM...") gemini_api_key = fetch_api_key_from_mdm(base_url, app_name, auth_api_key, device_id) if not gemini_api_key: - return + return False print("āœ… API key received") print("\nšŸ“ Setting environment variables system-wide...") success, env_changed = set_env_var_system_wide("GEMINI_API_KEY", gemini_api_key) if not success: print(f"āŒ Failed to set GEMINI_API_KEY") - return + return False debug_print("GEMINI_API_KEY set successfully") success, url_changed = set_env_var_system_wide("GOOGLE_GEMINI_BASE_URL", f"{gateway_url.rstrip('/')}/v1") if not success: print(f"āŒ Failed to set GOOGLE_GEMINI_BASE_URL") - return + return False debug_print("GOOGLE_GEMINI_BASE_URL set successfully") for username, home_dir in get_all_user_homes(): @@ -790,12 +794,16 @@ def main(): notify_setup_complete(gemini_api_key, "gemini-cli", backend_url=base_url, install_state=install_state, serial_number=device_id) + return True + if __name__ == "__main__": try: - main() + ok = main() except KeyboardInterrupt: print("\n\nāš ļø Setup cancelled by user.") + sys.exit(1) except Exception as e: print(f"\nāŒ An error occurred: {e}") - exit(1) + sys.exit(1) + sys.exit(0 if ok else 1)